summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/page
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components/page')
-rw-r--r--packages/frontend/src/components/page/page.block.vue44
-rw-r--r--packages/frontend/src/components/page/page.button.vue66
-rw-r--r--packages/frontend/src/components/page/page.canvas.vue49
-rw-r--r--packages/frontend/src/components/page/page.counter.vue52
-rw-r--r--packages/frontend/src/components/page/page.if.vue31
-rw-r--r--packages/frontend/src/components/page/page.image.vue28
-rw-r--r--packages/frontend/src/components/page/page.note.vue47
-rw-r--r--packages/frontend/src/components/page/page.number-input.vue55
-rw-r--r--packages/frontend/src/components/page/page.post.vue109
-rw-r--r--packages/frontend/src/components/page/page.radio-button.vue45
-rw-r--r--packages/frontend/src/components/page/page.section.vue60
-rw-r--r--packages/frontend/src/components/page/page.switch.vue55
-rw-r--r--packages/frontend/src/components/page/page.text-input.vue55
-rw-r--r--packages/frontend/src/components/page/page.text.vue68
-rw-r--r--packages/frontend/src/components/page/page.textarea-input.vue47
-rw-r--r--packages/frontend/src/components/page/page.textarea.vue39
-rw-r--r--packages/frontend/src/components/page/page.vue85
17 files changed, 935 insertions, 0 deletions
diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue
new file mode 100644
index 0000000000..f3e7764604
--- /dev/null
+++ b/packages/frontend/src/components/page/page.block.vue
@@ -0,0 +1,44 @@
+<template>
+<component :is="'x-' + block.type" :key="block.id" :block="block" :hpml="hpml" :h="h"/>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import XText from './page.text.vue';
+import XSection from './page.section.vue';
+import XImage from './page.image.vue';
+import XButton from './page.button.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 XIf from './page.if.vue';
+import XTextarea from './page.textarea.vue';
+import XPost from './page.post.vue';
+import XCounter from './page.counter.vue';
+import XRadioButton from './page.radio-button.vue';
+import XCanvas from './page.canvas.vue';
+import XNote from './page.note.vue';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { Block } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote,
+ },
+ props: {
+ block: {
+ type: Object as PropType<Block>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ h: {
+ type: Number,
+ required: true,
+ },
+ },
+});
+</script>
diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue
new file mode 100644
index 0000000000..83931021d8
--- /dev/null
+++ b/packages/frontend/src/components/page/page.button.vue
@@ -0,0 +1,66 @@
+<template>
+<div>
+ <MkButton class="kudkigyw" :primary="block.primary" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, unref } from 'vue';
+import MkButton from '../MkButton.vue';
+import * as os from '@/os';
+import { ButtonBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ },
+ props: {
+ block: {
+ type: Object as PropType<ButtonBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ methods: {
+ click() {
+ if (this.block.action === 'dialog') {
+ this.hpml.eval();
+ os.alert({
+ text: this.hpml.interpolate(this.block.content),
+ });
+ } else if (this.block.action === 'resetRandom') {
+ this.hpml.updateRandomSeed(Math.random());
+ this.hpml.eval();
+ } else if (this.block.action === 'pushEvent') {
+ os.api('page-push', {
+ pageId: this.hpml.page.id,
+ event: this.block.event,
+ ...(this.block.var ? {
+ var: unref(this.hpml.vars)[this.block.var],
+ } : {}),
+ });
+
+ os.alert({
+ type: 'success',
+ text: this.hpml.interpolate(this.block.message),
+ });
+ } else if (this.block.action === 'callAiScript') {
+ this.hpml.callAiScript(this.block.fn);
+ }
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+ display: inline-block;
+ min-width: 200px;
+ max-width: 450px;
+ margin: 8px 0;
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue
new file mode 100644
index 0000000000..80f6c8339c
--- /dev/null
+++ b/packages/frontend/src/components/page/page.canvas.vue
@@ -0,0 +1,49 @@
+<template>
+<div class="ysrxegms">
+ <canvas ref="canvas" :width="block.width" :height="block.height"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+import * as os from '@/os';
+import { CanvasBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+ props: {
+ block: {
+ type: Object as PropType<CanvasBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const canvas: Ref<any> = ref(null);
+
+ onMounted(() => {
+ props.hpml.registerCanvas(props.block.name, canvas.value);
+ });
+
+ return {
+ canvas,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ysrxegms {
+ display: inline-block;
+ vertical-align: bottom;
+ overflow: auto;
+ max-width: 100%;
+
+ > canvas {
+ display: block;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue
new file mode 100644
index 0000000000..a9e1f41a54
--- /dev/null
+++ b/packages/frontend/src/components/page/page.counter.vue
@@ -0,0 +1,52 @@
+<template>
+<div>
+ <MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkButton from '../MkButton.vue';
+import * as os from '@/os';
+import { CounterVarBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ },
+ props: {
+ block: {
+ type: Object as PropType<CounterVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function click() {
+ props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
+ props.hpml.eval();
+ }
+
+ return {
+ click,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.llumlmnx {
+ display: inline-block;
+ min-width: 300px;
+ max-width: 450px;
+ margin: 8px 0;
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue
new file mode 100644
index 0000000000..372a15f0c6
--- /dev/null
+++ b/packages/frontend/src/components/page/page.if.vue
@@ -0,0 +1,31 @@
+<template>
+<div v-show="hpml.vars.value[block.var]">
+ <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h"/>
+</div>
+</template>
+
+<script lang="ts">
+import { IfBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineComponent, defineAsyncComponent, PropType } from 'vue';
+
+export default defineComponent({
+ components: {
+ XBlock: defineAsyncComponent(() => import('./page.block.vue')),
+ },
+ props: {
+ block: {
+ type: Object as PropType<IfBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ h: {
+ type: Number,
+ required: true,
+ },
+ },
+});
+</script>
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
new file mode 100644
index 0000000000..8ba70c5855
--- /dev/null
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -0,0 +1,28 @@
+<template>
+<div class="lzyxtsnt">
+ <ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { defineComponent, PropType } from 'vue';
+import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
+import * as os from '@/os';
+import { ImageBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+const props = defineProps<{
+ block: PropType<ImageBlock>,
+ hpml: PropType<Hpml>,
+}>();
+
+const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
+</script>
+
+<style lang="scss" scoped>
+.lzyxtsnt {
+ > img {
+ max-width: 100%;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
new file mode 100644
index 0000000000..7d5c484a1b
--- /dev/null
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -0,0 +1,47 @@
+<template>
+<div class="voxdxuby">
+ <XNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
+ <XNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
+import XNote from '@/components/MkNote.vue';
+import XNoteDetailed from '@/components/MkNoteDetailed.vue';
+import * as os from '@/os';
+import { NoteBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ XNote,
+ XNoteDetailed,
+ },
+ props: {
+ block: {
+ type: Object as PropType<NoteBlock>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const note: Ref<Record<string, any> | null> = ref(null);
+
+ onMounted(() => {
+ os.api('notes/show', { noteId: props.block.note })
+ .then(result => {
+ note.value = result;
+ });
+ });
+
+ return {
+ note,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.voxdxuby {
+ margin: 1em 0;
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue
new file mode 100644
index 0000000000..50cf6d0770
--- /dev/null
+++ b/packages/frontend/src/components/page/page.number-input.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+ <MkInput class="kudkigyw" :model-value="value" type="number" @update:model-value="updateValue($event)">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkInput>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkInput from '../form/input.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { NumberInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ MkInput,
+ },
+ props: {
+ block: {
+ type: Object as PropType<NumberInputVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function updateValue(newValue) {
+ props.hpml.updatePageVar(props.block.name, newValue);
+ props.hpml.eval();
+ }
+
+ return {
+ value,
+ updateValue,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+ display: inline-block;
+ min-width: 300px;
+ max-width: 450px;
+ margin: 8px 0;
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue
new file mode 100644
index 0000000000..0ef50d65cd
--- /dev/null
+++ b/packages/frontend/src/components/page/page.post.vue
@@ -0,0 +1,109 @@
+<template>
+<div class="ngbfujlo">
+ <MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
+ <MkButton class="button" primary :disabled="posting || posted" @click="post()">
+ <i v-if="posted" class="ti ti-check"></i>
+ <i v-else class="ti ti-send"></i>
+ </MkButton>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+import MkButton from '../MkButton.vue';
+import { apiUrl } from '@/config';
+import * as os from '@/os';
+import { PostBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+ components: {
+ MkTextarea,
+ MkButton,
+ },
+ props: {
+ block: {
+ type: Object as PropType<PostBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ text: this.hpml.interpolate(this.block.text),
+ posted: false,
+ posting: false,
+ };
+ },
+ watch: {
+ 'hpml.vars': {
+ handler() {
+ this.text = this.hpml.interpolate(this.block.text);
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ upload() {
+ const promise = new Promise((ok) => {
+ const canvas = this.hpml.canvases[this.block.canvasId];
+ canvas.toBlob(blob => {
+ const formData = new FormData();
+ formData.append('file', blob);
+ formData.append('i', this.$i.token);
+ if (this.$store.state.uploadFolder) {
+ formData.append('folderId', this.$store.state.uploadFolder);
+ }
+
+ window.fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: formData,
+ })
+ .then(response => response.json())
+ .then(f => {
+ ok(f);
+ });
+ });
+ });
+ os.promiseDialog(promise);
+ return promise;
+ },
+ async post() {
+ this.posting = true;
+ const file = this.block.attachCanvasImage ? await this.upload() : null;
+ os.apiWithDialog('notes/create', {
+ text: this.text === '' ? null : this.text,
+ fileIds: file ? [file.id] : undefined,
+ }).then(() => {
+ this.posted = true;
+ });
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.ngbfujlo {
+ position: relative;
+ padding: 32px;
+ border-radius: 6px;
+ box-shadow: 0 2px 8px var(--shadow);
+ z-index: 1;
+
+ > .button {
+ margin-top: 32px;
+ }
+
+ @media (max-width: 600px) {
+ padding: 16px;
+
+ > .button {
+ margin-top: 16px;
+ }
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue
new file mode 100644
index 0000000000..b4d9e01a54
--- /dev/null
+++ b/packages/frontend/src/components/page/page.radio-button.vue
@@ -0,0 +1,45 @@
+<template>
+<div>
+ <div>{{ hpml.interpolate(block.title) }}</div>
+ <MkRadio v-for="item in block.values" :key="item" :modelValue="value" :value="item" @update:model-value="updateValue($event)">{{ item }}</MkRadio>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkRadio from '../form/radio.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { RadioButtonVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ MkRadio,
+ },
+ props: {
+ block: {
+ type: Object as PropType<RadioButtonVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function updateValue(newValue: string) {
+ props.hpml.updatePageVar(props.block.name, newValue);
+ props.hpml.eval();
+ }
+
+ return {
+ value,
+ updateValue,
+ };
+ },
+});
+</script>
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
new file mode 100644
index 0000000000..630c1f5179
--- /dev/null
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -0,0 +1,60 @@
+<template>
+<section class="sdgxphyu">
+ <component :is="'h' + h">{{ block.title }}</component>
+
+ <div class="children">
+ <XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h + 1"/>
+ </div>
+</section>
+</template>
+
+<script lang="ts">
+import { defineComponent, defineAsyncComponent, PropType } from 'vue';
+import * as os from '@/os';
+import { SectionBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+
+export default defineComponent({
+ components: {
+ XBlock: defineAsyncComponent(() => import('./page.block.vue')),
+ },
+ props: {
+ block: {
+ type: Object as PropType<SectionBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ h: {
+ required: true,
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.sdgxphyu {
+ margin: 1.5em 0;
+
+ > h2 {
+ font-size: 1.35em;
+ margin: 0 0 0.5em 0;
+ }
+
+ > h3 {
+ font-size: 1em;
+ margin: 0 0 0.5em 0;
+ }
+
+ > h4 {
+ font-size: 1em;
+ margin: 0 0 0.5em 0;
+ }
+
+ > .children {
+ //padding 16px
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.switch.vue b/packages/frontend/src/components/page/page.switch.vue
new file mode 100644
index 0000000000..64dc4ff8aa
--- /dev/null
+++ b/packages/frontend/src/components/page/page.switch.vue
@@ -0,0 +1,55 @@
+<template>
+<div class="hkcxmtwj">
+ <MkSwitch :model-value="value" @update:model-value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkSwitch from '../form/switch.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { SwitchVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ MkSwitch,
+ },
+ props: {
+ block: {
+ type: Object as PropType<SwitchVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function updateValue(newValue: boolean) {
+ props.hpml.updatePageVar(props.block.name, newValue);
+ props.hpml.eval();
+ }
+
+ return {
+ value,
+ updateValue,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.hkcxmtwj {
+ display: inline-block;
+ margin: 16px auto;
+
+ & + .hkcxmtwj {
+ margin-left: 16px;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue
new file mode 100644
index 0000000000..840649ece6
--- /dev/null
+++ b/packages/frontend/src/components/page/page.text-input.vue
@@ -0,0 +1,55 @@
+<template>
+<div>
+ <MkInput class="kudkigyw" :model-value="value" type="text" @update:model-value="updateValue($event)">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkInput>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkInput from '../form/input.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { TextInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ MkInput,
+ },
+ props: {
+ block: {
+ type: Object as PropType<TextInputVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function updateValue(newValue) {
+ props.hpml.updatePageVar(props.block.name, newValue);
+ props.hpml.eval();
+ }
+
+ return {
+ value,
+ updateValue,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.kudkigyw {
+ display: inline-block;
+ min-width: 300px;
+ max-width: 450px;
+ margin: 8px 0;
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
new file mode 100644
index 0000000000..689c484521
--- /dev/null
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -0,0 +1,68 @@
+<template>
+<div class="mrdgzndn">
+ <Mfm :key="text" :text="text" :is-note="false" :i="$i"/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" class="url"/>
+</div>
+</template>
+
+<script lang="ts">
+import { TextBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineAsyncComponent, defineComponent, PropType } from 'vue';
+import * as mfm from 'mfm-js';
+import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+
+export default defineComponent({
+ components: {
+ MkUrlPreview: defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')),
+ },
+ props: {
+ block: {
+ type: Object as PropType<TextBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ text: this.hpml.interpolate(this.block.text),
+ };
+ },
+ computed: {
+ urls(): string[] {
+ if (this.text) {
+ return extractUrlFromMfm(mfm.parse(this.text));
+ } else {
+ return [];
+ }
+ },
+ },
+ watch: {
+ 'hpml.vars': {
+ handler() {
+ this.text = this.hpml.interpolate(this.block.text);
+ },
+ deep: true,
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.mrdgzndn {
+ &:not(:first-child) {
+ margin-top: 0.5em;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 0.5em;
+ }
+
+ > .url {
+ margin: 0.5em 0;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/page/page.textarea-input.vue b/packages/frontend/src/components/page/page.textarea-input.vue
new file mode 100644
index 0000000000..507e1bd97b
--- /dev/null
+++ b/packages/frontend/src/components/page/page.textarea-input.vue
@@ -0,0 +1,47 @@
+<template>
+<div>
+ <MkTextarea :model-value="value" @update:model-value="updateValue($event)">
+ <template #label>{{ hpml.interpolate(block.text) }}</template>
+ </MkTextarea>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+import * as os from '@/os';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { HpmlTextInput } from '@/scripts/hpml';
+import { TextInputVarBlock } from '@/scripts/hpml/block';
+
+export default defineComponent({
+ components: {
+ MkTextarea,
+ },
+ props: {
+ block: {
+ type: Object as PropType<TextInputVarBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const value = computed(() => {
+ return props.hpml.vars.value[props.block.name];
+ });
+
+ function updateValue(newValue) {
+ props.hpml.updatePageVar(props.block.name, newValue);
+ props.hpml.eval();
+ }
+
+ return {
+ value,
+ updateValue,
+ };
+ },
+});
+</script>
diff --git a/packages/frontend/src/components/page/page.textarea.vue b/packages/frontend/src/components/page/page.textarea.vue
new file mode 100644
index 0000000000..f809925081
--- /dev/null
+++ b/packages/frontend/src/components/page/page.textarea.vue
@@ -0,0 +1,39 @@
+<template>
+<MkTextarea :model-value="text" readonly></MkTextarea>
+</template>
+
+<script lang="ts">
+import { TextBlock } from '@/scripts/hpml/block';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { defineComponent, PropType } from 'vue';
+import MkTextarea from '../form/textarea.vue';
+
+export default defineComponent({
+ components: {
+ MkTextarea,
+ },
+ props: {
+ block: {
+ type: Object as PropType<TextBlock>,
+ required: true,
+ },
+ hpml: {
+ type: Object as PropType<Hpml>,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ text: this.hpml.interpolate(this.block.text),
+ };
+ },
+ watch: {
+ 'hpml.vars': {
+ handler() {
+ this.text = this.hpml.interpolate(this.block.text);
+ },
+ deep: true,
+ },
+ },
+});
+</script>
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
new file mode 100644
index 0000000000..b5cb73c009
--- /dev/null
+++ b/packages/frontend/src/components/page/page.vue
@@ -0,0 +1,85 @@
+<template>
+<div v-if="hpml" class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
+ <XBlock v-for="child in page.content" :key="child.id" :block="child" :hpml="hpml" :h="2"/>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, nextTick, onUnmounted, PropType } from 'vue';
+import { parse } from '@syuilo/aiscript';
+import XBlock from './page.block.vue';
+import { Hpml } from '@/scripts/hpml/evaluator';
+import { url } from '@/config';
+import { $i } from '@/account';
+import { defaultStore } from '@/store';
+
+export default defineComponent({
+ components: {
+ XBlock,
+ },
+ props: {
+ page: {
+ type: Object as PropType<Record<string, any>>,
+ required: true,
+ },
+ },
+ setup(props, ctx) {
+ const hpml = new Hpml(props.page, {
+ randomSeed: Math.random(),
+ visitor: $i,
+ url: url,
+ enableAiScript: !defaultStore.state.disablePagesScript,
+ });
+
+ onMounted(() => {
+ nextTick(() => {
+ if (props.page.script && hpml.aiscript) {
+ let ast;
+ try {
+ ast = parse(props.page.script);
+ } catch (err) {
+ console.error(err);
+ /*os.alert({
+ type: 'error',
+ text: 'Syntax error :('
+ });*/
+ return;
+ }
+ hpml.aiscript.exec(ast).then(() => {
+ hpml.eval();
+ }).catch(err => {
+ console.error(err);
+ /*os.alert({
+ type: 'error',
+ text: err
+ });*/
+ });
+ } else {
+ hpml.eval();
+ }
+ });
+ onUnmounted(() => {
+ if (hpml.aiscript) hpml.aiscript.abort();
+ });
+ });
+
+ return {
+ hpml,
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.iroscrza {
+ &.serif {
+ > div {
+ font-family: serif;
+ }
+ }
+
+ &.center {
+ text-align: center;
+ }
+}
+</style>