2023-01-05 05:59:48 +01:00
|
|
|
<template>
|
|
|
|
<MkStickyContainer>
|
|
|
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
|
|
|
<MkSpacer :content-max="700">
|
|
|
|
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
|
|
|
<div v-if="flash" :key="flash.id">
|
|
|
|
<Transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
|
|
|
<div v-if="started" :class="$style.started">
|
|
|
|
<div class="main _panel">
|
|
|
|
<MkAsUi v-if="root" :component="root" :components="components"/>
|
|
|
|
</div>
|
|
|
|
<div class="actions _panel">
|
|
|
|
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" as-like class="button" rounded primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="flash.likedCount > 0" class="count">{{ flash.likedCount }}</span></MkButton>
|
|
|
|
<MkButton v-else v-tooltip="i18n.ts.like" as-like class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash.likedCount > 0" class="count">{{ flash.likedCount }}</span></MkButton>
|
|
|
|
<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></MkButton>
|
|
|
|
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-else :class="$style.ready">
|
|
|
|
<div class="_panel main">
|
|
|
|
<div class="title">{{ flash.title }}</div>
|
|
|
|
<div class="summary">{{ flash.summary }}</div>
|
|
|
|
<MkButton class="start" gradate rounded large @click="start">Play</MkButton>
|
|
|
|
<div class="info">
|
|
|
|
<span v-tooltip="i18n.ts.numberOfLikes"><i class="ti ti-heart"></i> {{ flash.likedCount }}</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Transition>
|
2023-01-05 13:04:56 +01:00
|
|
|
<FormFolder class="_margin">
|
2023-01-05 05:59:48 +01:00
|
|
|
<template #icon><i class="ti ti-code"></i></template>
|
|
|
|
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
|
|
|
|
|
|
|
<MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea>
|
|
|
|
</FormFolder>
|
|
|
|
<div :class="$style.footer">
|
|
|
|
<Mfm :text="`By @${flash.user.username}`"/>
|
|
|
|
<div class="date">
|
|
|
|
<div v-if="flash.createdAt != flash.updatedAt"><i class="ti ti-clock"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div>
|
|
|
|
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
|
|
|
|
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
|
|
|
</div>
|
|
|
|
<MkError v-else-if="error" @retry="fetchPage()"/>
|
|
|
|
<MkLoading v-else/>
|
|
|
|
</Transition>
|
|
|
|
</MkSpacer>
|
|
|
|
</MkStickyContainer>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
|
|
|
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
|
|
|
import MkButton from '@/components/MkButton.vue';
|
|
|
|
import * as os from '@/os';
|
|
|
|
import { url } from '@/config';
|
|
|
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
|
|
|
import MkContainer from '@/components/MkContainer.vue';
|
|
|
|
import MkPagination from '@/components/MkPagination.vue';
|
|
|
|
import MkPagePreview from '@/components/MkPagePreview.vue';
|
|
|
|
import { i18n } from '@/i18n';
|
|
|
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
|
|
import MkAsUi from '@/components/MkAsUi.vue';
|
|
|
|
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
|
|
|
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
|
|
|
import FormFolder from '@/components/form/folder.vue';
|
|
|
|
import MkTextarea from '@/components/form/textarea.vue';
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
id: string;
|
|
|
|
}>();
|
|
|
|
|
|
|
|
let flash = $ref(null);
|
|
|
|
let error = $ref(null);
|
|
|
|
|
|
|
|
function fetchFlash() {
|
|
|
|
flash = null;
|
|
|
|
os.api('flash/show', {
|
|
|
|
flashId: props.id,
|
|
|
|
}).then(_flash => {
|
|
|
|
flash = _flash;
|
|
|
|
}).catch(err => {
|
|
|
|
error = err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function share() {
|
|
|
|
navigator.share({
|
|
|
|
title: flash.title,
|
|
|
|
text: flash.summary,
|
|
|
|
url: `${url}/play/${flash.id}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function shareWithNote() {
|
|
|
|
os.post({
|
|
|
|
initialText: `${flash.title} ${url}/play/${flash.id}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function like() {
|
|
|
|
os.apiWithDialog('flash/like', {
|
|
|
|
flashId: flash.id,
|
|
|
|
}).then(() => {
|
|
|
|
flash.isLiked = true;
|
|
|
|
flash.likedCount++;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function unlike() {
|
|
|
|
const confirm = await os.confirm({
|
|
|
|
type: 'warning',
|
|
|
|
text: i18n.ts.unlikeConfirm,
|
|
|
|
});
|
|
|
|
if (confirm.canceled) return;
|
|
|
|
os.apiWithDialog('flash/unlike', {
|
|
|
|
flashId: flash.id,
|
|
|
|
}).then(() => {
|
|
|
|
flash.isLiked = false;
|
|
|
|
flash.likedCount--;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
watch(() => props.id, fetchFlash, { immediate: true });
|
|
|
|
|
|
|
|
const parser = new Parser();
|
|
|
|
|
|
|
|
let started = $ref(false);
|
|
|
|
let aiscript = $shallowRef<Interpreter | null>(null);
|
|
|
|
const root = ref<AsUiRoot>();
|
|
|
|
const components: Ref<AsUiComponent>[] = [];
|
|
|
|
|
|
|
|
function start() {
|
|
|
|
started = true;
|
|
|
|
run();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function run() {
|
|
|
|
if (aiscript) aiscript.abort();
|
|
|
|
|
|
|
|
aiscript = new Interpreter({
|
|
|
|
...createAiScriptEnv({
|
|
|
|
storageKey: 'flash:' + flash.id,
|
|
|
|
}),
|
|
|
|
...registerAsUiLib(components, (_root) => {
|
|
|
|
root.value = _root.value;
|
|
|
|
}),
|
|
|
|
}, {
|
|
|
|
in: (q) => {
|
|
|
|
return new Promise(ok => {
|
|
|
|
os.inputText({
|
|
|
|
title: q,
|
|
|
|
}).then(({ canceled, result: a }) => {
|
|
|
|
ok(a);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
out: (value) => {
|
|
|
|
// nop
|
|
|
|
},
|
|
|
|
log: (type, params) => {
|
|
|
|
// nop
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
let ast;
|
|
|
|
try {
|
|
|
|
ast = parser.parse(flash.script);
|
|
|
|
} catch (err) {
|
|
|
|
os.alert({
|
|
|
|
type: 'error',
|
|
|
|
text: 'Syntax error :(',
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
await aiscript.exec(ast);
|
|
|
|
} catch (err) {
|
|
|
|
os.alert({
|
|
|
|
type: 'error',
|
|
|
|
title: 'AiScript Error',
|
|
|
|
text: err.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onDeactivated(() => {
|
|
|
|
if (aiscript) aiscript.abort();
|
|
|
|
});
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
if (aiscript) aiscript.abort();
|
|
|
|
});
|
|
|
|
|
|
|
|
const headerActions = $computed(() => []);
|
|
|
|
|
|
|
|
const headerTabs = $computed(() => []);
|
|
|
|
|
|
|
|
definePageMetadata(computed(() => flash ? {
|
|
|
|
title: flash.title,
|
|
|
|
avatar: flash.user,
|
|
|
|
path: `/play/${flash.id}`,
|
|
|
|
share: {
|
|
|
|
title: flash.title,
|
|
|
|
text: flash.summary,
|
|
|
|
},
|
|
|
|
} : null));
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" module>
|
|
|
|
.ready {
|
|
|
|
&:global {
|
|
|
|
> .main {
|
|
|
|
padding: 32px;
|
|
|
|
|
|
|
|
> .title {
|
|
|
|
font-size: 1.4em;
|
|
|
|
font-weight: bold;
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .summary {
|
|
|
|
font-size: 1.1em;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .start {
|
|
|
|
margin: 1em auto 1em auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .info {
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.footer {
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
|
|
&:global {
|
|
|
|
> .date {
|
|
|
|
margin: 8px 0;
|
|
|
|
opacity: 0.6;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.started {
|
|
|
|
&:global {
|
|
|
|
> .main {
|
|
|
|
padding: 32px;
|
|
|
|
}
|
|
|
|
|
|
|
|
> .actions {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
gap: 12px;
|
|
|
|
margin-top: 16px;
|
|
|
|
padding: 16px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.fade-enter-active,
|
|
|
|
.fade-leave-active {
|
|
|
|
transition: opacity 0.125s ease;
|
|
|
|
}
|
|
|
|
.fade-enter-from,
|
|
|
|
.fade-leave-to {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.zoom-enter-active,
|
|
|
|
.zoom-leave-active {
|
|
|
|
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
|
|
|
}
|
|
|
|
.zoom-enter-from {
|
|
|
|
opacity: 0;
|
|
|
|
transform: scale(0.7);
|
|
|
|
}
|
|
|
|
.zoom-leave-to {
|
|
|
|
opacity: 0;
|
|
|
|
transform: scale(1.3);
|
|
|
|
}
|
|
|
|
</style>
|