2023-07-27 08:31:52 +03:00
|
|
|
<!--
|
|
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
-->
|
|
|
|
|
2023-01-05 06:59:48 +02:00
|
|
|
<template>
|
|
|
|
<MkStickyContainer>
|
|
|
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
2023-05-19 14:52:15 +03:00
|
|
|
<MkSpacer :contentMax="700">
|
2023-04-01 07:42:40 +03:00
|
|
|
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
2023-01-05 06:59:48 +02:00
|
|
|
<div v-if="flash" :key="flash.id">
|
2023-04-01 07:42:40 +03:00
|
|
|
<Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in">
|
2023-01-05 06:59:48 +02:00
|
|
|
<div v-if="started" :class="$style.started">
|
|
|
|
<div class="main _panel">
|
|
|
|
<MkAsUi v-if="root" :component="root" :components="components"/>
|
|
|
|
</div>
|
|
|
|
<div class="actions _panel">
|
2023-09-30 22:53:52 +03:00
|
|
|
<MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
|
|
|
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
|
|
|
|
<MkButton v-tooltip="i18n.ts.shareWithNote" class="button" rounded @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></MkButton>
|
|
|
|
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold pg-lg ti-fw"></i></MkButton>
|
2023-01-05 06:59:48 +02:00
|
|
|
</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">
|
2023-09-30 22:53:52 +03:00
|
|
|
<span v-tooltip="i18n.ts.numberOfLikes"><i class="ph-heart ph-bold ph-lg"></i> {{ flash.likedCount }}</span>
|
2023-01-05 06:59:48 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Transition>
|
2023-05-19 14:52:15 +03:00
|
|
|
<MkFolder :defaultOpen="false" :max-height="280" class="_margin">
|
2023-09-30 22:53:52 +03:00
|
|
|
<template #icon><i class="ph-code ph-bold pg-lg"></i></template>
|
2023-01-05 06:59:48 +02:00
|
|
|
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
|
|
|
|
2023-02-28 10:38:43 +02:00
|
|
|
<MkCode :code="flash.script" :inline="false" class="_monospace"/>
|
2023-01-09 02:41:25 +02:00
|
|
|
</MkFolder>
|
2023-01-05 06:59:48 +02:00
|
|
|
<div :class="$style.footer">
|
|
|
|
<Mfm :text="`By @${flash.user.username}`"/>
|
|
|
|
<div class="date">
|
2023-09-30 22:53:52 +03:00
|
|
|
<div v-if="flash.createdAt != flash.updatedAt"><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div>
|
|
|
|
<div><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
|
2023-01-05 06:59:48 +02:00
|
|
|
</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';
|
2023-02-16 16:09:41 +02:00
|
|
|
import { Interpreter, Parser, values } from '@syuilo/aiscript';
|
2023-01-05 06:59:48 +02:00
|
|
|
import MkButton from '@/components/MkButton.vue';
|
2023-09-19 10:37:43 +03:00
|
|
|
import * as os from '@/os.js';
|
|
|
|
import { url } from '@/config.js';
|
|
|
|
import { i18n } from '@/i18n.js';
|
|
|
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
2023-01-05 06:59:48 +02:00
|
|
|
import MkAsUi from '@/components/MkAsUi.vue';
|
2023-09-19 10:37:43 +03:00
|
|
|
import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui.js';
|
|
|
|
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
|
2023-01-09 02:41:25 +02:00
|
|
|
import MkFolder from '@/components/MkFolder.vue';
|
2023-02-28 10:38:43 +02:00
|
|
|
import MkCode from '@/components/MkCode.vue';
|
2023-09-19 10:37:43 +03:00
|
|
|
import { defaultStore } from '@/store.js';
|
|
|
|
import { $i } from '@/account.js';
|
2023-01-05 06:59:48 +02:00
|
|
|
|
|
|
|
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>();
|
2023-01-16 03:04:10 +02:00
|
|
|
const components: Ref<AsUiComponent>[] = $ref([]);
|
2023-01-05 06:59:48 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}),
|
2023-01-06 10:19:25 +02:00
|
|
|
THIS_ID: values.STR(flash.id),
|
|
|
|
THIS_URL: values.STR(`${url}/play/${flash.id}`),
|
2023-01-05 06:59:48 +02:00
|
|
|
}, {
|
|
|
|
in: (q) => {
|
|
|
|
return new Promise(ok => {
|
|
|
|
os.inputText({
|
|
|
|
title: q,
|
|
|
|
}).then(({ canceled, result: a }) => {
|
2023-02-05 13:29:10 +02:00
|
|
|
if (canceled) {
|
|
|
|
ok('');
|
|
|
|
} else {
|
|
|
|
ok(a);
|
|
|
|
}
|
2023-01-05 06:59:48 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
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>
|