MisskeyPlay (#9467)
* wip * wip * wip * wip * wip * Update ui.ts * wip * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * wip * wip * wip * wip * 🎨 * wip * ✌️
This commit is contained in:
107
packages/frontend/src/components/MkAsUi.vue
Normal file
107
packages/frontend/src/components/MkAsUi.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="c.type === 'root'" :class="$style.root">
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</div>
|
||||
<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
|
||||
<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, color: c.color ?? null }" :text="c.text"/>
|
||||
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton>
|
||||
<div v-else-if="c.type === 'buttons'" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
component: AsUiComponent;
|
||||
components: Ref<AsUiComponent>[];
|
||||
size: 'small' | 'medium' | 'large';
|
||||
}>(), {
|
||||
size: 'medium',
|
||||
});
|
||||
|
||||
const c = props.component;
|
||||
|
||||
function g(id) {
|
||||
return props.components.find(x => x.value.id === id).value;
|
||||
}
|
||||
|
||||
let valueForSwitch = $ref(c.default ?? false);
|
||||
|
||||
function onSwitchUpdate(v) {
|
||||
valueForSwitch = v;
|
||||
if (c.onChange) c.onChange(v);
|
||||
}
|
||||
|
||||
function openPostForm() {
|
||||
os.post({
|
||||
initialText: c.form.text,
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.containerCenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fontSerif {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.fontMonospace {
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||
}
|
||||
</style>
|
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="bghgjjyj _button"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, small }"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, small, large, asLike }"
|
||||
:type="type"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
@@ -41,6 +41,8 @@ const props = defineProps<{
|
||||
danger?: boolean;
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
asLike?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -131,6 +133,11 @@ function onMousedown(evt: MouseEvent): void {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -153,6 +160,37 @@ function onMousedown(evt: MouseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
&.asLike {
|
||||
background: rgba(255, 86, 125, 0.07);
|
||||
color: #ff002f;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: rgba(255, 74, 116, 0.11);
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: rgba(224, 57, 96, 0.125);
|
||||
}
|
||||
|
||||
> .ripples {
|
||||
::v-deep(div) {
|
||||
background: rgba(255, 60, 106, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: rgb(241 97 132);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: rgb(241 92 128);
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: rgb(241 92 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.gradate {
|
||||
font-weight: bold;
|
||||
color: var(--fgOnAccent) !important;
|
||||
|
@@ -59,7 +59,7 @@ defineExpose({
|
||||
|
||||
&.disabled {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
> .box {
|
||||
|
112
packages/frontend/src/components/MkFlashPreview.vue
Normal file
112
packages/frontend/src/components/MkFlashPreview.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _block" tabindex="-1">
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||
</header>
|
||||
<p v-if="flash.summary" :title="flash.summary">{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}</p>
|
||||
<footer>
|
||||
<img class="icon" :src="flash.user.avatarUrl"/>
|
||||
<p>{{ userName(flash.user) }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
//flash: misskey.entities.Flash;
|
||||
flash: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vhpxefrk {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> 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) {
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
font-size: 12px;
|
||||
|
||||
> article {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 10px;
|
||||
|
||||
> article {
|
||||
padding: 8px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 4px;
|
||||
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@@ -50,7 +50,7 @@ const menu = defaultStore.state.menu;
|
||||
|
||||
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
type: def.to ? 'link' : 'button',
|
||||
text: i18n.ts[def.title],
|
||||
text: def.title,
|
||||
icon: def.icon,
|
||||
to: def.to,
|
||||
action: def.action,
|
||||
|
@@ -14,22 +14,15 @@
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
userName,
|
||||
},
|
||||
});
|
||||
const props = defineProps<{
|
||||
page: misskey.entities.Page;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -126,7 +126,7 @@ const onClick = (ev: MouseEvent) => {
|
||||
const pushOption = (option: VNode) => {
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
active: computed(() => v.value === option.props.value),
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
|
@@ -8,97 +8,102 @@ import { unisonReload } from '@/scripts/unison-reload';
|
||||
|
||||
export const navbarItemDef = reactive({
|
||||
notifications: {
|
||||
title: 'notifications',
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'ti ti-bell',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadNotification),
|
||||
to: '/my/notifications',
|
||||
},
|
||||
messaging: {
|
||||
title: 'messaging',
|
||||
title: i18n.ts.messaging,
|
||||
icon: 'ti ti-messages',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
|
||||
to: '/my/messaging',
|
||||
},
|
||||
drive: {
|
||||
title: 'drive',
|
||||
title: i18n.ts.drive,
|
||||
icon: 'ti ti-cloud',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/drive',
|
||||
},
|
||||
followRequests: {
|
||||
title: 'followRequests',
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
show: computed(() => $i != null && $i.isLocked),
|
||||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||
to: '/my/follow-requests',
|
||||
},
|
||||
explore: {
|
||||
title: 'explore',
|
||||
title: i18n.ts.explore,
|
||||
icon: 'ti ti-hash',
|
||||
to: '/explore',
|
||||
},
|
||||
announcements: {
|
||||
title: 'announcements',
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
|
||||
to: '/announcements',
|
||||
},
|
||||
search: {
|
||||
title: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
action: () => search(),
|
||||
},
|
||||
lists: {
|
||||
title: 'lists',
|
||||
title: i18n.ts.lists,
|
||||
icon: 'ti ti-list',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/lists',
|
||||
},
|
||||
/*
|
||||
groups: {
|
||||
title: 'groups',
|
||||
title: i18n.ts.groups,
|
||||
icon: 'ti ti-users',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/groups',
|
||||
},
|
||||
*/
|
||||
antennas: {
|
||||
title: 'antennas',
|
||||
title: i18n.ts.antennas,
|
||||
icon: 'ti ti-antenna',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/antennas',
|
||||
},
|
||||
favorites: {
|
||||
title: 'favorites',
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'ti ti-star',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/favorites',
|
||||
},
|
||||
pages: {
|
||||
title: 'pages',
|
||||
title: i18n.ts.pages,
|
||||
icon: 'ti ti-news',
|
||||
to: '/pages',
|
||||
},
|
||||
play: {
|
||||
title: 'Play',
|
||||
icon: 'ti ti-player-play',
|
||||
to: '/play',
|
||||
},
|
||||
gallery: {
|
||||
title: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ti ti-icons',
|
||||
to: '/gallery',
|
||||
},
|
||||
clips: {
|
||||
title: 'clip',
|
||||
title: i18n.ts.clip,
|
||||
icon: 'ti ti-paperclip',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/clips',
|
||||
},
|
||||
channels: {
|
||||
title: 'channel',
|
||||
title: i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
to: '/channels',
|
||||
},
|
||||
ui: {
|
||||
title: 'switchUi',
|
||||
title: i18n.ts.switchUi,
|
||||
icon: 'ti ti-devices',
|
||||
action: (ev) => {
|
||||
os.popupMenu([{
|
||||
@@ -126,7 +131,7 @@ export const navbarItemDef = reactive({
|
||||
},
|
||||
},
|
||||
reload: {
|
||||
title: 'reload',
|
||||
title: i18n.ts.reload,
|
||||
icon: 'ti ti-refresh',
|
||||
action: (ev) => {
|
||||
location.reload();
|
||||
|
111
packages/frontend/src/pages/flash/flash-edit.vue
Normal file
111
packages/frontend/src/pages/flash/flash-edit.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<MkInput v-model="title" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._play.title }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="summary" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._play.summary }}</template>
|
||||
</MkTextarea>
|
||||
<MkTextarea v-model="script" class="_formBlock _monospace" tall spellcheck="false">
|
||||
<template #label>{{ i18n.ts._play.script }}</template>
|
||||
</MkTextarea>
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { url } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
let flash = $ref(null);
|
||||
|
||||
if (props.id) {
|
||||
flash = await os.api('flash/show', {
|
||||
flashId: props.id,
|
||||
});
|
||||
}
|
||||
|
||||
let title = $ref(flash?.title ?? 'New Play');
|
||||
let summary = $ref(flash?.summary ?? '');
|
||||
let permissions = $ref(flash?.permissions ?? []);
|
||||
let script = $ref(flash?.script ?? `/// @ 0.12.0
|
||||
|
||||
var name = ""
|
||||
|
||||
Ui:render([
|
||||
Ui:C:textInput({
|
||||
label: "Your name"
|
||||
onInput: @(v) { name = v }
|
||||
})
|
||||
Ui:C:button({
|
||||
text: "Hello"
|
||||
onClick: @() {
|
||||
Mk:dialog(null \`Hello, {name}!\`)
|
||||
}
|
||||
})
|
||||
])
|
||||
`);
|
||||
|
||||
async function save() {
|
||||
if (flash) {
|
||||
os.apiWithDialog('flash/update', {
|
||||
flashId: props.id,
|
||||
title,
|
||||
summary,
|
||||
permissions,
|
||||
script,
|
||||
});
|
||||
} else {
|
||||
const created = await os.apiWithDialog('flash/create', {
|
||||
title,
|
||||
summary,
|
||||
permissions,
|
||||
script,
|
||||
});
|
||||
router.push('/play/' + created.id + '/edit');
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (flash == null) {
|
||||
os.alert({
|
||||
text: 'Please save',
|
||||
});
|
||||
} else {
|
||||
os.pageWindow(`/play/${flash.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(computed(() => flash ? {
|
||||
title: i18n.ts._play.edit + ': ' + flash.title,
|
||||
} : {
|
||||
title: i18n.ts._play.new,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
99
packages/frontend/src/pages/flash/flash-index.vue
Normal file
99
packages/frontend/src/pages/flash/flash-index.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div v-if="tab === 'featured'" class="">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'my'" class="my">
|
||||
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" class="" :flash="flash"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'liked'" class="">
|
||||
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" class="" :flash="like.flash"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref('featured');
|
||||
|
||||
const featuredFlashsPagination = {
|
||||
endpoint: 'flash/featured' as const,
|
||||
noPaging: true,
|
||||
};
|
||||
const myFlashsPagination = {
|
||||
endpoint: 'flash/my' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const likedFlashsPagination = {
|
||||
endpoint: 'flash/my-likes' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push('/play/new');
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: create,
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'featured',
|
||||
title: i18n.ts._play.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._play.my,
|
||||
icon: 'ti ti-edit',
|
||||
}, {
|
||||
key: 'liked',
|
||||
title: i18n.ts._play.liked,
|
||||
icon: 'ti ti-heart',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: 'Play',
|
||||
icon: 'ti ti-player-play',
|
||||
})));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rknalgpo {
|
||||
&.my .ckltabjg:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ckltabjg:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.ckltabjg:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
291
packages/frontend/src/pages/flash/flash.vue
Normal file
291
packages/frontend/src/pages/flash/flash.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<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>
|
||||
<FormFolder class="_formBlock">
|
||||
<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>
|
@@ -18,8 +18,8 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" as-like primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" as-like @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
|
||||
@@ -207,20 +207,6 @@ definePageMetadata(computed(() => page ? {
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .like {
|
||||
> .button {
|
||||
--accent: rgb(241 97 132);
|
||||
--X8: rgb(241 92 128);
|
||||
--buttonBg: rgb(216 71 106 / 5%);
|
||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||
color: #ff002f;
|
||||
|
||||
::v-deep(.count) {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
|
||||
|
@@ -1,25 +1,34 @@
|
||||
<template>
|
||||
<div class="iltifgqe">
|
||||
<div class="editor _panel _gap">
|
||||
<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/>
|
||||
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
||||
</div>
|
||||
|
||||
<MkContainer :foldable="true" class="_gap">
|
||||
<template #header>{{ i18n.ts.output }}</template>
|
||||
<div class="bepmlvbi">
|
||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||
<MkSpacer :content-max="800">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.editor" class="_panel">
|
||||
<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/>
|
||||
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="ti ti-player-play"></i></MkButton>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="_gap">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
<MkContainer v-if="root && components.length > 0" :key="uiKey" :foldable="true">
|
||||
<template #header>UI</template>
|
||||
<div :class="$style.ui">
|
||||
<MkAsUi :component="root" :components="components" size="small"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<MkContainer :foldable="true" class="">
|
||||
<template #header>{{ i18n.ts.output }}</template>
|
||||
<div :class="$style.logs">
|
||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { onDeactivated, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
@@ -35,11 +44,16 @@ import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
||||
import MkAsUi from '@/components/MkAsUi.vue';
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
let aiscript: Interpreter;
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
const root = ref<AsUiRoot>();
|
||||
let components: Ref<AsUiComponent>[] = [];
|
||||
let uiKey = $ref(0);
|
||||
|
||||
const saved = localStorage.getItem('scratchpad');
|
||||
if (saved) {
|
||||
@@ -51,10 +65,19 @@ watch(code, () => {
|
||||
});
|
||||
|
||||
async function run() {
|
||||
if (aiscript) aiscript.abort();
|
||||
root.value = undefined;
|
||||
components = [];
|
||||
uiKey++;
|
||||
logs.value = [];
|
||||
const aiscript = new Interpreter(createAiScriptEnv({
|
||||
storageKey: 'scratchpad',
|
||||
token: $i?.token,
|
||||
aiscript = new Interpreter(({
|
||||
...createAiScriptEnv({
|
||||
storageKey: 'widget',
|
||||
token: $i?.token,
|
||||
}),
|
||||
...registerAsUiLib(components, (_root) => {
|
||||
root.value = _root.value;
|
||||
}),
|
||||
}), {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
@@ -96,10 +119,11 @@ async function run() {
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (error: any) {
|
||||
} catch (err: any) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: error.message,
|
||||
title: 'AiScript Error',
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -108,6 +132,14 @@ function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
}
|
||||
|
||||
onDeactivated(() => {
|
||||
if (aiscript) aiscript.abort();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (aiscript) aiscript.abort();
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
@@ -118,21 +150,29 @@ definePageMetadata({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iltifgqe {
|
||||
padding: 16px;
|
||||
|
||||
> .editor {
|
||||
position: relative;
|
||||
}
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--margin);
|
||||
}
|
||||
|
||||
.bepmlvbi {
|
||||
.editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ui {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.logs {
|
||||
padding: 16px;
|
||||
|
||||
> .log {
|
||||
&:not(.print) {
|
||||
opacity: 0.7;
|
||||
&:global {
|
||||
> .log {
|
||||
&:not(.print) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ async function addItem() {
|
||||
const { canceled, result: item } = await os.select({
|
||||
title: i18n.ts.addItem,
|
||||
items: [...menu.map(k => ({
|
||||
value: k, text: i18n.ts[navbarItemDef[k].title],
|
||||
value: k, text: navbarItemDef[k].title,
|
||||
})), {
|
||||
value: '-', text: i18n.ts.divider,
|
||||
}],
|
||||
|
@@ -262,6 +262,20 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/pages',
|
||||
component: page(() => import('./pages/pages.vue')),
|
||||
}, {
|
||||
path: '/play/:id/edit',
|
||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/new',
|
||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/:id',
|
||||
component: page(() => import('./pages/flash/flash.vue')),
|
||||
}, {
|
||||
path: '/play',
|
||||
component: page(() => import('./pages/flash/flash-index.vue')),
|
||||
}, {
|
||||
path: '/gallery/:postId/edit',
|
||||
component: page(() => import('./pages/gallery/edit.vue')),
|
||||
|
526
packages/frontend/src/scripts/aiscript/ui.ts
Normal file
526
packages/frontend/src/scripts/aiscript/ui.ts
Normal file
@@ -0,0 +1,526 @@
|
||||
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ref, Ref } from 'vue';
|
||||
|
||||
export type AsUiComponentBase = {
|
||||
id: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type AsUiRoot = AsUiComponentBase & {
|
||||
type: 'root';
|
||||
children: AsUiComponent['id'][];
|
||||
};
|
||||
|
||||
export type AsUiContainer = AsUiComponentBase & {
|
||||
type: 'container';
|
||||
children?: AsUiComponent['id'][];
|
||||
align?: 'left' | 'center' | 'right';
|
||||
bgColor?: string;
|
||||
fgColor?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
padding?: number;
|
||||
rounded?: boolean;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type AsUiText = AsUiComponentBase & {
|
||||
type: 'text';
|
||||
text?: string;
|
||||
size?: number;
|
||||
bold?: boolean;
|
||||
color?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
};
|
||||
|
||||
export type AsUiMfm = AsUiComponentBase & {
|
||||
type: 'mfm';
|
||||
text?: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
};
|
||||
|
||||
export type AsUiButton = AsUiComponentBase & {
|
||||
type: 'button';
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
primary?: boolean;
|
||||
rounded?: boolean;
|
||||
};
|
||||
|
||||
export type AsUiButtons = AsUiComponentBase & {
|
||||
type: 'buttons';
|
||||
buttons?: AsUiButton[];
|
||||
};
|
||||
|
||||
export type AsUiSwitch = AsUiComponentBase & {
|
||||
type: 'switch';
|
||||
onChange?: (v: boolean) => void;
|
||||
default?: boolean;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
export type AsUiTextarea = AsUiComponentBase & {
|
||||
type: 'textarea';
|
||||
onInput?: (v: string) => void;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
export type AsUiTextInput = AsUiComponentBase & {
|
||||
type: 'textInput';
|
||||
onInput?: (v: string) => void;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
export type AsUiNumberInput = AsUiComponentBase & {
|
||||
type: 'numberInput';
|
||||
onInput?: (v: number) => void;
|
||||
default?: number;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
export type AsUiSelect = AsUiComponentBase & {
|
||||
type: 'select';
|
||||
items?: {
|
||||
text: string;
|
||||
value: string;
|
||||
}[];
|
||||
onChange?: (v: string) => void;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
};
|
||||
|
||||
export type AsUiPostFormButton = AsUiComponentBase & {
|
||||
type: 'postFormButton';
|
||||
text?: string;
|
||||
primary?: boolean;
|
||||
rounded?: boolean;
|
||||
form?: {
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiPostFormButton;
|
||||
|
||||
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const children = def.value.get('children');
|
||||
utils.assertArray(children);
|
||||
|
||||
return {
|
||||
children: children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const children = def.value.get('children');
|
||||
if (children) utils.assertArray(children);
|
||||
const align = def.value.get('align');
|
||||
if (align) utils.assertString(align);
|
||||
const bgColor = def.value.get('bgColor');
|
||||
if (bgColor) utils.assertString(bgColor);
|
||||
const fgColor = def.value.get('fgColor');
|
||||
if (fgColor) utils.assertString(fgColor);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
const borderWidth = def.value.get('borderWidth');
|
||||
if (borderWidth) utils.assertNumber(borderWidth);
|
||||
const borderColor = def.value.get('borderColor');
|
||||
if (borderColor) utils.assertString(borderColor);
|
||||
const padding = def.value.get('padding');
|
||||
if (padding) utils.assertNumber(padding);
|
||||
const rounded = def.value.get('rounded');
|
||||
if (rounded) utils.assertBoolean(rounded);
|
||||
const hidden = def.value.get('hidden');
|
||||
if (hidden) utils.assertBoolean(hidden);
|
||||
|
||||
return {
|
||||
children: children ? children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
}) : [],
|
||||
align: align?.value,
|
||||
fgColor: fgColor?.value,
|
||||
bgColor: bgColor?.value,
|
||||
font: font?.value,
|
||||
borderWidth: borderWidth?.value,
|
||||
borderColor: borderColor?.value,
|
||||
padding: padding?.value,
|
||||
rounded: rounded?.value,
|
||||
hidden: hidden?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
if (text) utils.assertString(text);
|
||||
const size = def.value.get('size');
|
||||
if (size) utils.assertNumber(size);
|
||||
const bold = def.value.get('bold');
|
||||
if (bold) utils.assertBoolean(bold);
|
||||
const color = def.value.get('color');
|
||||
if (color) utils.assertString(color);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
size: size?.value,
|
||||
bold: bold?.value,
|
||||
color: color?.value,
|
||||
font: font?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
if (text) utils.assertString(text);
|
||||
const size = def.value.get('size');
|
||||
if (size) utils.assertNumber(size);
|
||||
const color = def.value.get('color');
|
||||
if (color) utils.assertString(color);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
size: size?.value,
|
||||
color: color?.value,
|
||||
font: font?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
if (onInput) utils.assertFunction(onInput);
|
||||
const defaultValue = def.value.get('default');
|
||||
if (defaultValue) utils.assertString(defaultValue);
|
||||
const label = def.value.get('label');
|
||||
if (label) utils.assertString(label);
|
||||
const caption = def.value.get('caption');
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
caption: caption?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
if (onInput) utils.assertFunction(onInput);
|
||||
const defaultValue = def.value.get('default');
|
||||
if (defaultValue) utils.assertString(defaultValue);
|
||||
const label = def.value.get('label');
|
||||
if (label) utils.assertString(label);
|
||||
const caption = def.value.get('caption');
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
caption: caption?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
if (onInput) utils.assertFunction(onInput);
|
||||
const defaultValue = def.value.get('default');
|
||||
if (defaultValue) utils.assertNumber(defaultValue);
|
||||
const label = def.value.get('label');
|
||||
if (label) utils.assertString(label);
|
||||
const caption = def.value.get('caption');
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
caption: caption?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
if (text) utils.assertString(text);
|
||||
const onClick = def.value.get('onClick');
|
||||
if (onClick) utils.assertFunction(onClick);
|
||||
const primary = def.value.get('primary');
|
||||
if (primary) utils.assertBoolean(primary);
|
||||
const rounded = def.value.get('rounded');
|
||||
if (rounded) utils.assertBoolean(rounded);
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
onClick: () => {
|
||||
if (onClick) call(onClick, []);
|
||||
},
|
||||
primary: primary?.value,
|
||||
rounded: rounded?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const buttons = def.value.get('buttons');
|
||||
if (buttons) utils.assertArray(buttons);
|
||||
|
||||
return {
|
||||
buttons: buttons ? buttons.value.map(button => {
|
||||
utils.assertObject(button);
|
||||
const text = button.value.get('text');
|
||||
utils.assertString(text);
|
||||
const onClick = button.value.get('onClick');
|
||||
utils.assertFunction(onClick);
|
||||
const primary = button.value.get('primary');
|
||||
if (primary) utils.assertBoolean(primary);
|
||||
const rounded = button.value.get('rounded');
|
||||
if (rounded) utils.assertBoolean(rounded);
|
||||
|
||||
return {
|
||||
text: text.value,
|
||||
onClick: () => {
|
||||
call(onClick, []);
|
||||
},
|
||||
primary: primary?.value,
|
||||
rounded: rounded?.value,
|
||||
};
|
||||
}) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onChange = def.value.get('onChange');
|
||||
if (onChange) utils.assertFunction(onChange);
|
||||
const defaultValue = def.value.get('default');
|
||||
if (defaultValue) utils.assertBoolean(defaultValue);
|
||||
const label = def.value.get('label');
|
||||
if (label) utils.assertString(label);
|
||||
const caption = def.value.get('caption');
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onChange: (v) => {
|
||||
if (onChange) call(onChange, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
caption: caption?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const items = def.value.get('items');
|
||||
if (items) utils.assertArray(items);
|
||||
const onChange = def.value.get('onChange');
|
||||
if (onChange) utils.assertFunction(onChange);
|
||||
const defaultValue = def.value.get('default');
|
||||
if (defaultValue) utils.assertString(defaultValue);
|
||||
const label = def.value.get('label');
|
||||
if (label) utils.assertString(label);
|
||||
const caption = def.value.get('caption');
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
items: items ? items.value.map(item => {
|
||||
utils.assertObject(item);
|
||||
const text = item.value.get('text');
|
||||
utils.assertString(text);
|
||||
const value = item.value.get('value');
|
||||
if (value) utils.assertString(value);
|
||||
return {
|
||||
text: text.value,
|
||||
value: value ? value.value : text.value,
|
||||
};
|
||||
}) : [],
|
||||
onChange: (v) => {
|
||||
if (onChange) call(onChange, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
caption: caption?.value,
|
||||
};
|
||||
}
|
||||
|
||||
function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
if (text) utils.assertString(text);
|
||||
const primary = def.value.get('primary');
|
||||
if (primary) utils.assertBoolean(primary);
|
||||
const rounded = def.value.get('rounded');
|
||||
if (rounded) utils.assertBoolean(rounded);
|
||||
const form = def.value.get('form');
|
||||
if (form) utils.assertObject(form);
|
||||
|
||||
const getForm = () => {
|
||||
const text = form!.value.get('text');
|
||||
utils.assertString(text);
|
||||
return {
|
||||
text: text.value,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
primary: primary?.value,
|
||||
rounded: rounded?.value,
|
||||
form: form ? getForm() : {
|
||||
text: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
|
||||
const instances = {};
|
||||
|
||||
function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
||||
if (id) utils.assertString(id);
|
||||
const _id = id?.value ?? uuid();
|
||||
const component = ref({
|
||||
...getOptions(def, call),
|
||||
type,
|
||||
id: _id,
|
||||
});
|
||||
components.push(component);
|
||||
const instance = values.OBJ(new Map([
|
||||
['id', values.STR(_id)],
|
||||
['update', values.FN_NATIVE(async ([def], opts) => {
|
||||
utils.assertObject(def);
|
||||
const updates = getOptions(def, call);
|
||||
for (const update of def.value.keys()) {
|
||||
if (!Object.hasOwn(updates, update)) continue;
|
||||
component.value[update] = updates[update];
|
||||
}
|
||||
})],
|
||||
]));
|
||||
instances[_id] = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
const rootInstance = createComponentInstance('root', utils.jsToVal({ children: [] }), utils.jsToVal('___root___'), getRootOptions, () => {});
|
||||
const rootComponent = components[0] as Ref<AsUiRoot>;
|
||||
done(rootComponent);
|
||||
|
||||
return {
|
||||
'Ui:root': rootInstance,
|
||||
|
||||
'Ui:patch': values.FN_NATIVE(async ([id, val], opts) => {
|
||||
utils.assertString(id);
|
||||
utils.assertArray(val);
|
||||
patch(id.value, val.value, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:get': values.FN_NATIVE(async ([id], opts) => {
|
||||
utils.assertString(id);
|
||||
const instance = instances[id.value];
|
||||
if (instance) {
|
||||
return instance;
|
||||
} else {
|
||||
return values.NULL;
|
||||
}
|
||||
}),
|
||||
|
||||
// Ui:root.update({ children: [...] }) の糖衣構文
|
||||
'Ui:render': values.FN_NATIVE(async ([children], opts) => {
|
||||
utils.assertArray(children);
|
||||
|
||||
rootComponent.value.children = children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
});
|
||||
}),
|
||||
|
||||
'Ui:C:container': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('container', def, id, getContainerOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:text': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('text', def, id, getTextOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:mfm': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('mfm', def, id, getMfmOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:textarea': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('textarea', def, id, getTextareaOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:textInput': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('textInput', def, id, getTextInputOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:numberInput': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:button': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('button', def, id, getButtonOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:buttons': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('buttons', def, id, getButtonsOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:switch': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('switch', def, id, getSwitchOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:select': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('select', def, id, getSelectOptions, opts.call);
|
||||
}),
|
||||
|
||||
'Ui:C:postFormButton': values.FN_NATIVE(async ([def, id], opts) => {
|
||||
return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.call);
|
||||
}),
|
||||
};
|
||||
}
|
@@ -14,7 +14,7 @@
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
|
||||
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||
</component>
|
||||
</template>
|
||||
|
@@ -17,14 +17,14 @@
|
||||
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
|
||||
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts[navbarItemDef[item].title]"
|
||||
v-tooltip.noDelay.right="navbarItemDef[item].title"
|
||||
class="item _button"
|
||||
:class="[item, { active: navbarItemDef[item].active }]"
|
||||
active-class="active"
|
||||
:to="navbarItemDef[item].to"
|
||||
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
|
||||
>
|
||||
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ i18n.ts[navbarItemDef[item].title] }}</span>
|
||||
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||
</component>
|
||||
</template>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</MkA>
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="$ts[navbarItemDef[item].title]" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||
</component>
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<template v-for="item in menu">
|
||||
<div v-if="item === '-'" class="divider"></div>
|
||||
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||
<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span>
|
||||
<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
|
||||
</component>
|
||||
</template>
|
||||
|
122
packages/frontend/src/widgets/aiscript-app.vue
Normal file
122
packages/frontend/src/widgets/aiscript-app.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscriptApp">
|
||||
<template #header>App</template>
|
||||
<div :class="$style.root">
|
||||
<MkAsUi v-if="root" :component="root" :components="components" size="small"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
|
||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||
import { GetFormResultType } from '@/scripts/form';
|
||||
import * as os from '@/os';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import { $i } from '@/account';
|
||||
import MkAsUi from '@/components/MkAsUi.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
||||
|
||||
const name = 'aiscriptApp';
|
||||
|
||||
const widgetPropsDef = {
|
||||
script: {
|
||||
type: 'string' as const,
|
||||
multiline: true,
|
||||
default: '',
|
||||
},
|
||||
showHeader: {
|
||||
type: 'boolean' as const,
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||
|
||||
// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
|
||||
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
|
||||
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
|
||||
|
||||
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit,
|
||||
);
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
const root = ref<AsUiRoot>();
|
||||
const components: Ref<AsUiComponent>[] = [];
|
||||
|
||||
async function run() {
|
||||
const aiscript = new Interpreter({
|
||||
...createAiScriptEnv({
|
||||
storageKey: 'widget',
|
||||
token: $i?.token,
|
||||
}),
|
||||
...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(widgetProps.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => widgetProps.script, () => {
|
||||
run();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
run();
|
||||
});
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
@@ -22,6 +22,7 @@ export default function(app: App) {
|
||||
app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue')));
|
||||
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
|
||||
app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
|
||||
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
||||
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
||||
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
||||
}
|
||||
@@ -48,6 +49,7 @@ export const widgets = [
|
||||
'jobQueue',
|
||||
'button',
|
||||
'aiscript',
|
||||
'aiscriptApp',
|
||||
'aichan',
|
||||
'userList',
|
||||
];
|
||||
|
Reference in New Issue
Block a user