Compare commits
No commits in common. "80a29f771f723ddf8b64609725e97b626663dff3" and "f06c74c85ccc5c5941f87c4c834547bdd96e2bbb" have entirely different histories.
80a29f771f
...
f06c74c85c
|
@ -23,6 +23,7 @@ import {
|
|||
NoteReaction,
|
||||
Notification,
|
||||
OriginType,
|
||||
Page,
|
||||
ServerInfo,
|
||||
Signin,
|
||||
Stats,
|
||||
|
@ -630,6 +631,8 @@ export type Endpoints = {
|
|||
};
|
||||
res: Notification[];
|
||||
};
|
||||
"i/page-likes": { req: TODO; res: TODO };
|
||||
"i/pages": { req: TODO; res: TODO };
|
||||
"i/pin": { req: { noteId: Note["id"] }; res: MeDetailed };
|
||||
"i/read-all-messaging-messages": { req: TODO; res: TODO };
|
||||
"i/read-all-unread-notes": { req: TODO; res: TODO };
|
||||
|
@ -883,6 +886,24 @@ export type Endpoints = {
|
|||
res: null;
|
||||
};
|
||||
|
||||
// page-push
|
||||
"page-push": {
|
||||
req: { pageId: Page["id"]; event: string; var?: any };
|
||||
res: null;
|
||||
};
|
||||
|
||||
// pages
|
||||
"pages/create": { req: TODO; res: Page };
|
||||
"pages/delete": { req: { pageId: Page["id"] }; res: null };
|
||||
"pages/featured": { req: NoParams; res: Page[] };
|
||||
"pages/like": { req: { pageId: Page["id"] }; res: null };
|
||||
"pages/show": {
|
||||
req: { pageId?: Page["id"]; name?: string; username?: string };
|
||||
res: Page;
|
||||
};
|
||||
"pages/unlike": { req: { pageId: Page["id"] }; res: null };
|
||||
"pages/update": { req: TODO; res: null };
|
||||
|
||||
// ping
|
||||
ping: { req: NoParams; res: { pong: number } };
|
||||
|
||||
|
@ -979,6 +1000,7 @@ export type Endpoints = {
|
|||
};
|
||||
res: Note[];
|
||||
};
|
||||
"users/pages": { req: TODO; res: TODO };
|
||||
"users/recommendation": { req: TODO; res: TODO };
|
||||
"users/relation": { req: TODO; res: TODO };
|
||||
"users/report-abuse": { req: TODO; res: TODO };
|
||||
|
|
|
@ -64,6 +64,8 @@ export type UserDetailed = UserLite & {
|
|||
notesCount: number;
|
||||
pinnedNoteIds: ID[];
|
||||
pinnedNotes: Note[];
|
||||
pinnedPage: Page | null;
|
||||
pinnedPageId: string | null;
|
||||
publicReactions: boolean;
|
||||
securityKeys: boolean;
|
||||
twoFactorEnabled: boolean;
|
||||
|
@ -306,6 +308,36 @@ export type Stats = {
|
|||
driveUsageRemote: number;
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
updatedAt: DateString;
|
||||
userId: User["id"];
|
||||
user: User;
|
||||
content: Record<string, any>[];
|
||||
variables: Record<string, any>[];
|
||||
title: string;
|
||||
name: string;
|
||||
summary: string | null;
|
||||
hideTitleWhenPinned: boolean;
|
||||
alignCenter: boolean;
|
||||
font: string;
|
||||
script: string;
|
||||
eyeCatchingImageId: DriveFile["id"] | null;
|
||||
eyeCatchingImage: DriveFile | null;
|
||||
attachedFiles: any;
|
||||
likedCount: number;
|
||||
isLiked?: boolean;
|
||||
};
|
||||
|
||||
export type PageEvent = {
|
||||
pageId: Page["id"];
|
||||
event: string;
|
||||
var: any;
|
||||
userId: User["id"];
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type Announcement = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,6 +13,7 @@
|
|||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/gulp": "4.0.11",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"vue": "3.3.4",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
"vue-plyr": "^7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue3-otp-input": "^0.4.1",
|
||||
"vuedraggable": "4.1.0"
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import { computed, inject, ref, toRaw } from "vue";
|
||||
import { computed, inject, onMounted, ref, toRaw } from "vue";
|
||||
import * as mfm from "mfm-js";
|
||||
import type * as misskey from "calckey-js";
|
||||
import XNoteSub from "@/components/MagNoteSub.vue";
|
||||
|
@ -238,7 +238,7 @@ import { getWordSoftMute } from "@/scripts/check-word-mute";
|
|||
import { useRouter } from "@/router";
|
||||
import { userPage } from "@/filters/user";
|
||||
import * as os from "@/os";
|
||||
import { defaultStore } from "@/store";
|
||||
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||
import { $i } from "@/account";
|
||||
import { i18n } from "@/i18n";
|
||||
|
@ -276,6 +276,17 @@ const softMuteReasonI18nSrc = (what?: string) => {
|
|||
return i18n.ts.userSaysSomething;
|
||||
};
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = structuredClone(toRaw(note));
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
}
|
||||
|
||||
const el = ref<HTMLElement | null>(null);
|
||||
const footerEl = ref<HTMLElement | null>(null);
|
||||
const menuButton = ref<HTMLElement | null>(null);
|
||||
|
|
|
@ -164,7 +164,7 @@ import { pleaseLogin } from "@/scripts/please-login";
|
|||
import { getWordSoftMute } from "@/scripts/check-word-mute";
|
||||
import { userPage } from "@/filters/user";
|
||||
import * as os from "@/os";
|
||||
import { defaultStore } from "@/store";
|
||||
import { defaultStore, noteViewInterruptors } from "@/store";
|
||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||
import { $i } from "@/account";
|
||||
import { i18n } from "@/i18n";
|
||||
|
@ -200,6 +200,17 @@ const softMuteReasonI18nSrc = (what?: string) => {
|
|||
return i18n.ts.userSaysSomething;
|
||||
};
|
||||
|
||||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result = structuredClone(toRaw(note));
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
}
|
||||
|
||||
const el = ref<HTMLElement | null>(null);
|
||||
const noteEl = ref<HTMLElement | null>(null);
|
||||
const menuButton = ref<HTMLElement | null>(null);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<template v-for="item in items">
|
||||
<button
|
||||
v-if="item.action"
|
||||
v-click-anime
|
||||
class="_button"
|
||||
@click="
|
||||
($event) => {
|
||||
|
@ -32,7 +33,12 @@
|
|||
><i class="ph-circle ph-fill"></i
|
||||
></span>
|
||||
</button>
|
||||
<MkA v-else :to="item.to" @click.passive="close()">
|
||||
<MkA
|
||||
v-else
|
||||
v-click-anime
|
||||
:to="item.to"
|
||||
@click.passive="close()"
|
||||
>
|
||||
<i class="icon" :class="item.icon"></i>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
<span v-if="item.indicate" class="indicator"
|
||||
|
@ -46,11 +52,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import { navbarItemDef } from "@/navbar";
|
||||
import { instanceName } from "@/config";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
import { deviceKind } from "@/scripts/device-kind";
|
||||
import * as os from "@/os";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<MkA
|
||||
:to="`/@${page.user.username}/pages/${page.name}`"
|
||||
class="vhpxefrj _block"
|
||||
tabindex="-1"
|
||||
:behavior="`${ui === 'deck' ? 'window' : null}`"
|
||||
>
|
||||
<div
|
||||
v-if="page.eyeCatchingImage"
|
||||
class="thumbnail"
|
||||
:style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"
|
||||
></div>
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="page.title">{{ page.title }}</h1>
|
||||
</header>
|
||||
<p v-if="page.summary" :title="page.summary">
|
||||
{{
|
||||
page.summary.length > 85
|
||||
? page.summary.slice(0, 85) + "…"
|
||||
: page.summary
|
||||
}}
|
||||
</p>
|
||||
<footer>
|
||||
<img
|
||||
class="icon"
|
||||
:src="page.user.avatarUrl"
|
||||
aria-label="none"
|
||||
/>
|
||||
<p>{{ userName(page.user) }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { userName } from "@/filters/user";
|
||||
import { ui } from "@/config";
|
||||
|
||||
defineProps<{
|
||||
page: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vhpxefrj {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> button {
|
||||
font-size: 3.5em;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
font-size: 4em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
& + article {
|
||||
left: 100px;
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
> 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) {
|
||||
> .thumbnail {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
|
||||
& + article {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
font-size: 12px;
|
||||
|
||||
> .thumbnail {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 10px;
|
||||
|
||||
> .thumbnail {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 8px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 4px;
|
||||
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -15,6 +15,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="$props.editId == null"
|
||||
v-click-anime
|
||||
v-tooltip="i18n.ts.switchAccount"
|
||||
class="account _button"
|
||||
@click="openAccountMenu"
|
||||
|
@ -197,6 +198,14 @@
|
|||
>
|
||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="postFormActions.length > 0"
|
||||
v-tooltip="i18n.ts.plugin"
|
||||
class="_button"
|
||||
@click="showActions"
|
||||
>
|
||||
<i class="ph-plug ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<!-- v-if="showMfmCheatsheet" -->
|
||||
<button
|
||||
v-tooltip="i18n.ts._mfm.cheatSheet"
|
||||
|
@ -236,7 +245,7 @@ import { formatTimeString } from "@/scripts/format-time-string";
|
|||
import { Autocomplete } from "@/scripts/autocomplete";
|
||||
import * as os from "@/os";
|
||||
import { selectFiles } from "@/scripts/select-file";
|
||||
import { defaultStore } from "@/store";
|
||||
import { defaultStore, notePostInterruptors, postFormActions } from "@/store";
|
||||
import MkInfo from "@/components/MkInfo.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import { instance } from "@/instance";
|
||||
|
@ -838,6 +847,13 @@ async function post() {
|
|||
: hashtags_;
|
||||
}
|
||||
|
||||
// plugin
|
||||
if (notePostInterruptors.length > 0) {
|
||||
for (const interruptor of notePostInterruptors) {
|
||||
postData = await interruptor.handler(structuredClone(postData));
|
||||
}
|
||||
}
|
||||
|
||||
let token = undefined;
|
||||
|
||||
if (postAccount) {
|
||||
|
@ -896,6 +912,27 @@ async function openCheatSheet(ev: MouseEvent) {
|
|||
os.popup(XCheatSheet, {}, {}, "closed");
|
||||
}
|
||||
|
||||
function showActions(ev) {
|
||||
os.popupMenu(
|
||||
postFormActions.map((action) => ({
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(
|
||||
{
|
||||
text: text,
|
||||
},
|
||||
(key, value) => {
|
||||
if (key === "text") {
|
||||
text = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
})),
|
||||
ev.currentTarget ?? ev.target
|
||||
);
|
||||
}
|
||||
|
||||
let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
|
||||
|
||||
function openAccountMenu(ev: MouseEvent) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h } from "vue";
|
||||
import { defineComponent, h, resolveDirective, withDirectives } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
@ -24,6 +24,7 @@ export default defineComponent({
|
|||
role: "tablist",
|
||||
},
|
||||
options.map((option) =>
|
||||
withDirectives(
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
|
@ -42,6 +43,8 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
option.children
|
||||
),
|
||||
[[resolveDirective("click-anime")]]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div ref="rootEl" class="meijqfqm">
|
||||
<canvas
|
||||
:id="idForCanvas"
|
||||
ref="canvasEl"
|
||||
class="canvas"
|
||||
:width="width"
|
||||
height="300"
|
||||
@contextmenu.prevent="() => {}"
|
||||
></canvas>
|
||||
<div :id="idForTags" ref="tagsEl" class="tags">
|
||||
<ul>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch, PropType, onBeforeUnmount } from "vue";
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
const loaded = !!window.TagCanvas;
|
||||
const SAFE_FOR_HTML_ID = "abcdefghijklmnopqrstuvwxyz";
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const idForCanvas = Array.from(Array(16))
|
||||
.map(
|
||||
() =>
|
||||
SAFE_FOR_HTML_ID[
|
||||
Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)
|
||||
]
|
||||
)
|
||||
.join("");
|
||||
const idForTags = Array.from(Array(16))
|
||||
.map(
|
||||
() =>
|
||||
SAFE_FOR_HTML_ID[
|
||||
Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)
|
||||
]
|
||||
)
|
||||
.join("");
|
||||
let available = $ref(false);
|
||||
let rootEl = $ref<HTMLElement | null>(null);
|
||||
let canvasEl = $ref<HTMLCanvasElement | null>(null);
|
||||
let tagsEl = $ref<HTMLElement | null>(null);
|
||||
let width = $ref(300);
|
||||
|
||||
watch($$(available), () => {
|
||||
try {
|
||||
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||
textColour: "#ffffff",
|
||||
outlineColour: tinycolor(
|
||||
computedStyle.getPropertyValue("--accent")
|
||||
).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.03, -0.01],
|
||||
frontSelect: true,
|
||||
imageRadius: 8,
|
||||
//dragControl: true,
|
||||
dragThreshold: 3,
|
||||
wheelZoom: false,
|
||||
reverse: true,
|
||||
depth: 0.5,
|
||||
maxSpeed: 0.2,
|
||||
minSpeed: 0.003,
|
||||
stretchX: 0.8,
|
||||
stretchY: 0.8,
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
width = rootEl.offsetWidth;
|
||||
|
||||
if (loaded) {
|
||||
available = true;
|
||||
} else {
|
||||
document.head
|
||||
.appendChild(
|
||||
Object.assign(document.createElement("script"), {
|
||||
async: true,
|
||||
src: "/client-assets/tagcanvas.min.js",
|
||||
})
|
||||
)
|
||||
.addEventListener("load", () => (available = true));
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (window.TagCanvas) window.TagCanvas.Delete(idForCanvas);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
update: () => {
|
||||
window.TagCanvas.Update(idForCanvas);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.meijqfqm {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
> .canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .tags {
|
||||
position: absolute;
|
||||
top: 999px;
|
||||
left: 999px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<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>
|
|
@ -0,0 +1,70 @@
|
|||
<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>
|
|
@ -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>
|
|
@ -0,0 +1,56 @@
|
|||
<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 { 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>
|
|
@ -0,0 +1,37 @@
|
|||
<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>
|
|
@ -0,0 +1,37 @@
|
|||
<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>
|
|
@ -0,0 +1,61 @@
|
|||
<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/MagNote.vue";
|
||||
import XNoteDetailed from "@/components/MagNoteDetailed.vue";
|
||||
import * as os from "@/os";
|
||||
import { NoteBlock } from "@/scripts/hpml/block";
|
||||
import { endpoints, packed } from "magnetar-common";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote,
|
||||
XNoteDetailed,
|
||||
},
|
||||
props: {
|
||||
block: {
|
||||
type: Object as PropType<NoteBlock>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const note: Ref<packed.PackNoteMaybeFull | null> = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.block.note) return;
|
||||
|
||||
os.magApi(
|
||||
endpoints.GetNoteById,
|
||||
{ context: props.block.detailed, attachments: true },
|
||||
{ id: props.block.note }
|
||||
).then((result) => {
|
||||
note.value = result;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
note,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.voxdxuby {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkInput
|
||||
class="kudkigyw"
|
||||
:model-value="value"
|
||||
type="number"
|
||||
@update:modelValue="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>
|
|
@ -0,0 +1,121 @@
|
|||
<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="ph-check ph-bold ph-lg"></i>
|
||||
<i v-else class="ph-paper-plane-tilt ph-bold ph-lg"></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);
|
||||
if (this.$store.state.uploadFolder) {
|
||||
formData.append(
|
||||
"folderId",
|
||||
this.$store.state.uploadFolder
|
||||
);
|
||||
}
|
||||
|
||||
fetch(apiUrl + "/drive/files/create", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
authorization: `Bearer ${this.$i.token}`,
|
||||
},
|
||||
})
|
||||
.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>
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div>
|
||||
<div>{{ hpml.interpolate(block.title) }}</div>
|
||||
<MkRadio
|
||||
v-for="item in block.values"
|
||||
:key="item"
|
||||
:modelValue="value"
|
||||
:value="item"
|
||||
@update:modelValue="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>
|
|
@ -0,0 +1,66 @@
|
|||
<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>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="hkcxmtwj">
|
||||
<MkSwitch
|
||||
:model-value="value"
|
||||
@update:modelValue="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>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkInput
|
||||
class="kudkigyw"
|
||||
:model-value="value"
|
||||
type="text"
|
||||
@update:modelValue="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>
|
|
@ -0,0 +1,67 @@
|
|||
<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>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkTextarea
|
||||
:model-value="value"
|
||||
@update:modelValue="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>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<MkTextarea :model-value="text" readonly></MkTextarea>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from "vue";
|
||||
import MkTextarea from "../form/textarea.vue";
|
||||
import { TextBlock } from "@/scripts/hpml/block";
|
||||
import { Hpml } from "@/scripts/hpml/evaluator";
|
||||
|
||||
const props = defineProps<{
|
||||
block: TextBlock;
|
||||
hpml: Hpml;
|
||||
}>();
|
||||
|
||||
let text = $ref("");
|
||||
|
||||
watch(
|
||||
props.hpml.vars,
|
||||
() => {
|
||||
text = props.hpml.interpolate(props.block.text) as string;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,104 @@
|
|||
<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>
|
|
@ -0,0 +1,31 @@
|
|||
import { Directive } from "vue";
|
||||
import { defaultStore } from "@/store";
|
||||
|
||||
export default {
|
||||
mounted(el, binding, vn) {
|
||||
/*
|
||||
if (!defaultStore.state.animation) return;
|
||||
|
||||
el.classList.add('_anime_bounce_standBy');
|
||||
|
||||
el.addEventListener('mousedown', () => {
|
||||
el.classList.add('_anime_bounce_standBy');
|
||||
el.classList.add('_anime_bounce_ready');
|
||||
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.classList.remove('_anime_bounce_ready');
|
||||
});
|
||||
});
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
el.classList.add('_anime_bounce');
|
||||
});
|
||||
|
||||
el.addEventListener('animationend', () => {
|
||||
el.classList.remove('_anime_bounce_ready');
|
||||
el.classList.remove('_anime_bounce');
|
||||
el.classList.add('_anime_bounce_standBy');
|
||||
});
|
||||
*/
|
||||
},
|
||||
} as Directive;
|
|
@ -8,6 +8,7 @@ import tooltip from "./tooltip";
|
|||
import hotkey from "./hotkey";
|
||||
import appear from "./appear";
|
||||
import anim from "./anim";
|
||||
import clickAnime from "./click-anime";
|
||||
import panel from "./panel";
|
||||
import adaptiveBorder from "./adaptive-border";
|
||||
import focus from "./focus";
|
||||
|
@ -22,6 +23,7 @@ export default function (app: App) {
|
|||
app.directive("hotkey", hotkey);
|
||||
app.directive("appear", appear);
|
||||
app.directive("anim", anim);
|
||||
app.directive("click-anime", clickAnime);
|
||||
app.directive("panel", panel);
|
||||
app.directive("adaptive-border", adaptiveBorder);
|
||||
app.directive("focus", focus);
|
||||
|
|
|
@ -437,6 +437,14 @@ function checkForSplash() {
|
|||
//store.commit('instance/set', );
|
||||
});
|
||||
|
||||
for (const plugin of ColdDeviceStorage.get("plugins").filter(
|
||||
(p) => p.active
|
||||
)) {
|
||||
import("./plugin").then(({ install }) => {
|
||||
install(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
const hotkeys = {
|
||||
d: (): void => {
|
||||
defaultStore.set("darkMode", !defaultStore.state.darkMode);
|
||||
|
|
|
@ -63,6 +63,11 @@ export const navbarItemDef = reactive({
|
|||
show: computed(() => $i != null),
|
||||
to: "/my/favorites",
|
||||
},
|
||||
pages: {
|
||||
title: "pages",
|
||||
icon: "ph-file-text ph-bold ph-lg",
|
||||
to: "/pages",
|
||||
},
|
||||
gallery: {
|
||||
title: "gallery",
|
||||
icon: "ph-image-square ph-bold ph-lg",
|
||||
|
|
|
@ -60,7 +60,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||
import {
|
||||
markRaw,
|
||||
version as vueVersion,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import XFederation from "./overview.federation.vue";
|
||||
import XInstances from "./overview.instances.vue";
|
||||
import XQueue from "./overview.queue.vue";
|
||||
|
@ -71,10 +77,14 @@ import XStats from "./overview.stats.vue";
|
|||
import XModerators from "./overview.moderators.vue";
|
||||
import XHeatmap from "./overview.heatmap.vue";
|
||||
// import XMetrics from "./overview.metrics.vue";
|
||||
import MkTagCloud from "@/components/MkTagCloud.vue";
|
||||
import { version, url } from "@/config";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { defaultStore } from "@/store";
|
||||
import MkFileListForAdmin from "@/components/MkFileListForAdmin.vue";
|
||||
import MkFolder from "@/components/MkFolder.vue";
|
||||
|
||||
const rootEl = $shallowRef<HTMLElement>();
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
<button
|
||||
v-if="$i && $i.id === post.user.id"
|
||||
v-tooltip="i18n.ts.edit"
|
||||
v-click-anime
|
||||
class="_button"
|
||||
@click="edit"
|
||||
>
|
||||
|
@ -69,6 +70,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-tooltip="i18n.ts.shareWithNote"
|
||||
v-click-anime
|
||||
class="_button"
|
||||
@click="shareWithNote"
|
||||
>
|
||||
|
@ -79,6 +81,7 @@
|
|||
<button
|
||||
v-if="shareAvailable()"
|
||||
v-tooltip="i18n.ts.share"
|
||||
v-click-anime
|
||||
class="_button"
|
||||
@click="share"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.button }}</template
|
||||
>
|
||||
|
||||
<section class="xfhsjczc">
|
||||
<MkInput v-model="value.text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._button.text
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkSwitch v-model="value.primary"
|
||||
><span>{{
|
||||
i18n.ts._pages.blocks._button.colored
|
||||
}}</span></MkSwitch
|
||||
>
|
||||
<MkSelect v-model="value.action">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._button.action
|
||||
}}</template>
|
||||
<option value="dialog">
|
||||
{{ i18n.ts._pages.blocks._button._action.dialog }}
|
||||
</option>
|
||||
<option value="resetRandom">
|
||||
{{ i18n.ts._pages.blocks._button._action.resetRandom }}
|
||||
</option>
|
||||
<option value="pushEvent">
|
||||
{{ i18n.ts._pages.blocks._button._action.pushEvent }}
|
||||
</option>
|
||||
<option value="callAiScript">
|
||||
{{ i18n.ts._pages.blocks._button._action.callAiScript }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
<template v-if="value.action === 'dialog'">
|
||||
<MkInput v-model="value.content"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._button._action._dialog.content
|
||||
}}</template></MkInput
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="value.action === 'pushEvent'">
|
||||
<MkInput v-model="value.event"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._button._action._pushEvent.event
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.message"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._button._action._pushEvent.message
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkSelect v-model="value.var">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._button._action._pushEvent
|
||||
.variable
|
||||
}}</template>
|
||||
<option :value="null">
|
||||
{{
|
||||
i18n.t(
|
||||
"_pages.blocks._button._action._pushEvent.no-variable"
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option v-for="v in hpml.getVarsByType()" :value="v.name">
|
||||
{{ v.name }}
|
||||
</option>
|
||||
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
||||
<option
|
||||
v-for="v in hpml.getPageVarsByType()"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
:label="i18n.ts._pages.script.enviromentVariables"
|
||||
>
|
||||
<option v-for="v in hpml.getEnvVarsByType()" :value="v">
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
</template>
|
||||
<template v-else-if="value.action === 'callAiScript'">
|
||||
<MkInput v-model="value.fn"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._button._action._callAiScript
|
||||
.functionName
|
||||
}}</template></MkInput
|
||||
>
|
||||
</template>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkSelect from "@/components/form/select.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
hpml: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
text: "",
|
||||
action: "dialog",
|
||||
content: null,
|
||||
event: null,
|
||||
message: null,
|
||||
primary: false,
|
||||
var: null,
|
||||
fn: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xfhsjczc {
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-paint-brush-household ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.canvas }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 0 16px">
|
||||
<MkInput v-model="value.name">
|
||||
<template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._canvas.id
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.width" type="number">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._canvas.width
|
||||
}}</template>
|
||||
<template #suffix>px</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.height" type="number">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._canvas.height
|
||||
}}</template>
|
||||
<template #suffix>px</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
width: 300,
|
||||
height: 200,
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.counter }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 0 16px">
|
||||
<MkInput v-model="value.name">
|
||||
<template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._counter.name
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.text">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._counter.text
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.inc" type="number">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._counter.inc
|
||||
}}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-question ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.if }}</template
|
||||
>
|
||||
<template #func>
|
||||
<button class="_button" @click="add()">
|
||||
<i class="ph-plus ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section class="romcojzs">
|
||||
<MkSelect v-model="value.var">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._if.variable
|
||||
}}</template>
|
||||
<option
|
||||
v-for="v in hpml.getVarsByType('boolean')"
|
||||
:value="v.name"
|
||||
>
|
||||
{{ v.name }}
|
||||
</option>
|
||||
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
||||
<option
|
||||
v-for="v in hpml.getPageVarsByType('boolean')"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts._pages.script.enviromentVariables">
|
||||
<option
|
||||
v-for="v in hpml.getEnvVarsByType('boolean')"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
|
||||
<XBlocks v-model="value.children" class="children" :hpml="hpml" />
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, inject } from "vue";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkSelect from "@/components/form/select.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const XBlocks = defineAsyncComponent(() => import("../page-editor.blocks.vue"));
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
hpml: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
children: [],
|
||||
var: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const getPageBlockList = inject<(any) => any>("getPageBlockList");
|
||||
|
||||
async function add() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
title: i18n.ts._pages.chooseBlock,
|
||||
groupedItems: getPageBlockList(),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid();
|
||||
props.value.children.push({ id, type });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.romcojzs {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-image ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.image }}</template
|
||||
>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
<i class="ph-folder-notch-open ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section class="oyyftmcf">
|
||||
<MkDriveFileThumbnail
|
||||
v-if="file"
|
||||
class="preview"
|
||||
:file="file"
|
||||
fit="contain"
|
||||
@click="choose()"
|
||||
/>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
fileId: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let file: any = $ref(null);
|
||||
|
||||
async function choose() {
|
||||
os.selectDriveFile(false).then((fileResponse: any) => {
|
||||
file = fileResponse;
|
||||
props.value.fileId = fileResponse.id;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.value.fileId == null) {
|
||||
await choose();
|
||||
} else {
|
||||
os.api("drive/files/show", {
|
||||
fileId: props.value.fileId,
|
||||
}).then((fileResponse) => {
|
||||
file = fileResponse;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oyyftmcf {
|
||||
> .preview {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-sticker ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.note }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 0 16px">
|
||||
<MkInput v-model="id">
|
||||
<template #label>{{ i18n.ts._pages.blocks._note.id }}</template>
|
||||
<template #caption>{{
|
||||
i18n.ts._pages.blocks._note.idDescription
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="value.detailed"
|
||||
><span>{{
|
||||
i18n.ts._pages.blocks._note.detailed
|
||||
}}</span></MkSwitch
|
||||
>
|
||||
|
||||
<XNote
|
||||
v-if="note && !value.detailed"
|
||||
:key="note.id + ':normal'"
|
||||
v-model:note="note"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<XNoteDetailed
|
||||
v-if="note && value.detailed"
|
||||
:key="note.id + ':detail'"
|
||||
v-model:note="note"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import XNote from "@/components/MagNote.vue";
|
||||
import XNoteDetailed from "@/components/MagNoteDetailed.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import { endpoints, packed } from "magnetar-common";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
note: null,
|
||||
detailed: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let id = ref<string | null>(props.value.note);
|
||||
let note = ref<packed.PackNoteMaybeFull | null>(null);
|
||||
|
||||
watch(
|
||||
id,
|
||||
async () => {
|
||||
if (
|
||||
id.value &&
|
||||
(id.value.startsWith("http://") || id.value.startsWith("https://"))
|
||||
) {
|
||||
props.value.note = (
|
||||
id.value.endsWith("/") ? id.value.slice(0, -1) : id.value
|
||||
)
|
||||
.split("/")
|
||||
.pop();
|
||||
} else {
|
||||
props.value.note = id.value;
|
||||
}
|
||||
|
||||
if (props.value.note) return;
|
||||
|
||||
note.value = await os.magApi(
|
||||
endpoints.GetNoteById,
|
||||
{ context: true, attachments: true },
|
||||
{ id: props.value.note }
|
||||
);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.numberInput }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 0 16px">
|
||||
<MkInput v-model="value.name">
|
||||
<template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._numberInput.name
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.text">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._numberInput.text
|
||||
}}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="value.default" type="number">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.blocks._numberInput.default
|
||||
}}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-paper-plane-tilt ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.post }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 16px">
|
||||
<MkTextarea v-model="value.text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._post.text
|
||||
}}</template></MkTextarea
|
||||
>
|
||||
<MkSwitch v-model="value.attachCanvasImage"
|
||||
><span>{{
|
||||
i18n.ts._pages.blocks._post.attachCanvasImage
|
||||
}}</span></MkSwitch
|
||||
>
|
||||
<MkInput v-if="value.attachCanvasImage" v-model="value.canvasId"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._post.canvasId
|
||||
}}</template></MkInput
|
||||
>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
text: "",
|
||||
attachCanvasImage: false,
|
||||
canvasId: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.radioButton }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 16px 16px">
|
||||
<MkInput v-model="value.name"
|
||||
><template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i></template
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._radioButton.name
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.title"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._radioButton.title
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkTextarea v-model="values"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._radioButton.values
|
||||
}}</template></MkTextarea
|
||||
>
|
||||
<MkInput v-model="value.default"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._radioButton.default
|
||||
}}</template></MkInput
|
||||
>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
title: "",
|
||||
values: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let values: string = $ref(props.value.values.join("\n"));
|
||||
|
||||
watch(
|
||||
values,
|
||||
() => {
|
||||
props.value.values = values.split("\n");
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-sticker ph-bold ph-lg"></i>
|
||||
{{ value.title }}</template
|
||||
>
|
||||
<template #func>
|
||||
<button class="_button" @click="rename()">
|
||||
<i class="ph-pencil ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button class="_button" @click="add()">
|
||||
<i class="ph-plus ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section class="ilrvjyvi">
|
||||
<XBlocks v-model="value.children" class="children" :hpml="hpml" />
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, inject, onMounted } from "vue";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const XBlocks = defineAsyncComponent(() => import("../page-editor.blocks.vue"));
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
hpml: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
title: null,
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const getPageBlockList = inject<(any) => any>("getPageBlockList");
|
||||
|
||||
async function rename() {
|
||||
const { canceled, result: title } = await os.inputText({
|
||||
title: "Enter title",
|
||||
default: props.value.title,
|
||||
});
|
||||
if (canceled) return;
|
||||
props.value.title = title;
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
title: i18n.ts._pages.chooseBlock,
|
||||
groupedItems: getPageBlockList(),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid();
|
||||
props.value.children.push({ id, type });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.value.title == null) {
|
||||
rename();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ilrvjyvi {
|
||||
> .children {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.switch }}</template
|
||||
>
|
||||
|
||||
<section class="kjuadyyj">
|
||||
<MkInput v-model="value.name"
|
||||
><template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i></template
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._switch.name
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._switch.text
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkSwitch v-model="value.default"
|
||||
><span>{{
|
||||
i18n.ts._pages.blocks._switch.default
|
||||
}}</span></MkSwitch
|
||||
>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kjuadyyj {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.textInput }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 0 16px">
|
||||
<MkInput v-model="value.name"
|
||||
><template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i></template
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textInput.name
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textInput.text
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.default" type="text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textInput.default
|
||||
}}</template></MkInput
|
||||
>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-align-left ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.text }}</template
|
||||
>
|
||||
|
||||
<section class="vckmsadr">
|
||||
<textarea v-model="value.text"></textarea>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
text: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vckmsadr {
|
||||
> textarea {
|
||||
display: block;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 150px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-lightning ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.textareaInput }}</template
|
||||
>
|
||||
|
||||
<section style="padding: 0 16px 16px 16px">
|
||||
<MkInput v-model="value.name"
|
||||
><template #prefix
|
||||
><i class="ph-magic-wand ph-bold ph-lg"></i></template
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textareaInput.name
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkInput v-model="value.text"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textareaInput.text
|
||||
}}</template></MkInput
|
||||
>
|
||||
<MkTextarea v-model="value.default"
|
||||
><template #label>{{
|
||||
i18n.ts._pages.blocks._textareaInput.default
|
||||
}}</template></MkTextarea
|
||||
>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
name: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<template #header
|
||||
><i class="ph-align-left ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.blocks.textarea }}</template
|
||||
>
|
||||
|
||||
<section class="ihymsbbe">
|
||||
<textarea v-model="value.text"></textarea>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XContainer from "../page-editor.container.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
value: any;
|
||||
}>(),
|
||||
{
|
||||
value: {
|
||||
text: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ihymsbbe {
|
||||
> textarea {
|
||||
display: block;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 150px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<XDraggable
|
||||
v-model="blocks"
|
||||
tag="div"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
:group="{ name: 'blocks' }"
|
||||
animation="150"
|
||||
swap-threshold="0.5"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<component
|
||||
:is="'x-' + element.type"
|
||||
:value="element"
|
||||
:hpml="hpml"
|
||||
@update:value="updateItem"
|
||||
@remove="() => removeItem(element)"
|
||||
/>
|
||||
</template>
|
||||
</XDraggable>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from "vue";
|
||||
import XSection from "./els/page-editor.el.section.vue";
|
||||
import XText from "./els/page-editor.el.text.vue";
|
||||
import XTextarea from "./els/page-editor.el.textarea.vue";
|
||||
import XImage from "./els/page-editor.el.image.vue";
|
||||
import XButton from "./els/page-editor.el.button.vue";
|
||||
import XTextInput from "./els/page-editor.el.text-input.vue";
|
||||
import XTextareaInput from "./els/page-editor.el.textarea-input.vue";
|
||||
import XNumberInput from "./els/page-editor.el.number-input.vue";
|
||||
import XSwitch from "./els/page-editor.el.switch.vue";
|
||||
import XIf from "./els/page-editor.el.if.vue";
|
||||
import XPost from "./els/page-editor.el.post.vue";
|
||||
import XCounter from "./els/page-editor.el.counter.vue";
|
||||
import XRadioButton from "./els/page-editor.el.radio-button.vue";
|
||||
import XCanvas from "./els/page-editor.el.canvas.vue";
|
||||
import XNote from "./els/page-editor.el.note.vue";
|
||||
import * as os from "@/os";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XDraggable: defineAsyncComponent(() =>
|
||||
import("vuedraggable").then((x) => x.default)
|
||||
),
|
||||
XSection,
|
||||
XText,
|
||||
XImage,
|
||||
XButton,
|
||||
XTextarea,
|
||||
XTextInput,
|
||||
XTextareaInput,
|
||||
XNumberInput,
|
||||
XSwitch,
|
||||
XIf,
|
||||
XPost,
|
||||
XCounter,
|
||||
XRadioButton,
|
||||
XCanvas,
|
||||
XNote,
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
hpml: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ["update:modelValue"],
|
||||
|
||||
computed: {
|
||||
blocks: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateItem(v) {
|
||||
const i = this.blocks.findIndex((x) => x.id === v.id);
|
||||
const newValue = [
|
||||
...this.blocks.slice(0, i),
|
||||
v,
|
||||
...this.blocks.slice(i + 1),
|
||||
];
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
|
||||
removeItem(el) {
|
||||
const i = this.blocks.findIndex((x) => x.id === el.id);
|
||||
const newValue = [
|
||||
...this.blocks.slice(0, i),
|
||||
...this.blocks.slice(i + 1),
|
||||
];
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
|
||||
<header>
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="buttons">
|
||||
<slot name="func"></slot>
|
||||
<button v-if="removable" class="_button" @click="remove()">
|
||||
<i class="ph-trash ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button v-if="draggable" class="drag-handle _button">
|
||||
<i class="ph-list ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button class="_button" @click="toggleContent(!showBody)">
|
||||
<template v-if="showBody"
|
||||
><i class="ph-caret-up ph-bold ph-lg"></i
|
||||
></template>
|
||||
<template v-else
|
||||
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||
></template>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<p v-show="showBody" v-if="error != null" class="error">
|
||||
{{
|
||||
i18n.t("_pages.script.typeError", {
|
||||
slot: error.arg + 1,
|
||||
expect: i18n.t(`script.types.${error.expect}`),
|
||||
actual: i18n.t(`script.types.${error.actual}`),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p v-show="showBody" v-if="warn != null" class="warn">
|
||||
{{
|
||||
i18n.t("_pages.script.thereIsEmptySlot", {
|
||||
slot: warn.slot + 1,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div v-show="showBody" class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
removable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
warn: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["toggle", "remove"],
|
||||
data() {
|
||||
return {
|
||||
showBody: this.expanded,
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleContent(show: boolean) {
|
||||
this.showBody = show;
|
||||
this.$emit("toggle", show);
|
||||
},
|
||||
remove() {
|
||||
this.$emit("remove");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cpjygsrt {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--panel);
|
||||
border: solid 2px var(--X12);
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
border: solid 2px var(--X13);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
border: solid 2px #f6c177;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border: solid 2px #eb6f92;
|
||||
}
|
||||
|
||||
& + .cpjygsrt {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
> header {
|
||||
> .title {
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
line-height: 42px;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 1px rgba(#000, 0.07);
|
||||
|
||||
> i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
> button {
|
||||
padding: 0;
|
||||
width: 42px;
|
||||
font-size: 0.9em;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .warn {
|
||||
color: #ea9d34;
|
||||
margin: 0;
|
||||
padding: 16px 16px 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .error {
|
||||
color: #b4637a;
|
||||
margin: 0;
|
||||
padding: 16px 16px 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
::v-deep(.juejbjww),
|
||||
::v-deep(.eiipwacr) {
|
||||
&:not(.inline):first-child {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
&:not(.inline):last-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,408 @@
|
|||
<template>
|
||||
<XContainer
|
||||
:removable="removable"
|
||||
:error="error"
|
||||
:warn="warn"
|
||||
:draggable="draggable"
|
||||
@remove="() => $emit('remove')"
|
||||
>
|
||||
<template #header
|
||||
><i v-if="icon" :class="icon"></i>
|
||||
<template v-if="title"
|
||||
>{{ title }}
|
||||
<span v-if="typeText" class="turmquns"
|
||||
>({{ typeText }})</span
|
||||
></template
|
||||
><template v-else-if="typeText">{{ typeText }}</template></template
|
||||
>
|
||||
<template #func>
|
||||
<button class="_button" @click="changeType()">
|
||||
<i class="ph-pencil ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section
|
||||
v-if="modelValue.type === null"
|
||||
class="pbglfege"
|
||||
@click="changeType()"
|
||||
>
|
||||
{{ i18n.ts._pages.script.emptySlot }}
|
||||
</section>
|
||||
<section v-else-if="modelValue.type === 'text'" class="tbwccoaw">
|
||||
<input v-model="modelValue.value" />
|
||||
</section>
|
||||
<section
|
||||
v-else-if="modelValue.type === 'multiLineText'"
|
||||
class="tbwccoaw"
|
||||
>
|
||||
<textarea v-model="modelValue.value"></textarea>
|
||||
</section>
|
||||
<section v-else-if="modelValue.type === 'textList'" class="tbwccoaw">
|
||||
<textarea
|
||||
v-model="modelValue.value"
|
||||
:placeholder="i18n.ts._pages.script.blocks._textList.info"
|
||||
></textarea>
|
||||
</section>
|
||||
<section v-else-if="modelValue.type === 'number'" class="tbwccoaw">
|
||||
<input v-model="modelValue.value" type="number" />
|
||||
</section>
|
||||
<section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs">
|
||||
<select v-model="modelValue.value">
|
||||
<option
|
||||
v-for="v in hpml
|
||||
.getVarsByType(
|
||||
getExpectedType ? getExpectedType() : null
|
||||
)
|
||||
.filter((x) => x.name !== name)"
|
||||
:value="v.name"
|
||||
>
|
||||
{{ v.name }}
|
||||
</option>
|
||||
<optgroup :label="i18n.ts._pages.script.argVariables">
|
||||
<option v-for="v in fnSlots" :value="v.name">
|
||||
{{ v.name }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts._pages.script.pageVariables">
|
||||
<option
|
||||
v-for="v in hpml.getPageVarsByType(
|
||||
getExpectedType ? getExpectedType() : null
|
||||
)"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup :label="i18n.ts._pages.script.enviromentVariables">
|
||||
<option
|
||||
v-for="v in hpml.getEnvVarsByType(
|
||||
getExpectedType ? getExpectedType() : null
|
||||
)"
|
||||
:value="v"
|
||||
>
|
||||
{{ v }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</section>
|
||||
<section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw">
|
||||
<input v-model="modelValue.value" />
|
||||
</section>
|
||||
<section
|
||||
v-else-if="modelValue.type === 'fn'"
|
||||
class=""
|
||||
style="padding: 0 16px 16px 16px"
|
||||
>
|
||||
<MkTextarea v-model="slots">
|
||||
<template #label>{{
|
||||
i18n.ts._pages.script.blocks._fn.slots
|
||||
}}</template>
|
||||
<template #caption>{{
|
||||
i18n.t("_pages.script.blocks._fn.slots-info")
|
||||
}}</template>
|
||||
</MkTextarea>
|
||||
<XV
|
||||
v-if="modelValue.value.expression"
|
||||
v-model="modelValue.value.expression"
|
||||
:title="i18n.t(`_pages.script.blocks._fn.arg1`)"
|
||||
:get-expected-type="() => null"
|
||||
:hpml="hpml"
|
||||
:fn-slots="modelValue.value.slots"
|
||||
:name="name"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
v-else-if="modelValue.type.startsWith('fn:')"
|
||||
class=""
|
||||
style="padding: 16px"
|
||||
>
|
||||
<XV
|
||||
v-for="(x, i) in modelValue.args"
|
||||
:key="i"
|
||||
v-model="modelValue.args[i]"
|
||||
:title="
|
||||
hpml.getVarByName(modelValue.type.split(':')[1]).value
|
||||
.slots[i].name
|
||||
"
|
||||
:get-expected-type="() => null"
|
||||
:hpml="hpml"
|
||||
:name="name"
|
||||
/>
|
||||
</section>
|
||||
<section v-else class="" style="padding: 16px">
|
||||
<XV
|
||||
v-for="(x, i) in modelValue.args"
|
||||
:key="i"
|
||||
v-model="modelValue.args[i]"
|
||||
:title="
|
||||
i18n.t(
|
||||
`_pages.script.blocks._${modelValue.type}.arg${i + 1}`
|
||||
)
|
||||
"
|
||||
:get-expected-type="() => _getExpectedType(i)"
|
||||
:hpml="hpml"
|
||||
:name="name"
|
||||
:fn-slots="fnSlots"
|
||||
/>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from "vue";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import XContainer from "./page-editor.container.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import { blockDefs } from "@/scripts/hpml/index";
|
||||
import * as os from "@/os";
|
||||
import { isLiteralValue } from "@/scripts/hpml/expr";
|
||||
import { funcDefs } from "@/scripts/hpml/lib";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XContainer,
|
||||
MkTextarea,
|
||||
XV: defineAsyncComponent(
|
||||
() => import("./page-editor.script-block.vue")
|
||||
),
|
||||
},
|
||||
|
||||
inject: ["getScriptBlockList"],
|
||||
|
||||
props: {
|
||||
getExpectedType: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
required: false,
|
||||
},
|
||||
removable: {
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hpml: {
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
fnSlots: {
|
||||
required: false,
|
||||
},
|
||||
draggable: {
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
warn: null,
|
||||
slots: "",
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
icon(): any {
|
||||
if (this.modelValue.type === null) return null;
|
||||
if (this.modelValue.type.startsWith("fn:"))
|
||||
return "ph-plug ph-bold ph-lg";
|
||||
return blockDefs.find((x) => x.type === this.modelValue.type).icon;
|
||||
},
|
||||
typeText(): any {
|
||||
if (this.modelValue.type === null) return null;
|
||||
if (this.modelValue.type.startsWith("fn:"))
|
||||
return this.modelValue.type.split(":")[1];
|
||||
return i18n.t(`_pages.script.blocks.${this.modelValue.type}`);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
slots: {
|
||||
handler() {
|
||||
this.modelValue.value.slots = this.slots
|
||||
.split("\n")
|
||||
.map((x) => ({
|
||||
name: x,
|
||||
type: null,
|
||||
}));
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.modelValue.value == null) this.modelValue.value = null;
|
||||
|
||||
if (this.modelValue.value && this.modelValue.value.slots)
|
||||
this.slots = this.modelValue.value.slots
|
||||
.map((x) => x.name)
|
||||
.join("\n");
|
||||
|
||||
this.$watch(
|
||||
() => this.modelValue.type,
|
||||
(t) => {
|
||||
this.warn = null;
|
||||
|
||||
if (this.modelValue.type === "fn") {
|
||||
const id = uuid();
|
||||
this.modelValue.value = {
|
||||
slots: [],
|
||||
expression: { id, type: null },
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.modelValue.type &&
|
||||
this.modelValue.type.startsWith("fn:")
|
||||
) {
|
||||
const fnName = this.modelValue.type.split(":")[1];
|
||||
const fn = this.hpml.getVarByName(fnName);
|
||||
|
||||
const empties = [];
|
||||
for (let i = 0; i < fn.value.slots.length; i++) {
|
||||
const id = uuid();
|
||||
empties.push({ id, type: null });
|
||||
}
|
||||
this.modelValue.args = empties;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLiteralValue(this.modelValue)) return;
|
||||
|
||||
const empties = [];
|
||||
for (
|
||||
let i = 0;
|
||||
i < funcDefs[this.modelValue.type].in.length;
|
||||
i++
|
||||
) {
|
||||
const id = uuid();
|
||||
empties.push({ id, type: null });
|
||||
}
|
||||
this.modelValue.args = empties;
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < funcDefs[this.modelValue.type].in.length;
|
||||
i++
|
||||
) {
|
||||
const inType = funcDefs[this.modelValue.type].in[i];
|
||||
if (typeof inType !== "number") {
|
||||
if (inType === "number")
|
||||
this.modelValue.args[i].type = "number";
|
||||
if (inType === "string")
|
||||
this.modelValue.args[i].type = "text";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.$watch(
|
||||
() => this.modelValue.args,
|
||||
(args) => {
|
||||
if (args == null) {
|
||||
this.warn = null;
|
||||
return;
|
||||
}
|
||||
const emptySlotIndex = args.findIndex((x) => x.type === null);
|
||||
if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
|
||||
this.warn = {
|
||||
slot: emptySlotIndex,
|
||||
};
|
||||
} else {
|
||||
this.warn = null;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
this.$watch(
|
||||
() => this.hpml.variables,
|
||||
() => {
|
||||
if (this.type != null && this.modelValue) {
|
||||
this.error = this.hpml.typeCheck(this.modelValue);
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async changeType() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
title: i18n.ts._pages.selectType,
|
||||
groupedItems: this.getScriptBlockList(
|
||||
this.getExpectedType ? this.getExpectedType() : null
|
||||
),
|
||||
});
|
||||
if (canceled) return;
|
||||
this.modelValue.type = type;
|
||||
},
|
||||
|
||||
_getExpectedType(slot: number) {
|
||||
return this.hpml.getExpectedType(this.modelValue, slot);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.turmquns {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.pbglfege {
|
||||
opacity: 0.5;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.tbwccoaw {
|
||||
> input,
|
||||
> textarea {
|
||||
display: block;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.hpdwcrvs {
|
||||
padding: 16px;
|
||||
|
||||
> select {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,645 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
v-model:tab="tab"
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="jqqmcavi">
|
||||
<MkButton
|
||||
v-if="pageId"
|
||||
class="button"
|
||||
inline
|
||||
link
|
||||
:to="`/@${author.username}/pages/${currentName}`"
|
||||
><i class="ph-arrow-square-out ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.viewPage }}</MkButton
|
||||
>
|
||||
<MkButton
|
||||
v-if="!readonly"
|
||||
inline
|
||||
primary
|
||||
class="button"
|
||||
@click="save"
|
||||
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.save }}</MkButton
|
||||
>
|
||||
<MkButton v-if="pageId" inline class="button" @click="duplicate"
|
||||
><i class="ph-clipboard-text ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.duplicate }}</MkButton
|
||||
>
|
||||
<MkButton
|
||||
v-if="pageId && !readonly"
|
||||
inline
|
||||
class="button"
|
||||
danger
|
||||
@click="del"
|
||||
><i class="ph-trash ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.delete }}</MkButton
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'settings'">
|
||||
<div class="_formRoot">
|
||||
<MkInput v-model="title" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._pages.title }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="summary" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._pages.summary }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<template #prefix
|
||||
>{{ url }}/@{{ author.username }}/pages/</template
|
||||
>
|
||||
<template #label>{{ i18n.ts._pages.url }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSwitch v-model="isPublic" class="_formBlock">{{
|
||||
i18n.ts.public
|
||||
}}</MkSwitch>
|
||||
<MkSwitch v-model="alignCenter" class="_formBlock">{{
|
||||
i18n.ts._pages.alignCenter
|
||||
}}</MkSwitch>
|
||||
|
||||
<MkSelect v-model="font" class="_formBlock">
|
||||
<template #label>{{ i18n.ts._pages.font }}</template>
|
||||
<option value="serif">
|
||||
{{ i18n.ts._pages.fontSerif }}
|
||||
</option>
|
||||
<option value="sans-serif">
|
||||
{{ i18n.ts._pages.fontSansSerif }}
|
||||
</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSwitch
|
||||
v-model="hideTitleWhenPinned"
|
||||
class="_formBlock"
|
||||
>{{ i18n.ts._pages.hideTitleWhenPinned }}</MkSwitch
|
||||
>
|
||||
|
||||
<div class="eyeCatch">
|
||||
<MkButton
|
||||
v-if="eyeCatchingImageId == null && !readonly"
|
||||
@click="setEyeCatchingImage"
|
||||
><i class="ph-plus ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton
|
||||
>
|
||||
<div v-else-if="eyeCatchingImage">
|
||||
<img
|
||||
:src="eyeCatchingImage.url"
|
||||
:alt="eyeCatchingImage.name"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
<MkButton
|
||||
v-if="!readonly"
|
||||
@click="removeEyeCatchingImage()"
|
||||
><i class="ph-trash ph-bold ph-lg"></i>
|
||||
{{
|
||||
i18n.ts._pages.eyeCatchingImageRemove
|
||||
}}</MkButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'contents'">
|
||||
<div>
|
||||
<XBlocks v-model="content" class="content" :hpml="hpml" />
|
||||
<MkButton v-if="!readonly" @click="add()"
|
||||
><i class="ph-plus ph-bold ph-lg"></i
|
||||
></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'variables'">
|
||||
<div class="qmuvgica">
|
||||
<XDraggable
|
||||
v-show="variables.length > 0"
|
||||
v-model="variables"
|
||||
tag="div"
|
||||
class="variables"
|
||||
item-key="name"
|
||||
handle=".drag-handle"
|
||||
:group="{ name: 'variables' }"
|
||||
animation="150"
|
||||
swap-threshold="0.5"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<XVariable
|
||||
:model-value="element"
|
||||
:removable="true"
|
||||
:hpml="hpml"
|
||||
:name="element.name"
|
||||
:title="element.name"
|
||||
:draggable="true"
|
||||
@remove="() => removeVariable(element)"
|
||||
/>
|
||||
</template>
|
||||
</XDraggable>
|
||||
|
||||
<MkButton
|
||||
v-if="!readonly"
|
||||
class="add"
|
||||
@click="addVariable()"
|
||||
><i class="ph-plus ph-bold ph-lg"></i
|
||||
></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'script'">
|
||||
<div>
|
||||
<MkTextarea v-model="script" class="_code" />
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed, provide, watch } from "vue";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import XVariable from "./page-editor.script-block.vue";
|
||||
import XBlocks from "./page-editor.blocks.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import MkSelect from "@/components/form/select.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import { blockDefs } from "@/scripts/hpml/index";
|
||||
import { HpmlTypeChecker } from "@/scripts/hpml/type-checker";
|
||||
import { url } from "@/config";
|
||||
import { collectPageVars } from "@/scripts/collect-page-vars";
|
||||
import * as os from "@/os";
|
||||
import { selectFile } from "@/scripts/select-file";
|
||||
import { mainRouter } from "@/router";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { $i } from "@/account";
|
||||
|
||||
const XDraggable = defineAsyncComponent(() =>
|
||||
import("vuedraggable").then((x) => x.default)
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
initPageName?: string;
|
||||
initUser?: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref("settings");
|
||||
let author = $ref($i);
|
||||
let readonly = $ref(false);
|
||||
let page = $ref(null);
|
||||
let pageId = $ref(null);
|
||||
let currentName = $ref(null);
|
||||
let title = $ref("");
|
||||
let summary = $ref(null);
|
||||
let name = $ref(Date.now().toString());
|
||||
let eyeCatchingImage = $ref(null);
|
||||
let eyeCatchingImageId = $ref(null);
|
||||
let font = $ref("sans-serif");
|
||||
let content = $ref([]);
|
||||
let alignCenter = $ref(false);
|
||||
let isPublic = $ref(true);
|
||||
let hideTitleWhenPinned = $ref(false);
|
||||
let variables = $ref([]);
|
||||
let hpml = $ref(null);
|
||||
let script = $ref("");
|
||||
|
||||
provide("readonly", readonly);
|
||||
provide("getScriptBlockList", getScriptBlockList);
|
||||
provide("getPageBlockList", getPageBlockList);
|
||||
|
||||
watch($$(eyeCatchingImageId), async () => {
|
||||
if (eyeCatchingImageId == null) {
|
||||
eyeCatchingImage = null;
|
||||
} else {
|
||||
eyeCatchingImage = await os.api("drive/files/show", {
|
||||
fileId: eyeCatchingImageId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getSaveOptions() {
|
||||
return {
|
||||
title: title.trim(),
|
||||
name: name.trim(),
|
||||
summary: summary,
|
||||
font: font,
|
||||
script: script,
|
||||
hideTitleWhenPinned: hideTitleWhenPinned,
|
||||
alignCenter: alignCenter,
|
||||
isPublic: isPublic,
|
||||
content: content,
|
||||
variables: variables,
|
||||
eyeCatchingImageId: eyeCatchingImageId,
|
||||
};
|
||||
}
|
||||
|
||||
function save() {
|
||||
const options = getSaveOptions();
|
||||
|
||||
const onError = (err) => {
|
||||
if (err.id === "3d81ceae-475f-4600-b2a8-2bc116157532") {
|
||||
if (err.info.param === "name") {
|
||||
os.alert({
|
||||
type: "error",
|
||||
title: i18n.ts._pages.invalidNameTitle,
|
||||
text: i18n.ts._pages.invalidNameText,
|
||||
});
|
||||
}
|
||||
} else if (err.code === "NAME_ALREADY_EXISTS") {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: i18n.ts._pages.nameAlreadyExists,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (pageId) {
|
||||
options.pageId = pageId;
|
||||
os.api("pages/update", options)
|
||||
.then((page) => {
|
||||
currentName = name.trim();
|
||||
os.alert({
|
||||
type: "success",
|
||||
text: i18n.ts._pages.updated,
|
||||
});
|
||||
})
|
||||
.catch(onError);
|
||||
} else {
|
||||
os.api("pages/create", options)
|
||||
.then((created) => {
|
||||
pageId = created.id;
|
||||
currentName = name.trim();
|
||||
os.alert({
|
||||
type: "success",
|
||||
text: i18n.ts._pages.created,
|
||||
});
|
||||
mainRouter.push(`/pages/edit/${pageId}`);
|
||||
})
|
||||
.catch(onError);
|
||||
}
|
||||
}
|
||||
|
||||
function del() {
|
||||
os.confirm({
|
||||
type: "warning",
|
||||
text: i18n.t("removeAreYouSure", { x: title.trim() }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.api("pages/delete", {
|
||||
pageId: pageId,
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: "success",
|
||||
text: i18n.ts._pages.deleted,
|
||||
});
|
||||
mainRouter.push("/pages");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function duplicate() {
|
||||
title = title + " - copy";
|
||||
name = name + "-copy";
|
||||
os.api("pages/create", getSaveOptions()).then((created) => {
|
||||
pageId = created.id;
|
||||
currentName = name.trim();
|
||||
os.alert({
|
||||
type: "success",
|
||||
text: i18n.ts._pages.created,
|
||||
});
|
||||
mainRouter.push(`/pages/edit/${pageId}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const { canceled, result: type } = await os.select({
|
||||
type: null,
|
||||
title: i18n.ts._pages.chooseBlock,
|
||||
groupedItems: getPageBlockList(),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid();
|
||||
content.push({ id, type });
|
||||
}
|
||||
|
||||
async function addVariable() {
|
||||
let { canceled, result: name } = await os.inputText({
|
||||
title: i18n.ts._pages.enterVariableName,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
name = name.trim();
|
||||
|
||||
if (hpml.isUsedName(name)) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: i18n.ts._pages.variableNameIsAlreadyUsed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
variables.push({ id, name, type: null });
|
||||
}
|
||||
|
||||
function removeVariable(v) {
|
||||
variables = variables.filter((x) => x.name !== v.name);
|
||||
}
|
||||
|
||||
function getPageBlockList() {
|
||||
return [
|
||||
{
|
||||
label: i18n.ts._pages.contentBlocks,
|
||||
items: [
|
||||
{ value: "section", text: i18n.ts._pages.blocks.section },
|
||||
{ value: "text", text: i18n.ts._pages.blocks.text },
|
||||
{ value: "image", text: i18n.ts._pages.blocks.image },
|
||||
{ value: "textarea", text: i18n.ts._pages.blocks.textarea },
|
||||
{ value: "note", text: i18n.ts._pages.blocks.note },
|
||||
{ value: "canvas", text: i18n.ts._pages.blocks.canvas },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: i18n.ts._pages.inputBlocks,
|
||||
items: [
|
||||
{ value: "button", text: i18n.ts._pages.blocks.button },
|
||||
{
|
||||
value: "radioButton",
|
||||
text: i18n.ts._pages.blocks.radioButton,
|
||||
},
|
||||
{ value: "textInput", text: i18n.ts._pages.blocks.textInput },
|
||||
{
|
||||
value: "textareaInput",
|
||||
text: i18n.ts._pages.blocks.textareaInput,
|
||||
},
|
||||
{
|
||||
value: "numberInput",
|
||||
text: i18n.ts._pages.blocks.numberInput,
|
||||
},
|
||||
{ value: "switch", text: i18n.ts._pages.blocks.switch },
|
||||
{ value: "counter", text: i18n.ts._pages.blocks.counter },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: i18n.ts._pages.specialBlocks,
|
||||
items: [
|
||||
{ value: "if", text: i18n.ts._pages.blocks.if },
|
||||
{ value: "post", text: i18n.ts._pages.blocks.post },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getScriptBlockList(type: string = null) {
|
||||
const list = [];
|
||||
|
||||
const blocks = blockDefs.filter(
|
||||
(block) =>
|
||||
type == null ||
|
||||
block.out == null ||
|
||||
block.out === type ||
|
||||
typeof block.out === "number"
|
||||
);
|
||||
|
||||
for (const block of blocks) {
|
||||
const category = list.find((x) => x.category === block.category);
|
||||
if (category) {
|
||||
category.items.push({
|
||||
value: block.type,
|
||||
text: i18n.t(`_pages.script.blocks.${block.type}`),
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
category: block.category,
|
||||
label: i18n.t(`_pages.script.categories.${block.category}`),
|
||||
items: [
|
||||
{
|
||||
value: block.type,
|
||||
text: i18n.t(`_pages.script.blocks.${block.type}`),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const userFns = variables.filter((x) => x.type === "fn");
|
||||
if (userFns.length > 0) {
|
||||
list.unshift({
|
||||
label: i18n.t("_pages.script.categories.fn"),
|
||||
items: userFns.map((v) => ({
|
||||
value: "fn:" + v.name,
|
||||
text: v.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function setEyeCatchingImage(img) {
|
||||
selectFile(img.currentTarget ?? img.target, null).then((file) => {
|
||||
eyeCatchingImageId = file.id;
|
||||
});
|
||||
}
|
||||
|
||||
function removeEyeCatchingImage() {
|
||||
eyeCatchingImageId = null;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
hpml = new HpmlTypeChecker();
|
||||
|
||||
watch(
|
||||
$$(variables),
|
||||
() => {
|
||||
hpml.variables = variables;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
$$(content),
|
||||
() => {
|
||||
hpml.pageVars = collectPageVars(content);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
if (props.initPageId) {
|
||||
page = await os.api("pages/show", {
|
||||
pageId: props.initPageId,
|
||||
});
|
||||
} else if (props.initPageName && props.initUser) {
|
||||
page = await os.api("pages/show", {
|
||||
name: props.initPageName,
|
||||
username: props.initUser,
|
||||
});
|
||||
readonly = true;
|
||||
}
|
||||
|
||||
if (page) {
|
||||
author = page.user;
|
||||
pageId = page.id;
|
||||
title = page.title;
|
||||
name = page.name;
|
||||
currentName = page.name;
|
||||
summary = page.summary;
|
||||
font = page.font;
|
||||
script = page.script;
|
||||
hideTitleWhenPinned = page.hideTitleWhenPinned;
|
||||
alignCenter = page.alignCenter;
|
||||
isPublic = page.isPublic;
|
||||
content = page.content;
|
||||
variables = page.variables;
|
||||
eyeCatchingImageId = page.eyeCatchingImageId;
|
||||
} else {
|
||||
const id = uuid();
|
||||
content = [
|
||||
{
|
||||
id,
|
||||
type: "text",
|
||||
text: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => [
|
||||
{
|
||||
key: "settings",
|
||||
title: i18n.ts._pages.pageSetting,
|
||||
icon: "ph-gear-six ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "contents",
|
||||
title: i18n.ts._pages.contents,
|
||||
icon: "ph-sticker ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "variables",
|
||||
title: i18n.ts._pages.variables,
|
||||
icon: "ph-magic-wand ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "script",
|
||||
title: i18n.ts.script,
|
||||
icon: "ph-code ph-bold ph-lg",
|
||||
},
|
||||
]);
|
||||
|
||||
definePageMetadata(
|
||||
computed(() => {
|
||||
let title = i18n.ts._pages.newPage;
|
||||
if (props.initPageId) {
|
||||
title = i18n.ts._pages.editPage;
|
||||
} else if (props.initPageName && props.initUser) {
|
||||
title = i18n.ts._pages.readPage;
|
||||
}
|
||||
return {
|
||||
title: title,
|
||||
icon: "ph-pencil ph-bold ph-lg",
|
||||
};
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jqqmcavi {
|
||||
> .button {
|
||||
& + .button {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gwbmwxkm {
|
||||
position: relative;
|
||||
|
||||
> header {
|
||||
> .title {
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
line-height: 42px;
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 1px rgba(#000, 0.07);
|
||||
|
||||
> i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
> button {
|
||||
padding: 0;
|
||||
width: 42px;
|
||||
font-size: 0.9em;
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> section {
|
||||
padding: 0 32px 32px 32px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
> .view {
|
||||
display: inline-block;
|
||||
margin: 16px 0 0 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
> .eyeCatch {
|
||||
margin-bottom: 16px;
|
||||
|
||||
> div {
|
||||
> img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qmuvgica {
|
||||
padding: 16px;
|
||||
|
||||
> .variables {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
> .add {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,469 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
:display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<transition
|
||||
:name="$store.state.animation ? 'fade' : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="page"
|
||||
:key="page.id"
|
||||
v-size="{ max: [450] }"
|
||||
class="xcukqgmh"
|
||||
>
|
||||
<div class="footer">
|
||||
<div>
|
||||
<i class="ph-alarm ph-bold" />
|
||||
{{ i18n.ts.createdAt }}:
|
||||
<MkTime :time="page.createdAt" mode="detail" />
|
||||
</div>
|
||||
<div v-if="page.createdAt != page.updatedAt">
|
||||
<i class="ph-alarm ph-bold"></i>
|
||||
{{ i18n.ts.updatedAt }}:
|
||||
<MkTime :time="page.updatedAt" mode="detail" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="_block main">
|
||||
<div class="banner">
|
||||
<div class="banner-image">
|
||||
<div class="header">
|
||||
<h1>{{ page.title }}</h1>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<button
|
||||
v-tooltip="i18n.ts.copyUrl"
|
||||
@click="copyUrl"
|
||||
class="menu _button"
|
||||
>
|
||||
<i
|
||||
class="ph-link-simple ph-bold ph-lg"
|
||||
/>
|
||||
</button>
|
||||
<MkA
|
||||
v-tooltip="i18n.ts._pages.viewSource"
|
||||
:to="`/@${username}/pages/${pageName}/view-source`"
|
||||
class="menu _button"
|
||||
style="transform: translateY(2px)"
|
||||
><i class="ph-code ph-bold ph-lg"
|
||||
/></MkA>
|
||||
<template
|
||||
v-if="$i && $i.id === page.userId"
|
||||
>
|
||||
<MkA
|
||||
v-tooltip="i18n.ts._pages.editPage"
|
||||
class="menu _button"
|
||||
:to="`/pages/edit/${page.id}`"
|
||||
style="transform: translateY(2px)"
|
||||
><i class="ph-pencil ph-bold ph-lg"
|
||||
/></MkA>
|
||||
<button
|
||||
v-if="$i.pinnedPageId === page.id"
|
||||
v-tooltip="i18n.ts.unpin"
|
||||
class="menu _button"
|
||||
@click="pin(false)"
|
||||
>
|
||||
<i
|
||||
class="ph-push-pin-slash ph-bold ph-lg"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
v-tooltip="i18n.ts.pin"
|
||||
class="menu _button"
|
||||
@click="pin(true)"
|
||||
>
|
||||
<i
|
||||
class="ph-push-pin ph-bold ph-lg"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<XPage :page="page" />
|
||||
</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="ph-heart ph-fill ph-lg"></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="ph-heart ph-bold"></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="ph-repeat ph-bold ph-lg ph-fw ph-lg"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="shareAvailable()"
|
||||
v-tooltip="i18n.ts.share"
|
||||
v-click-anime
|
||||
class="_button"
|
||||
@click="share"
|
||||
>
|
||||
<i
|
||||
class="ph-share-network ph-bold ph-lg ph-fw ph-lg"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MagAvatarResolvingProxy
|
||||
:user="page.user"
|
||||
class="avatar"
|
||||
/>
|
||||
<div class="name">
|
||||
<MkUserName
|
||||
:user="page.user"
|
||||
style="display: block"
|
||||
/>
|
||||
<MkAcct :user="page.user" />
|
||||
</div>
|
||||
<MkFollowButton
|
||||
v-if="!$i || $i.id != page.user.id"
|
||||
:user="page.user"
|
||||
:inline="true"
|
||||
:transparent="false"
|
||||
:full="true"
|
||||
class="koudoku"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="links">
|
||||
<MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
|
||||
<template v-if="$i && $i.id === page.userId">
|
||||
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
|
||||
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
|
||||
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
|
||||
</template>
|
||||
</div> -->
|
||||
</div>
|
||||
<MkContainer
|
||||
:max-height="300"
|
||||
:foldable="true"
|
||||
:expanded="false"
|
||||
class="other"
|
||||
>
|
||||
<template #header
|
||||
><i class="ph-clock ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.recentPosts }}</template
|
||||
>
|
||||
<MkPagination
|
||||
v-slot="{ items }"
|
||||
:pagination="otherPostsPagination"
|
||||
>
|
||||
<MkPagePreview
|
||||
v-for="page in items"
|
||||
:key="page.id"
|
||||
:page="page"
|
||||
class="_gap"
|
||||
/>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchPage()" />
|
||||
<MkLoading v-else />
|
||||
</transition>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import XPage from "@/components/page/page.vue";
|
||||
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 copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { shareAvailable } from "@/scripts/share-available";
|
||||
import { $i } from "@/account";
|
||||
|
||||
const props = defineProps<{
|
||||
pageName: string;
|
||||
username: string;
|
||||
}>();
|
||||
|
||||
let page = $ref(null);
|
||||
let bgImg = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: "users/pages" as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: page.user.id,
|
||||
})),
|
||||
};
|
||||
const path = $computed(() => props.username + "/" + props.pageName);
|
||||
|
||||
function fetchPage() {
|
||||
page = null;
|
||||
os.api("pages/show", {
|
||||
name: props.pageName,
|
||||
username: props.username,
|
||||
})
|
||||
.then((_page) => {
|
||||
page = _page;
|
||||
bgImg = getBgImg();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
});
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
copyToClipboard(window.location.href);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function getBgImg(): string {
|
||||
if (page.eyeCatchingImage != null) {
|
||||
return `url(${page.eyeCatchingImage.url})`;
|
||||
} else {
|
||||
return "linear-gradient(to bottom right, #31748f, #9ccfd8)";
|
||||
}
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: page.title ?? page.name,
|
||||
text: page.summary,
|
||||
url: `${url}/@${page.user.username}/pages/${page.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${page.title || page.name} ${url}/@${
|
||||
page.user.username
|
||||
}/pages/${page.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
os.api("pages/like", {
|
||||
pageId: page.id,
|
||||
}).then(() => {
|
||||
page.isLiked = true;
|
||||
page.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
os.api("pages/unlike", {
|
||||
pageId: page.id,
|
||||
}).then(() => {
|
||||
page.isLiked = false;
|
||||
page.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
function pin(pin) {
|
||||
os.apiWithDialog("i/update", {
|
||||
pinnedPageId: pin ? page.id : null,
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => path, fetchPage, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata(
|
||||
computed(() =>
|
||||
page
|
||||
? {
|
||||
title: computed(() => page.title || page.name),
|
||||
avatar: page.user,
|
||||
path: `/@${page.user.username}/pages/${page.name}`,
|
||||
share: {
|
||||
title: page.title || page.name,
|
||||
text: page.summary,
|
||||
},
|
||||
}
|
||||
: null
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.xcukqgmh {
|
||||
> .main {
|
||||
> * {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
> .banner {
|
||||
margin: 0rem !important;
|
||||
|
||||
> .banner-image {
|
||||
// TODO: 良い感じのアスペクト比で表示
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-image: v-bind("bgImg");
|
||||
|
||||
> .header {
|
||||
padding: 16px;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
color: white;
|
||||
text-shadow: 0 0 8px var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
> .menu-actions {
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(8px));
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 8px;
|
||||
border-radius: 24px;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
left: 1rem;
|
||||
|
||||
> .menu {
|
||||
vertical-align: bottom;
|
||||
height: 31px;
|
||||
width: 31px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px var(--shadow);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
margin-left: 4px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .like {
|
||||
> .button {
|
||||
--accent: #eb6f92;
|
||||
--X8: #eb6f92;
|
||||
--buttonBg: rgb(216 71 106 / 5%);
|
||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||
color: #eb6f92;
|
||||
|
||||
::v-deep(.count) {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .other {
|
||||
> button {
|
||||
padding: 2px;
|
||||
margin: 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .user {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
margin-left: auto;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .links {
|
||||
margin-top: 16px;
|
||||
padding: 14px 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .link {
|
||||
margin-right: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
margin: var(--margin) 0 var(--margin) 0;
|
||||
font-size: 85%;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header
|
||||
><MkPageHeader
|
||||
v-model:tab="tab"
|
||||
:actions="headerActions"
|
||||
:tabs="headerTabs"
|
||||
/></template>
|
||||
<MkSpacer :content-max="700">
|
||||
<swiper
|
||||
:round-lengths="true"
|
||||
:touch-angle="25"
|
||||
:threshold="10"
|
||||
:centeredSlides="true"
|
||||
:modules="[Virtual]"
|
||||
:space-between="20"
|
||||
:virtual="true"
|
||||
:allow-touch-move="
|
||||
!(
|
||||
deviceKind === 'desktop' &&
|
||||
!defaultStore.state.swipeOnDesktop
|
||||
)
|
||||
"
|
||||
@swiper="setSwiperRef"
|
||||
@slide-change="onSlideChange"
|
||||
>
|
||||
<swiper-slide>
|
||||
<div class="rknalgpo">
|
||||
<MkPagination
|
||||
v-slot="{ items }"
|
||||
:pagination="featuredPagesPagination"
|
||||
>
|
||||
<MkPagePreview
|
||||
v-for="page in items"
|
||||
:key="page.id"
|
||||
class="ckltabjg"
|
||||
:page="page"
|
||||
/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<div class="rknalgpo liked">
|
||||
<MkPagination
|
||||
v-slot="{ items }"
|
||||
:pagination="likedPagesPagination"
|
||||
>
|
||||
<MkPagePreview
|
||||
v-for="like in items"
|
||||
:key="like.page.id"
|
||||
class="ckltabjg"
|
||||
:page="like.page"
|
||||
/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
<swiper-slide>
|
||||
<div class="rknalgpo my">
|
||||
<div class="buttoncontainer">
|
||||
<MkButton class="new primary" @click="create()"
|
||||
><i class="ph-plus ph-bold ph-lg"></i>
|
||||
{{ i18n.ts._pages.newPage }}</MkButton
|
||||
>
|
||||
</div>
|
||||
<MkPagination
|
||||
v-slot="{ items }"
|
||||
:pagination="myPagesPagination"
|
||||
>
|
||||
<MkPagePreview
|
||||
v-for="page in items"
|
||||
:key="page.id"
|
||||
class="ckltabjg"
|
||||
:page="page"
|
||||
/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkPagePreview from "@/components/MkPagePreview.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";
|
||||
import { deviceKind } from "@/scripts/device-kind";
|
||||
import { defaultStore } from "@/store";
|
||||
import "swiper/scss";
|
||||
import "swiper/scss/virtual";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref("featured");
|
||||
const tabs = ["featured", "liked", "my"];
|
||||
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
|
||||
|
||||
const featuredPagesPagination = {
|
||||
endpoint: "pages/featured" as const,
|
||||
limit: 10,
|
||||
};
|
||||
const likedPagesPagination = {
|
||||
endpoint: "i/page-likes" as const,
|
||||
limit: 10,
|
||||
};
|
||||
const myPagesPagination = {
|
||||
endpoint: "i/pages" as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push("/pages/new");
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [
|
||||
{
|
||||
icon: "ph-plus ph-bold ph-lg",
|
||||
text: i18n.ts.create,
|
||||
handler: create,
|
||||
},
|
||||
]);
|
||||
|
||||
const headerTabs = $computed(() => [
|
||||
{
|
||||
key: "featured",
|
||||
title: i18n.ts._pages.featured,
|
||||
icon: "ph-fire-simple ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "liked",
|
||||
title: i18n.ts._pages.liked,
|
||||
icon: "ph-heart ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "my",
|
||||
title: i18n.ts._pages.my,
|
||||
icon: "ph-crown-simple ph-bold ph-lg",
|
||||
},
|
||||
]);
|
||||
|
||||
definePageMetadata(
|
||||
computed(() => ({
|
||||
title: i18n.ts.pages,
|
||||
icon: "ph-file-text ph-bold ph-lg",
|
||||
}))
|
||||
);
|
||||
|
||||
let swiperRef = null;
|
||||
|
||||
function setSwiperRef(swiper) {
|
||||
swiperRef = swiper;
|
||||
syncSlide(tabs.indexOf(tab));
|
||||
}
|
||||
|
||||
function onSlideChange() {
|
||||
tab = tabs[swiperRef.activeIndex];
|
||||
}
|
||||
|
||||
function syncSlide(index) {
|
||||
swiperRef.slideTo(index);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncSlide(tabs.indexOf(swiperRef.activeIndex));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rknalgpo {
|
||||
> .buttoncontainer {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&.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>
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<div class="iltifgqe">
|
||||
<div class="editor _panel _gap">
|
||||
<PrismEditor
|
||||
v-model="code"
|
||||
class="_code code"
|
||||
style="height: 30vh"
|
||||
:highlight="highlighter"
|
||||
:line-numbers="false"
|
||||
/>
|
||||
<MkButton
|
||||
style="position: absolute; top: 8px; right: 8px"
|
||||
primary
|
||||
@click="run()"
|
||||
><i class="ph-play ph-bold ph-lg"></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>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="_gap">
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import "prismjs";
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import "prismjs/components/prism-clike";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/themes/prism-okaidia.css";
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||
import { AiScript, parse, utils } from "@syuilo/aiscript";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { createAiScriptEnv } from "@/scripts/aiscript/api";
|
||||
import * as os from "@/os";
|
||||
import { $i } from "@/account";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const code = ref("");
|
||||
const logs = ref<any[]>([]);
|
||||
|
||||
const saved = localStorage.getItem("scratchpad");
|
||||
if (saved) {
|
||||
code.value = saved;
|
||||
}
|
||||
|
||||
watch(code, () => {
|
||||
localStorage.setItem("scratchpad", code.value);
|
||||
});
|
||||
|
||||
async function run() {
|
||||
logs.value = [];
|
||||
const aiscript = new AiScript(
|
||||
createAiScriptEnv({
|
||||
storageKey: "scratchpad",
|
||||
token: $i?.token,
|
||||
}),
|
||||
{
|
||||
in: (q) => {
|
||||
return new Promise((ok) => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text:
|
||||
value.type === "str"
|
||||
? value.value
|
||||
: utils.valToString(value),
|
||||
print: true,
|
||||
});
|
||||
},
|
||||
log: (type, params) => {
|
||||
switch (type) {
|
||||
case "end":
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text: utils.valToString(params.val, true),
|
||||
print: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(code.value);
|
||||
} catch (error) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "Syntax error :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (error: any) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function highlighter(code) {
|
||||
return highlight(code, languages.js, "javascript");
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.scratchpad,
|
||||
icon: "ph-terminal-window ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iltifgqe {
|
||||
padding: 16px;
|
||||
|
||||
> .editor {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.bepmlvbi {
|
||||
padding: 16px;
|
||||
|
||||
> .log {
|
||||
&:not(.print) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -6,6 +6,12 @@
|
|||
<FormLink to="/settings/apps" class="_formBlock">{{
|
||||
i18n.ts.manageAccessTokens
|
||||
}}</FormLink>
|
||||
<FormLink
|
||||
to="/api-console"
|
||||
:behavior="isDesktop ? 'window' : null"
|
||||
class="_formBlock"
|
||||
>API console</FormLink
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -157,6 +157,12 @@ const menuDef = computed(() => [
|
|||
to: "/settings/sounds",
|
||||
active: currentPage?.route.name === "sounds",
|
||||
},
|
||||
{
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
text: i18n.ts.plugins,
|
||||
to: "/settings/plugin",
|
||||
active: currentPage?.route.name === "plugin",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormInfo warn class="_formBlock">{{
|
||||
i18n.ts._plugin.installWarn
|
||||
}}</FormInfo>
|
||||
|
||||
<FormTextarea v-model="code" tall class="_formBlock">
|
||||
<template #label>{{ i18n.ts.code }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<div class="_formBlock">
|
||||
<FormButton :disabled="code == null" primary inline @click="install"
|
||||
><i class="ph-check ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.install }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, ref } from "vue";
|
||||
import { AiScript, parse } from "@syuilo/aiscript";
|
||||
import { serialize } from "@syuilo/aiscript/built/serializer";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import FormTextarea from "@/components/form/textarea.vue";
|
||||
import FormButton from "@/components/MkButton.vue";
|
||||
import FormInfo from "@/components/MkInfo.vue";
|
||||
import * as os from "@/os";
|
||||
import { ColdDeviceStorage } from "@/store";
|
||||
import { unisonReload } from "@/scripts/unison-reload";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const code = ref(null);
|
||||
|
||||
function installPlugin({ id, meta, ast, token }) {
|
||||
ColdDeviceStorage.set(
|
||||
"plugins",
|
||||
ColdDeviceStorage.get("plugins").concat({
|
||||
...meta,
|
||||
id,
|
||||
active: true,
|
||||
configData: {},
|
||||
token: token,
|
||||
ast: ast,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function install() {
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(code.value);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "Syntax error :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = AiScript.collectMetadata(ast);
|
||||
if (meta == null) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "No metadata found :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = meta.get(null);
|
||||
if (metadata == null) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "No metadata found :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, version, author, description, permissions, config } =
|
||||
metadata;
|
||||
if (name == null || version == null || author == null) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "Required property not found :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token =
|
||||
permissions == null || permissions.length === 0
|
||||
? null
|
||||
: await new Promise((res, rej) => {
|
||||
os.popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkTokenGenerateWindow.vue")
|
||||
),
|
||||
{
|
||||
title: i18n.ts.tokenRequested,
|
||||
information: i18n.ts.pluginTokenRequestedDescription,
|
||||
initialName: name,
|
||||
initialPermissions: permissions,
|
||||
},
|
||||
{
|
||||
done: async (result) => {
|
||||
const { name, permissions } = result;
|
||||
const { token } = await os.api(
|
||||
"miauth/gen-token",
|
||||
{
|
||||
session: null,
|
||||
name: name,
|
||||
permission: permissions,
|
||||
}
|
||||
);
|
||||
res(token);
|
||||
},
|
||||
},
|
||||
"closed"
|
||||
);
|
||||
});
|
||||
|
||||
installPlugin({
|
||||
id: uuid(),
|
||||
meta: {
|
||||
name,
|
||||
version,
|
||||
author,
|
||||
description,
|
||||
permissions,
|
||||
config,
|
||||
},
|
||||
token,
|
||||
ast: serialize(ast),
|
||||
});
|
||||
|
||||
os.success();
|
||||
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts._plugin.install,
|
||||
icon: "ph-download-simple ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormLink to="/settings/plugin/install"
|
||||
><template #icon
|
||||
><i class="ph-download-simple ph-bold ph-lg"></i></template
|
||||
>{{ i18n.ts._plugin.install }}</FormLink
|
||||
>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.manage }}</template>
|
||||
<div
|
||||
v-for="plugin in plugins"
|
||||
:key="plugin.id"
|
||||
class="_formBlock _panel"
|
||||
style="padding: 20px"
|
||||
>
|
||||
<span style="display: flex"
|
||||
><b>{{ plugin.name }}</b
|
||||
><span style="margin-left: auto"
|
||||
>v{{ plugin.version }}</span
|
||||
></span
|
||||
>
|
||||
|
||||
<FormSwitch
|
||||
class="_formBlock"
|
||||
:model-value="plugin.active"
|
||||
@update:modelValue="changeActive(plugin, $event)"
|
||||
>{{ i18n.ts.makeActive }}</FormSwitch
|
||||
>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.author }}</template>
|
||||
<template #value>{{ plugin.author }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value>{{ plugin.description }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>{{ plugin.permission }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap">
|
||||
<MkButton
|
||||
v-if="plugin.config"
|
||||
inline
|
||||
@click="config(plugin)"
|
||||
><i class="ph-gear-six ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.settings }}</MkButton
|
||||
>
|
||||
<MkButton inline danger @click="uninstall(plugin)"
|
||||
><i class="ph-trash ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.uninstall }}</MkButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref } from "vue";
|
||||
import FormLink from "@/components/form/link.vue";
|
||||
import FormSwitch from "@/components/form/switch.vue";
|
||||
import FormSection from "@/components/form/section.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import MkKeyValue from "@/components/MkKeyValue.vue";
|
||||
import * as os from "@/os";
|
||||
import { ColdDeviceStorage } from "@/store";
|
||||
import { unisonReload } from "@/scripts/unison-reload";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
|
||||
const plugins = ref(ColdDeviceStorage.get("plugins"));
|
||||
|
||||
function uninstall(plugin) {
|
||||
ColdDeviceStorage.set(
|
||||
"plugins",
|
||||
plugins.value.filter((x) => x.id !== plugin.id)
|
||||
);
|
||||
os.success();
|
||||
nextTick(() => {
|
||||
unisonReload();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
|
||||
async function config(plugin) {
|
||||
const config = plugin.config;
|
||||
for (const key in plugin.configData) {
|
||||
config[key].default = plugin.configData[key];
|
||||
}
|
||||
|
||||
const { canceled, result } = await os.form(plugin.name, config);
|
||||
if (canceled) return;
|
||||
|
||||
const coldPlugins = ColdDeviceStorage.get("plugins");
|
||||
coldPlugins.find((p) => p.id === plugin.id)!.configData = result;
|
||||
ColdDeviceStorage.set("plugins", coldPlugins);
|
||||
|
||||
nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function changeActive(plugin, active) {
|
||||
const coldPlugins = ColdDeviceStorage.get("plugins");
|
||||
coldPlugins.find((p) => p.id === plugin.id)!.active = active;
|
||||
ColdDeviceStorage.set("plugins", coldPlugins);
|
||||
|
||||
nextTick(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.plugins,
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -121,6 +121,7 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
|||
"lightTheme",
|
||||
"darkTheme",
|
||||
"syncDeviceDarkMode",
|
||||
"plugins",
|
||||
"mediaVolume",
|
||||
"sound_masterVolume",
|
||||
"sound_note",
|
||||
|
|
|
@ -308,15 +308,27 @@
|
|||
</dl>
|
||||
</div>
|
||||
<div class="status">
|
||||
<MkA :to="userPage(user)">
|
||||
<MkA
|
||||
v-click-anime
|
||||
:to="userPage(user)"
|
||||
:class="{ active: page === 'index' }"
|
||||
>
|
||||
<b>{{ number(user.note_count) }}</b>
|
||||
<span>{{ i18n.ts.notes }}</span>
|
||||
</MkA>
|
||||
<MkA :to="userPage(user, 'following')">
|
||||
<MkA
|
||||
v-click-anime
|
||||
:to="userPage(user, 'following')"
|
||||
:class="{ active: page === 'following' }"
|
||||
>
|
||||
<b>{{ number(user.following_count) }}</b>
|
||||
<span>{{ i18n.ts.following }}</span>
|
||||
</MkA>
|
||||
<MkA :to="userPage(user, 'followers')">
|
||||
<MkA
|
||||
v-click-anime
|
||||
:to="userPage(user, 'followers')"
|
||||
:class="{ active: page === 'followers' }"
|
||||
>
|
||||
<b>{{ number(user.follower_count) }}</b>
|
||||
<span>{{ i18n.ts.followers }}</span>
|
||||
</MkA>
|
||||
|
@ -383,6 +395,7 @@ import MkUserName from "@/components/global/MkUserName.vue";
|
|||
import Mfm from "@/components/mfm.vue";
|
||||
import MkTime from "@/components/global/MkTime.vue";
|
||||
import MkA from "@/components/global/MkA.vue";
|
||||
import page from "@/components/page/page.vue";
|
||||
import { packed } from "magnetar-common";
|
||||
import number from "../../filters/number";
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user" />
|
||||
<XClips v-else-if="tab === 'clips'" :user="user" />
|
||||
<XPages v-else-if="tab === 'pages'" :user="user" />
|
||||
<XGallery v-else-if="tab === 'gallery'" :user="user" />
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchUser()" />
|
||||
|
@ -41,6 +42,7 @@ import * as Acct from "calckey-js/built/acct";
|
|||
const XHome = defineAsyncComponent(() => import("./home.vue"));
|
||||
const XReactions = defineAsyncComponent(() => import("./reactions.vue"));
|
||||
const XClips = defineAsyncComponent(() => import("./clips.vue"));
|
||||
const XPages = defineAsyncComponent(() => import("./pages.vue"));
|
||||
const XGallery = defineAsyncComponent(() => import("./gallery.vue"));
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -142,6 +144,11 @@ const headerTabs = $computed(() =>
|
|||
title: i18n.ts.clips,
|
||||
icon: "ph-paperclip ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "pages",
|
||||
title: i18n.ts.pages,
|
||||
icon: "ph-file-text ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
key: "gallery",
|
||||
title: i18n.ts.gallery,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination v-slot="{ items }" ref="list" :pagination="pagination">
|
||||
<MkPagePreview
|
||||
v-for="page in items"
|
||||
:key="page.id"
|
||||
:page="page"
|
||||
class="_gap"
|
||||
/>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import MkPagePreview from "@/components/MkPagePreview.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import { packed } from "magnetar-common";
|
||||
|
||||
const props = defineProps<{
|
||||
user: packed.PackUserBase;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: "users/pages" as const,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,192 @@
|
|||
import { AiScript, utils, values } from "@syuilo/aiscript";
|
||||
import { deserialize } from "@syuilo/aiscript/built/serializer";
|
||||
import { jsToVal } from "@syuilo/aiscript/built/interpreter/util";
|
||||
import { createAiScriptEnv } from "@/scripts/aiscript/api";
|
||||
import { inputText } from "@/os";
|
||||
import {
|
||||
noteActions,
|
||||
notePostInterruptors,
|
||||
noteViewInterruptors,
|
||||
postFormActions,
|
||||
userActions,
|
||||
} from "@/store";
|
||||
|
||||
const pluginContexts = new Map<string, AiScript>();
|
||||
|
||||
export function install(plugin) {
|
||||
console.info("Plugin installed:", plugin.name, `v${plugin.version}`);
|
||||
|
||||
const aiscript = new AiScript(
|
||||
createPluginEnv({
|
||||
plugin: plugin,
|
||||
storageKey: `plugins:${plugin.id}`,
|
||||
}),
|
||||
{
|
||||
in: (q) => {
|
||||
return new Promise((ok) => {
|
||||
inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
log: (type, params) => {},
|
||||
}
|
||||
);
|
||||
|
||||
initPlugin({ plugin, aiscript });
|
||||
|
||||
aiscript.exec(deserialize(plugin.ast));
|
||||
}
|
||||
|
||||
function createPluginEnv(opts) {
|
||||
const config = new Map();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
|
||||
config.set(
|
||||
k,
|
||||
jsToVal(
|
||||
typeof opts.plugin.configData[k] !== "undefined"
|
||||
? opts.plugin.configData[k]
|
||||
: v.default
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
|
||||
//#region Deprecated
|
||||
"Mk:register_post_form_action": values.FN_NATIVE(([title, handler]) => {
|
||||
registerPostFormAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}),
|
||||
"Mk:register_user_action": values.FN_NATIVE(([title, handler]) => {
|
||||
registerUserAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}),
|
||||
"Mk:register_note_action": values.FN_NATIVE(([title, handler]) => {
|
||||
registerNoteAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}),
|
||||
//#endregion
|
||||
"Plugin:register_post_form_action": values.FN_NATIVE(
|
||||
([title, handler]) => {
|
||||
registerPostFormAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
),
|
||||
"Plugin:register_user_action": values.FN_NATIVE(([title, handler]) => {
|
||||
registerUserAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}),
|
||||
"Plugin:register_note_action": values.FN_NATIVE(([title, handler]) => {
|
||||
registerNoteAction({
|
||||
pluginId: opts.plugin.id,
|
||||
title: title.value,
|
||||
handler,
|
||||
});
|
||||
}),
|
||||
"Plugin:register_note_view_interruptor": values.FN_NATIVE(
|
||||
([handler]) => {
|
||||
registerNoteViewInterruptor({
|
||||
pluginId: opts.plugin.id,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
),
|
||||
"Plugin:register_note_post_interruptor": values.FN_NATIVE(
|
||||
([handler]) => {
|
||||
registerNotePostInterruptor({
|
||||
pluginId: opts.plugin.id,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
),
|
||||
"Plugin:open_url": values.FN_NATIVE(([url]) => {
|
||||
window.open(url.value, "_blank");
|
||||
}),
|
||||
"Plugin:config": values.OBJ(config),
|
||||
};
|
||||
}
|
||||
|
||||
function initPlugin({ plugin, aiscript }) {
|
||||
pluginContexts.set(plugin.id, aiscript);
|
||||
}
|
||||
|
||||
function registerPostFormAction({ pluginId, title, handler }) {
|
||||
postFormActions.push({
|
||||
title,
|
||||
handler: (form, update) => {
|
||||
pluginContexts.get(pluginId)?.execFn(handler, [
|
||||
utils.jsToVal(form),
|
||||
values.FN_NATIVE(([key, value]) => {
|
||||
update(key.value, value.value);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerUserAction({ pluginId, title, handler }) {
|
||||
userActions.push({
|
||||
title,
|
||||
handler: (user) => {
|
||||
pluginContexts
|
||||
.get(pluginId)
|
||||
?.execFn(handler, [utils.jsToVal(user)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNoteAction({ pluginId, title, handler }) {
|
||||
noteActions.push({
|
||||
title,
|
||||
handler: (note) => {
|
||||
pluginContexts
|
||||
.get(pluginId)
|
||||
?.execFn(handler, [utils.jsToVal(note)]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNoteViewInterruptor({ pluginId, handler }) {
|
||||
noteViewInterruptors.push({
|
||||
handler: async (note) => {
|
||||
return utils.valToJs(
|
||||
await pluginContexts
|
||||
.get(pluginId)
|
||||
.execFn(handler, [utils.jsToVal(note)])
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerNotePostInterruptor({ pluginId, handler }) {
|
||||
notePostInterruptors.push({
|
||||
handler: async (note) => {
|
||||
return utils.valToJs(
|
||||
await pluginContexts
|
||||
.get(pluginId)
|
||||
.execFn(handler, [utils.jsToVal(note)])
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -23,6 +23,14 @@ const page = (loader: AsyncComponentLoader<any>) =>
|
|||
});
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: "/@:initUser/pages/:initPageName/view-source",
|
||||
component: page(() => import("./pages/page-editor/page-editor.vue")),
|
||||
},
|
||||
{
|
||||
path: "/@:username/pages/:pageName",
|
||||
component: page(() => import("./pages/page.vue")),
|
||||
},
|
||||
{
|
||||
path: "/@:acct/following",
|
||||
component: page(() => import("./pages/user/following.vue")),
|
||||
|
@ -160,6 +168,18 @@ export const routes = [
|
|||
name: "sounds",
|
||||
component: page(() => import("./pages/settings/sounds.vue")),
|
||||
},
|
||||
{
|
||||
path: "/plugin/install",
|
||||
name: "plugin",
|
||||
component: page(
|
||||
() => import("./pages/settings/plugin.install.vue")
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/plugin",
|
||||
name: "plugin",
|
||||
component: page(() => import("./pages/settings/plugin.vue")),
|
||||
},
|
||||
{
|
||||
path: "/import-export",
|
||||
name: "import-export",
|
||||
|
@ -338,10 +358,19 @@ export const routes = [
|
|||
component: page(() => import("./pages/share.vue")),
|
||||
loginRequired: true,
|
||||
},
|
||||
{
|
||||
path: "/api-console",
|
||||
component: page(() => import("./pages/api-console.vue")),
|
||||
loginRequired: true,
|
||||
},
|
||||
{
|
||||
path: "/mfm-cheat-sheet",
|
||||
component: page(() => import("./pages/mfm-cheat-sheet.vue")),
|
||||
},
|
||||
{
|
||||
path: "/scratchpad",
|
||||
component: page(() => import("./pages/scratchpad.vue")),
|
||||
},
|
||||
{
|
||||
path: "/preview",
|
||||
component: page(() => import("./pages/preview.vue")),
|
||||
|
@ -364,6 +393,20 @@ export const routes = [
|
|||
path: "/tags/:tag",
|
||||
component: page(() => import("./pages/tag.vue")),
|
||||
},
|
||||
{
|
||||
path: "/pages/new",
|
||||
component: page(() => import("./pages/page-editor/page-editor.vue")),
|
||||
loginRequired: true,
|
||||
},
|
||||
{
|
||||
path: "/pages/edit/:initPageId",
|
||||
component: page(() => import("./pages/page-editor/page-editor.vue")),
|
||||
loginRequired: true,
|
||||
},
|
||||
{
|
||||
path: "/pages",
|
||||
component: page(() => import("./pages/pages.vue")),
|
||||
},
|
||||
{
|
||||
path: "/gallery/:postId/edit",
|
||||
component: page(() => import("./pages/gallery/edit.vue")),
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { utils, values } from "@syuilo/aiscript";
|
||||
import * as os from "@/os";
|
||||
import { $i } from "@/account";
|
||||
|
||||
export function createAiScriptEnv(opts) {
|
||||
let apiRequests = 0;
|
||||
return {
|
||||
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
||||
USER_NAME: $i ? values.STR($i.name) : values.NULL,
|
||||
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
|
||||
"Mk:dialog": values.FN_NATIVE(async ([title, text, type]) => {
|
||||
await os.alert({
|
||||
type: type ? type.value : "info",
|
||||
title: title.value,
|
||||
text: text.value,
|
||||
});
|
||||
}),
|
||||
"Mk:confirm": values.FN_NATIVE(async ([title, text, type]) => {
|
||||
const confirm = await os.confirm({
|
||||
type: type ? type.value : "question",
|
||||
title: title.value,
|
||||
text: text.value,
|
||||
});
|
||||
return confirm.canceled ? values.FALSE : values.TRUE;
|
||||
}),
|
||||
"Mk:api": values.FN_NATIVE(async ([ep, param, token]) => {
|
||||
if (token) {
|
||||
utils.assertString(token);
|
||||
// バグがあればundefinedもあり得るため念のため
|
||||
if (typeof token.value !== "string")
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
apiRequests++;
|
||||
if (apiRequests > 16) return values.NULL;
|
||||
const res = await os.api(
|
||||
ep.value,
|
||||
utils.valToJs(param),
|
||||
token ? token.value : opts.token || null
|
||||
);
|
||||
return utils.jsToVal(res);
|
||||
}),
|
||||
"Mk:save": values.FN_NATIVE(([key, value]) => {
|
||||
utils.assertString(key);
|
||||
localStorage.setItem(
|
||||
`aiscript:${opts.storageKey}:${key.value}`,
|
||||
JSON.stringify(utils.valToJs(value))
|
||||
);
|
||||
return values.NULL;
|
||||
}),
|
||||
"Mk:load": values.FN_NATIVE(([key]) => {
|
||||
utils.assertString(key);
|
||||
return utils.jsToVal(
|
||||
JSON.parse(
|
||||
localStorage.getItem(
|
||||
`aiscript:${opts.storageKey}:${key.value}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
export function collectPageVars(content) {
|
||||
const pageVars = [];
|
||||
const collect = (xs: any[]) => {
|
||||
for (const x of xs) {
|
||||
if (x.type === "textInput") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "string",
|
||||
value: x.default || "",
|
||||
});
|
||||
} else if (x.type === "textareaInput") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "string",
|
||||
value: x.default || "",
|
||||
});
|
||||
} else if (x.type === "numberInput") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "number",
|
||||
value: x.default || 0,
|
||||
});
|
||||
} else if (x.type === "switch") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "boolean",
|
||||
value: x.default,
|
||||
});
|
||||
} else if (x.type === "counter") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "number",
|
||||
value: 0,
|
||||
});
|
||||
} else if (x.type === "radioButton") {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: "string",
|
||||
value: x.default || "",
|
||||
});
|
||||
} else if (x.children) {
|
||||
collect(x.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
collect(content);
|
||||
return pageVars;
|
||||
}
|
|
@ -6,6 +6,7 @@ import { instance } from "@/instance";
|
|||
import * as os from "@/os";
|
||||
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||
import { url } from "@/config";
|
||||
import { noteActions } from "@/store";
|
||||
import { shareAvailable } from "@/scripts/share-available";
|
||||
import { getUserMenu } from "@/scripts/get-user-menu";
|
||||
import { magEffectiveNote, magTransUsername } from "@/scripts-mag/mag-util";
|
||||
|
@ -486,5 +487,18 @@ export function getNoteMenu(props: {
|
|||
].filter((x) => x !== undefined);
|
||||
}
|
||||
|
||||
if (noteActions.length > 0) {
|
||||
menu = menu.concat([
|
||||
null,
|
||||
...noteActions.map((action) => ({
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(appearNote);
|
||||
},
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { i18n } from "@/i18n";
|
|||
import copyToClipboard from "@/scripts/copy-to-clipboard";
|
||||
import { host } from "@/config";
|
||||
import * as os from "@/os";
|
||||
import { userActions } from "@/store";
|
||||
import { $i, iAmModerator } from "@/account";
|
||||
import { mainRouter } from "@/router";
|
||||
import { Router } from "@/nirax";
|
||||
|
@ -366,5 +367,18 @@ export function getUserMenu(
|
|||
]);
|
||||
}
|
||||
|
||||
if (userActions.length > 0) {
|
||||
menu = menu.concat([
|
||||
null,
|
||||
...userActions.map((action) => ({
|
||||
icon: "ph-plug ph-bold ph-lg",
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(user);
|
||||
},
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,14 @@ export function openHelpMenu_(ev: MouseEvent) {
|
|||
window.open(instance.tosUrl, "_blank");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
text: i18n.ts.apps,
|
||||
icon: "ph-device-mobile ph-bold ph-lg",
|
||||
action: () => {
|
||||
window.open("https://calckey.org/apps", "_blank");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
action: async () => {
|
||||
|
@ -47,7 +55,25 @@ export function openHelpMenu_(ev: MouseEvent) {
|
|||
text: i18n.ts.developer,
|
||||
icon: "ph-code ph-bold ph-lg",
|
||||
children: [
|
||||
// TODO: Magnetar developer tools
|
||||
{
|
||||
type: "link",
|
||||
to: "/api-console",
|
||||
text: "API Console",
|
||||
icon: "ph-terminal-window ph-bold ph-lg",
|
||||
},
|
||||
{
|
||||
text: i18n.ts.document,
|
||||
icon: "ph-file-doc ph-bold ph-lg",
|
||||
action: () => {
|
||||
window.open("/api-doc", "_blank");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
to: "/scratchpad",
|
||||
text: "AiScript Scratchpad",
|
||||
icon: "ph-scribble-loop ph-bold ph-lg",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// blocks
|
||||
|
||||
export type BlockBase = {
|
||||
id: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type TextBlock = BlockBase & {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SectionBlock = BlockBase & {
|
||||
type: "section";
|
||||
title: string;
|
||||
children: (Block | VarBlock)[];
|
||||
};
|
||||
|
||||
export type ImageBlock = BlockBase & {
|
||||
type: "image";
|
||||
fileId: string | null;
|
||||
};
|
||||
|
||||
export type ButtonBlock = BlockBase & {
|
||||
type: "button";
|
||||
text: any;
|
||||
primary: boolean;
|
||||
action: string;
|
||||
content: string;
|
||||
event: string;
|
||||
message: string;
|
||||
var: string;
|
||||
fn: string;
|
||||
};
|
||||
|
||||
export type IfBlock = BlockBase & {
|
||||
type: "if";
|
||||
var: string;
|
||||
children: Block[];
|
||||
};
|
||||
|
||||
export type TextareaBlock = BlockBase & {
|
||||
type: "textarea";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type PostBlock = BlockBase & {
|
||||
type: "post";
|
||||
text: string;
|
||||
attachCanvasImage: boolean;
|
||||
canvasId: string;
|
||||
};
|
||||
|
||||
export type CanvasBlock = BlockBase & {
|
||||
type: "canvas";
|
||||
name: string; // canvas id
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type NoteBlock = BlockBase & {
|
||||
type: "note";
|
||||
detailed: boolean;
|
||||
note: string | null;
|
||||
};
|
||||
|
||||
export type Block =
|
||||
| TextBlock
|
||||
| SectionBlock
|
||||
| ImageBlock
|
||||
| ButtonBlock
|
||||
| IfBlock
|
||||
| TextareaBlock
|
||||
| PostBlock
|
||||
| CanvasBlock
|
||||
| NoteBlock
|
||||
| VarBlock;
|
||||
|
||||
// variable blocks
|
||||
|
||||
export type VarBlockBase = BlockBase & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type NumberInputVarBlock = VarBlockBase & {
|
||||
type: "numberInput";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type TextInputVarBlock = VarBlockBase & {
|
||||
type: "textInput";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type SwitchVarBlock = VarBlockBase & {
|
||||
type: "switch";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type RadioButtonVarBlock = VarBlockBase & {
|
||||
type: "radioButton";
|
||||
title: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type CounterVarBlock = VarBlockBase & {
|
||||
type: "counter";
|
||||
text: string;
|
||||
inc: number;
|
||||
};
|
||||
|
||||
export type VarBlock =
|
||||
| NumberInputVarBlock
|
||||
| TextInputVarBlock
|
||||
| SwitchVarBlock
|
||||
| RadioButtonVarBlock
|
||||
| CounterVarBlock;
|
||||
|
||||
const varBlock = [
|
||||
"numberInput",
|
||||
"textInput",
|
||||
"switch",
|
||||
"radioButton",
|
||||
"counter",
|
||||
];
|
||||
export function isVarBlock(block: Block): block is VarBlock {
|
||||
return varBlock.includes(block.type);
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
import autobind from "autobind-decorator";
|
||||
import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from ".";
|
||||
import { version } from "@/config";
|
||||
import { AiScript, utils, values } from "@syuilo/aiscript";
|
||||
import { createAiScriptEnv } from "../aiscript/api";
|
||||
import { collectPageVars } from "../collect-page-vars";
|
||||
import { initHpmlLib, initAiLib } from "./lib";
|
||||
import * as os from "@/os";
|
||||
import { markRaw, ref, Ref, unref } from "vue";
|
||||
import { Expr, isLiteralValue, Variable } from "./expr";
|
||||
|
||||
/**
|
||||
* Hpml evaluator
|
||||
*/
|
||||
export class Hpml {
|
||||
private variables: Variable[];
|
||||
private pageVars: PageVar[];
|
||||
private envVars: Record<keyof typeof envVarsDef, any>;
|
||||
public aiscript?: AiScript;
|
||||
public pageVarUpdatedCallback?: values.VFn;
|
||||
public canvases: Record<string, HTMLCanvasElement> = {};
|
||||
public vars: Ref<Record<string, any>> = ref({});
|
||||
public page: Record<string, any>;
|
||||
|
||||
private opts: {
|
||||
randomSeed: string;
|
||||
visitor?: any;
|
||||
url?: string;
|
||||
enableAiScript: boolean;
|
||||
};
|
||||
|
||||
constructor(page: Hpml["page"], opts: Hpml["opts"]) {
|
||||
this.page = page;
|
||||
this.variables = this.page.variables;
|
||||
this.pageVars = collectPageVars(this.page.content);
|
||||
this.opts = opts;
|
||||
|
||||
if (this.opts.enableAiScript) {
|
||||
this.aiscript = markRaw(
|
||||
new AiScript(
|
||||
{
|
||||
...createAiScriptEnv({
|
||||
storageKey: `pages:${this.page.id}`,
|
||||
}),
|
||||
...initAiLib(this),
|
||||
},
|
||||
{
|
||||
in: (q) => {
|
||||
return new Promise((ok) => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
log: (type, params) => {},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.aiscript.scope.opts.onUpdated = (name, value) => {
|
||||
this.eval();
|
||||
};
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
|
||||
this.envVars = {
|
||||
AI: "kawaii",
|
||||
VERSION: version,
|
||||
URL: this.page
|
||||
? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}`
|
||||
: "",
|
||||
LOGIN: opts.visitor != null,
|
||||
NAME: opts.visitor
|
||||
? opts.visitor.name || opts.visitor.username
|
||||
: "",
|
||||
USERNAME: opts.visitor ? opts.visitor.username : "",
|
||||
USERID: opts.visitor ? opts.visitor.id : "",
|
||||
NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
|
||||
FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
|
||||
FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
|
||||
IS_CAT: opts.visitor ? opts.visitor.isCat : false,
|
||||
SEED: opts.randomSeed ? opts.randomSeed : "",
|
||||
YMD: `${date.getFullYear()}/${
|
||||
date.getMonth() + 1
|
||||
}/${date.getDate()}`,
|
||||
AISCRIPT_DISABLED: !this.opts.enableAiScript,
|
||||
NULL: null,
|
||||
};
|
||||
|
||||
this.eval();
|
||||
}
|
||||
|
||||
@autobind
|
||||
public eval() {
|
||||
try {
|
||||
this.vars.value = this.evaluateVars();
|
||||
} catch (err) {
|
||||
//this.onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public interpolate(str: string) {
|
||||
if (str == null) return null;
|
||||
return str.replace(/{(.+?)}/g, (match) => {
|
||||
const v = unref(this.vars)[match.slice(1, -1).trim()];
|
||||
return v == null ? "NULL" : v.toString();
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public callAiScript(fn: string) {
|
||||
try {
|
||||
if (this.aiscript)
|
||||
this.aiscript.execFn(this.aiscript.scope.get(fn), []);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public registerCanvas(id: string, canvas: any) {
|
||||
this.canvases[id] = canvas;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updatePageVar(name: string, value: any) {
|
||||
const pageVar = this.pageVars.find((v) => v.name === name);
|
||||
if (pageVar !== undefined) {
|
||||
pageVar.value = value;
|
||||
if (this.pageVarUpdatedCallback) {
|
||||
if (this.aiscript)
|
||||
this.aiscript.execFn(this.pageVarUpdatedCallback, [
|
||||
values.STR(name),
|
||||
utils.jsToVal(value),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
throw new HpmlError(`No such page var '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updateRandomSeed(seed: string) {
|
||||
this.opts.randomSeed = seed;
|
||||
this.envVars.SEED = seed;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private _interpolateScope(str: string, scope: HpmlScope) {
|
||||
return str.replace(/{(.+?)}/g, (match) => {
|
||||
const v = scope.getState(match.slice(1, -1).trim());
|
||||
return v == null ? "NULL" : v.toString();
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public evaluateVars(): Record<string, any> {
|
||||
const values: Record<string, any> = {};
|
||||
|
||||
for (const [k, v] of Object.entries(this.envVars)) {
|
||||
values[k] = v;
|
||||
}
|
||||
|
||||
for (const v of this.pageVars) {
|
||||
values[v.name] = v.value;
|
||||
}
|
||||
|
||||
for (const v of this.variables) {
|
||||
values[v.name] = this.evaluate(v, new HpmlScope([values]));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private evaluate(expr: Expr, scope: HpmlScope): any {
|
||||
if (isLiteralValue(expr)) {
|
||||
if (expr.type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expr.type === "number") {
|
||||
return parseInt(expr.value as any, 10);
|
||||
}
|
||||
|
||||
if (expr.type === "text" || expr.type === "multiLineText") {
|
||||
return this._interpolateScope(expr.value || "", scope);
|
||||
}
|
||||
|
||||
if (expr.type === "textList") {
|
||||
return this._interpolateScope(expr.value || "", scope)
|
||||
.trim()
|
||||
.split("\n");
|
||||
}
|
||||
|
||||
if (expr.type === "ref") {
|
||||
return scope.getState(expr.value);
|
||||
}
|
||||
|
||||
if (expr.type === "aiScriptVar") {
|
||||
if (this.aiscript) {
|
||||
try {
|
||||
return utils.valToJs(
|
||||
this.aiscript.scope.get(expr.value)
|
||||
);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Define user function
|
||||
if (expr.type === "fn") {
|
||||
return {
|
||||
slots: expr.value.slots.map((x) => x.name),
|
||||
exec: (slotArg: Record<string, any>) => {
|
||||
return this.evaluate(
|
||||
expr.value.expression,
|
||||
scope.createChildScope(slotArg, expr.id)
|
||||
);
|
||||
},
|
||||
} as Fn;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Call user function
|
||||
if (expr.type.startsWith("fn:")) {
|
||||
const fnName = expr.type.split(":")[1];
|
||||
const fn = scope.getState(fnName);
|
||||
const args = {} as Record<string, any>;
|
||||
for (let i = 0; i < fn.slots.length; i++) {
|
||||
const name = fn.slots[i];
|
||||
args[name] = this.evaluate(expr.args[i], scope);
|
||||
}
|
||||
return fn.exec(args);
|
||||
}
|
||||
|
||||
if (expr.args === undefined) return null;
|
||||
|
||||
const funcs = initHpmlLib(
|
||||
expr,
|
||||
scope,
|
||||
this.opts.randomSeed,
|
||||
this.opts.visitor
|
||||
);
|
||||
|
||||
// Call function
|
||||
const fnName = expr.type;
|
||||
const fn = (funcs as any)[fnName];
|
||||
if (fn == null) {
|
||||
throw new HpmlError(`No such function '${fnName}'`);
|
||||
} else {
|
||||
return fn(...expr.args.map((x) => this.evaluate(x, scope)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { literalDefs, Type } from ".";
|
||||
|
||||
export type ExprBase = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
// value
|
||||
|
||||
export type EmptyValue = ExprBase & {
|
||||
type: null;
|
||||
value: null;
|
||||
};
|
||||
|
||||
export type TextValue = ExprBase & {
|
||||
type: "text";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type MultiLineTextValue = ExprBase & {
|
||||
type: "multiLineText";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TextListValue = ExprBase & {
|
||||
type: "textList";
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type NumberValue = ExprBase & {
|
||||
type: "number";
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type RefValue = ExprBase & {
|
||||
type: "ref";
|
||||
value: string; // value is variable name
|
||||
};
|
||||
|
||||
export type AiScriptRefValue = ExprBase & {
|
||||
type: "aiScriptVar";
|
||||
value: string; // value is variable name
|
||||
};
|
||||
|
||||
export type UserFnValue = ExprBase & {
|
||||
type: "fn";
|
||||
value: UserFnInnerValue;
|
||||
};
|
||||
type UserFnInnerValue = {
|
||||
slots: {
|
||||
name: string;
|
||||
type: Type;
|
||||
}[];
|
||||
expression: Expr;
|
||||
};
|
||||
|
||||
export type Value =
|
||||
| EmptyValue
|
||||
| TextValue
|
||||
| MultiLineTextValue
|
||||
| TextListValue
|
||||
| NumberValue
|
||||
| RefValue
|
||||
| AiScriptRefValue
|
||||
| UserFnValue;
|
||||
|
||||
export function isLiteralValue(expr: Expr): expr is Value {
|
||||
if (expr.type == null) return true;
|
||||
if (literalDefs[expr.type]) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// call function
|
||||
|
||||
export type CallFn = ExprBase & {
|
||||
// "fn:hoge" or string
|
||||
type: string;
|
||||
args: Expr[];
|
||||
value: null;
|
||||
};
|
||||
|
||||
// variable
|
||||
export type Variable = (Value | CallFn) & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
// expression
|
||||
export type Expr = Variable | Value | CallFn;
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Hpml
|
||||
*/
|
||||
|
||||
import autobind from "autobind-decorator";
|
||||
import { Hpml } from "./evaluator";
|
||||
import { funcDefs } from "./lib";
|
||||
|
||||
export type Fn = {
|
||||
slots: string[];
|
||||
exec: (args: Record<string, any>) => ReturnType<Hpml["evaluate"]>;
|
||||
};
|
||||
|
||||
export type Type = "string" | "number" | "boolean" | "stringArray" | null;
|
||||
|
||||
export const literalDefs: Record<
|
||||
string,
|
||||
{ out: any; category: string; icon: any }
|
||||
> = {
|
||||
text: { out: "string", category: "value", icon: "ph-quotes ph-bold ph-lg" },
|
||||
multiLineText: {
|
||||
out: "string",
|
||||
category: "value",
|
||||
icon: "ph-align-left ph-bold ph-lg",
|
||||
},
|
||||
textList: {
|
||||
out: "stringArray",
|
||||
category: "value",
|
||||
icon: "ph-list ph-bold ph-lg",
|
||||
},
|
||||
number: {
|
||||
out: "number",
|
||||
category: "value",
|
||||
icon: "ph-sort-descending-up ph-bold ph-lg",
|
||||
},
|
||||
ref: { out: null, category: "value", icon: "ph-magic-wand ph-bold ph-lg" },
|
||||
aiScriptVar: {
|
||||
out: null,
|
||||
category: "value",
|
||||
icon: "ph-magic-wand ph-bold ph-lg",
|
||||
},
|
||||
fn: {
|
||||
out: "function",
|
||||
category: "value",
|
||||
icon: "ph-radical ph-bold ph-lg",
|
||||
},
|
||||
};
|
||||
|
||||
export const blockDefs = [
|
||||
...Object.entries(literalDefs).map(([k, v]) => ({
|
||||
type: k,
|
||||
out: v.out,
|
||||
category: v.category,
|
||||
icon: v.icon,
|
||||
})),
|
||||
...Object.entries(funcDefs).map(([k, v]) => ({
|
||||
type: k,
|
||||
out: v.out,
|
||||
category: v.category,
|
||||
icon: v.icon,
|
||||
})),
|
||||
];
|
||||
|
||||
export type PageVar = { name: string; value: any; type: Type };
|
||||
|
||||
export const envVarsDef: Record<string, Type> = {
|
||||
AI: "string",
|
||||
URL: "string",
|
||||
VERSION: "string",
|
||||
LOGIN: "boolean",
|
||||
NAME: "string",
|
||||
USERNAME: "string",
|
||||
USERID: "string",
|
||||
NOTES_COUNT: "number",
|
||||
FOLLOWERS_COUNT: "number",
|
||||
FOLLOWING_COUNT: "number",
|
||||
IS_CAT: "boolean",
|
||||
SEED: null,
|
||||
YMD: "string",
|
||||
AISCRIPT_DISABLED: "boolean",
|
||||
NULL: null,
|
||||
};
|
||||
|
||||
export class HpmlScope {
|
||||
private layerdStates: Record<string, any>[];
|
||||
public name: string;
|
||||
|
||||
constructor(
|
||||
layerdStates: HpmlScope["layerdStates"],
|
||||
name?: HpmlScope["name"]
|
||||
) {
|
||||
this.layerdStates = layerdStates;
|
||||
this.name = name || "anonymous";
|
||||
}
|
||||
|
||||
@autobind
|
||||
public createChildScope(
|
||||
states: Record<string, any>,
|
||||
name?: HpmlScope["name"]
|
||||
): HpmlScope {
|
||||
const layer = [states, ...this.layerdStates];
|
||||
return new HpmlScope(layer, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した名前の変数の値を取得します
|
||||
* @param name 変数名
|
||||
*/
|
||||
@autobind
|
||||
public getState(name: string): any {
|
||||
for (const later of this.layerdStates) {
|
||||
const state = later[name];
|
||||
if (state !== undefined) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
throw new HpmlError(
|
||||
`No such variable '${name}' in scope '${this.name}'`,
|
||||
{
|
||||
scope: this.layerdStates,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class HpmlError extends Error {
|
||||
public info?: any;
|
||||
|
||||
constructor(message: string, info?: any) {
|
||||
super(message);
|
||||
|
||||
this.info = info;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, HpmlError);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,587 @@
|
|||
import tinycolor from "tinycolor2";
|
||||
import { Hpml } from "./evaluator";
|
||||
import { values, utils } from "@syuilo/aiscript";
|
||||
import { Fn, HpmlScope } from ".";
|
||||
import { Expr } from "./expr";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
|
||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||
Chart.pluginService.register({
|
||||
beforeDraw: (chart, easing) => {
|
||||
if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
|
||||
const ctx = chart.chart.ctx;
|
||||
ctx.save();
|
||||
ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
|
||||
ctx.fillRect(0, 0, chart.chart.width, chart.chart.height);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
export function initAiLib(hpml: Hpml) {
|
||||
return {
|
||||
"MkPages:updated": values.FN_NATIVE(([callback]) => {
|
||||
hpml.pageVarUpdatedCallback = callback as values.VFn;
|
||||
}),
|
||||
"MkPages:get_canvas": values.FN_NATIVE(([id]) => {
|
||||
utils.assertString(id);
|
||||
const canvas = hpml.canvases[id.value];
|
||||
const ctx = canvas.getContext("2d");
|
||||
return values.OBJ(
|
||||
new Map([
|
||||
[
|
||||
"clear_rect",
|
||||
values.FN_NATIVE(([x, y, width, height]) => {
|
||||
ctx.clearRect(
|
||||
x.value,
|
||||
y.value,
|
||||
width.value,
|
||||
height.value
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"fill_rect",
|
||||
values.FN_NATIVE(([x, y, width, height]) => {
|
||||
ctx.fillRect(
|
||||
x.value,
|
||||
y.value,
|
||||
width.value,
|
||||
height.value
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"stroke_rect",
|
||||
values.FN_NATIVE(([x, y, width, height]) => {
|
||||
ctx.strokeRect(
|
||||
x.value,
|
||||
y.value,
|
||||
width.value,
|
||||
height.value
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"fill_text",
|
||||
values.FN_NATIVE(([text, x, y, width]) => {
|
||||
ctx.fillText(
|
||||
text.value,
|
||||
x.value,
|
||||
y.value,
|
||||
width ? width.value : undefined
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"stroke_text",
|
||||
values.FN_NATIVE(([text, x, y, width]) => {
|
||||
ctx.strokeText(
|
||||
text.value,
|
||||
x.value,
|
||||
y.value,
|
||||
width ? width.value : undefined
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"set_line_width",
|
||||
values.FN_NATIVE(([width]) => {
|
||||
ctx.lineWidth = width.value;
|
||||
}),
|
||||
],
|
||||
[
|
||||
"set_font",
|
||||
values.FN_NATIVE(([font]) => {
|
||||
ctx.font = font.value;
|
||||
}),
|
||||
],
|
||||
[
|
||||
"set_fill_style",
|
||||
values.FN_NATIVE(([style]) => {
|
||||
ctx.fillStyle = style.value;
|
||||
}),
|
||||
],
|
||||
[
|
||||
"set_stroke_style",
|
||||
values.FN_NATIVE(([style]) => {
|
||||
ctx.strokeStyle = style.value;
|
||||
}),
|
||||
],
|
||||
[
|
||||
"begin_path",
|
||||
values.FN_NATIVE(() => {
|
||||
ctx.beginPath();
|
||||
}),
|
||||
],
|
||||
[
|
||||
"close_path",
|
||||
values.FN_NATIVE(() => {
|
||||
ctx.closePath();
|
||||
}),
|
||||
],
|
||||
[
|
||||
"move_to",
|
||||
values.FN_NATIVE(([x, y]) => {
|
||||
ctx.moveTo(x.value, y.value);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"line_to",
|
||||
values.FN_NATIVE(([x, y]) => {
|
||||
ctx.lineTo(x.value, y.value);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"arc",
|
||||
values.FN_NATIVE(
|
||||
([x, y, radius, startAngle, endAngle]) => {
|
||||
ctx.arc(
|
||||
x.value,
|
||||
y.value,
|
||||
radius.value,
|
||||
startAngle.value,
|
||||
endAngle.value
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"rect",
|
||||
values.FN_NATIVE(([x, y, width, height]) => {
|
||||
ctx.rect(
|
||||
x.value,
|
||||
y.value,
|
||||
width.value,
|
||||
height.value
|
||||
);
|
||||
}),
|
||||
],
|
||||
[
|
||||
"fill",
|
||||
values.FN_NATIVE(() => {
|
||||
ctx.fill();
|
||||
}),
|
||||
],
|
||||
[
|
||||
"stroke",
|
||||
values.FN_NATIVE(() => {
|
||||
ctx.stroke();
|
||||
}),
|
||||
],
|
||||
])
|
||||
);
|
||||
}),
|
||||
"MkPages:chart": values.FN_NATIVE(([id, opts]) => {
|
||||
/* TODO
|
||||
utils.assertString(id);
|
||||
utils.assertObject(opts);
|
||||
const canvas = hpml.canvases[id.value];
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
||||
Chart.defaults.color = '#555';
|
||||
const chart = new Chart(canvas, {
|
||||
type: opts.value.get('type').value,
|
||||
data: {
|
||||
labels: opts.value.get('labels').value.map(x => x.value),
|
||||
datasets: opts.value.get('datasets').value.map(x => ({
|
||||
label: x.value.has('label') ? x.value.get('label').value : '',
|
||||
data: x.value.get('data').value.map(x => x.value),
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.value.has('color') ? x.value.get('color') : color,
|
||||
backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(),
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: false,
|
||||
devicePixelRatio: 1.5,
|
||||
title: {
|
||||
display: opts.value.has('title'),
|
||||
text: opts.value.has('title') ? opts.value.get('title').value : '',
|
||||
fontSize: 14,
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 32,
|
||||
right: 32,
|
||||
top: opts.value.has('title') ? 16 : 32,
|
||||
bottom: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
chartArea: {
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
...(opts.value.get('type').value === 'radar' ? {
|
||||
scale: {
|
||||
ticks: {
|
||||
display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false,
|
||||
min: opts.value.has('min') ? opts.value.get('min').value : undefined,
|
||||
max: opts.value.has('max') ? opts.value.get('max').value : undefined,
|
||||
maxTicksLimit: 8,
|
||||
},
|
||||
pointLabels: {
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
} : {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true,
|
||||
min: opts.value.has('min') ? opts.value.get('min').value : undefined,
|
||||
max: opts.value.has('max') ? opts.value.get('max').value : undefined,
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
*/
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const funcDefs: Record<
|
||||
string,
|
||||
{ in: any[]; out: any; category: string; icon: any }
|
||||
> = {
|
||||
if: {
|
||||
in: ["boolean", 0, 0],
|
||||
out: 0,
|
||||
category: "flow",
|
||||
icon: "ph-share-network ph-bold ph-lg",
|
||||
},
|
||||
for: {
|
||||
in: ["number", "function"],
|
||||
out: null,
|
||||
category: "flow",
|
||||
icon: "ph-recycle ph-bold ph-lg",
|
||||
},
|
||||
not: {
|
||||
in: ["boolean"],
|
||||
out: "boolean",
|
||||
category: "logical",
|
||||
icon: "ph-flag ph-bold ph-lg",
|
||||
},
|
||||
or: {
|
||||
in: ["boolean", "boolean"],
|
||||
out: "boolean",
|
||||
category: "logical",
|
||||
icon: "ph-flag ph-bold ph-lg",
|
||||
},
|
||||
and: {
|
||||
in: ["boolean", "boolean"],
|
||||
out: "boolean",
|
||||
category: "logical",
|
||||
icon: "ph-flag ph-bold ph-lg",
|
||||
},
|
||||
add: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-plus ph-bold ph-lg",
|
||||
},
|
||||
subtract: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-minus ph-bold ph-lg",
|
||||
},
|
||||
multiply: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-x ph-bold ph-lg",
|
||||
},
|
||||
divide: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-divide ph-bold ph-lg",
|
||||
},
|
||||
mod: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-divide ph-bold ph-lg",
|
||||
},
|
||||
round: {
|
||||
in: ["number"],
|
||||
out: "number",
|
||||
category: "operation",
|
||||
icon: "ph-calculator ph-bold ph-lg",
|
||||
},
|
||||
eq: {
|
||||
in: [0, 0],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-equals ph-bold ph-lg",
|
||||
},
|
||||
notEq: {
|
||||
in: [0, 0],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-prohibit-insert ph-bold ph-lg",
|
||||
},
|
||||
gt: {
|
||||
in: ["number", "number"],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-caret-right ph-bold ph-lg",
|
||||
},
|
||||
lt: {
|
||||
in: ["number", "number"],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-caret-left ph-bold ph-lg",
|
||||
},
|
||||
gtEq: {
|
||||
in: ["number", "number"],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-caret-double-right ph-bold ph-lg",
|
||||
},
|
||||
ltEq: {
|
||||
in: ["number", "number"],
|
||||
out: "boolean",
|
||||
category: "comparison",
|
||||
icon: "ph-caret-double-right ph-bold ph-lg",
|
||||
},
|
||||
strLen: {
|
||||
in: ["string"],
|
||||
out: "number",
|
||||
category: "text",
|
||||
icon: "ph-quotes ph-bold ph-lg",
|
||||
},
|
||||
strPick: {
|
||||
in: ["string", "number"],
|
||||
out: "string",
|
||||
category: "text",
|
||||
icon: "ph-quotes ph-bold ph-lg",
|
||||
},
|
||||
strReplace: {
|
||||
in: ["string", "string", "string"],
|
||||
out: "string",
|
||||
category: "text",
|
||||
icon: "ph-quotes ph-bold ph-lg",
|
||||
},
|
||||
strReverse: {
|
||||
in: ["string"],
|
||||
out: "string",
|
||||
category: "text",
|
||||
icon: "ph-quotes ph-bold ph-lg",
|
||||
},
|
||||
join: {
|
||||
in: ["stringArray", "string"],
|
||||
out: "string",
|
||||
category: "text",
|
||||
icon: "ph-quotes ph-bold ph-lg",
|
||||
},
|
||||
stringToNumber: {
|
||||
in: ["string"],
|
||||
out: "number",
|
||||
category: "convert",
|
||||
icon: "ph-swap ph-bold ph-lg",
|
||||
},
|
||||
numberToString: {
|
||||
in: ["number"],
|
||||
out: "string",
|
||||
category: "convert",
|
||||
icon: "ph-swap ph-bold ph-lg",
|
||||
},
|
||||
splitStrByLine: {
|
||||
in: ["string"],
|
||||
out: "stringArray",
|
||||
category: "convert",
|
||||
icon: "ph-swap ph-bold ph-lg",
|
||||
},
|
||||
pick: {
|
||||
in: [null, "number"],
|
||||
out: null,
|
||||
category: "list",
|
||||
icon: "ph-text-indent ph-bold ph-lg",
|
||||
},
|
||||
listLen: {
|
||||
in: [null],
|
||||
out: "number",
|
||||
category: "list",
|
||||
icon: "ph-text-indent ph-bold ph-lg",
|
||||
},
|
||||
rannum: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
dailyRannum: {
|
||||
in: ["number", "number"],
|
||||
out: "number",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
seedRannum: {
|
||||
in: [null, "number", "number"],
|
||||
out: "number",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
random: {
|
||||
in: ["number"],
|
||||
out: "boolean",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
dailyRandom: {
|
||||
in: ["number"],
|
||||
out: "boolean",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
seedRandom: {
|
||||
in: [null, "number"],
|
||||
out: "boolean",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
randomPick: {
|
||||
in: [0],
|
||||
out: 0,
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
dailyRandomPick: {
|
||||
in: [0],
|
||||
out: 0,
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
seedRandomPick: {
|
||||
in: [null, 0],
|
||||
out: 0,
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
},
|
||||
DRPWPM: {
|
||||
in: ["stringArray"],
|
||||
out: "string",
|
||||
category: "random",
|
||||
icon: "ph-dice-five ph-bold ph-lg",
|
||||
}, // dailyRandomPickWithProbabilityMapping
|
||||
};
|
||||
|
||||
export function initHpmlLib(
|
||||
expr: Expr,
|
||||
scope: HpmlScope,
|
||||
randomSeed: string,
|
||||
visitor?: any
|
||||
) {
|
||||
const date = new Date();
|
||||
const day = `${visitor ? visitor.id : ""} ${date.getFullYear()}/${
|
||||
date.getMonth() + 1
|
||||
}/${date.getDate()}`;
|
||||
|
||||
// SHOULD be fine to ignore since it's intended + function shape isn't defined
|
||||
const funcs: Record<string, Function> = {
|
||||
not: (a: boolean) => !a,
|
||||
or: (a: boolean, b: boolean) => a || b,
|
||||
and: (a: boolean, b: boolean) => a && b,
|
||||
eq: (a: any, b: any) => a === b,
|
||||
notEq: (a: any, b: any) => a !== b,
|
||||
gt: (a: number, b: number) => a > b,
|
||||
lt: (a: number, b: number) => a < b,
|
||||
gtEq: (a: number, b: number) => a >= b,
|
||||
ltEq: (a: number, b: number) => a <= b,
|
||||
if: (bool: boolean, a: any, b: any) => (bool ? a : b),
|
||||
for: (times: number, fn: Fn) => {
|
||||
const result: any[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result.push(
|
||||
fn.exec({
|
||||
[fn.slots[0]]: i + 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
add: (a: number, b: number) => a + b,
|
||||
subtract: (a: number, b: number) => a - b,
|
||||
multiply: (a: number, b: number) => a * b,
|
||||
divide: (a: number, b: number) => a / b,
|
||||
mod: (a: number, b: number) => a % b,
|
||||
round: (a: number) => Math.round(a),
|
||||
strLen: (a: string) => a.length,
|
||||
strPick: (a: string, b: number) => a[b - 1],
|
||||
strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
|
||||
strReverse: (a: string) => a.split("").reverse().join(""),
|
||||
join: (texts: string[], separator: string) =>
|
||||
texts.join(separator || ""),
|
||||
stringToNumber: (a: string) => parseInt(a),
|
||||
numberToString: (a: number) => a.toString(),
|
||||
splitStrByLine: (a: string) => a.split("\n"),
|
||||
pick: (list: any[], i: number) => list[i - 1],
|
||||
listLen: (list: any[]) => list.length,
|
||||
random: (probability: number) =>
|
||||
Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) <
|
||||
probability,
|
||||
rannum: (min: number, max: number) =>
|
||||
min +
|
||||
Math.floor(
|
||||
seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)
|
||||
),
|
||||
randomPick: (list: any[]) =>
|
||||
list[
|
||||
Math.floor(
|
||||
seedrandom(`${randomSeed}:${expr.id}`)() * list.length
|
||||
)
|
||||
],
|
||||
dailyRandom: (probability: number) =>
|
||||
Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability,
|
||||
dailyRannum: (min: number, max: number) =>
|
||||
min +
|
||||
Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)),
|
||||
dailyRandomPick: (list: any[]) =>
|
||||
list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)],
|
||||
seedRandom: (seed: any, probability: number) =>
|
||||
Math.floor(seedrandom(seed)() * 100) < probability,
|
||||
seedRannum: (seed: any, min: number, max: number) =>
|
||||
min + Math.floor(seedrandom(seed)() * (max - min + 1)),
|
||||
seedRandomPick: (seed: any, list: any[]) =>
|
||||
list[Math.floor(seedrandom(seed)() * list.length)],
|
||||
DRPWPM: (list: string[]) => {
|
||||
const xs: any[] = [];
|
||||
let totalFactor = 0;
|
||||
for (const x of list) {
|
||||
const parts = x.split(" ");
|
||||
const factor = parseInt(parts.pop()!, 10);
|
||||
const text = parts.join(" ");
|
||||
totalFactor += factor;
|
||||
xs.push({ factor, text });
|
||||
}
|
||||
const r = seedrandom(`${day}:${expr.id}`)() * totalFactor;
|
||||
let stackedFactor = 0;
|
||||
for (const x of xs) {
|
||||
if (r >= stackedFactor && r <= stackedFactor + x.factor) {
|
||||
return x.text;
|
||||
} else {
|
||||
stackedFactor += x.factor;
|
||||
}
|
||||
}
|
||||
return xs[0].text;
|
||||
},
|
||||
};
|
||||
|
||||
return funcs;
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import autobind from "autobind-decorator";
|
||||
import { Type, envVarsDef, PageVar } from ".";
|
||||
import { Expr, isLiteralValue, Variable } from "./expr";
|
||||
import { funcDefs } from "./lib";
|
||||
|
||||
type TypeError = {
|
||||
arg: number;
|
||||
expect: Type;
|
||||
actual: Type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hpml type checker
|
||||
*/
|
||||
export class HpmlTypeChecker {
|
||||
public variables: Variable[];
|
||||
public pageVars: PageVar[];
|
||||
|
||||
constructor(
|
||||
variables: HpmlTypeChecker["variables"] = [],
|
||||
pageVars: HpmlTypeChecker["pageVars"] = []
|
||||
) {
|
||||
this.variables = variables;
|
||||
this.pageVars = pageVars;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public typeCheck(v: Expr): TypeError | null {
|
||||
if (isLiteralValue(v)) return null;
|
||||
|
||||
const def = funcDefs[v.type || ""];
|
||||
if (def == null) {
|
||||
throw new Error(`Unknown type: ${v.type}`);
|
||||
}
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
const type = this.infer(v.args[i]);
|
||||
if (type === null) continue;
|
||||
|
||||
if (typeof arg === "number") {
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
} else if (type !== generic[arg]) {
|
||||
return {
|
||||
arg: i,
|
||||
expect: generic[arg],
|
||||
actual: type,
|
||||
};
|
||||
}
|
||||
} else if (type !== arg) {
|
||||
return {
|
||||
arg: i,
|
||||
expect: arg,
|
||||
actual: type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getExpectedType(v: Expr, slot: number): Type {
|
||||
const def = funcDefs[v.type || ""];
|
||||
if (def == null) {
|
||||
throw new Error(`Unknown type: ${v.type}`);
|
||||
}
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
const type = this.infer(v.args[i]);
|
||||
if (type === null) continue;
|
||||
|
||||
if (typeof arg === "number") {
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof def.in[slot] === "number") {
|
||||
return generic[def.in[slot]] || null;
|
||||
} else {
|
||||
return def.in[slot];
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public infer(v: Expr): Type {
|
||||
if (v.type === null) return null;
|
||||
if (v.type === "text") return "string";
|
||||
if (v.type === "multiLineText") return "string";
|
||||
if (v.type === "textList") return "stringArray";
|
||||
if (v.type === "number") return "number";
|
||||
if (v.type === "ref") {
|
||||
const variable = this.variables.find((va) => va.name === v.value);
|
||||
if (variable) {
|
||||
return this.infer(variable);
|
||||
}
|
||||
|
||||
const pageVar = this.pageVars.find((va) => va.name === v.value);
|
||||
if (pageVar) {
|
||||
return pageVar.type;
|
||||
}
|
||||
|
||||
const envVar = envVarsDef[v.value || ""];
|
||||
if (envVar !== undefined) {
|
||||
return envVar;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
if (v.type === "aiScriptVar") return null;
|
||||
if (v.type === "fn") return null; // todo
|
||||
if (v.type.startsWith("fn:")) return null; // todo
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
const def = funcDefs[v.type];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
if (typeof arg === "number") {
|
||||
const type = this.infer(v.args[i]);
|
||||
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
} else {
|
||||
if (type !== generic[arg]) {
|
||||
generic[arg] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof def.out === "number") {
|
||||
return generic[def.out];
|
||||
} else {
|
||||
return def.out;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarByName(name: string): Variable {
|
||||
const v = this.variables.find((x) => x.name === name);
|
||||
if (v !== undefined) {
|
||||
return v;
|
||||
} else {
|
||||
throw new Error(`No such variable '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarsByType(type: Type): Variable[] {
|
||||
if (type == null) return this.variables;
|
||||
return this.variables.filter(
|
||||
(x) => this.infer(x) === null || this.infer(x) === type
|
||||
);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getEnvVarsByType(type: Type): string[] {
|
||||
if (type == null) return Object.keys(envVarsDef);
|
||||
return Object.entries(envVarsDef)
|
||||
.filter(([k, v]) => v === null || type === v)
|
||||
.map(([k, v]) => k);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getPageVarsByType(type: Type): string[] {
|
||||
if (type == null) return this.pageVars.map((v) => v.name);
|
||||
return this.pageVars.filter((v) => type === v.type).map((v) => v.name);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public isUsedName(name: string) {
|
||||
if (this.variables.some((v) => v.name === name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.pageVars.some((v) => v.name === name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (envVarsDef[name]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,17 @@ import { Storage } from "./pizzax";
|
|||
import lightTheme from "@/themes/l-rosepinedawn.json5";
|
||||
import darkTheme from "@/themes/d-rosepine.json5";
|
||||
|
||||
export const postFormActions: {
|
||||
title: any;
|
||||
handler: (form: any, update: any) => void;
|
||||
}[] = [];
|
||||
export const userActions: { title: any; handler: (user: any) => void }[] = [];
|
||||
export const noteActions: { title: any; handler: (note: any) => void }[] = [];
|
||||
export const noteViewInterruptors: { handler: (note: any) => Promise<any> }[] =
|
||||
[];
|
||||
export const notePostInterruptors: { handler: (note: any) => Promise<any> }[] =
|
||||
[];
|
||||
|
||||
const menuOptions = [
|
||||
"notifications",
|
||||
"followRequests",
|
||||
|
|
|
@ -735,6 +735,35 @@ hr {
|
|||
}
|
||||
}
|
||||
|
||||
._anime_bounce {
|
||||
will-change: transform;
|
||||
animation: bounce ease 0.7s;
|
||||
animation-iteration-count: 1;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
._anime_bounce_ready {
|
||||
will-change: transform;
|
||||
transform: scaleX(0.9) scaleY(0.9);
|
||||
}
|
||||
._anime_bounce_standBy {
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: scaleX(0.9) scaleY(0.9);
|
||||
}
|
||||
19% {
|
||||
transform: scaleX(1.1) scaleY(1.1);
|
||||
}
|
||||
48% {
|
||||
transform: scaleX(0.95) scaleY(0.95);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1) scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.ph-xxs {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:style="{ backgroundImage: `url(${$i.bannerUrl})` }"
|
||||
></div>
|
||||
<button
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="
|
||||
`${i18n.ts.account}: @${$i.username}`
|
||||
"
|
||||
|
@ -22,7 +23,13 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="middle">
|
||||
<MkA class="item index" active-class="active" to="/" exact>
|
||||
<MkA
|
||||
v-click-anime
|
||||
class="item index"
|
||||
active-class="active"
|
||||
to="/"
|
||||
exact
|
||||
>
|
||||
<i class="icon ph-house ph-bold ph-lg ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.timeline }}</span>
|
||||
</MkA>
|
||||
|
@ -34,6 +41,7 @@
|
|||
navbarItemDef[item] &&
|
||||
navbarItemDef[item].show !== false
|
||||
"
|
||||
v-click-anime
|
||||
class="item _button"
|
||||
:class="[item, { active: navbarItemDef[item].active }]"
|
||||
active-class="active"
|
||||
|
@ -61,6 +69,7 @@
|
|||
<div class="divider"></div>
|
||||
<MkA
|
||||
v-if="$i.isAdmin || $i.isModerator"
|
||||
v-click-anime
|
||||
class="item"
|
||||
active-class="active"
|
||||
to="/admin"
|
||||
|
@ -68,7 +77,7 @@
|
|||
<i class="icon ph-door ph-bold ph-lg ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<button class="item _button" @click="more">
|
||||
<button v-click-anime class="item _button" @click="more">
|
||||
<i
|
||||
class="icon ph-dots-three-outline ph-bold ph-lg ph-fw ph-lg"
|
||||
></i
|
||||
|
@ -77,7 +86,12 @@
|
|||
><i class="icon ph-circle ph-fill"></i
|
||||
></span>
|
||||
</button>
|
||||
<MkA class="item" active-class="active" to="/settings">
|
||||
<MkA
|
||||
v-click-anime
|
||||
class="item"
|
||||
active-class="active"
|
||||
to="/settings"
|
||||
>
|
||||
<i class="icon ph-gear-six ph-bold ph-lg ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.settings }}</span>
|
||||
</MkA>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:style="{ backgroundImage: `url(${$i.bannerUrl})` }"
|
||||
></div>
|
||||
<button
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="
|
||||
`${i18n.ts.account}: @${$i.username}`
|
||||
"
|
||||
|
@ -23,6 +24,7 @@
|
|||
</div>
|
||||
<nav class="middle">
|
||||
<MkA
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.timeline"
|
||||
class="item index"
|
||||
active-class="active"
|
||||
|
@ -40,6 +42,7 @@
|
|||
navbarItemDef[item] &&
|
||||
navbarItemDef[item].show !== false
|
||||
"
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="
|
||||
i18n.ts[navbarItemDef[item].title]
|
||||
"
|
||||
|
@ -70,6 +73,7 @@
|
|||
<div class="divider"></div>
|
||||
<MkA
|
||||
v-if="$i.isAdmin || $i.isModerator"
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.controlPanel"
|
||||
class="item _button"
|
||||
active-class="active"
|
||||
|
@ -88,6 +92,7 @@
|
|||
><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<button
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.more"
|
||||
class="item _button"
|
||||
@click="more"
|
||||
|
@ -101,6 +106,7 @@
|
|||
></span>
|
||||
</button>
|
||||
<MkA
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.settings"
|
||||
class="item _button"
|
||||
active-class="active"
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
>
|
||||
<small
|
||||
>Powered by
|
||||
<a
|
||||
href="https://git.astolfo.cool/natty/magnetar"
|
||||
target="_blank"
|
||||
<a href="https://git.astolfo.cool/natty/magnetar" target="_blank"
|
||||
>Magnetar</a
|
||||
></small
|
||||
>
|
||||
|
@ -47,6 +45,10 @@
|
|||
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.explore }}</MkA
|
||||
>
|
||||
<MkA to="/pages" class="link" active-class="active"
|
||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.pages }}</MkA
|
||||
>
|
||||
<MkA to="/gallery" class="link" active-class="active"
|
||||
><i class="ph-image-square ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.gallery }}</MkA
|
||||
|
@ -72,11 +74,17 @@ import { host, instanceName } from "@/config";
|
|||
import { search } from "@/scripts/search";
|
||||
import * as os from "@/os";
|
||||
import { instance } from "@/instance";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import XSigninDialog from "@/components/MkSigninDialog.vue";
|
||||
import XSignupDialog from "@/components/MkSignupDialog.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { ColdDeviceStorage, defaultStore } from "@/store";
|
||||
import { mainRouter } from "@/router";
|
||||
import { PageMetadata, provideMetadataReceiver } from "@/scripts/page-metadata";
|
||||
import {
|
||||
PageMetadata,
|
||||
provideMetadataReceiver,
|
||||
setPageMetadata,
|
||||
} from "@/scripts/page-metadata";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const DESKTOP_THRESHOLD = 1000;
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
><i class="ph-compass ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.explore }}</MkA
|
||||
>
|
||||
<MkA to="/pages" class="link" active-class="active"
|
||||
><i class="ph-file-text ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.pages }}</MkA
|
||||
>
|
||||
<MkA to="/gallery" class="link" active-class="active"
|
||||
><i class="ph-image-square ph-bold ph-lg icon"></i
|
||||
>{{ i18n.ts.gallery }}</MkA
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript">
|
||||
<template #header
|
||||
><i class="ph-terminal-window ph-bold ph-lg"></i
|
||||
>{{ i18n.ts._widgets.aiscript }}</template
|
||||
>
|
||||
|
||||
<div class="uylguesu _monospace">
|
||||
<textarea
|
||||
v-model="widgetProps.script"
|
||||
placeholder="(1 + 1)"
|
||||
></textarea>
|
||||
<button class="_buttonPrimary" @click="run">RUN</button>
|
||||
<div class="logs">
|
||||
<div
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
class="log"
|
||||
:class="{ print: log.print }"
|
||||
>
|
||||
{{ log.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { AiScript, parse, utils } from "@syuilo/aiscript";
|
||||
import {
|
||||
useWidgetPropsManager,
|
||||
Widget,
|
||||
WidgetComponentEmits,
|
||||
WidgetComponentExpose,
|
||||
WidgetComponentProps,
|
||||
} from "./widget";
|
||||
import { GetFormResultType } from "@/scripts/form";
|
||||
import * as os from "@/os";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import { createAiScriptEnv } from "@/scripts/aiscript/api";
|
||||
import { $i } from "@/account";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const name = "aiscript";
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: "boolean" as const,
|
||||
default: true,
|
||||
},
|
||||
script: {
|
||||
type: "string" as const,
|
||||
multiline: true,
|
||||
default: "(1 + 1)",
|
||||
hidden: 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 logs = ref<
|
||||
{
|
||||
id: string;
|
||||
text: string;
|
||||
print: boolean;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const run = async () => {
|
||||
logs.value = [];
|
||||
const aiscript = new AiScript(
|
||||
createAiScriptEnv({
|
||||
storageKey: "widget",
|
||||
token: $i?.token,
|
||||
}),
|
||||
{
|
||||
in: (q) => {
|
||||
return new Promise((ok) => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
logs.value.push({
|
||||
id: Math.random().toString(),
|
||||
text:
|
||||
value.type === "str"
|
||||
? value.value
|
||||
: utils.valToString(value),
|
||||
print: true,
|
||||
});
|
||||
},
|
||||
log: (type, params) => {
|
||||
switch (type) {
|
||||
case "end":
|
||||
logs.value.push({
|
||||
id: Math.random().toString(),
|
||||
text: utils.valToString(params.val, true),
|
||||
print: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(widgetProps.script);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "Syntax error :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uylguesu {
|
||||
text-align: right;
|
||||
|
||||
> textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
padding: 16px;
|
||||
color: var(--fg);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
display: inline-block;
|
||||
margin: 8px;
|
||||
padding: 0 10px;
|
||||
height: 28px;
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .logs {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .log {
|
||||
&:not(.print) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="mkw-button">
|
||||
<MkButton :primary="widgetProps.colored" full @click="run">
|
||||
{{ widgetProps.label }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { AiScript, parse, utils } 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 MkButton from "@/components/MkButton.vue";
|
||||
|
||||
const name = "button";
|
||||
|
||||
const widgetPropsDef = {
|
||||
label: {
|
||||
type: "string" as const,
|
||||
default: "BUTTON",
|
||||
},
|
||||
colored: {
|
||||
type: "boolean" as const,
|
||||
default: true,
|
||||
},
|
||||
script: {
|
||||
type: "string" as const,
|
||||
multiline: true,
|
||||
default: 'Mk:dialog("hello" "world")',
|
||||
},
|
||||
};
|
||||
|
||||
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 run = async () => {
|
||||
const aiscript = new AiScript(
|
||||
createAiScriptEnv({
|
||||
storageKey: "widget",
|
||||
token: $i?.token,
|
||||
}),
|
||||
{
|
||||
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 = parse(widgetProps.script);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: "Syntax error :(",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (err) {
|
||||
os.alert({
|
||||
type: "error",
|
||||
text: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mkw-button {
|
||||
}
|
||||
</style>
|
|
@ -61,6 +61,10 @@ export default function (app: App) {
|
|||
"MkwSlideshow",
|
||||
defineAsyncComponent(() => import("./slideshow.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwServerMetric",
|
||||
defineAsyncComponent(() => import("./server-metric/index.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwOnlineUsers",
|
||||
defineAsyncComponent(() => import("./online-users.vue"))
|
||||
|
@ -69,6 +73,18 @@ export default function (app: App) {
|
|||
"MkwJobQueue",
|
||||
defineAsyncComponent(() => import("./job-queue.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwInstanceCloud",
|
||||
defineAsyncComponent(() => import("./instance-cloud.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwButton",
|
||||
defineAsyncComponent(() => import("./button.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwAiscript",
|
||||
defineAsyncComponent(() => import("./aiscript.vue"))
|
||||
);
|
||||
app.component(
|
||||
"MkwUserList",
|
||||
defineAsyncComponent(() => import("./user-list.vue"))
|
||||
|
@ -94,9 +110,13 @@ export const widgets = [
|
|||
"digitalClock",
|
||||
"unixClock",
|
||||
"federation",
|
||||
"instanceCloud",
|
||||
"postForm",
|
||||
"slideshow",
|
||||
"serverMetric",
|
||||
"serverInfo",
|
||||
"onlineUsers",
|
||||
"jobQueue",
|
||||
"button",
|
||||
"aiscript",
|
||||
];
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<MkContainer
|
||||
:naked="widgetProps.transparent"
|
||||
:show-header="false"
|
||||
class="mkw-instance-cloud"
|
||||
>
|
||||
<div class="">
|
||||
<MkTagCloud v-if="activeInstances">
|
||||
<li v-for="instance in activeInstances" :key="instance.id">
|
||||
<a @click.prevent="onInstanceClick(instance)">
|
||||
<img
|
||||
style="width: 32px"
|
||||
:src="getInstanceIcon(instance)"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</MkTagCloud>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import {
|
||||
useWidgetPropsManager,
|
||||
WidgetComponentEmits,
|
||||
WidgetComponentProps,
|
||||
} from "./widget";
|
||||
import type { Widget, WidgetComponentExpose } from "./widget";
|
||||
import type { GetFormResultType } from "@/scripts/form";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import MkTagCloud from "@/components/MkTagCloud.vue";
|
||||
import * as os from "@/os";
|
||||
import { useInterval } from "@/scripts/use-interval";
|
||||
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
|
||||
|
||||
const name = "instanceCloud";
|
||||
|
||||
const widgetPropsDef = {
|
||||
transparent: {
|
||||
type: "boolean" as const,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
let cloud = $ref<InstanceType<typeof MkTagCloud> | null>();
|
||||
let activeInstances = $shallowRef(null);
|
||||
|
||||
function onInstanceClick(i) {
|
||||
os.pageWindow(`/instance-info/${i.host}`);
|
||||
}
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
os.api("federation/instances", {
|
||||
sort: "+lastCommunicatedAt",
|
||||
limit: 25,
|
||||
}).then((res) => {
|
||||
activeInstances = res;
|
||||
if (cloud) cloud.update();
|
||||
});
|
||||
},
|
||||
1000 * 60 * 3,
|
||||
{
|
||||
immediate: true,
|
||||
afterMounted: true,
|
||||
}
|
||||
);
|
||||
|
||||
function getInstanceIcon(instance): string {
|
||||
return (
|
||||
getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
|
||||
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
|
||||
"/client-assets/dummy.png"
|
||||
);
|
||||
}
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<div class="lcfyofjk">
|
||||
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
|
||||
<defs>
|
||||
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(189, 43%, 73%)"></stop>
|
||||
<stop offset="100%" stop-color="hsl(343, 76%, 68%)"></stop>
|
||||
</linearGradient>
|
||||
<mask
|
||||
:id="cpuMaskId"
|
||||
x="0"
|
||||
y="0"
|
||||
:width="viewBoxX"
|
||||
:height="viewBoxY"
|
||||
>
|
||||
<polygon
|
||||
:points="cpuPolygonPoints"
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="cpuPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle :cx="cpuHeadX" :cy="cpuHeadY" r="1.5" fill="#fff" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="-2"
|
||||
y="-2"
|
||||
:width="viewBoxX + 4"
|
||||
:height="viewBoxY + 4"
|
||||
:style="`stroke: none; fill: url(#${cpuGradientId}); mask: url(#${cpuMaskId})`"
|
||||
/>
|
||||
<text x="1" y="5">
|
||||
CPU
|
||||
<tspan>{{ cpuP }}%</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
|
||||
<defs>
|
||||
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(189, 43%, 73%)"></stop>
|
||||
<stop offset="100%" stop-color="hsl(343, 76%, 68%)"></stop>
|
||||
</linearGradient>
|
||||
<mask
|
||||
:id="memMaskId"
|
||||
x="0"
|
||||
y="0"
|
||||
:width="viewBoxX"
|
||||
:height="viewBoxY"
|
||||
>
|
||||
<polygon
|
||||
:points="memPolygonPoints"
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="memPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle :cx="memHeadX" :cy="memHeadY" r="1.5" fill="#fff" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="-2"
|
||||
y="-2"
|
||||
:width="viewBoxX + 4"
|
||||
:height="viewBoxY + 4"
|
||||
:style="`stroke: none; fill: url(#${memGradientId}); mask: url(#${memMaskId})`"
|
||||
/>
|
||||
<text x="1" y="5">
|
||||
MEM
|
||||
<tspan>{{ memP }}%</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
let viewBoxX: number = $ref(50);
|
||||
let viewBoxY: number = $ref(30);
|
||||
let stats: any[] = $ref([]);
|
||||
const cpuGradientId = uuid();
|
||||
const cpuMaskId = uuid();
|
||||
const memGradientId = uuid();
|
||||
const memMaskId = uuid();
|
||||
let cpuPolylinePoints: string = $ref("");
|
||||
let memPolylinePoints: string = $ref("");
|
||||
let cpuPolygonPoints: string = $ref("");
|
||||
let memPolygonPoints: string = $ref("");
|
||||
let cpuHeadX: any = $ref(null);
|
||||
let cpuHeadY: any = $ref(null);
|
||||
let memHeadX: any = $ref(null);
|
||||
let memHeadY: any = $ref(null);
|
||||
let cpuP: string = $ref("");
|
||||
let memP: string = $ref("");
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on("stats", onStats);
|
||||
props.connection.on("statsLog", onStatsLog);
|
||||
props.connection.send("requestLog", {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
props.connection.off("statsLog", onStatsLog);
|
||||
});
|
||||
|
||||
function onStats(connStats) {
|
||||
stats.push(connStats);
|
||||
if (stats.length > 50) stats.shift();
|
||||
|
||||
let cpuPolylinePointsStats = stats.map((s, i) => [
|
||||
viewBoxX - (stats.length - 1 - i),
|
||||
(1 - s.cpu) * viewBoxY,
|
||||
]);
|
||||
let memPolylinePointsStats = stats.map((s, i) => [
|
||||
viewBoxX - (stats.length - 1 - i),
|
||||
(1 - s.mem.active / props.meta.mem.total) * viewBoxY,
|
||||
]);
|
||||
cpuPolylinePoints = cpuPolylinePointsStats
|
||||
.map((xy) => `${xy[0]},${xy[1]}`)
|
||||
.join(" ");
|
||||
memPolylinePoints = memPolylinePointsStats
|
||||
.map((xy) => `${xy[0]},${xy[1]}`)
|
||||
.join(" ");
|
||||
|
||||
cpuPolygonPoints = `${
|
||||
viewBoxX - (stats.length - 1)
|
||||
},${viewBoxY} ${cpuPolylinePoints} ${viewBoxX},${viewBoxY}`;
|
||||
memPolygonPoints = `${
|
||||
viewBoxX - (stats.length - 1)
|
||||
},${viewBoxY} ${memPolylinePoints} ${viewBoxX},${viewBoxY}`;
|
||||
|
||||
cpuHeadX = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][0];
|
||||
cpuHeadY = cpuPolylinePointsStats[cpuPolylinePointsStats.length - 1][1];
|
||||
memHeadX = memPolylinePointsStats[memPolylinePointsStats.length - 1][0];
|
||||
memHeadY = memPolylinePointsStats[memPolylinePointsStats.length - 1][1];
|
||||
|
||||
cpuP = (connStats.cpu * 100).toFixed(0);
|
||||
memP = ((connStats.mem.active / props.meta.mem.total) * 100).toFixed(0);
|
||||
}
|
||||
|
||||
function onStatsLog(statsLog) {
|
||||
for (const revStats of [...statsLog].reverse()) {
|
||||
onStats(revStats);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lcfyofjk {
|
||||
display: flex;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 5px;
|
||||
fill: currentColor;
|
||||
|
||||
> tspan {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="vrvdvrys">
|
||||
<XPie class="pie" :value="usage" />
|
||||
<div>
|
||||
<p><i class="ph-cpu ph-bold ph-lg"></i>CPU</p>
|
||||
<p>{{ meta.cpu.cores }} Logical cores</p>
|
||||
<p>{{ meta.cpu.model }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import XPie from "./pie.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
let usage: number = $ref(0);
|
||||
|
||||
function onStats(stats) {
|
||||
usage = stats.cpu;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on("stats", onStats);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vrvdvrys {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="zbwaqsat">
|
||||
<XPie class="pie" :value="usage" />
|
||||
<div>
|
||||
<p><i class="ph-hard-drives ph-bold ph-lg"></i>Disk</p>
|
||||
<p>Total: {{ bytes(total, 1) }}</p>
|
||||
<p>Free: {{ bytes(available, 1) }}</p>
|
||||
<p>Used: {{ bytes(used, 1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
import XPie from "./pie.vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
|
||||
const props = defineProps<{
|
||||
meta: any; // TODO
|
||||
}>();
|
||||
|
||||
const usage = $computed(() => props.meta.fs.used / props.meta.fs.total);
|
||||
const total = $computed(() => props.meta.fs.total);
|
||||
const used = $computed(() => props.meta.fs.used);
|
||||
const available = $computed(() => props.meta.fs.total - props.meta.fs.used);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbwaqsat {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<MkContainer
|
||||
:show-header="widgetProps.showHeader"
|
||||
:naked="widgetProps.transparent"
|
||||
>
|
||||
<template #header
|
||||
><i class="ph-hard-drives ph-bold ph-lg"></i
|
||||
>{{ i18n.ts._widgets.serverMetric }}</template
|
||||
>
|
||||
<template #func
|
||||
><button class="_button" @click="toggleView()">
|
||||
<i class="ph-sort-ascending ph-bold ph-lg"></i></button
|
||||
></template>
|
||||
|
||||
<div v-if="meta" class="mkw-serverMetric">
|
||||
<XCpuMemory
|
||||
v-if="widgetProps.view === 0"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XNet
|
||||
v-else-if="widgetProps.view === 1"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XCpu
|
||||
v-else-if="widgetProps.view === 2"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XMemory
|
||||
v-else-if="widgetProps.view === 3"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XDisk
|
||||
v-else-if="widgetProps.view === 4"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
<XMeili
|
||||
v-else-if="
|
||||
instance.features.searchFilters && widgetProps.view === 5
|
||||
"
|
||||
:connection="connection"
|
||||
:meta="meta"
|
||||
/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import {
|
||||
useWidgetPropsManager,
|
||||
Widget,
|
||||
WidgetComponentEmits,
|
||||
WidgetComponentExpose,
|
||||
WidgetComponentProps,
|
||||
} from "../widget";
|
||||
import XCpuMemory from "./cpu-mem.vue";
|
||||
import XNet from "./net.vue";
|
||||
import XCpu from "./cpu.vue";
|
||||
import XMemory from "./mem.vue";
|
||||
import XDisk from "./disk.vue";
|
||||
import XMeili from "./meilisearch.vue";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import { GetFormResultType } from "@/scripts/form";
|
||||
import * as os from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import { i18n } from "@/i18n";
|
||||
import { instance } from "@/instance";
|
||||
|
||||
const name = "serverMetric";
|
||||
|
||||
const widgetPropsDef = {
|
||||
showHeader: {
|
||||
type: "boolean" as const,
|
||||
default: true,
|
||||
},
|
||||
transparent: {
|
||||
type: "boolean" as const,
|
||||
default: false,
|
||||
},
|
||||
view: {
|
||||
type: "number" as const,
|
||||
default: 0,
|
||||
hidden: 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, save } = useWidgetPropsManager(
|
||||
name,
|
||||
widgetPropsDef,
|
||||
props,
|
||||
emit
|
||||
);
|
||||
|
||||
const meta = ref(null);
|
||||
|
||||
os.api("server-info", {}).then((res) => {
|
||||
meta.value = res;
|
||||
});
|
||||
|
||||
const toggleView = () => {
|
||||
if (
|
||||
(widgetProps.view === 5 && instance.features.searchFilters) ||
|
||||
(widgetProps.view === 4 && !instance.features.searchFilters)
|
||||
) {
|
||||
widgetProps.view = 0;
|
||||
} else {
|
||||
widgetProps.view++;
|
||||
}
|
||||
save();
|
||||
};
|
||||
|
||||
const connection = stream.useChannel("serverStats");
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
});
|
||||
|
||||
defineExpose<WidgetComponentExpose>({
|
||||
name,
|
||||
configure,
|
||||
id: props.widget ? props.widget.id : null,
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div class="verusivbr">
|
||||
<XPie
|
||||
v-tooltip="i18n.ts.meiliIndexCount"
|
||||
class="pie"
|
||||
:value="progress"
|
||||
:reverse="true"
|
||||
/>
|
||||
<div>
|
||||
<p><i class="ph-file-search ph-bold ph-lg"></i>MeiliSearch</p>
|
||||
<p>{{ i18n.ts._widgets.meiliStatus }}: {{ available }}</p>
|
||||
<p>{{ i18n.ts._widgets.meiliSize }}: {{ bytes(totalSize, 1) }}</p>
|
||||
<p>{{ i18n.ts._widgets.meiliIndexCount }}: {{ indexCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted } from "vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
import XPie from "./pie.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
let progress: number = $ref(0);
|
||||
let serverStats = $ref(null);
|
||||
let totalSize: number = $ref(0);
|
||||
let indexCount: number = $ref(0);
|
||||
let available: string = $ref("unavailable");
|
||||
|
||||
function onStats(stats) {
|
||||
totalSize = stats.meilisearch.size;
|
||||
indexCount = stats.meilisearch.indexed_count;
|
||||
available = stats.meilisearch.health;
|
||||
progress = indexCount / serverStats.notesCount;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
os.api("stats", {}).then((res) => {
|
||||
serverStats = res;
|
||||
});
|
||||
props.connection.on("stats", onStats);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.verusivbr {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="zlxnikvl">
|
||||
<XPie class="pie" :value="usage" />
|
||||
<div>
|
||||
<p><i class="ph-microchip ph-bold ph-lg"></i>RAM</p>
|
||||
<p>Total: {{ bytes(total, 1) }}</p>
|
||||
<p>Used: {{ bytes(used, 1) }}</p>
|
||||
<p>Free: {{ bytes(free, 1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import XPie from "./pie.vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
let usage: number = $ref(0);
|
||||
let total: number = $ref(0);
|
||||
let used: number = $ref(0);
|
||||
let free: number = $ref(0);
|
||||
|
||||
function onStats(stats) {
|
||||
usage = stats.mem.active / props.meta.mem.total;
|
||||
total = props.meta.mem.total;
|
||||
used = stats.mem.active;
|
||||
free = total - used;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on("stats", onStats);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zlxnikvl {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<div class="oxxrhrto">
|
||||
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
|
||||
<polygon
|
||||
:points="inPolygonPoints"
|
||||
fill="#f6c177"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="inPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#f6c177"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle :cx="inHeadX" :cy="inHeadY" r="1.5" fill="#f6c177" />
|
||||
<text x="1" y="5">
|
||||
NET rx
|
||||
<tspan>{{ bytes(inRecent) }}</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
<svg :viewBox="`0 0 ${viewBoxX} ${viewBoxY}`">
|
||||
<polygon
|
||||
:points="outPolygonPoints"
|
||||
fill="#31748f"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="outPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#31748f"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle :cx="outHeadX" :cy="outHeadY" r="1.5" fill="#31748f" />
|
||||
<text x="1" y="5">
|
||||
NET tx
|
||||
<tspan>{{ bytes(outRecent) }}</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onBeforeUnmount } from "vue";
|
||||
import bytes from "@/filters/bytes";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: any;
|
||||
meta: any;
|
||||
}>();
|
||||
|
||||
let viewBoxX: number = $ref(50);
|
||||
let viewBoxY: number = $ref(30);
|
||||
let stats: any[] = $ref([]);
|
||||
let inPolylinePoints: string = $ref("");
|
||||
let outPolylinePoints: string = $ref("");
|
||||
let inPolygonPoints: string = $ref("");
|
||||
let outPolygonPoints: string = $ref("");
|
||||
let inHeadX: any = $ref(null);
|
||||
let inHeadY: any = $ref(null);
|
||||
let outHeadX: any = $ref(null);
|
||||
let outHeadY: any = $ref(null);
|
||||
let inRecent: number = $ref(0);
|
||||
let outRecent: number = $ref(0);
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on("stats", onStats);
|
||||
props.connection.on("statsLog", onStatsLog);
|
||||
props.connection.send("requestLog", {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.connection.off("stats", onStats);
|
||||
props.connection.off("statsLog", onStatsLog);
|
||||
});
|
||||
|
||||
function onStats(connStats) {
|
||||
stats.push(connStats);
|
||||
if (stats.length > 50) stats.shift();
|
||||
|
||||
const inPeak = Math.max(1024 * 64, Math.max(...stats.map((s) => s.net.rx)));
|
||||
const outPeak = Math.max(
|
||||
1024 * 64,
|
||||
Math.max(...stats.map((s) => s.net.tx))
|
||||
);
|
||||
|
||||
let inPolylinePointsStats = stats.map((s, i) => [
|
||||
viewBoxX - (stats.length - 1 - i),
|
||||
(1 - s.net.rx / inPeak) * viewBoxY,
|
||||
]);
|
||||
let outPolylinePointsStats = stats.map((s, i) => [
|
||||
viewBoxX - (stats.length - 1 - i),
|
||||
(1 - s.net.tx / outPeak) * viewBoxY,
|
||||
]);
|
||||
inPolylinePoints = inPolylinePointsStats
|
||||
.map((xy) => `${xy[0]},${xy[1]}`)
|
||||
.join(" ");
|
||||
outPolylinePoints = outPolylinePointsStats
|
||||
.map((xy) => `${xy[0]},${xy[1]}`)
|
||||
.join(" ");
|
||||
|
||||
inPolygonPoints = `${
|
||||
viewBoxX - (stats.length - 1)
|
||||
},${viewBoxY} ${inPolylinePoints} ${viewBoxX},${viewBoxY}`;
|
||||
outPolygonPoints = `${
|
||||
viewBoxX - (stats.length - 1)
|
||||
},${viewBoxY} ${outPolylinePoints} ${viewBoxX},${viewBoxY}`;
|
||||
|
||||
inHeadX = inPolylinePointsStats[inPolylinePointsStats.length - 1][0];
|
||||
inHeadY = inPolylinePointsStats[inPolylinePointsStats.length - 1][1];
|
||||
outHeadX = outPolylinePointsStats[outPolylinePointsStats.length - 1][0];
|
||||
outHeadY = outPolylinePointsStats[outPolylinePointsStats.length - 1][1];
|
||||
|
||||
inRecent = connStats.net.rx;
|
||||
outRecent = connStats.net.tx;
|
||||
}
|
||||
|
||||
function onStatsLog(statsLog) {
|
||||
for (const revStats of [...statsLog].reverse()) {
|
||||
onStats(revStats);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oxxrhrto {
|
||||
display: flex;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 5px;
|
||||
fill: currentColor;
|
||||
|
||||
> tspan {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
|
||||
<circle
|
||||
:r="r"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
stroke="rgba(0, 0, 0, 0.05)"
|
||||
/>
|
||||
<circle
|
||||
:r="r"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
:stroke-dasharray="Math.PI * (r * 2)"
|
||||
:stroke-dashoffset="strokeDashoffset"
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
:stroke="color"
|
||||
/>
|
||||
<text x="50%" y="50%" dy="0.05" text-anchor="middle">
|
||||
{{ (value * 100).toFixed(0) }}%
|
||||
</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
value: number;
|
||||
reverse?: boolean;
|
||||
}>();
|
||||
|
||||
const r = 0.45;
|
||||
|
||||
const color = $computed(
|
||||
() =>
|
||||
`hsl(${
|
||||
props.reverse ? props.value * 180 : 180 - props.value * 180
|
||||
}, 80%, 70%)`
|
||||
);
|
||||
const strokeDashoffset = $computed(
|
||||
() => (1 - props.value) * (Math.PI * (r * 2))
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hsalcinq {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
> circle {
|
||||
transform-origin: center;
|
||||
transform: rotate(-90deg);
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 0.15px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -122,6 +122,9 @@ importers:
|
|||
'@rollup/pluginutils':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
'@syuilo/aiscript':
|
||||
specifier: 0.11.1
|
||||
version: 0.11.1
|
||||
'@types/escape-regexp':
|
||||
specifier: 0.0.1
|
||||
version: 0.0.1
|
||||
|
@ -350,6 +353,9 @@ importers:
|
|||
vue-plyr:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
vue-prism-editor:
|
||||
specifier: 2.0.0-alpha.2
|
||||
version: 2.0.0-alpha.2(vue@3.3.4)
|
||||
vue3-otp-input:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(vue@3.3.4)
|
||||
|
@ -1004,7 +1010,7 @@ packages:
|
|||
hasBin: true
|
||||
peerDependencies:
|
||||
'@swc/core': ^1.2.66
|
||||
chokidar: ^3.5.1
|
||||
chokidar: ^3.3.1
|
||||
peerDependenciesMeta:
|
||||
chokidar:
|
||||
optional: true
|
||||
|
@ -1141,6 +1147,16 @@ packages:
|
|||
/@swc/wasm@1.2.130:
|
||||
resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==}
|
||||
|
||||
/@syuilo/aiscript@0.11.1:
|
||||
resolution: {integrity: sha512-chwOIA3yLUKvOB0G611hjLArKTeOWNmTm3lHERSaDW1d+dS6do56naX6Lkwy2UpnwWC0qzeNSgg35elk6t2gZg==}
|
||||
dependencies:
|
||||
autobind-decorator: 2.4.0
|
||||
chalk: 4.0.0
|
||||
seedrandom: 3.0.5
|
||||
stringz: 2.1.0
|
||||
uuid: 7.0.3
|
||||
dev: true
|
||||
|
||||
/@szmarczak/http-timer@4.0.6:
|
||||
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -2312,6 +2328,14 @@ packages:
|
|||
supports-color: 5.5.0
|
||||
dev: true
|
||||
|
||||
/chalk@4.0.0:
|
||||
resolution: {integrity: sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
dev: true
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -7274,6 +7298,11 @@ packages:
|
|||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/uuid@7.0.3:
|
||||
resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
@ -7433,6 +7462,15 @@ packages:
|
|||
vue: 2.7.14
|
||||
dev: true
|
||||
|
||||
/vue-prism-editor@2.0.0-alpha.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
dependencies:
|
||||
vue: 3.3.4
|
||||
dev: true
|
||||
|
||||
/vue3-otp-input@0.4.1(vue@3.3.4):
|
||||
resolution: {integrity: sha512-wVl9i3DcWlO0C7fBI9V+RIP3crm/1tY72fuhvb3YM2JfbLoYofB96aPl5AgFhA0Cse5bQEMYtIvOeiqW3rfbAw==}
|
||||
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||
|
|
|
@ -96,6 +96,7 @@ pub struct UserProfileExt {
|
|||
#[ts(export)]
|
||||
pub struct UserProfilePinsEx {
|
||||
pub pinned_notes: Vec<PackNoteMaybeFull>,
|
||||
// pub pinned_page: Option<Page>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
|
|
Loading…
Reference in New Issue