Improve MisskeyPages

This commit is contained in:
syuilo 2019-04-30 12:15:41 +09:00
parent e1effe7eda
commit a96aa07688
21 changed files with 520 additions and 93 deletions

View file

@ -1867,28 +1867,50 @@ pages:
select-type: "種類を選択" select-type: "種類を選択"
enter-variable-name: "変数名を決めてください" enter-variable-name: "変数名を決めてください"
the-variable-name-is-already-used: "その変数名は既に使われています" the-variable-name-is-already-used: "その変数名は既に使われています"
content-blocks: "コンテンツ"
input-blocks: "入力"
special-blocks: "特殊"
post-from-post-form: "この内容を投稿"
posted-from-post-form: "投稿しました"
blocks: blocks:
text: "テキスト" text: "テキスト"
textarea: "テキストエリア"
section: "セクション" section: "セクション"
image: "画像" image: "画像"
button: "ボタン" button: "ボタン"
if: "もし" if: "もし"
_if: _if:
variable: "変数" variable: "変数"
input: "ユーザー入力"
_input: post: "投稿フォーム"
_post:
text: "内容"
textInput: "テキスト入力"
_textInput:
name: "変数名" name: "変数名"
text: "タイトル" text: "タイトル"
default: "デフォルト値" default: "デフォルト値"
inputType: "入力の種類"
_inputType: textareaInput: "複数行テキスト入力"
string: "テキスト" _textareaInput:
number: "数値" name: "変数名"
text: "タイトル"
default: "デフォルト値"
numberInput: "数値入力"
_numberInput:
name: "変数名"
text: "タイトル"
default: "デフォルト値"
switch: "スイッチ" switch: "スイッチ"
_switch: _switch:
name: "変数名" name: "変数名"
text: "タイトル" text: "タイトル"
default: "デフォルト値" default: "デフォルト値"
_button: _button:
text: "タイトル" text: "タイトル"
action: "ボタンを押したときの動作" action: "ボタンを押したときの動作"
@ -1897,6 +1919,7 @@ pages:
_dialog: _dialog:
content: "内容" content: "内容"
resetRandom: "乱数をリセット" resetRandom: "乱数をリセット"
script: script:
categories: categories:
flow: "制御" flow: "制御"

View file

@ -97,6 +97,7 @@ type PageVar = { name: string; value: any; type: Type; };
const envVarsDef = { const envVarsDef = {
AI: 'string', AI: 'string',
URL: 'string',
VERSION: 'string', VERSION: 'string',
LOGIN: 'boolean', LOGIN: 'boolean',
NAME: 'string', NAME: 'string',
@ -120,7 +121,7 @@ export class AiScript {
public static blockDefs = blockDefs; public static blockDefs = blockDefs;
public static funcDefs = funcDefs; public static funcDefs = funcDefs;
private opts: { private opts: {
randomSeed?: string; user?: any; visitor?: any; randomSeed?: string; user?: any; visitor?: any; page?: any; url?: string;
}; };
constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) { constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) {
@ -131,6 +132,7 @@ export class AiScript {
this.envVars = { this.envVars = {
AI: 'kawaii', AI: 'kawaii',
VERSION: version, VERSION: version,
URL: opts.page ? `${opts.url}/@${opts.page.user.username}/pages/${opts.page.name}` : '',
LOGIN: opts.visitor != null, LOGIN: opts.visitor != null,
NAME: opts.visitor ? opts.visitor.name : '', NAME: opts.visitor ? opts.visitor.name : '',
USERNAME: opts.visitor ? opts.visitor.username : '', USERNAME: opts.visitor ? opts.visitor.username : '',

View file

@ -2,10 +2,22 @@ export function collectPageVars(content) {
const pageVars = []; const pageVars = [];
const collect = (xs: any[]) => { const collect = (xs: any[]) => {
for (const x of xs) { for (const x of xs) {
if (x.type === 'input') { if (x.type === 'textInput') {
pageVars.push({ pageVars.push({
name: x.name, name: x.name,
type: x.inputType, type: 'string',
value: x.default
});
} else if (x.type === 'textareaInput') {
pageVars.push({
name: x.name,
type: 'string',
value: x.default
});
} else if (x.type === 'numberInput') {
pageVars.push({
name: x.name,
type: 'number',
value: x.default value: x.default
}); });
} else if (x.type === 'switch') { } else if (x.type === 'switch') {

View file

@ -72,7 +72,7 @@ export default Vue.extend({
type: null, type: null,
title: this.$t('choose-block'), title: this.$t('choose-block'),
select: { select: {
items: this.getPageBlockList() groupedItems: this.getPageBlockList()
}, },
showCancelButton: true showCancelButton: true
}); });

View file

@ -1,61 +0,0 @@
<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 { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
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');
this.$watch('value.inputType', t => {
if (this.value.default != null) {
if (t === 'number') this.value.default = parseInt(this.value.default, 10);
if (t === 'string') this.value.default = this.value.default.toString();
}
});
},
});
</script>
<style lang="stylus" scoped>
.dnvasjon
padding 0 16px 0 16px
</style>

View file

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.numberInput') }}</template>
<section style="padding: 0 16px 0 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._numberInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._numberInput.text') }}</span></ui-input>
<ui-input v-model="value.default" type="number"><span>{{ $t('blocks._numberInput.default') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
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>

View file

@ -0,0 +1,44 @@
<template>
<x-container @remove="() => $emit('remove')">
<template #header><fa :icon="faPaperPlane"/> {{ $t('blocks.post') }}</template>
<section>
<textarea v-model="value.text"></textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../../../i18n';
import XContainer from '../page-editor.container.vue';
export default Vue.extend({
i18n: i18n('pages'),
components: {
XContainer
},
props: {
value: {
required: true
},
},
data() {
return {
faPaperPlane
};
},
beforeCreate() {
this.$options.components.XBlock = require('../page-editor.block.vue').default
},
created() {
if (this.value.text == null) Vue.set(this.value, 'text', '');
},
});
</script>

View file

@ -84,7 +84,7 @@ export default Vue.extend({
type: null, type: null,
title: this.$t('choose-block'), title: this.$t('choose-block'),
select: { select: {
items: this.getPageBlockList() groupedItems: this.getPageBlockList()
}, },
showCancelButton: true showCancelButton: true
}); });

View file

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.textInput') }}</template>
<section style="padding: 0 16px 0 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._textInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._textInput.text') }}</span></ui-input>
<ui-input v-model="value.default" type="text"><span>{{ $t('blocks._textInput.default') }}</span></ui-input>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
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>

View file

@ -0,0 +1,42 @@
<template>
<x-container @remove="() => $emit('remove')">
<template #header><fa :icon="faBolt"/> {{ $t('blocks.textareaInput') }}</template>
<section style="padding: 0 16px 16px 16px;">
<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._textareaInput.name') }}</span></ui-input>
<ui-input v-model="value.text"><span>{{ $t('blocks._textareaInput.text') }}</span></ui-input>
<ui-textarea v-model="value.default"><span>{{ $t('blocks._textareaInput.default') }}</span></ui-textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
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>

View file

@ -0,0 +1,58 @@
<template>
<x-container @remove="() => $emit('remove')">
<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.textarea') }}</template>
<section class="ihymsbbe">
<textarea v-model="value.text"></textarea>
</section>
</x-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../../../../i18n';
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)
font-size 14px
</style>

View file

@ -6,15 +6,19 @@
import Vue from 'vue'; import Vue from 'vue';
import XSection from './els/page-editor.el.section.vue'; import XSection from './els/page-editor.el.section.vue';
import XText from './els/page-editor.el.text.vue'; import XText from './els/page-editor.el.text.vue';
import XTextarea from './els/page-editor.el.textarea.vue';
import XImage from './els/page-editor.el.image.vue'; import XImage from './els/page-editor.el.image.vue';
import XButton from './els/page-editor.el.button.vue'; import XButton from './els/page-editor.el.button.vue';
import XInput from './els/page-editor.el.input.vue'; import XTextInput from './els/page-editor.el.text-input.vue';
import XTextareaInput from './els/page-editor.el.textarea-input.vue';
import XNumberInput from './els/page-editor.el.text-input.vue';
import XSwitch from './els/page-editor.el.switch.vue'; import XSwitch from './els/page-editor.el.switch.vue';
import XIf from './els/page-editor.el.if.vue'; import XIf from './els/page-editor.el.if.vue';
import XPost from './els/page-editor.el.post.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XSection, XText, XImage, XButton, XInput, XSwitch, XIf XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost
}, },
props: { props: {

View file

@ -259,7 +259,7 @@ export default Vue.extend({
type: null, type: null,
title: this.$t('choose-block'), title: this.$t('choose-block'),
select: { select: {
items: this.getPageBlockList() groupedItems: this.getPageBlockList()
}, },
showCancelButton: true showCancelButton: true
}); });
@ -323,19 +323,28 @@ export default Vue.extend({
getPageBlockList() { getPageBlockList() {
return [{ return [{
value: 'section', text: this.$t('blocks.section') label: this.$t('content-blocks'),
items: [
{ value: 'section', text: this.$t('blocks.section') },
{ value: 'text', text: this.$t('blocks.text') },
{ value: 'image', text: this.$t('blocks.image') },
{ value: 'textarea', text: this.$t('blocks.textarea') },
]
}, { }, {
value: 'text', text: this.$t('blocks.text') label: this.$t('input-blocks'),
items: [
{ value: 'button', text: this.$t('blocks.button') },
{ value: 'textInput', text: this.$t('blocks.textInput') },
{ value: 'textareaInput', text: this.$t('blocks.textareaInput') },
{ value: 'numberInput', text: this.$t('blocks.numberInput') },
{ value: 'switch', text: this.$t('blocks.switch') }
]
}, { }, {
value: 'image', text: this.$t('blocks.image') label: this.$t('special-blocks'),
}, { items: [
value: 'button', text: this.$t('blocks.button') { value: 'if', text: this.$t('blocks.if') },
}, { { value: 'post', text: this.$t('blocks.post') }
value: 'input', text: this.$t('blocks.input') ]
}, {
value: 'switch', text: this.$t('blocks.switch')
}, {
value: 'if', text: this.$t('blocks.if')
}]; }];
}, },

View file

@ -8,13 +8,17 @@ import XText from './page.text.vue';
import XSection from './page.section.vue'; import XSection from './page.section.vue';
import XImage from './page.image.vue'; import XImage from './page.image.vue';
import XButton from './page.button.vue'; import XButton from './page.button.vue';
import XInput from './page.input.vue'; import XNumberInput from './page.number-input.vue';
import XTextInput from './page.text-input.vue';
import XTextareaInput from './page.textarea-input.vue';
import XSwitch from './page.switch.vue'; import XSwitch from './page.switch.vue';
import XIf from './page.if.vue'; import XIf from './page.if.vue';
import XTextarea from './page.textarea.vue';
import XPost from './page.post.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XText, XSection, XImage, XButton, XInput, XSwitch, XIf XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf
}, },
props: { props: {

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<ui-input class="kudkigyw" v-model="v" :type="value.inputType">{{ script.interpolate(value.text) }}</ui-input> <ui-input class="kudkigyw" v-model="v" type="number">{{ script.interpolate(value.text) }}</ui-input>
</div> </div>
</template> </template>
@ -25,9 +25,7 @@ export default Vue.extend({
watch: { watch: {
v() { v() {
let v = this.v; this.script.aiScript.updatePageVar(this.value.name, this.v);
if (this.value.inputType === 'number') v = parseInt(v, 10);
this.script.aiScript.updatePageVar(this.value.name, v);
this.script.reEval(); this.script.reEval();
} }
} }

View file

@ -0,0 +1,68 @@
<template>
<div class="ngbfujlo">
<ui-textarea class="textarea" :value="text" readonly></ui-textarea>
<ui-button primary @click="post()" :disabled="posting || posted">{{ posted ? $t('posted-from-post-form') : $t('post-from-post-form') }}</ui-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
export default Vue.extend({
i18n: i18n('pages'),
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
text: this.script.interpolate(this.value.text),
posted: false,
posting: false,
};
},
created() {
this.$watch('script.vars', () => {
this.text = this.script.interpolate(this.value.text);
}, { deep: true });
},
methods: {
post() {
this.posting = true;
this.$root.api('notes/create', {
text: this.text,
}).then(() => {
this.posted = true;
this.$root.dialog({
type: 'success',
splash: true
});
});
}
}
});
</script>
<style lang="stylus" scoped>
.ngbfujlo
padding 0 32px 32px 32px
border solid 2px var(--pageBlockBorder)
border-radius 6px
@media (max-width 600px)
padding 0 16px 16px 16px
> .textarea
margin-top 16px
margin-bottom 16px
</style>

View file

@ -0,0 +1,41 @@
<template>
<div>
<ui-input class="kudkigyw" v-model="v" type="text">{{ script.interpolate(value.text) }}</ui-input>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.reEval();
}
}
});
</script>
<style lang="stylus" scoped>
.kudkigyw
display inline-block
min-width 300px
max-width 450px
margin 8px 0
</style>

View file

@ -0,0 +1,36 @@
<template>
<div>
<ui-textarea class="" v-model="v">{{ script.interpolate(value.text) }}</ui-textarea>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
v: this.value.default,
};
},
watch: {
v() {
this.script.aiScript.updatePageVar(this.value.name, this.v);
this.script.reEval();
}
}
});
</script>
<style lang="stylus" scoped>
</style>

View file

@ -0,0 +1,33 @@
<template>
<ui-textarea class="" :value="text" readonly></ui-textarea>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {
required: true
},
script: {
required: true
}
},
data() {
return {
text: this.script.interpolate(this.value.text),
};
},
created() {
this.$watch('script.vars', () => {
this.text = this.script.interpolate(this.value.text);
}, { deep: true });
}
});
</script>
<style lang="stylus" scoped>
</style>

View file

@ -23,6 +23,7 @@ import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue'; import XBlock from './page.block.vue';
import { AiScript } from '../../../scripts/aiscript'; import { AiScript } from '../../../scripts/aiscript';
import { collectPageVars } from '../../../scripts/collect-page-vars'; import { collectPageVars } from '../../../scripts/collect-page-vars';
import { url } from '../../../../config';
class Script { class Script {
public aiScript: AiScript; public aiScript: AiScript;
@ -82,7 +83,9 @@ export default Vue.extend({
this.script = new Script(new AiScript(this.page.variables, pageVars, { this.script = new Script(new AiScript(this.page.variables, pageVars, {
randomSeed: Math.random(), randomSeed: Math.random(),
user: page.user, user: page.user,
visitor: this.$store.state.i visitor: this.$store.state.i,
page: page,
url: url
})); }));
}); });
}, },

View file

@ -27,6 +27,33 @@ export class PageRepository extends Repository<Page> {
} }
}; };
collectFile(src.content); collectFile(src.content);
// 後方互換性のため
let migrated = false;
const migrate = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'input') {
if (x.inputType === 'text') {
x.type = 'textInput';
}
if (x.inputType === 'number') {
x.type = 'numberInput';
if (x.default) x.default = parseInt(x.default, 10);
}
migrated = true;
}
if (x.children) {
migrate(x.children);
}
}
};
migrate(src.content);
if (migrated) {
this.update(src.id, {
content: src.content
});
}
return await awaitAll({ return await awaitAll({
id: src.id, id: src.id,
createdAt: src.createdAt.toISOString(), createdAt: src.createdAt.toISOString(),