diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2019-04-29 09:11:57 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-04-29 09:11:57 +0900 |
| commit | 05b8111c1906c1285c9ddde758eda45b83792244 (patch) | |
| tree | da5d58c4ae18436f739eaee9e1801c6c48056be5 /src/client/app/common/views/components | |
| parent | Update define.ts (diff) | |
| download | misskey-05b8111c1906c1285c9ddde758eda45b83792244.tar.gz misskey-05b8111c1906c1285c9ddde758eda45b83792244.tar.bz2 misskey-05b8111c1906c1285c9ddde758eda45b83792244.zip | |
Pages (#4811)
* wip
* wip
* wip
* Update page-editor.vue
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update page-editor.variable.core.vue
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update aiscript.ts
* wip
* Update package.json
* wip
* wip
* wip
* wip
* wip
* Update page.vue
* wip
* wip
* wip
* wip
* more info
* wip fn
* wip
* wip
* wip
Diffstat (limited to 'src/client/app/common/views/components')
13 files changed, 1450 insertions, 3 deletions
diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue index c1ee7958c0..020c88f699 100644 --- a/src/client/app/common/views/components/dialog.vue +++ b/src/client/app/common/views/components/dialog.vue @@ -22,7 +22,14 @@ <ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input> <ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input> <ui-select v-if="select" v-model="selectedValue" autofocus> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + <template v-if="select.items"> + <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + </template> + <template v-else> + <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label"> + <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> + </optgroup> + </template> </ui-select> <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)"> <ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button> @@ -230,7 +237,7 @@ export default Vue.extend({ font-size 32px &.success - color #37ec92 + color #85da5a &.error color #ec4137 diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue index 2559907512..6db4b40dd8 100644 --- a/src/client/app/common/views/components/media-image.vue +++ b/src/client/app/common/views/components/media-image.vue @@ -36,7 +36,7 @@ export default Vue.extend({ return { hide: true }; - } + }, computed: { style(): any { let url = `url(${ diff --git a/src/client/app/common/views/components/page-editor/page-editor.block.vue b/src/client/app/common/views/components/page-editor/page-editor.block.vue new file mode 100644 index 0000000000..a3e1488d1b --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.block.vue @@ -0,0 +1,25 @@ +<template> +<component :is="'x-' + value.type" :value="value" @input="v => updateItem(v)" @remove="() => $emit('remove', value)" :key="value.id"/> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XSection from './page-editor.section.vue'; +import XText from './page-editor.text.vue'; +import XImage from './page-editor.image.vue'; +import XButton from './page-editor.button.vue'; +import XInput from './page-editor.input.vue'; +import XSwitch from './page-editor.switch.vue'; + +export default Vue.extend({ + components: { + XSection, XText, XImage, XButton, XInput, XSwitch + }, + + props: { + value: { + required: true + } + }, +}); +</script> diff --git a/src/client/app/common/views/components/page-editor/page-editor.button.vue b/src/client/app/common/views/components/page-editor/page-editor.button.vue new file mode 100644 index 0000000000..d5fc243818 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.button.vue @@ -0,0 +1,54 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template> + + <section class="xfhsjczc"> + <ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input> + <ui-select v-model="value.action"> + <template #label>{{ $t('blocks._button.action') }}</template> + <option value="dialog">{{ $t('blocks._button._action.dialog') }}</option> + <option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option> + </ui-select> + <ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faBolt } from '@fortawesome/free-solid-svg-icons'; +import XContainer from './page-editor.container.vue'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt + }; + }, + + created() { + if (this.value.text == null) Vue.set(this.value, 'text', ''); + if (this.value.action == null) Vue.set(this.value, 'action', 'dialog'); + if (this.value.content == null) Vue.set(this.value, 'content', null); + }, +}); +</script> + +<style lang="stylus" scoped> +.xfhsjczc + padding 0 16px 0 16px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.container.vue b/src/client/app/common/views/components/page-editor/page-editor.container.vue new file mode 100644 index 0000000000..698fdfee45 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.container.vue @@ -0,0 +1,135 @@ +<template> +<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }"> + <header> + <div class="title"><slot name="header"></slot></div> + <div class="buttons"> + <slot name="func"></slot> + <button v-if="removable" @click="remove()"> + <fa :icon="faTrashAlt"/> + </button> + <button @click="toggleContent(!showBody)"> + <template v-if="showBody"><fa icon="angle-up"/></template> + <template v-else><fa icon="angle-down"/></template> + </button> + </div> + </header> + <p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> + <p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> + <div v-show="showBody"> + <slot></slot> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../../../../i18n'; + +export default Vue.extend({ + i18n: i18n('pages'), + + props: { + expanded: { + type: Boolean, + default: true + }, + removable: { + type: Boolean, + default: true + }, + error: { + required: false, + default: null + }, + warn: { + required: false, + default: null + } + }, + data() { + return { + showBody: this.expanded, + faTrashAlt + }; + }, + methods: { + toggleContent(show: boolean) { + this.showBody = show; + this.$emit('toggle', show); + }, + remove() { + this.$emit('remove'); + } + } +}); +</script> + +<style lang="stylus" scoped> +.cpjygsrt + overflow hidden + background var(--face) + border solid 2px var(--pageBlockBorder) + border-radius 6px + + &:hover + border solid 2px var(--pageBlockBorderHover) + + &.warn + border solid 2px #dec44c + + &.error + border solid 2px #f00 + + & + .cpjygsrt + margin-top 16px + + > header + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color var(--faceHeaderText) + box-shadow 0 1px rgba(#000, 0.07) + + > [data-icon] + margin-right 6px + + &:empty + display none + + > .buttons + position absolute + z-index 2 + top 0 + right 0 + + > button + padding 0 + width 42px + font-size 0.9em + line-height 42px + color var(--faceTextButton) + + &:hover + color var(--faceTextButtonHover) + + &:active + color var(--faceTextButtonActive) + + > .warn + color #b19e49 + margin 0 + padding 16px 16px 0 16px + font-size 14px + + > .error + color #f00 + margin 0 + padding 16px 16px 0 16px + font-size 14px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.image.vue b/src/client/app/common/views/components/page-editor/page-editor.image.vue new file mode 100644 index 0000000000..0bc1816e8d --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.image.vue @@ -0,0 +1,78 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template> + <template #func> + <button @click="choose()"> + <fa :icon="faFolderOpen"/> + </button> + </template> + + <section class="oyyftmcf"> + <x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; +import XContainer from './page-editor.container.vue'; +import XFileThumbnail from '../drive-file-thumbnail.vue'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer, XFileThumbnail + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + file: null, + faPencilAlt, faImage, faFolderOpen + }; + }, + + created() { + if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null); + }, + + mounted() { + if (this.value.fileId == null) { + this.choose(); + } else { + this.$root.api('drive/files/show', { + fileId: this.value.fileId + }).then(file => { + this.file = file; + }); + } + }, + + methods: { + async choose() { + this.$chooseDriveFile({ + multiple: false + }).then(file => { + this.file = file; + this.value.fileId = file.id; + }); + }, + } +}); +</script> + +<style lang="stylus" scoped> +.oyyftmcf + > .preview + height 150px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.input.vue b/src/client/app/common/views/components/page-editor/page-editor.input.vue new file mode 100644 index 0000000000..1f3754252b --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.input.vue @@ -0,0 +1,54 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faBolt"/> {{ $t('blocks.input') }}</template> + + <section class="dnvasjon"> + <ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._input.name') }}</span></ui-input> + <ui-input v-model="value.text"><span>{{ $t('blocks._input.text') }}</span></ui-input> + <ui-select v-model="value.inputType"> + <template #label>{{ $t('blocks._input.inputType') }}</template> + <option value="string">{{ $t('blocks._input._inputType.string') }}</option> + <option value="number">{{ $t('blocks._input._inputType.number') }}</option> + </ui-select> + <ui-input v-model="value.default" :type="value.inputType"><span>{{ $t('blocks._input.default') }}</span></ui-input> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons'; +import XContainer from './page-editor.container.vue'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faSquareRootAlt + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + if (this.value.inputType == null) Vue.set(this.value, 'inputType', 'string'); + }, +}); +</script> + +<style lang="stylus" scoped> +.dnvasjon + padding 0 16px 0 16px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue new file mode 100644 index 0000000000..3122832030 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue @@ -0,0 +1,263 @@ +<template> +<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn"> + <template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> + <template #func> + <button @click="changeType()"> + <fa :icon="faPencilAlt"/> + </button> + </template> + + <section v-if="value.type === null" class="pbglfege" @click="changeType()"> + {{ $t('script.emptySlot') }} + </section> + <section v-else-if="value.type === 'text'" class="tbwccoaw"> + <input v-model="value.value"/> + </section> + <section v-else-if="value.type === 'multiLineText'" class="tbwccoaw"> + <textarea v-model="value.value"></textarea> + </section> + <section v-else-if="value.type === 'textList'" class="frvuzvoi"> + <ui-textarea v-model="value.value"></ui-textarea> + </section> + <section v-else-if="value.type === 'number'" class="tbwccoaw"> + <input v-model="value.value" type="number"/> + </section> + <section v-else-if="value.type === 'ref'" class="hpdwcrvs"> + <select v-model="value.value"> + <option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> + <optgroup :label="$t('script.pageVariables')"> + <option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> + </optgroup> + <optgroup :label="$t('script.enviromentVariables')"> + <option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> + </optgroup> + </select> + </section> + <section v-else-if="value.type === 'in'" class="hpdwcrvs"> + <select v-model="value.value"> + <option v-for="v in fnSlots" :value="v">{{ v }}</option> + </select> + </section> + <section v-else-if="value.type === 'fn'" class="" style="padding:16px;"> + <ui-textarea v-model="slots"></ui-textarea> + <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> + </section> + <section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> + <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i]" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/> + </section> + <section v-else class="" style="padding:16px;"> + <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import XContainer from './page-editor.container.vue'; +import { faSuperscript, faPencilAlt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons'; +import { AiScript } from '../../../scripts/aiscript'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + inject: ['getScriptBlockList'], + + props: { + getExpectedType: { + required: false, + default: null + }, + value: { + required: true + }, + title: { + required: false + }, + removable: { + required: false, + default: false + }, + aiScript: { + required: true, + }, + name: { + required: true, + }, + fnSlots: { + required: false, + }, + }, + + data() { + return { + AiScript, + error: null, + warn: null, + slots: '', + faSuperscript, faPencilAlt, faSquareRootAlt + }; + }, + + computed: { + icon(): any { + if (this.value.type === null) return null; + if (this.value.type.startsWith('fn:')) return null; + return AiScript.blockDefs.find(x => x.type === this.value.type).icon; + }, + typeText(): any { + if (this.value.type === null) return null; + return this.$t(`script.blocks.${this.value.type}`); + }, + }, + + watch: { + slots() { + this.value.value.slots = this.slots.split('\n'); + } + }, + + beforeCreate() { + this.$options.components.XV = require('./page-editor.script-block.vue').default; + }, + + created() { + if (this.value.value == null) Vue.set(this.value, 'value', null); + + if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.join('\n'); + + this.$watch('value.type', (t) => { + this.warn = null; + + if (this.value.type === 'fn') { + const id = uuid.v4(); + this.value.value = {}; + Vue.set(this.value.value, 'slots', []); + Vue.set(this.value.value, 'expression', { id, type: null }); + return; + } + + if (this.value.type && this.value.type.startsWith('fn:')) { + const fnName = this.value.type.split(':')[1]; + const fn = this.aiScript.getVarByName(fnName); + + const empties = []; + for (let i = 0; i < fn.value.slots.length; i++) { + const id = uuid.v4(); + empties.push({ id, type: null }); + } + Vue.set(this.value, 'args', empties); + return; + } + + if (AiScript.isLiteralBlock(this.value)) return; + + const empties = []; + for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) { + const id = uuid.v4(); + empties.push({ id, type: null }); + } + Vue.set(this.value, 'args', empties); + + for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) { + const inType = AiScript.funcDefs[this.value.type].in[i]; + if (typeof inType !== 'number') { + if (inType === 'number') this.value.args[i].type = 'number'; + if (inType === 'string') this.value.args[i].type = 'text'; + } + } + }); + + this.$watch('value.args', (args) => { + if (args == null) { + this.warn = null; + return; + } + const emptySlotIndex = args.findIndex(x => x.type === null); + if (emptySlotIndex !== -1 && emptySlotIndex < args.length) { + this.warn = { + slot: emptySlotIndex + }; + } else { + this.warn = null; + } + }, { + deep: true + }); + + this.$watch('aiScript.variables', () => { + if (this.type != null && this.value) { + this.error = this.aiScript.typeCheck(this.value); + } + }, { + deep: true + }); + }, + + methods: { + async changeType() { + const { canceled, result: type } = await this.$root.dialog({ + type: null, + title: this.$t('select-type'), + select: { + groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null) + }, + showCancelButton: true + }); + if (canceled) return; + this.value.type = type; + }, + + _getExpectedType(slot: number) { + return this.aiScript.getExpectedType(this.value, slot); + } + } +}); +</script> + +<style lang="stylus" scoped> +.turmquns + opacity 0.7 + +.pbglfege + opacity 0.5 + padding 16px + text-align center + cursor pointer + color var(--text) + +.tbwccoaw + > input + > textarea + display block + -webkit-appearance none + -moz-appearance none + appearance none + width 100% + max-width 100% + min-width 100% + border none + box-shadow none + padding 16px + font-size 16px + background transparent + color var(--text) + + > textarea + min-height 100px + +.hpdwcrvs + padding 16px + + > select + display block + padding 4px + font-size 16px + width 100% + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.section.vue b/src/client/app/common/views/components/page-editor/page-editor.section.vue new file mode 100644 index 0000000000..d7a247b0b1 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.section.vue @@ -0,0 +1,133 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faStickyNote"/> {{ value.title }}</template> + <template #func> + <button @click="rename()"> + <fa :icon="faPencilAlt"/> + </button> + <button @click="add()"> + <fa :icon="faPlus"/> + </button> + </template> + + <section class="ilrvjyvi"> + <div class="children"> + <x-block v-for="child in value.children" :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/> + </div> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; +import XContainer from './page-editor.container.vue'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faStickyNote, faPlus, faPencilAlt + }; + }, + + beforeCreate() { + this.$options.components.XBlock = require('./page-editor.block.vue').default + }, + + created() { + if (this.value.title == null) Vue.set(this.value, 'title', null); + if (this.value.children == null) Vue.set(this.value, 'children', []); + }, + + mounted() { + if (this.value.title == null) { + this.rename(); + } + }, + + methods: { + async rename() { + const { canceled, result: title } = await this.$root.dialog({ + title: 'Enter title', + input: { + type: 'text', + default: this.value.title + }, + showCancelButton: true + }); + if (canceled) return; + this.value.title = title; + }, + + async add() { + const { canceled, result: type } = await this.$root.dialog({ + type: null, + title: this.$t('choose-block'), + select: { + items: [{ + value: 'section', text: this.$t('blocks.section') + }, { + value: 'text', text: this.$t('blocks.text') + }, { + value: 'image', text: this.$t('blocks.image') + }, { + value: 'button', text: this.$t('blocks.button') + }, { + value: 'input', text: this.$t('blocks.input') + }, { + value: 'switch', text: this.$t('blocks.switch') + }] + }, + showCancelButton: true + }); + if (canceled) return; + + const id = uuid.v4(); + this.value.children.push({ id, type }); + }, + + updateItem(v) { + const i = this.value.children.findIndex(x => x.id === v.id); + const newValue = [ + ...this.value.children.slice(0, i), + v, + ...this.value.children.slice(i + 1) + ]; + this.value.children = newValue; + this.$emit('input', this.value); + }, + + remove(el) { + const i = this.value.children.findIndex(x => x.id === el.id); + const newValue = [ + ...this.value.children.slice(0, i), + ...this.value.children.slice(i + 1) + ]; + this.value.children = newValue; + this.$emit('input', this.value); + } + } +}); +</script> + +<style lang="stylus" scoped> +.ilrvjyvi + > .children + padding 16px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.switch.vue b/src/client/app/common/views/components/page-editor/page-editor.switch.vue new file mode 100644 index 0000000000..a9cfa2844f --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.switch.vue @@ -0,0 +1,48 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template> + + <section class="kjuadyyj"> + <ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input> + <ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input> + <ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons'; +import XContainer from './page-editor.container.vue'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faBolt, faSquareRootAlt + }; + }, + + created() { + if (this.value.name == null) Vue.set(this.value, 'name', ''); + }, +}); +</script> + +<style lang="stylus" scoped> +.kjuadyyj + padding 0 16px 16px 16px + +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.text.vue b/src/client/app/common/views/components/page-editor/page-editor.text.vue new file mode 100644 index 0000000000..7368931b2f --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.text.vue @@ -0,0 +1,57 @@ +<template> +<x-container @remove="() => $emit('remove')"> + <template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template> + + <section class="ihymsbbe"> + <textarea v-model="value.text"></textarea> + </section> +</x-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faAlignLeft } from '@fortawesome/free-solid-svg-icons'; +import XContainer from './page-editor.container.vue'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XContainer + }, + + props: { + value: { + required: true + }, + }, + + data() { + return { + faAlignLeft, + }; + }, + + created() { + if (this.value.text == null) Vue.set(this.value, 'text', ''); + }, +}); +</script> + +<style lang="stylus" scoped> +.ihymsbbe + > textarea + display block + -webkit-appearance none + -moz-appearance none + appearance none + width 100% + min-width 100% + min-height 150px + border none + box-shadow none + padding 16px + background transparent + color var(--text) +</style> diff --git a/src/client/app/common/views/components/page-editor/page-editor.vue b/src/client/app/common/views/components/page-editor/page-editor.vue new file mode 100644 index 0000000000..1bcaaa0330 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.vue @@ -0,0 +1,452 @@ +<template> +<div> + <div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> + <header> + <div class="title"><fa :icon="faStickyNote"/> {{ pageId ? $t('edit-page') : $t('new-page') }}</div> + <div class="buttons"> + <button @click="del()"><fa :icon="faTrashAlt"/></button> + <button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button> + <button @click="save()"><fa :icon="faSave"/></button> + </div> + </header> + + <section> + <ui-input v-model="title"> + <span>{{ $t('title') }}</span> + </ui-input> + + <template v-if="showOptions"> + <ui-input v-model="summary"> + <span>{{ $t('summary') }}</span> + </ui-input> + + <ui-input v-model="name"> + <template #prefix>{{ url }}/@{{ $store.state.i.username }}/pages/</template> + <span>{{ $t('url') }}</span> + </ui-input> + + <ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch> + + <ui-select v-model="font"> + <template #label>{{ $t('font') }}</template> + <option value="serif">{{ $t('fontSerif') }}</option> + <option value="sans-serif">{{ $t('fontSansSerif') }}</option> + </ui-select> + + <div class="eyeCatch"> + <ui-button v-if="eyeCatchingImageId == null" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catchig-image') }}</ui-button> + <div v-else-if="eyeCatchingImage"> + <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/> + <ui-button @click="removeEyeCatchingImage()"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catchig-image') }}</ui-button> + </div> + </div> + </template> + + <div class="content" v-for="child in content"> + <x-block :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/> + </div> + + <ui-button @click="add()"><fa :icon="faPlus"/></ui-button> + </section> + </div> + + <ui-container :body-togglable="true"> + <template #header><fa :icon="faSquareRootAlt"/> {{ $t('variables') }}</template> + <div class="qmuvgica"> + <div class="variables" v-show="variables.length > 0"> + <template v-for="variable in variables"> + <x-variable + :value="variable" + :removable="true" + @input="v => updateVariable(v)" + @remove="() => removeVariable(variable)" + :key="variable.name" + :ai-script="aiScript" + :name="variable.name" + :title="variable.name" + /> + </template> + </div> + + <ui-button @click="addVariable()" class="add"><fa :icon="faPlus"/></ui-button> + + <ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info> + + <template v-if="moreDetails"> + <ui-info><span v-html="$t('variables-info2')"></span></ui-info> + <ui-info><span v-html="$t('variables-info3')"></span></ui-info> + </template> + </div> + </ui-container> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../../i18n'; +import { faICursor, faPlus, faSquareRootAlt, faCog } from '@fortawesome/free-solid-svg-icons'; +import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; +import XVariable from './page-editor.script-block.vue'; +import XBlock from './page-editor.block.vue'; +import * as uuid from 'uuid'; +import { AiScript } from '../../../scripts/aiscript'; +import { url } from '../../../../config'; +import { collectPageVars } from '../../../scripts/collect-page-vars'; + +export default Vue.extend({ + i18n: i18n('pages'), + + components: { + XVariable, XBlock + }, + + props: { + page: { + type: String, + required: false + } + }, + + data() { + return { + pageId: null, + title: '', + summary: null, + name: Date.now().toString(), + eyeCatchingImage: null, + eyeCatchingImageId: null, + font: 'sans-serif', + content: [], + alignCenter: false, + variables: [], + aiScript: null, + showOptions: false, + moreDetails: false, + url, + faPlus, faICursor, faSave, faStickyNote, faSquareRootAlt, faCog, faTrashAlt + }; + }, + + watch: { + async eyeCatchingImageId() { + if (this.eyeCatchingImageId == null) { + this.eyeCatchingImage = null; + } else { + this.eyeCatchingImage = await this.$root.api('drive/files/show', { + fileId: this.eyeCatchingImageId, + }); + } + }, + }, + + created() { + this.aiScript = new AiScript(); + + this.$watch('variables', () => { + this.aiScript.injectVars(this.variables); + }, { deep: true }); + + this.$watch('content', () => { + this.aiScript.injectPageVars(collectPageVars(this.content)); + }, { deep: true }); + + if (this.page) { + this.$root.api('pages/show', { + pageId: this.page, + }).then(page => { + this.pageId = page.id; + this.title = page.title; + this.name = page.name; + this.summary = page.summary; + this.font = page.font; + this.alignCenter = page.alignCenter; + this.content = page.content; + this.variables = page.variables; + this.eyeCatchingImageId = page.eyeCatchingImageId; + }); + } else { + const id = uuid.v4(); + this.content = [{ + id, + type: 'text', + text: 'Hello World!' + }]; + } + }, + + provide() { + return { + getScriptBlockList: this.getScriptBlockList + } + }, + + methods: { + save() { + if (this.pageId) { + this.$root.api('pages/update', { + pageId: this.pageId, + title: this.title.trim(), + name: this.name.trim(), + summary: this.summary, + font: this.font, + alignCenter: this.alignCenter, + content: this.content, + variables: this.variables, + eyeCatchingImageId: this.eyeCatchingImageId, + }).then(page => { + this.$root.dialog({ + type: 'success', + text: this.$t('page-updated') + }); + }); + } else { + this.$root.api('pages/create', { + title: this.title.trim(), + name: this.name.trim(), + summary: this.summary, + font: this.font, + alignCenter: this.alignCenter, + content: this.content, + variables: this.variables, + eyeCatchingImageId: this.eyeCatchingImageId, + }).then(page => { + this.pageId = page.id; + this.$root.dialog({ + type: 'success', + text: this.$t('page-created') + }); + this.$router.push(`/i/pages/edit/${this.pageId}`); + }); + } + }, + + del() { + this.$root.dialog({ + type: 'warning', + text: this.$t('are-you-sure-delete'), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + this.$root.api('pages/delete', { + pageId: this.pageId, + }).then(() => { + this.$root.dialog({ + type: 'success', + text: this.$t('page-deleted') + }); + this.$router.push(`/i/pages`); + }); + }); + }, + + async add() { + const { canceled, result: type } = await this.$root.dialog({ + type: null, + title: this.$t('choose-block'), + select: { + items: [{ + value: 'section', text: this.$t('blocks.section') + }, { + value: 'text', text: this.$t('blocks.text') + }, { + value: 'image', text: this.$t('blocks.image') + }, { + value: 'button', text: this.$t('blocks.button') + }, { + value: 'input', text: this.$t('blocks.input') + }, { + value: 'switch', text: this.$t('blocks.switch') + }] + }, + showCancelButton: true + }); + if (canceled) return; + + const id = uuid.v4(); + this.content.push({ id, type }); + }, + + async addVariable() { + let { canceled, result: name } = await this.$root.dialog({ + title: this.$t('enter-variable-name'), + input: { + type: 'text', + }, + showCancelButton: true + }); + if (canceled) return; + + name = name.trim(); + + if (this.aiScript.isUsedName(name)) { + this.$root.dialog({ + type: 'error', + text: this.$t('the-variable-name-is-already-used') + }); + return; + } + + const id = uuid.v4(); + this.variables.push({ id, name, type: null }); + }, + + updateItem(v) { + const i = this.content.findIndex(x => x.id === v.id); + const newValue = [ + ...this.content.slice(0, i), + v, + ...this.content.slice(i + 1) + ]; + this.content = newValue; + }, + + remove(el) { + const i = this.content.findIndex(x => x.id === el.id); + const newValue = [ + ...this.content.slice(0, i), + ...this.content.slice(i + 1) + ]; + this.content = newValue; + }, + + removeVariable(v) { + const i = this.variables.findIndex(x => x.name === v.name); + const newValue = [ + ...this.variables.slice(0, i), + ...this.variables.slice(i + 1) + ]; + this.variables = newValue; + }, + + getScriptBlockList(type: string = null) { + const list = []; + + const blocks = AiScript.blockDefs.filter(block => type === null || block.out === null || block.out === type); + + for (const block of blocks) { + const category = list.find(x => x.category === block.category); + if (category) { + category.items.push({ + value: block.type, + text: this.$t(`script.blocks.${block.type}`) + }); + } else { + list.push({ + category: block.category, + label: this.$t(`script.categories.${block.category}`), + items: [{ + value: block.type, + text: this.$t(`script.blocks.${block.type}`) + }] + }); + } + } + + const userFns = this.variables.filter(x => x.type === 'fn'); + if (userFns.length > 0) { + list.unshift({ + label: this.$t(`script.categories.fn`), + items: userFns.map(v => ({ + value: 'fn:' + v.name, + text: v.name + })) + }); + } + + return list; + }, + + setEyeCatchingImage() { + this.$chooseDriveFile({ + multiple: false + }).then(file => { + this.eyeCatchingImageId = file.id; + }); + }, + + removeEyeCatchingImage() { + this.eyeCatchingImageId = null; + } + } +}); +</script> + +<style lang="stylus" scoped> +.gwbmwxkm + overflow hidden + background var(--face) + margin-bottom 16px + + &.round + border-radius 6px + + &.shadow + box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) + + > header + background var(--faceHeader) + + > .title + z-index 1 + margin 0 + padding 0 16px + line-height 42px + font-size 0.9em + font-weight bold + color var(--faceHeaderText) + box-shadow 0 var(--lineWidth) rgba(#000, 0.07) + + > [data-icon] + margin-right 6px + + &:empty + display none + + > .buttons + position absolute + z-index 2 + top 0 + right 0 + + > button + padding 0 + width 42px + font-size 0.9em + line-height 42px + color var(--faceTextButton) + + &:hover + color var(--faceTextButtonHover) + + &:active + color var(--faceTextButtonActive) + + > section + padding 0 32px 32px 32px + + @media (max-width 500px) + padding 0 16px 16px 16px + + > .content + margin-bottom 16px + + > .eyeCatch + margin-bottom 16px + + > div + > img + max-width 100% + +.qmuvgica + padding 32px + + @media (max-width 500px) + padding 16px + + > .variables + margin-bottom 16px + + > .add + margin-bottom 16px + +</style> diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue new file mode 100644 index 0000000000..d8fdbf4b04 --- /dev/null +++ b/src/client/app/common/views/components/page-preview.vue @@ -0,0 +1,141 @@ +<template> +<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> + <div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div> + <article> + <header> + <h1 :title="page.title">{{ page.title }}</h1> + </header> + <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> + <footer> + <img class="icon" :src="page.user.avatarUrl"/> + <p>{{ page.user | userName }}</p> + </footer> + </article> +</router-link> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + page: { + type: Object, + required: true + }, + }, +}); +</script> + +<style lang="stylus" scoped> +.vhpxefrj + display block + overflow hidden + width 100% + background var(--face) + + &.round + border-radius 8px + + &.shadow + box-shadow 0 4px 16px rgba(#000, 0.1) + + @media (min-width 500px) + box-shadow 0 8px 32px rgba(#000, 0.1) + + > .thumbnail + position absolute + width 100px + height 100% + background-position center + background-size cover + display flex + justify-content center + align-items center + + > button + font-size 3.5em + opacity: 0.7 + + &:hover + font-size 4em + opacity 0.9 + + & + article + left 100px + width calc(100% - 100px) + + > article + padding 16px + + > header + margin-bottom 8px + + > h1 + margin 0 + font-size 1em + color var(--urlPreviewTitle) + + > p + margin 0 + color var(--urlPreviewText) + font-size 0.8em + + > footer + margin-top 8px + height 16px + + > img + display inline-block + width 16px + height 16px + margin-right 4px + vertical-align top + + > p + display inline-block + margin 0 + color var(--urlPreviewInfo) + font-size 0.8em + line-height 16px + vertical-align top + + @media (max-width 700px) + > .thumbnail + position relative + width 100% + height 100px + + & + article + left 0 + width 100% + + @media (max-width 550px) + font-size 12px + + > .thumbnail + height 80px + + > article + padding 12px + + @media (max-width 500px) + font-size 10px + + > .thumbnail + height 70px + + > article + padding 8px + + > header + margin-bottom 4px + + > footer + margin-top 4px + + > img + width 12px + height 12px + +</style> |