Frontend: Removed the update check

This commit is contained in:
Natty 2023-07-23 14:19:12 +02:00
parent 39f9ecd8d3
commit 40c471ff56
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
8 changed files with 1977 additions and 2058 deletions

View File

@ -2034,10 +2034,6 @@ export type Endpoints = {
req: NoParams; req: NoParams;
res: ServerInfo; res: ServerInfo;
}; };
"latest-version": {
req: NoParams;
res: TODO;
};
"sw/register": { "sw/register": {
req: TODO; req: TODO;
res: TODO; res: TODO;

View File

@ -1698,10 +1698,6 @@ export declare type Endpoints = {
req: NoParams; req: NoParams;
res: ServerInfo; res: ServerInfo;
}; };
"latest-version": {
req: NoParams;
res: TODO;
};
"sw/register": { "sw/register": {
req: TODO; req: TODO;
res: TODO; res: TODO;

View File

@ -5,7 +5,6 @@ import {
App, App,
AuthSession, AuthSession,
Blocking, Blocking,
Channel,
Clip, Clip,
DateString, DateString,
DetailedInstanceMetadata, DetailedInstanceMetadata,
@ -17,24 +16,23 @@ import {
FollowRequest, FollowRequest,
GalleryPost, GalleryPost,
Instance, Instance,
InstanceMetadata,
LiteInstanceMetadata, LiteInstanceMetadata,
MeDetailed, MeDetailed,
MessagingMessage,
Note, Note,
NoteFavorite, NoteFavorite,
NoteReaction,
Notification,
OriginType, OriginType,
Page, Page,
ServerInfo, ServerInfo,
Signin,
Stats, Stats,
User, User,
UserDetailed, UserDetailed,
UserGroup, UserGroup,
UserList, UserList,
UserSorting, UserSorting,
Notification,
NoteReaction,
Signin,
MessagingMessage,
} from "./entities"; } from "./entities";
type TODO = Record<string, any> | null; type TODO = Record<string, any> | null;
@ -758,10 +756,7 @@ export type Endpoints = {
$cases: [ $cases: [
[{ detail: true }, DetailedInstanceMetadata], [{ detail: true }, DetailedInstanceMetadata],
[{ detail: false }, LiteInstanceMetadata], [{ detail: false }, LiteInstanceMetadata],
[ [{ detail: boolean }, LiteInstanceMetadata | DetailedInstanceMetadata]
{ detail: boolean },
LiteInstanceMetadata | DetailedInstanceMetadata,
],
]; ];
$default: LiteInstanceMetadata; $default: LiteInstanceMetadata;
}; };
@ -977,9 +972,6 @@ export type Endpoints = {
// server-info // server-info
"server-info": { req: NoParams; res: ServerInfo }; "server-info": { req: NoParams; res: ServerInfo };
// ck specific
"latest-version": { req: NoParams; res: TODO };
// sw // sw
"sw/register": { req: TODO; res: TODO }; "sw/register": { req: TODO; res: TODO };

View File

@ -1,93 +1,70 @@
<template> <template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || currentPage?.route.name == null" class="nav"> <div v-if="!narrow || currentPage?.route.name == null" class="nav">
<MkSpacer :content-max="700" :margin-min="16"> <MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu"> <div class="lxpfedzu">
<div class="banner"> <div class="banner">
<img <img
:src="$instance.iconUrl || '/favicon.ico'" :src="$instance.iconUrl || '/favicon.ico'"
alt="" alt=""
class="icon" class="icon"
/> />
</div> </div>
<MkInfo <MkInfo
v-if="thereIsUnresolvedAbuseReport" v-if="thereIsUnresolvedAbuseReport"
warn warn
class="info" class="info"
>{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} >{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }}
<MkA to="/admin/abuses" class="_link">{{ <MkA to="/admin/abuses" class="_link">{{
i18n.ts.check i18n.ts.check
}}</MkA></MkInfo }}</MkA></MkInfo
> >
<MkInfo v-if="noMaintainerInformation" warn class="info" <MkInfo v-if="noMaintainerInformation" warn class="info"
>{{ i18n.ts.noMaintainerInformationWarning }} >{{ i18n.ts.noMaintainerInformationWarning }}
<MkA to="/admin/settings" class="_link">{{ <MkA to="/admin/settings" class="_link">{{
i18n.ts.configure i18n.ts.configure
}}</MkA></MkInfo }}</MkA></MkInfo
> >
<MkInfo v-if="noBotProtection" warn class="info" <MkInfo v-if="noBotProtection" warn class="info"
>{{ i18n.ts.noBotProtectionWarning }} >{{ i18n.ts.noBotProtectionWarning }}
<MkA to="/admin/security" class="_link">{{ <MkA to="/admin/security" class="_link">{{
i18n.ts.configure i18n.ts.configure
}}</MkA></MkInfo }}</MkA></MkInfo
> >
<MkInfo v-if="noEmailServer" warn class="info" <MkInfo v-if="noEmailServer" warn class="info"
>{{ i18n.ts.noEmailServerWarning }} >{{ i18n.ts.noEmailServerWarning }}
<MkA to="/admin/email-settings" class="_link">{{ <MkA to="/admin/email-settings" class="_link">{{
i18n.ts.configure i18n.ts.configure
}}</MkA></MkInfo }}</MkA></MkInfo
> >
<MkInfo v-if="updateAvailable" warn class="info"
>{{ i18n.ts.updateAvailable }}
<a
href="https://codeberg.org/calckey/calckey/releases"
target="_bank"
class="_link"
>{{ i18n.ts.check }}</a
></MkInfo
>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu> <MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
</div> </div>
</MkSpacer> </MkSpacer>
</div> </div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main"> <div v-if="!(narrow && currentPage?.route.name == null)" class="main">
<RouterView /> <RouterView />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { onActivated, onMounted, onUnmounted, provide, ref, watch } from "vue";
defineAsyncComponent,
inject,
nextTick,
onMounted,
onUnmounted,
onActivated,
provide,
watch,
ref,
} from "vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import MkSuperMenu from "@/components/MkSuperMenu.vue"; import MkSuperMenu from "@/components/MkSuperMenu.vue";
import MkInfo from "@/components/MkInfo.vue"; import MkInfo from "@/components/MkInfo.vue";
import { scroll } from "@/scripts/scroll";
import { instance } from "@/instance"; import { instance } from "@/instance";
import { version } from "@/config";
import { $i } from "@/account"; import { $i } from "@/account";
import * as os from "@/os"; import * as os from "@/os";
import { lookupUser } from "@/scripts/lookup-user"; import { lookupUser } from "@/scripts/lookup-user";
import { lookupFile } from "@/scripts/lookup-file"; import { lookupFile } from "@/scripts/lookup-file";
import { lookupInstance } from "@/scripts/lookup-instance"; import { lookupInstance } from "@/scripts/lookup-instance";
import { indexPosts } from "@/scripts/index-posts"; import { indexPosts } from "@/scripts/index-posts";
import { defaultStore } from "@/store";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { import {
definePageMetadata, definePageMetadata,
provideMetadataReceiver, provideMetadataReceiver,
setPageMetadata,
} from "@/scripts/page-metadata"; } from "@/scripts/page-metadata";
const isEmpty = (x: string | null) => x == null || x === ""; const isEmpty = (x: string | null) => x == null || x === "";
@ -95,9 +72,9 @@ const el = ref<HTMLElement | null>(null);
const router = useRouter(); const router = useRouter();
const indexInfo = { const indexInfo = {
title: i18n.ts.controlPanel, title: i18n.ts.controlPanel,
icon: "ph-gear-six ph-bold ph-lg", icon: "ph-gear-six ph-bold ph-lg",
hideHeader: true, hideHeader: true,
}; };
provide("shouldOmitHeaderTitle", false); provide("shouldOmitHeaderTitle", false);
@ -108,326 +85,312 @@ let narrow = $ref(false);
let view = $ref(null); let view = $ref(null);
let pageProps = $ref({}); let pageProps = $ref({});
let noMaintainerInformation = let noMaintainerInformation =
isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = let noBotProtection =
!instance.disableRegistration && !instance.disableRegistration &&
!instance.enableHcaptcha && !instance.enableHcaptcha &&
!instance.enableRecaptcha; !instance.enableRecaptcha;
let noEmailServer = !instance.enableEmail; let noEmailServer = !instance.enableEmail;
let thereIsUnresolvedAbuseReport = $ref(false); let thereIsUnresolvedAbuseReport = $ref(false);
let updateAvailable = $ref(false);
let currentPage = $computed(() => router.currentRef.value.child); let currentPage = $computed(() => router.currentRef.value.child);
os.api("admin/abuse-user-reports", { os.api("admin/abuse-user-reports", {
state: "unresolved", state: "unresolved",
limit: 1, limit: 1,
}).then((reports) => { }).then((reports) => {
if (reports?.length > 0) thereIsUnresolvedAbuseReport = true; if (reports?.length > 0) thereIsUnresolvedAbuseReport = true;
}); });
if (defaultStore.state.showAdminUpdates) {
os.api("latest-version").then((res) => {
const cleanRes = parseInt(res?.tag_name.replace(/[^0-9]/g, ""));
const cleanVersion = parseInt(version.replace(/[^0-9]/g, ""));
if (cleanRes > cleanVersion) {
updateAvailable = true;
}
});
}
const NARROW_THRESHOLD = 600; const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => { const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return; if (entries.length === 0) return;
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
}); });
const menuDef = $computed(() => [ const menuDef = $computed(() => [
{ {
title: i18n.ts.quickAction, title: i18n.ts.quickAction,
items: [ items: [
{ {
type: "button", type: "button",
icon: "ph-magnifying-glass ph-bold ph-lg", icon: "ph-magnifying-glass ph-bold ph-lg",
text: i18n.ts.lookup, text: i18n.ts.lookup,
action: lookup, action: lookup,
}, },
...(instance.disableRegistration ...(instance.disableRegistration
? [ ? [
{ {
type: "button", type: "button",
icon: "ph-user-plus ph-bold ph-lg", icon: "ph-user-plus ph-bold ph-lg",
text: i18n.ts.invite, text: i18n.ts.invite,
action: invite, action: invite,
}, },
] ]
: []), : []),
...($i.isAdmin ...($i.isAdmin
? [ ? [
{ {
type: "button", type: "button",
icon: "ph-list-magnifying-glass ph-bold ph-lg", icon: "ph-list-magnifying-glass ph-bold ph-lg",
text: i18n.ts.indexPosts, text: i18n.ts.indexPosts,
action: indexPosts, action: indexPosts,
}, },
] ]
: []), : []),
], ],
}, },
{ {
title: i18n.ts.administration, title: i18n.ts.administration,
items: [ items: [
{ {
icon: "ph-gauge ph-bold ph-lg", icon: "ph-gauge ph-bold ph-lg",
text: i18n.ts.dashboard, text: i18n.ts.dashboard,
to: "/admin/overview", to: "/admin/overview",
active: currentPage?.route.name === "overview", active: currentPage?.route.name === "overview",
}, },
{ {
icon: "ph-users ph-bold ph-lg", icon: "ph-users ph-bold ph-lg",
text: i18n.ts.users, text: i18n.ts.users,
to: "/admin/users", to: "/admin/users",
active: currentPage?.route.name === "users", active: currentPage?.route.name === "users",
}, },
{ {
icon: "ph-smiley ph-bold ph-lg", icon: "ph-smiley ph-bold ph-lg",
text: i18n.ts.customEmojis, text: i18n.ts.customEmojis,
to: "/admin/emojis", to: "/admin/emojis",
active: currentPage?.route.name === "emojis", active: currentPage?.route.name === "emojis",
}, },
{ {
icon: "ph-planet ph-bold ph-lg", icon: "ph-planet ph-bold ph-lg",
text: i18n.ts.federation, text: i18n.ts.federation,
to: "/admin/federation", to: "/admin/federation",
active: currentPage?.route.name === "federation", active: currentPage?.route.name === "federation",
}, },
{ {
icon: "ph-queue ph-bold ph-lg", icon: "ph-queue ph-bold ph-lg",
text: i18n.ts.jobQueue, text: i18n.ts.jobQueue,
to: "/admin/queue", to: "/admin/queue",
active: currentPage?.route.name === "queue", active: currentPage?.route.name === "queue",
}, },
{ {
icon: "ph-cloud ph-bold ph-lg", icon: "ph-cloud ph-bold ph-lg",
text: i18n.ts.files, text: i18n.ts.files,
to: "/admin/files", to: "/admin/files",
active: currentPage?.route.name === "files", active: currentPage?.route.name === "files",
}, },
{ {
icon: "ph-megaphone-simple ph-bold ph-lg", icon: "ph-megaphone-simple ph-bold ph-lg",
text: i18n.ts.announcements, text: i18n.ts.announcements,
to: "/admin/announcements", to: "/admin/announcements",
active: currentPage?.route.name === "announcements", active: currentPage?.route.name === "announcements",
}, },
{ {
icon: "ph-money ph-bold ph-lg", icon: "ph-money ph-bold ph-lg",
text: i18n.ts.ads, text: i18n.ts.ads,
to: "/admin/ads", to: "/admin/ads",
active: currentPage?.route.name === "ads", active: currentPage?.route.name === "ads",
}, },
{ {
icon: "ph-warning-circle ph-bold ph-lg", icon: "ph-warning-circle ph-bold ph-lg",
text: i18n.ts.abuseReports, text: i18n.ts.abuseReports,
to: "/admin/abuses", to: "/admin/abuses",
active: currentPage?.route.name === "abuses", active: currentPage?.route.name === "abuses",
}, },
], ],
}, },
...($i?.isAdmin ...($i?.isAdmin
? [ ? [
{ {
title: i18n.ts.settings, title: i18n.ts.settings,
items: [ items: [
{ {
icon: "ph-gear-six ph-bold ph-lg", icon: "ph-gear-six ph-bold ph-lg",
text: i18n.ts.general, text: i18n.ts.general,
to: "/admin/settings", to: "/admin/settings",
active: currentPage?.route.name === "settings", active: currentPage?.route.name === "settings",
}, },
{ {
icon: "ph-envelope-simple-open ph-bold ph-lg", icon: "ph-envelope-simple-open ph-bold ph-lg",
text: i18n.ts.emailServer, text: i18n.ts.emailServer,
to: "/admin/email-settings", to: "/admin/email-settings",
active: active: currentPage?.route.name === "email-settings",
currentPage?.route.name === "email-settings", },
}, {
{ icon: "ph-cloud ph-bold ph-lg",
icon: "ph-cloud ph-bold ph-lg", text: i18n.ts.objectStorage,
text: i18n.ts.objectStorage, to: "/admin/object-storage",
to: "/admin/object-storage", active: currentPage?.route.name === "object-storage",
active: },
currentPage?.route.name === "object-storage", {
}, icon: "ph-lock ph-bold ph-lg",
{ text: i18n.ts.security,
icon: "ph-lock ph-bold ph-lg", to: "/admin/security",
text: i18n.ts.security, active: currentPage?.route.name === "security",
to: "/admin/security", },
active: currentPage?.route.name === "security", {
}, icon: "ph-flow-arrow ph-bold ph-lg",
{ text: i18n.ts.relays,
icon: "ph-flow-arrow ph-bold ph-lg", to: "/admin/relays",
text: i18n.ts.relays, active: currentPage?.route.name === "relays",
to: "/admin/relays", },
active: currentPage?.route.name === "relays", {
}, icon: "ph-plug ph-bold ph-lg",
{ text: i18n.ts.integration,
icon: "ph-plug ph-bold ph-lg", to: "/admin/integrations",
text: i18n.ts.integration, active: currentPage?.route.name === "integrations",
to: "/admin/integrations", },
active: currentPage?.route.name === "integrations", {
}, icon: "ph-prohibit ph-bold ph-lg",
{ text: i18n.ts.instanceBlocking,
icon: "ph-prohibit ph-bold ph-lg", to: "/admin/instance-block",
text: i18n.ts.instanceBlocking, active: currentPage?.route.name === "instance-block",
to: "/admin/instance-block", },
active: {
currentPage?.route.name === "instance-block", icon: "ph-hash ph-bold ph-lg",
}, text: i18n.ts.hiddenTags,
{ to: "/admin/hashtags",
icon: "ph-hash ph-bold ph-lg", active: currentPage?.route.name === "hashtags",
text: i18n.ts.hiddenTags, },
to: "/admin/hashtags", {
active: currentPage?.route.name === "hashtags", icon: "ph-ghost ph-bold ph-lg",
}, text: i18n.ts.proxyAccount,
{ to: "/admin/proxy-account",
icon: "ph-ghost ph-bold ph-lg", active: currentPage?.route.name === "proxy-account",
text: i18n.ts.proxyAccount, },
to: "/admin/proxy-account", {
active: currentPage?.route.name === "proxy-account", icon: "ph-database ph-bold ph-lg",
}, text: i18n.ts.database,
{ to: "/admin/database",
icon: "ph-database ph-bold ph-lg", active: currentPage?.route.name === "database",
text: i18n.ts.database, },
to: "/admin/database", {
active: currentPage?.route.name === "database", icon: "ph-flask ph-bold ph-lg",
}, text: i18n.ts._experiments.title,
{ to: "/admin/experiments",
icon: "ph-flask ph-bold ph-lg", active: currentPage?.route.name === "experiments",
text: i18n.ts._experiments.title, },
to: "/admin/experiments", ],
active: currentPage?.route.name === "experiments", },
}, ]
], : []),
},
]
: []),
]); ]);
watch(narrow, () => { watch(narrow, () => {
if (currentPage?.route.name == null && !narrow) { if (currentPage?.route.name == null && !narrow) {
router.push("/admin/overview"); router.push("/admin/overview");
} }
}); });
onMounted(() => { onMounted(() => {
ro.observe(el.value); ro.observe(el.value);
narrow = el.value.offsetWidth < NARROW_THRESHOLD; narrow = el.value.offsetWidth < NARROW_THRESHOLD;
if (currentPage?.route.name == null && !narrow) { if (currentPage?.route.name == null && !narrow) {
router.push("/admin/overview"); router.push("/admin/overview");
} }
}); });
onActivated(() => { onActivated(() => {
narrow = el.value.offsetWidth < NARROW_THRESHOLD; narrow = el.value.offsetWidth < NARROW_THRESHOLD;
if (!narrow && currentPage?.route.name == null) { if (!narrow && currentPage?.route.name == null) {
router.replace("/admin/overview"); router.replace("/admin/overview");
} }
}); });
onUnmounted(() => { onUnmounted(() => {
ro.disconnect(); ro.disconnect();
}); });
watch(router.currentRef, (to) => { watch(router.currentRef, (to) => {
if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) { if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) {
router.replace("/admin/overview"); router.replace("/admin/overview");
} }
}); });
provideMetadataReceiver((info) => { provideMetadataReceiver((info) => {
if (info == null) { if (info == null) {
childInfo = null; childInfo = null;
} else { } else {
childInfo = info; childInfo = info;
} }
}); });
const invite = () => { const invite = () => {
os.api("admin/invite") os.api("admin/invite")
.then((x) => { .then((x) => {
os.alert({ os.alert({
type: "info", type: "info",
text: x.code, text: x.code,
}); });
}) })
.catch((err) => { .catch((err) => {
os.alert({ os.alert({
type: "error", type: "error",
text: err, text: err,
}); });
}); });
}; };
async function lookupNote() { async function lookupNote() {
const { canceled, result: q } = await os.inputText({ const { canceled, result: q } = await os.inputText({
title: i18n.ts.noteId, title: i18n.ts.noteId,
}); });
if (canceled) return; if (canceled) return;
os.api( os.api(
"notes/show", "notes/show",
q.startsWith("http://") || q.startsWith("https://") q.startsWith("http://") || q.startsWith("https://")
? { url: q.trim() } ? { url: q.trim() }
: { noteId: q.trim() } : { noteId: q.trim() }
) )
.then((note) => { .then((note) => {
os.pageWindow(`/notes/${note.id}`); os.pageWindow(`/notes/${note.id}`);
}) })
.catch((err) => { .catch((err) => {
if (err.code === "NO_SUCH_NOTE") { if (err.code === "NO_SUCH_NOTE") {
os.alert({ os.alert({
type: "error", type: "error",
text: i18n.ts.notFound, text: i18n.ts.notFound,
}); });
} }
}); });
} }
const lookup = (ev) => { const lookup = (ev) => {
os.popupMenu( os.popupMenu(
[ [
{ {
text: i18n.ts.user, text: i18n.ts.user,
icon: "ph-user ph-bold ph-lg", icon: "ph-user ph-bold ph-lg",
action: () => { action: () => {
lookupUser(); lookupUser();
}, },
}, },
{ {
text: i18n.ts.note, text: i18n.ts.note,
icon: "ph-pencil ph-bold ph-lg", icon: "ph-pencil ph-bold ph-lg",
action: () => { action: () => {
lookupNote(); lookupNote();
}, },
}, },
{ {
text: i18n.ts.file, text: i18n.ts.file,
icon: "ph-cloud ph-bold ph-lg", icon: "ph-cloud ph-bold ph-lg",
action: () => { action: () => {
lookupFile(); lookupFile();
}, },
}, },
{ {
text: i18n.ts.instance, text: i18n.ts.instance,
icon: "ph-planet ph-bold ph-lg", icon: "ph-planet ph-bold ph-lg",
action: () => { action: () => {
lookupInstance(); lookupInstance();
}, },
}, },
], ],
ev.currentTarget ?? ev.target ev.currentTarget ?? ev.target
); );
}; };
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
@ -437,51 +400,51 @@ const headerTabs = $computed(() => []);
definePageMetadata(INFO); definePageMetadata(INFO);
defineExpose({ defineExpose({
header: { header: {
title: i18n.ts.controlPanel, title: i18n.ts.controlPanel,
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.hiyeyicy { .hiyeyicy {
&.wide { &.wide {
display: flex; display: flex;
margin: 0 auto; margin: 0 auto;
height: 100%; height: 100%;
> .nav { > .nav {
width: 32%; width: 32%;
max-width: 280px; max-width: 280px;
box-sizing: border-box; box-sizing: border-box;
border-right: solid 0.5px var(--divider); border-right: solid 0.5px var(--divider);
overflow: auto; overflow: auto;
height: 100%; height: 100%;
} }
> .main { > .main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
} }
> .nav { > .nav {
.lxpfedzu { .lxpfedzu {
> .info { > .info {
margin: 16px 0; margin: 16px 0;
} }
> .banner { > .banner {
margin: 16px; margin: 16px;
> .icon { > .icon {
display: block; display: block;
margin: auto; margin: auto;
height: 42px; height: 42px;
border-radius: 8px; border-radius: 8px;
} }
} }
} }
} }
} }
</style> </style>

View File

@ -1,136 +1,136 @@
<template> <template>
<div class="_formRoot"> <div class="_formRoot">
<FormSelect v-model="lang" class="_formBlock"> <FormSelect v-model="lang" class="_formBlock">
<template #label>{{ i18n.ts.uiLanguage }}</template> <template #label>{{ i18n.ts.uiLanguage }}</template>
<option v-for="x in langs" :key="x[0]" :value="x[0]"> <option v-for="x in langs" :key="x[0]" :value="x[0]">
{{ x[1] }} {{ x[1] }}
</option> </option>
<template #caption> <template #caption>
<I18n :src="i18n.ts.i18nInfo" tag="span"> <I18n :src="i18n.ts.i18nInfo" tag="span">
<template #link> <template #link>
<MkLink url="https://hosted.weblate.org/engage/calckey/" <MkLink url="https://hosted.weblate.org/engage/calckey/"
>Weblate</MkLink >Weblate</MkLink
> >
</template> </template>
</I18n> </I18n>
</template> </template>
</FormSelect> </FormSelect>
<FormRadios v-model="overridedDeviceKind" class="_formBlock"> <FormRadios v-model="overridedDeviceKind" class="_formBlock">
<template #label>{{ i18n.ts.overridedDeviceKind }}</template> <template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option> <option :value="null">{{ i18n.ts.auto }}</option>
<option value="smartphone"> <option value="smartphone">
<i class="ph-device-mobile ph-bold ph-lg" /> <i class="ph-device-mobile ph-bold ph-lg" />
{{ i18n.ts.smartphone }} {{ i18n.ts.smartphone }}
</option> </option>
<option value="tablet"> <option value="tablet">
<i class="ph-device-tablet ph-bold ph-lg" /> <i class="ph-device-tablet ph-bold ph-lg" />
{{ i18n.ts.tablet }} {{ i18n.ts.tablet }}
</option> </option>
<option value="desktop"> <option value="desktop">
<i class="ph-desktop ph-bold ph-lg" /> {{ i18n.ts.desktop }} <i class="ph-desktop ph-bold ph-lg" /> {{ i18n.ts.desktop }}
</option> </option>
</FormRadios> </FormRadios>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.behavior }}</template> <template #label>{{ i18n.ts.behavior }}</template>
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ <FormSwitch v-model="imageNewTab" class="_formBlock">{{
i18n.ts.openImageInNewTab i18n.ts.openImageInNewTab
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ <FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{
i18n.ts.enableInfiniteScroll i18n.ts.enableInfiniteScroll
}}</FormSwitch> }}</FormSwitch>
<FormSwitch <FormSwitch
v-model="useReactionPickerForContextMenu" v-model="useReactionPickerForContextMenu"
class="_formBlock" class="_formBlock"
>{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch >{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch
> >
<FormSwitch v-model="swipeOnDesktop" class="_formBlock">{{ <FormSwitch v-model="swipeOnDesktop" class="_formBlock">{{
i18n.ts.swipeOnDesktop i18n.ts.swipeOnDesktop
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="enterSendsMessage" class="_formBlock">{{ <FormSwitch v-model="enterSendsMessage" class="_formBlock">{{
i18n.ts.enterSendsMessage i18n.ts.enterSendsMessage
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{ <FormSwitch v-model="disablePagesScript" class="_formBlock">{{
i18n.ts.disablePagesScript i18n.ts.disablePagesScript
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="showTimelineReplies" class="_formBlock" <FormSwitch v-model="showTimelineReplies" class="_formBlock"
>{{ i18n.ts.flagShowTimelineReplies >{{ i18n.ts.flagShowTimelineReplies
}}<template #caption }}<template #caption
>{{ i18n.ts.flagShowTimelineRepliesDescription }} >{{ i18n.ts.flagShowTimelineRepliesDescription }}
{{ i18n.ts.reflectMayTakeTime }}</template {{ i18n.ts.reflectMayTakeTime }}</template
></FormSwitch ></FormSwitch
> >
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock"> <FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template> <template #label>{{ i18n.ts.whenServerDisconnected }}</template>
<option value="reload"> <option value="reload">
{{ i18n.ts._serverDisconnectedBehavior.reload }} {{ i18n.ts._serverDisconnectedBehavior.reload }}
</option> </option>
<option value="dialog"> <option value="dialog">
{{ i18n.ts._serverDisconnectedBehavior.dialog }} {{ i18n.ts._serverDisconnectedBehavior.dialog }}
</option> </option>
<option value="quiet"> <option value="quiet">
{{ i18n.ts._serverDisconnectedBehavior.quiet }} {{ i18n.ts._serverDisconnectedBehavior.quiet }}
</option> </option>
<option value="nothing"> <option value="nothing">
{{ i18n.ts._serverDisconnectedBehavior.nothing }} {{ i18n.ts._serverDisconnectedBehavior.nothing }}
</option> </option>
</FormSelect> </FormSelect>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.accessibility }}</template> <template #label>{{ i18n.ts.accessibility }}</template>
<FormSwitch v-model="expandOnNoteClick" class="_formBlock" <FormSwitch v-model="expandOnNoteClick" class="_formBlock"
>{{ i18n.ts.expandOnNoteClick >{{ i18n.ts.expandOnNoteClick
}}<template #caption>{{ }}<template #caption>{{
i18n.ts.expandOnNoteClickDesc i18n.ts.expandOnNoteClickDesc
}}</template> }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="advancedMfm" class="_formBlock"> <FormSwitch v-model="advancedMfm" class="_formBlock">
{{ i18n.ts._mfm.advanced {{ i18n.ts._mfm.advanced
}}<template #caption>{{ }}<template #caption>{{
i18n.ts._mfm.advancedDescription i18n.ts._mfm.advancedDescription
}}</template> }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="autoplayMfm" class="_formBlock"> <FormSwitch v-model="autoplayMfm" class="_formBlock">
{{ i18n.ts._mfm.alwaysPlay }} {{ i18n.ts._mfm.alwaysPlay }}
<template #caption> <template #caption>
<i <i
class="ph-warning ph-bold ph-lg" class="ph-warning ph-bold ph-lg"
style="color: var(--warn)" style="color: var(--warn)"
></i> ></i>
{{ i18n.ts._mfm.warn }} {{ i18n.ts._mfm.warn }}
</template> </template>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ <FormSwitch v-model="reduceAnimation" class="_formBlock">{{
i18n.ts.reduceUiAnimation i18n.ts.reduceUiAnimation
}}</FormSwitch> }}</FormSwitch>
<FormSwitch <FormSwitch
v-model="disableShowingAnimatedImages" v-model="disableShowingAnimatedImages"
class="_formBlock" class="_formBlock"
>{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch >{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch
> >
<FormRadios v-model="fontSize" class="_formBlock"> <FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template> <template #label>{{ i18n.ts.fontSize }}</template>
<option :value="null"> <option :value="null">
<span style="font-size: 14px">14</span> <span style="font-size: 14px">14</span>
</option> </option>
<option value="15"> <option value="15">
<span style="font-size: 15px">15</span> <span style="font-size: 15px">15</span>
</option> </option>
<option value="16"> <option value="16">
<span style="font-size: 16px">16</span> <span style="font-size: 16px">16</span>
</option> </option>
<option value="17"> <option value="17">
<span style="font-size: 17px">17</span> <span style="font-size: 17px">17</span>
</option> </option>
<option value="18"> <option value="18">
<span style="font-size: 18px">18</span> <span style="font-size: 18px">18</span>
</option> </option>
</FormRadios> </FormRadios>
<!-- <FormRange <!-- <FormRange
v-model="fontSize" v-model="fontSize"
:min="12" :min="12"
:max="18" :max="18"
@ -142,104 +142,97 @@
> >
<template #label>{{ i18n.ts.fontSize }}</template> <template #label>{{ i18n.ts.fontSize }}</template>
</FormRange> --> </FormRange> -->
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.appearance }}</template> <template #label>{{ i18n.ts.appearance }}</template>
<FormSwitch v-model="showAds" class="_formBlock">{{ <FormSwitch v-model="showAds" class="_formBlock">{{
i18n.ts.showAds i18n.ts.showAds
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ <FormSwitch v-model="useBlurEffect" class="_formBlock">{{
i18n.ts.useBlurEffect i18n.ts.useBlurEffect
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ <FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{
i18n.ts.useBlurEffectForModal i18n.ts.useBlurEffectForModal
}}</FormSwitch> }}</FormSwitch>
<FormSwitch <FormSwitch
v-model="showGapBetweenNotesInTimeline" v-model="showGapBetweenNotesInTimeline"
class="_formBlock" class="_formBlock"
>{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch >{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch
> >
<FormSwitch v-model="loadRawImages" class="_formBlock">{{ <FormSwitch v-model="loadRawImages" class="_formBlock">{{
i18n.ts.loadRawImages i18n.ts.loadRawImages
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ <FormSwitch v-model="squareAvatars" class="_formBlock">{{
i18n.ts.squareAvatars i18n.ts.squareAvatars
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="seperateRenoteQuote" class="_formBlock">{{ <FormSwitch v-model="seperateRenoteQuote" class="_formBlock">{{
i18n.ts.seperateRenoteQuote i18n.ts.seperateRenoteQuote
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ <FormSwitch v-model="useSystemFont" class="_formBlock">{{
i18n.ts.useSystemFont i18n.ts.useSystemFont
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock"> <FormSwitch v-model="useOsNativeEmojis" class="_formBlock">
{{ i18n.ts.useOsNativeEmojis }} {{ i18n.ts.useOsNativeEmojis }}
<div> <div>
<Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" /> <Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" />
</div> </div>
</FormSwitch> </FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ <FormSwitch v-model="disableDrawer" class="_formBlock">{{
i18n.ts.disableDrawer i18n.ts.disableDrawer
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="showUpdates" class="_formBlock">{{ <FormSwitch v-model="showUpdates" class="_formBlock">{{
i18n.ts.showUpdates i18n.ts.showUpdates
}}</FormSwitch> }}</FormSwitch>
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ <FormSwitch v-model="showFixedPostForm" class="_formBlock">{{
i18n.ts.showFixedPostForm i18n.ts.showFixedPostForm
}}</FormSwitch> }}</FormSwitch>
<FormSwitch <FormSelect v-model="instanceTicker" class="_formBlock">
v-if="$i?.isAdmin" <template #label>{{ i18n.ts.instanceTicker }}</template>
v-model="showAdminUpdates" <option value="none">{{ i18n.ts._instanceTicker.none }}</option>
class="_formBlock" <option value="remote">
>{{ i18n.ts.showAdminUpdates }}</FormSwitch {{ i18n.ts._instanceTicker.remote }}
> </option>
<FormSelect v-model="instanceTicker" class="_formBlock"> <option value="always">
<template #label>{{ i18n.ts.instanceTicker }}</template> {{ i18n.ts._instanceTicker.always }}
<option value="none">{{ i18n.ts._instanceTicker.none }}</option> </option>
<option value="remote"> </FormSelect>
{{ i18n.ts._instanceTicker.remote }}
</option>
<option value="always">
{{ i18n.ts._instanceTicker.always }}
</option>
</FormSelect>
<FormSelect v-model="nsfw" class="_formBlock"> <FormSelect v-model="nsfw" class="_formBlock">
<template #label>{{ i18n.ts.nsfw }}</template> <template #label>{{ i18n.ts.nsfw }}</template>
<option value="respect">{{ i18n.ts._nsfw.respect }}</option> <option value="respect">{{ i18n.ts._nsfw.respect }}</option>
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option> <option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
<option value="force">{{ i18n.ts._nsfw.force }}</option> <option value="force">{{ i18n.ts._nsfw.force }}</option>
</FormSelect> </FormSelect>
</FormSection> </FormSection>
<FormRange <FormRange
v-model="numberOfPageCache" v-model="numberOfPageCache"
:min="1" :min="1"
:max="10" :max="10"
:step="1" :step="1"
easing easing
class="_formBlock" class="_formBlock"
> >
<template #label>{{ i18n.ts.numberOfPageCache }}</template> <template #label>{{ i18n.ts.numberOfPageCache }}</template>
<template #caption>{{ <template #caption>{{
i18n.ts.numberOfPageCacheDescription i18n.ts.numberOfPageCacheDescription
}}</template> }}</template>
</FormRange> </FormRange>
<FormLink to="/settings/deck" class="_formBlock">{{ <FormLink to="/settings/deck" class="_formBlock">{{
i18n.ts.deck i18n.ts.deck
}}</FormLink> }}</FormLink>
<FormLink to="/settings/custom-katex-macro" class="_formBlock" <FormLink to="/settings/custom-katex-macro" class="_formBlock"
><template #icon><i class="ph-radical ph-bold ph-lg"></i></template ><template #icon><i class="ph-radical ph-bold ph-lg"></i></template
>{{ i18n.ts.customKaTeXMacro }}</FormLink >{{ i18n.ts.customKaTeXMacro }}</FormLink
> >
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { $i } from "@/account";
import FormSwitch from "@/components/form/switch.vue"; import FormSwitch from "@/components/form/switch.vue";
import FormSelect from "@/components/form/select.vue"; import FormSelect from "@/components/form/select.vue";
import FormRadios from "@/components/form/radios.vue"; import FormRadios from "@/components/form/radios.vue";
@ -259,136 +252,132 @@ const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") != null); const useSystemFont = ref(localStorage.getItem("useSystemFont") != null);
async function reloadAsk() { async function reloadAsk() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "info", type: "info",
text: i18n.ts.reloadToApplySetting, text: i18n.ts.reloadToApplySetting,
}); });
if (canceled) return; if (canceled) return;
unisonReload(); unisonReload();
} }
const overridedDeviceKind = computed( const overridedDeviceKind = computed(
defaultStore.makeGetterSetter("overridedDeviceKind") defaultStore.makeGetterSetter("overridedDeviceKind")
); );
const serverDisconnectedBehavior = computed( const serverDisconnectedBehavior = computed(
defaultStore.makeGetterSetter("serverDisconnectedBehavior") defaultStore.makeGetterSetter("serverDisconnectedBehavior")
); );
const reduceAnimation = computed( const reduceAnimation = computed(
defaultStore.makeGetterSetter( defaultStore.makeGetterSetter(
"animation", "animation",
(v) => !v, (v) => !v,
(v) => !v (v) => !v
) )
); );
const useBlurEffectForModal = computed( const useBlurEffectForModal = computed(
defaultStore.makeGetterSetter("useBlurEffectForModal") defaultStore.makeGetterSetter("useBlurEffectForModal")
); );
const useBlurEffect = computed(defaultStore.makeGetterSetter("useBlurEffect")); const useBlurEffect = computed(defaultStore.makeGetterSetter("useBlurEffect"));
const showGapBetweenNotesInTimeline = computed( const showGapBetweenNotesInTimeline = computed(
defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline") defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline")
); );
const showAds = computed(defaultStore.makeGetterSetter("showAds")); const showAds = computed(defaultStore.makeGetterSetter("showAds"));
const advancedMfm = computed(defaultStore.makeGetterSetter("advancedMfm")); const advancedMfm = computed(defaultStore.makeGetterSetter("advancedMfm"));
const autoplayMfm = computed( const autoplayMfm = computed(
defaultStore.makeGetterSetter( defaultStore.makeGetterSetter(
"animatedMfm", "animatedMfm",
(v) => !v, (v) => !v,
(v) => !v (v) => !v
) )
); );
const useOsNativeEmojis = computed( const useOsNativeEmojis = computed(
defaultStore.makeGetterSetter("useOsNativeEmojis") defaultStore.makeGetterSetter("useOsNativeEmojis")
); );
const disableDrawer = computed(defaultStore.makeGetterSetter("disableDrawer")); const disableDrawer = computed(defaultStore.makeGetterSetter("disableDrawer"));
const disableShowingAnimatedImages = computed( const disableShowingAnimatedImages = computed(
defaultStore.makeGetterSetter("disableShowingAnimatedImages") defaultStore.makeGetterSetter("disableShowingAnimatedImages")
); );
const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages")); const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages"));
const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab")); const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab"));
const nsfw = computed(defaultStore.makeGetterSetter("nsfw")); const nsfw = computed(defaultStore.makeGetterSetter("nsfw"));
const disablePagesScript = computed( const disablePagesScript = computed(
defaultStore.makeGetterSetter("disablePagesScript") defaultStore.makeGetterSetter("disablePagesScript")
); );
const expandOnNoteClick = computed( const expandOnNoteClick = computed(
defaultStore.makeGetterSetter("expandOnNoteClick") defaultStore.makeGetterSetter("expandOnNoteClick")
); );
const showFixedPostForm = computed( const showFixedPostForm = computed(
defaultStore.makeGetterSetter("showFixedPostForm") defaultStore.makeGetterSetter("showFixedPostForm")
); );
const numberOfPageCache = computed( const numberOfPageCache = computed(
defaultStore.makeGetterSetter("numberOfPageCache") defaultStore.makeGetterSetter("numberOfPageCache")
); );
const instanceTicker = computed( const instanceTicker = computed(
defaultStore.makeGetterSetter("instanceTicker") defaultStore.makeGetterSetter("instanceTicker")
); );
const enableInfiniteScroll = computed( const enableInfiniteScroll = computed(
defaultStore.makeGetterSetter("enableInfiniteScroll") defaultStore.makeGetterSetter("enableInfiniteScroll")
); );
const enterSendsMessage = computed( const enterSendsMessage = computed(
defaultStore.makeGetterSetter("enterSendsMessage") defaultStore.makeGetterSetter("enterSendsMessage")
); );
const useReactionPickerForContextMenu = computed( const useReactionPickerForContextMenu = computed(
defaultStore.makeGetterSetter("useReactionPickerForContextMenu") defaultStore.makeGetterSetter("useReactionPickerForContextMenu")
); );
const seperateRenoteQuote = computed( const seperateRenoteQuote = computed(
defaultStore.makeGetterSetter("seperateRenoteQuote") defaultStore.makeGetterSetter("seperateRenoteQuote")
); );
const squareAvatars = computed(defaultStore.makeGetterSetter("squareAvatars")); const squareAvatars = computed(defaultStore.makeGetterSetter("squareAvatars"));
const showUpdates = computed(defaultStore.makeGetterSetter("showUpdates")); const showUpdates = computed(defaultStore.makeGetterSetter("showUpdates"));
const swipeOnDesktop = computed( const swipeOnDesktop = computed(
defaultStore.makeGetterSetter("swipeOnDesktop") defaultStore.makeGetterSetter("swipeOnDesktop")
);
const showAdminUpdates = computed(
defaultStore.makeGetterSetter("showAdminUpdates")
); );
const showTimelineReplies = computed( const showTimelineReplies = computed(
defaultStore.makeGetterSetter("showTimelineReplies") defaultStore.makeGetterSetter("showTimelineReplies")
); );
watch(lang, () => { watch(lang, () => {
localStorage.setItem("lang", lang.value as string); localStorage.setItem("lang", lang.value as string);
localStorage.removeItem("locale"); localStorage.removeItem("locale");
}); });
watch(fontSize, () => { watch(fontSize, () => {
if (fontSize.value == null) { if (fontSize.value == null) {
localStorage.removeItem("fontSize"); localStorage.removeItem("fontSize");
} else { } else {
localStorage.setItem("fontSize", fontSize.value); localStorage.setItem("fontSize", fontSize.value);
} }
}); });
watch(useSystemFont, () => { watch(useSystemFont, () => {
if (useSystemFont.value) { if (useSystemFont.value) {
localStorage.setItem("useSystemFont", "t"); localStorage.setItem("useSystemFont", "t");
} else { } else {
localStorage.removeItem("useSystemFont"); localStorage.removeItem("useSystemFont");
} }
}); });
watch( watch(
[ [
lang, lang,
fontSize, fontSize,
useSystemFont, useSystemFont,
enableInfiniteScroll, enableInfiniteScroll,
squareAvatars, squareAvatars,
showGapBetweenNotesInTimeline, showGapBetweenNotesInTimeline,
instanceTicker, instanceTicker,
overridedDeviceKind, overridedDeviceKind,
showAds, showAds,
showUpdates, showUpdates,
swipeOnDesktop, swipeOnDesktop,
seperateRenoteQuote, seperateRenoteQuote,
showAdminUpdates, advancedMfm,
advancedMfm, autoplayMfm,
autoplayMfm, expandOnNoteClick,
expandOnNoteClick, ],
], async () => {
async () => { await reloadAsk();
await reloadAsk(); }
}
); );
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
@ -396,7 +385,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.general, title: i18n.ts.general,
icon: "ph-gear-six ph-bold ph-lg", icon: "ph-gear-six ph-bold ph-lg",
}); });
</script> </script>

View File

@ -1,58 +1,58 @@
<template> <template>
<div class="_formRoot"> <div class="_formRoot">
<div :class="$style.buttons"> <div :class="$style.buttons">
<MkButton inline primary @click="saveNew">{{ <MkButton inline primary @click="saveNew">{{
ts._preferencesBackups.saveNew ts._preferencesBackups.saveNew
}}</MkButton> }}</MkButton>
<MkButton inline @click="loadFile">{{ <MkButton inline @click="loadFile">{{
ts._preferencesBackups.loadFile ts._preferencesBackups.loadFile
}}</MkButton> }}</MkButton>
</div> </div>
<FormSection> <FormSection>
<template #label>{{ ts._preferencesBackups.list }}</template> <template #label>{{ ts._preferencesBackups.list }}</template>
<template v-if="profiles && Object.keys(profiles).length > 0"> <template v-if="profiles && Object.keys(profiles).length > 0">
<div <div
v-for="(profile, id) in profiles" v-for="(profile, id) in profiles"
:key="id" :key="id"
class="_formBlock _panel" class="_formBlock _panel"
:class="$style.profile" :class="$style.profile"
@click="($event) => menu($event, id)" @click="($event) => menu($event, id)"
@contextmenu.prevent.stop="($event) => menu($event, id)" @contextmenu.prevent.stop="($event) => menu($event, id)"
> >
<div :class="$style.profileName">{{ profile.name }}</div> <div :class="$style.profileName">{{ profile.name }}</div>
<div :class="$style.profileTime"> <div :class="$style.profileTime">
{{ {{
t("_preferencesBackups.createdAt", { t("_preferencesBackups.createdAt", {
date: new Date( date: new Date(
profile.createdAt profile.createdAt
).toLocaleDateString(), ).toLocaleDateString(),
time: new Date( time: new Date(
profile.createdAt profile.createdAt
).toLocaleTimeString(), ).toLocaleTimeString(),
}) })
}} }}
</div> </div>
<div v-if="profile.updatedAt" :class="$style.profileTime"> <div v-if="profile.updatedAt" :class="$style.profileTime">
{{ {{
t("_preferencesBackups.updatedAt", { t("_preferencesBackups.updatedAt", {
date: new Date( date: new Date(
profile.updatedAt profile.updatedAt
).toLocaleDateString(), ).toLocaleDateString(),
time: new Date( time: new Date(
profile.updatedAt profile.updatedAt
).toLocaleTimeString(), ).toLocaleTimeString(),
}) })
}} }}
</div> </div>
</div> </div>
</template> </template>
<div v-else-if="profiles"> <div v-else-if="profiles">
<MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo> <MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo>
</div> </div>
<MkLoading v-else /> <MkLoading v-else />
</FormSection> </FormSection>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -67,95 +67,95 @@ import { unisonReload } from "@/scripts/unison-reload";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { version, host } from "@/config"; import { host, version } from "@/config";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
const { t, ts } = i18n; const { t, ts } = i18n;
useCssModule(); useCssModule();
const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"menu", "menu",
"visibility", "visibility",
"localOnly", "localOnly",
"statusbars", "statusbars",
"widgets", "widgets",
"tl", "tl",
"overridedDeviceKind", "overridedDeviceKind",
"serverDisconnectedBehavior", "serverDisconnectedBehavior",
"nsfw", "nsfw",
"showAds", "showAds",
"animation", "animation",
"animatedMfm", "animatedMfm",
"loadRawImages", "loadRawImages",
"imageNewTab", "imageNewTab",
"disableShowingAnimatedImages", "disableShowingAnimatedImages",
"disablePagesScript", "disablePagesScript",
"enterSendsMessage", "enterSendsMessage",
"useOsNativeEmojis", "useOsNativeEmojis",
"disableDrawer", "disableDrawer",
"useBlurEffectForModal", "useBlurEffectForModal",
"useBlurEffect", "useBlurEffect",
"showFixedPostForm", "showFixedPostForm",
"enableInfiniteScroll", "enableInfiniteScroll",
"useReactionPickerForContextMenu", "useReactionPickerForContextMenu",
"showGapBetweenNotesInTimeline", "showGapBetweenNotesInTimeline",
"instanceTicker", "instanceTicker",
"reactionPickerSize", "reactionPickerSize",
"reactionPickerWidth", "reactionPickerWidth",
"reactionPickerHeight", "reactionPickerHeight",
"reactionPickerUseDrawerForMobile", "reactionPickerUseDrawerForMobile",
"defaultSideView", "defaultSideView",
"menuDisplay", "menuDisplay",
"reportError", "reportError",
"squareAvatars", "squareAvatars",
"numberOfPageCache", "numberOfPageCache",
"showUpdates", "showUpdates",
"swipeOnDesktop", "swipeOnDesktop",
"showAdminUpdates", "enableCustomKaTeXMacro",
"enableCustomKaTeXMacro", "enableEmojiReactions",
"enableEmojiReactions", "showEmojisInReactionNotifications",
"showEmojisInReactionNotifications", "showTimelineReplies",
"showTimelineReplies",
]; ];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme", "lightTheme",
"darkTheme", "darkTheme",
"syncDeviceDarkMode", "syncDeviceDarkMode",
"plugins", "plugins",
"mediaVolume", "mediaVolume",
"sound_masterVolume", "sound_masterVolume",
"sound_note", "sound_note",
"sound_noteMy", "sound_noteMy",
"sound_notification", "sound_notification",
"sound_chat", "sound_chat",
"sound_chatBg", "sound_chatBg",
"sound_antenna", "sound_antenna",
"sound_channel", "sound_channel",
]; ];
const scope = ["clientPreferencesProfiles"]; const scope = ["clientPreferencesProfiles"];
const profileProps = [ const profileProps = [
"name", "name",
"createdAt", "createdAt",
"updatedAt", "updatedAt",
"misskeyVersion", "misskeyVersion",
"settings", "settings",
]; ];
type Profile = { type Profile = {
name: string; name: string;
createdAt: string; createdAt: string;
updatedAt: string | null; updatedAt: string | null;
misskeyVersion: string; misskeyVersion: string;
host: string; host: string;
settings: { settings: {
hot: Record<keyof typeof defaultStoreSaveKeys, unknown>; hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>; cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
fontSize: string | null; fontSize: string | null;
useSystemFont: "t" | null; useSystemFont: "t" | null;
wallpaper: string | null; wallpaper: string | null;
}; };
}; };
const connection = $i && stream.useChannel("main"); const connection = $i && stream.useChannel("main");
@ -163,375 +163,375 @@ const connection = $i && stream.useChannel("main");
let profiles = $ref<Record<string, Profile> | null>(null); let profiles = $ref<Record<string, Profile> | null>(null);
os.api("i/registry/get-all", { scope }).then((res) => { os.api("i/registry/get-all", { scope }).then((res) => {
profiles = res || {}; profiles = res || {};
}); });
function isObject(value: unknown): value is Record<string, unknown> { function isObject(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === "object" && !Array.isArray(value); return value != null && typeof value === "object" && !Array.isArray(value);
} }
function validate(profile: unknown): void { function validate(profile: unknown): void {
if (!isObject(profile)) throw new Error("not an object"); if (!isObject(profile)) throw new Error("not an object");
// Check if unnecessary properties exist // Check if unnecessary properties exist
if ( if (
Object.keys(profile).some( Object.keys(profile).some(
(key) => !profileProps.includes(key) && key !== "host" (key) => !profileProps.includes(key) && key !== "host"
) )
) )
throw new Error("Unnecessary properties exist"); throw new Error("Unnecessary properties exist");
if (!profile.name) throw new Error("Missing required prop: name"); if (!profile.name) throw new Error("Missing required prop: name");
if (!profile.misskeyVersion) if (!profile.misskeyVersion)
throw new Error("Missing required prop: misskeyVersion"); throw new Error("Missing required prop: misskeyVersion");
// Check if createdAt and updatedAt is Date // Check if createdAt and updatedAt is Date
// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
if ( if (
!profile.createdAt || !profile.createdAt ||
Number.isNaN(new Date(profile.createdAt).getTime()) Number.isNaN(new Date(profile.createdAt).getTime())
) )
throw new Error("createdAt is falsy or not Date"); throw new Error("createdAt is falsy or not Date");
if (profile.updatedAt) { if (profile.updatedAt) {
if (Number.isNaN(new Date(profile.updatedAt).getTime())) { if (Number.isNaN(new Date(profile.updatedAt).getTime())) {
throw new Error("updatedAt is not Date"); throw new Error("updatedAt is not Date");
} }
} else if (profile.updatedAt !== null) { } else if (profile.updatedAt !== null) {
throw new Error("updatedAt is not null"); throw new Error("updatedAt is not null");
} }
if (!profile.settings) throw new Error("Missing required prop: settings"); if (!profile.settings) throw new Error("Missing required prop: settings");
if (!isObject(profile.settings)) throw new Error("Invalid prop: settings"); if (!isObject(profile.settings)) throw new Error("Invalid prop: settings");
} }
function getSettings(): Profile["settings"] { function getSettings(): Profile["settings"] {
const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>; const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>;
for (const key of defaultStoreSaveKeys) { for (const key of defaultStoreSaveKeys) {
hot[key] = defaultStore.state[key]; hot[key] = defaultStore.state[key];
} }
const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>; const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
for (const key of coldDeviceStorageSaveKeys) { for (const key of coldDeviceStorageSaveKeys) {
cold[key] = ColdDeviceStorage.get(key); cold[key] = ColdDeviceStorage.get(key);
} }
return { return {
hot, hot,
cold, cold,
fontSize: localStorage.getItem("fontSize"), fontSize: localStorage.getItem("fontSize"),
useSystemFont: localStorage.getItem("useSystemFont") as "t" | null, useSystemFont: localStorage.getItem("useSystemFont") as "t" | null,
wallpaper: localStorage.getItem("wallpaper"), wallpaper: localStorage.getItem("wallpaper"),
}; };
} }
async function saveNew(): Promise<void> { async function saveNew(): Promise<void> {
if (!profiles) return; if (!profiles) return;
const { canceled, result: name } = await os.inputText({ const { canceled, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName, title: ts._preferencesBackups.inputName,
}); });
if (canceled) return; if (canceled) return;
if (Object.values(profiles).some((x) => x.name === name)) { if (Object.values(profiles).some((x) => x.name === name)) {
return os.alert({ return os.alert({
title: ts._preferencesBackups.cannotSave, title: ts._preferencesBackups.cannotSave,
text: t("_preferencesBackups.nameAlreadyExists", { name }), text: t("_preferencesBackups.nameAlreadyExists", { name }),
}); });
} }
const id = uuid(); const id = uuid();
const profile: Profile = { const profile: Profile = {
name, name,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: null, updatedAt: null,
misskeyVersion: version, misskeyVersion: version,
host, host,
settings: getSettings(), settings: getSettings(),
}; };
await os.apiWithDialog("i/registry/set", { await os.apiWithDialog("i/registry/set", {
scope, scope,
key: id, key: id,
value: profile, value: profile,
}); });
} }
function loadFile(): void { function loadFile(): void {
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.multiple = false; input.multiple = false;
input.onchange = async () => { input.onchange = async () => {
if (!profiles) return; if (!profiles) return;
if (!input.files || input.files.length === 0) return; if (!input.files || input.files.length === 0) return;
const file = input.files[0]; const file = input.files[0];
if (file.type !== "application/json") { if (file.type !== "application/json") {
return os.alert({ return os.alert({
type: "error", type: "error",
title: ts._preferencesBackups.cannotLoad, title: ts._preferencesBackups.cannotLoad,
text: ts._preferencesBackups.invalidFile, text: ts._preferencesBackups.invalidFile,
}); });
} }
let profile: Profile; let profile: Profile;
try { try {
profile = JSON.parse(await file.text()) as unknown as Profile; profile = JSON.parse(await file.text()) as unknown as Profile;
validate(profile); validate(profile);
} catch (err) { } catch (err) {
return os.alert({ return os.alert({
type: "error", type: "error",
title: ts._preferencesBackups.cannotLoad, title: ts._preferencesBackups.cannotLoad,
text: err?.message, text: err?.message,
}); });
} }
const id = uuid(); const id = uuid();
await os.apiWithDialog("i/registry/set", { await os.apiWithDialog("i/registry/set", {
scope, scope,
key: id, key: id,
value: profile, value: profile,
}); });
// //
(window as any).__misskey_input_ref__ = null; (window as any).__misskey_input_ref__ = null;
}; };
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d // https://qiita.com/fukasawah/items/b9dc732d95d99551013d
// iOS Safari // iOS Safari
(window as any).__misskey_input_ref__ = input; (window as any).__misskey_input_ref__ = input;
input.click(); input.click();
} }
async function applyProfile(id: string): Promise<void> { async function applyProfile(id: string): Promise<void> {
if (!profiles) return; if (!profiles) return;
const profile = profiles[id]; const profile = profiles[id];
const { canceled: cancel1 } = await os.confirm({ const { canceled: cancel1 } = await os.confirm({
type: "warning", type: "warning",
title: ts._preferencesBackups.apply, title: ts._preferencesBackups.apply,
text: t("_preferencesBackups.applyConfirm", { name: profile.name }), text: t("_preferencesBackups.applyConfirm", { name: profile.name }),
}); });
if (cancel1) return; if (cancel1) return;
// TODO: or // TODO: or
const settings = profile.settings; const settings = profile.settings;
// defaultStore // defaultStore
for (const key of defaultStoreSaveKeys) { for (const key of defaultStoreSaveKeys) {
if (settings.hot[key] !== undefined) { if (settings.hot[key] !== undefined) {
defaultStore.set(key, settings.hot[key]); defaultStore.set(key, settings.hot[key]);
} }
} }
// coldDeviceStorage // coldDeviceStorage
for (const key of coldDeviceStorageSaveKeys) { for (const key of coldDeviceStorageSaveKeys) {
if (settings.cold[key] !== undefined) { if (settings.cold[key] !== undefined) {
ColdDeviceStorage.set(key, settings.cold[key]); ColdDeviceStorage.set(key, settings.cold[key]);
} }
} }
// fontSize // fontSize
if (settings.fontSize) { if (settings.fontSize) {
localStorage.setItem("fontSize", settings.fontSize); localStorage.setItem("fontSize", settings.fontSize);
} else { } else {
localStorage.removeItem("fontSize"); localStorage.removeItem("fontSize");
} }
// useSystemFont // useSystemFont
if (settings.useSystemFont) { if (settings.useSystemFont) {
localStorage.setItem("useSystemFont", settings.useSystemFont); localStorage.setItem("useSystemFont", settings.useSystemFont);
} else { } else {
localStorage.removeItem("useSystemFont"); localStorage.removeItem("useSystemFont");
} }
// wallpaper // wallpaper
if (settings.wallpaper != null) { if (settings.wallpaper != null) {
localStorage.setItem("wallpaper", settings.wallpaper); localStorage.setItem("wallpaper", settings.wallpaper);
} else { } else {
localStorage.removeItem("wallpaper"); localStorage.removeItem("wallpaper");
} }
const { canceled: cancel2 } = await os.confirm({ const { canceled: cancel2 } = await os.confirm({
type: "info", type: "info",
text: ts.reloadToApplySetting, text: ts.reloadToApplySetting,
}); });
if (cancel2) return; if (cancel2) return;
unisonReload(); unisonReload();
} }
async function deleteProfile(id: string): Promise<void> { async function deleteProfile(id: string): Promise<void> {
if (!profiles) return; if (!profiles) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "info", type: "info",
title: ts.delete, title: ts.delete,
text: t("deleteAreYouSure", { x: profiles[id].name }), text: t("deleteAreYouSure", { x: profiles[id].name }),
}); });
if (canceled) return; if (canceled) return;
await os.apiWithDialog("i/registry/remove", { scope, key: id }); await os.apiWithDialog("i/registry/remove", { scope, key: id });
delete profiles[id]; delete profiles[id];
} }
async function save(id: string): Promise<void> { async function save(id: string): Promise<void> {
if (!profiles) return; if (!profiles) return;
const { name, createdAt } = profiles[id]; const { name, createdAt } = profiles[id];
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "info", type: "info",
title: ts._preferencesBackups.save, title: ts._preferencesBackups.save,
text: t("_preferencesBackups.saveConfirm", { name }), text: t("_preferencesBackups.saveConfirm", { name }),
}); });
if (canceled) return; if (canceled) return;
const profile: Profile = { const profile: Profile = {
name, name,
createdAt, createdAt,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
misskeyVersion: version, misskeyVersion: version,
host, host,
settings: getSettings(), settings: getSettings(),
}; };
await os.apiWithDialog("i/registry/set", { await os.apiWithDialog("i/registry/set", {
scope, scope,
key: id, key: id,
value: profile, value: profile,
}); });
} }
async function rename(id: string): Promise<void> { async function rename(id: string): Promise<void> {
if (!profiles) return; if (!profiles) return;
const { canceled: cancel1, result: name } = await os.inputText({ const { canceled: cancel1, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName, title: ts._preferencesBackups.inputName,
}); });
if (cancel1 || profiles[id].name === name) return; if (cancel1 || profiles[id].name === name) return;
if (Object.values(profiles).some((x) => x.name === name)) { if (Object.values(profiles).some((x) => x.name === name)) {
return os.alert({ return os.alert({
title: ts._preferencesBackups.cannotSave, title: ts._preferencesBackups.cannotSave,
text: t("_preferencesBackups.nameAlreadyExists", { name }), text: t("_preferencesBackups.nameAlreadyExists", { name }),
}); });
} }
const registry = Object.assign({}, { ...profiles[id] }); const registry = Object.assign({}, { ...profiles[id] });
const { canceled: cancel2 } = await os.confirm({ const { canceled: cancel2 } = await os.confirm({
type: "info", type: "info",
title: ts._preferencesBackups.rename, title: ts._preferencesBackups.rename,
text: t("_preferencesBackups.renameConfirm", { text: t("_preferencesBackups.renameConfirm", {
old: registry.name, old: registry.name,
new: name, new: name,
}), }),
}); });
if (cancel2) return; if (cancel2) return;
registry.name = name; registry.name = name;
await os.apiWithDialog("i/registry/set", { await os.apiWithDialog("i/registry/set", {
scope, scope,
key: id, key: id,
value: registry, value: registry,
}); });
} }
function menu(ev: MouseEvent, profileId: string) { function menu(ev: MouseEvent, profileId: string) {
if (!profiles) return; if (!profiles) return;
return os.popupMenu( return os.popupMenu(
[ [
{ {
text: ts._preferencesBackups.apply, text: ts._preferencesBackups.apply,
icon: "ph-caret-circle-down ph-bold ph-lg", icon: "ph-caret-circle-down ph-bold ph-lg",
action: () => applyProfile(profileId), action: () => applyProfile(profileId),
}, },
{ {
type: "a", type: "a",
text: ts.download, text: ts.download,
icon: "ph-download-simple ph-bold ph-lg", icon: "ph-download-simple ph-bold ph-lg",
href: URL.createObjectURL( href: URL.createObjectURL(
new Blob([JSON.stringify(profiles[profileId], null, 2)], { new Blob([JSON.stringify(profiles[profileId], null, 2)], {
type: "application/json", type: "application/json",
}) })
), ),
download: `${profiles[profileId].name}.json`, download: `${profiles[profileId].name}.json`,
}, },
null, null,
{ {
text: ts.rename, text: ts.rename,
icon: "ph-cursor-text ph-bold ph-lg", icon: "ph-cursor-text ph-bold ph-lg",
action: () => rename(profileId), action: () => rename(profileId),
}, },
{ {
text: ts._preferencesBackups.save, text: ts._preferencesBackups.save,
icon: "ph-floppy-disk ph-bold ph-lg", icon: "ph-floppy-disk ph-bold ph-lg",
action: () => save(profileId), action: () => save(profileId),
}, },
null, null,
{ {
text: ts._preferencesBackups.delete, text: ts._preferencesBackups.delete,
icon: "ph-trash ph-bold ph-lg", icon: "ph-trash ph-bold ph-lg",
action: () => deleteProfile(profileId), action: () => deleteProfile(profileId),
danger: true, danger: true,
}, },
], ],
ev.currentTarget ?? ev.target ev.currentTarget ?? ev.target
); );
} }
onMounted(() => { onMounted(() => {
// streaminguser storage update // streaminguser storage update
connection?.on( connection?.on(
"registryUpdated", "registryUpdated",
({ scope: recievedScope, key, value }) => { ({ scope: recievedScope, key, value }) => {
if ( if (
!recievedScope || !recievedScope ||
recievedScope.length !== scope.length || recievedScope.length !== scope.length ||
recievedScope[0] !== scope[0] recievedScope[0] !== scope[0]
) )
return; return;
if (!profiles) return; if (!profiles) return;
profiles[key] = value; profiles[key] = value;
} }
); );
}); });
onUnmounted(() => { onUnmounted(() => {
connection?.off("registryUpdated"); connection?.off("registryUpdated");
}); });
definePageMetadata( definePageMetadata(
computed(() => ({ computed(() => ({
title: ts.preferencesBackups, title: ts.preferencesBackups,
icon: "ph-floppy-disk ph-bold ph-lg", icon: "ph-floppy-disk ph-bold ph-lg",
bg: "var(--bg)", bg: "var(--bg)",
})) }))
); );
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.buttons { .buttons {
display: flex; display: flex;
gap: var(--margin); gap: var(--margin);
flex-wrap: wrap; flex-wrap: wrap;
} }
.profile { .profile {
padding: 20px; padding: 20px;
cursor: pointer; cursor: pointer;
&Name { &Name {
font-weight: 700; font-weight: 700;
} }
&Time { &Time {
font-size: 0.85em; font-size: 0.85em;
opacity: 0.7; opacity: 0.7;
} }
} }
</style> </style>

View File

@ -1,6 +1,10 @@
import { markRaw, ref } from "vue"; import { markRaw, ref } from "vue";
import { Storage } from "./pizzax"; import { Storage } from "./pizzax";
import { Theme } from "./scripts/theme"; /**
* ()
*/
import lightTheme from "@/themes/l-rosepinedawn.json5";
import darkTheme from "@/themes/d-rosepine.json5";
export const postFormActions = []; export const postFormActions = [];
export const userActions = []; export const userActions = [];
@ -9,334 +13,330 @@ export const noteViewInterruptors = [];
export const notePostInterruptors = []; export const notePostInterruptors = [];
const menuOptions = [ const menuOptions = [
"notifications", "notifications",
"followRequests", "followRequests",
"explore", "explore",
"favorites", "favorites",
"search", "search",
]; ];
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) // TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない // あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
export const defaultStore = markRaw( export const defaultStore = markRaw(
new Storage("base", { new Storage("base", {
tutorial: { tutorial: {
where: "account", where: "account",
default: 0, default: 0,
}, },
tlHomeHintClosed: { tlHomeHintClosed: {
where: "device", where: "device",
default: false, default: false,
}, },
tlLocalHintClosed: { tlLocalHintClosed: {
where: "device", where: "device",
default: false, default: false,
}, },
tlRecommendedHintClosed: { tlRecommendedHintClosed: {
where: "device", where: "device",
default: false, default: false,
}, },
tlSocialHintClosed: { tlSocialHintClosed: {
where: "device", where: "device",
default: false, default: false,
}, },
tlGlobalHintClosed: { tlGlobalHintClosed: {
where: "device", where: "device",
default: false, default: false,
}, },
keepCw: { keepCw: {
where: "account", where: "account",
default: true, default: true,
}, },
showFullAcct: { showFullAcct: {
where: "account", where: "account",
default: false, default: false,
}, },
rememberNoteVisibility: { rememberNoteVisibility: {
where: "account", where: "account",
default: false, default: false,
}, },
defaultNoteVisibility: { defaultNoteVisibility: {
where: "account", where: "account",
default: "public", default: "public",
}, },
defaultNoteLocalOnly: { defaultNoteLocalOnly: {
where: "account", where: "account",
default: false, default: false,
}, },
uploadFolder: { uploadFolder: {
where: "account", where: "account",
default: null as string | null, default: null as string | null,
}, },
pastedFileName: { pastedFileName: {
where: "account", where: "account",
default: "yyyy-MM-dd HH-mm-ss [{{number}}]", default: "yyyy-MM-dd HH-mm-ss [{{number}}]",
}, },
keepOriginalUploading: { keepOriginalUploading: {
where: "account", where: "account",
default: false, default: false,
}, },
memo: { memo: {
where: "account", where: "account",
default: null, default: null,
}, },
reactions: { reactions: {
where: "account", where: "account",
default: [ default: [
"⭐", "⭐",
"❤️", "❤️",
"😆", "😆",
"🤔", "🤔",
"😮", "😮",
"🎉", "🎉",
"💢", "💢",
"😥", "😥",
"😇", "😇",
"🦋", "🦋",
"🦊", "🦊",
], ],
}, },
mutedWords: { mutedWords: {
where: "account", where: "account",
default: [], default: [],
}, },
mutedAds: { mutedAds: {
where: "account", where: "account",
default: [] as string[], default: [] as string[],
}, },
showAds: { showAds: {
where: "account", where: "account",
default: true, default: true,
}, },
menu: { menu: {
where: "deviceAccount", where: "deviceAccount",
default: menuOptions, default: menuOptions,
}, },
visibility: { visibility: {
where: "deviceAccount", where: "deviceAccount",
default: "public" as "public" | "home" | "followers" | "specified", default: "public" as "public" | "home" | "followers" | "specified",
}, },
localOnly: { localOnly: {
where: "deviceAccount", where: "deviceAccount",
default: false, default: false,
}, },
statusbars: { statusbars: {
where: "deviceAccount", where: "deviceAccount",
default: [] as { default: [] as {
name: string; name: string;
id: string; id: string;
type: string; type: string;
size: "verySmall" | "small" | "medium" | "large" | "veryLarge"; size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
black: boolean; black: boolean;
props: Record<string, any>; props: Record<string, any>;
}[], }[],
}, },
widgets: { widgets: {
where: "deviceAccount", where: "deviceAccount",
default: [] as { default: [] as {
name: string; name: string;
id: string; id: string;
place: string | null; place: string | null;
data: Record<string, any>; data: Record<string, any>;
}[], }[],
}, },
tl: { tl: {
where: "deviceAccount", where: "deviceAccount",
default: { default: {
src: "home" as "home" | "local" | "social" | "global", src: "home" as "home" | "local" | "social" | "global",
arg: null, arg: null,
}, },
}, },
overridedDeviceKind: { overridedDeviceKind: {
where: "device", where: "device",
default: null as null | "smartphone" | "tablet" | "desktop", default: null as null | "smartphone" | "tablet" | "desktop",
}, },
serverDisconnectedBehavior: { serverDisconnectedBehavior: {
where: "device", where: "device",
default: "nothing" as "nothing" | "quiet" | "reload" | "dialog", default: "nothing" as "nothing" | "quiet" | "reload" | "dialog",
}, },
seperateRenoteQuote: { seperateRenoteQuote: {
where: "device", where: "device",
default: true, default: true,
}, },
expandOnNoteClick: { expandOnNoteClick: {
where: "device", where: "device",
default: true, default: true,
}, },
nsfw: { nsfw: {
where: "device", where: "device",
default: "respect" as "respect" | "force" | "ignore", default: "respect" as "respect" | "force" | "ignore",
}, },
animation: { animation: {
where: "device", where: "device",
default: true, default: true,
}, },
advancedMfm: { advancedMfm: {
where: "device", where: "device",
default: true, default: true,
}, },
animatedMfm: { animatedMfm: {
where: "device", where: "device",
default: true, default: true,
}, },
animatedMfmWarnShown: { animatedMfmWarnShown: {
where: "device", where: "device",
default: false, default: false,
}, },
loadRawImages: { loadRawImages: {
where: "device", where: "device",
default: false, default: false,
}, },
imageNewTab: { imageNewTab: {
where: "device", where: "device",
default: false, default: false,
}, },
disableShowingAnimatedImages: { disableShowingAnimatedImages: {
where: "device", where: "device",
default: false, default: false,
}, },
disablePagesScript: { disablePagesScript: {
where: "device", where: "device",
default: false, default: false,
}, },
useOsNativeEmojis: { useOsNativeEmojis: {
where: "device", where: "device",
default: false, default: false,
}, },
disableDrawer: { disableDrawer: {
where: "device", where: "device",
default: false, default: false,
}, },
useBlurEffectForModal: { useBlurEffectForModal: {
where: "device", where: "device",
default: true, default: true,
}, },
useBlurEffect: { useBlurEffect: {
where: "device", where: "device",
default: true, default: true,
}, },
showFixedPostForm: { showFixedPostForm: {
where: "device", where: "device",
default: false, default: false,
}, },
enableInfiniteScroll: { enableInfiniteScroll: {
where: "device", where: "device",
default: true, default: true,
}, },
useReactionPickerForContextMenu: { useReactionPickerForContextMenu: {
where: "device", where: "device",
default: false, default: false,
}, },
showGapBetweenNotesInTimeline: { showGapBetweenNotesInTimeline: {
where: "device", where: "device",
default: true, default: true,
}, },
darkMode: { darkMode: {
where: "device", where: "device",
default: false, default: false,
}, },
instanceTicker: { instanceTicker: {
where: "device", where: "device",
default: "remote" as "none" | "remote" | "always", default: "remote" as "none" | "remote" | "always",
}, },
reactionPickerSkinTone: { reactionPickerSkinTone: {
where: "account", where: "account",
default: 1, default: 1,
}, },
reactionPickerSize: { reactionPickerSize: {
where: "device", where: "device",
default: 3, default: 3,
}, },
reactionPickerWidth: { reactionPickerWidth: {
where: "device", where: "device",
default: 3, default: 3,
}, },
reactionPickerHeight: { reactionPickerHeight: {
where: "device", where: "device",
default: 3, default: 3,
}, },
reactionPickerUseDrawerForMobile: { reactionPickerUseDrawerForMobile: {
where: "device", where: "device",
default: true, default: true,
}, },
recentlyUsedEmojis: { recentlyUsedEmojis: {
where: "device", where: "device",
default: [] as string[], default: [] as string[],
}, },
recentlyUsedUsers: { recentlyUsedUsers: {
where: "device", where: "device",
default: [] as string[], default: [] as string[],
}, },
defaultSideView: { defaultSideView: {
where: "device", where: "device",
default: false, default: false,
}, },
menuDisplay: { menuDisplay: {
where: "device", where: "device",
default: "sideFull" as "sideFull" | "sideIcon" | "top", default: "sideFull" as "sideFull" | "sideIcon" | "top",
}, },
reportError: { reportError: {
where: "device", where: "device",
default: false, default: false,
}, },
squareAvatars: { squareAvatars: {
where: "device", where: "device",
default: true, default: true,
}, },
postFormWithHashtags: { postFormWithHashtags: {
where: "device", where: "device",
default: false, default: false,
}, },
postFormHashtags: { postFormHashtags: {
where: "device", where: "device",
default: "", default: "",
}, },
themeInitial: { themeInitial: {
where: "device", where: "device",
default: true, default: true,
}, },
numberOfPageCache: { numberOfPageCache: {
where: "device", where: "device",
default: 5, default: 5,
}, },
enterSendsMessage: { enterSendsMessage: {
where: "device", where: "device",
default: true, default: true,
}, },
showUpdates: { showUpdates: {
where: "device", where: "device",
default: true, default: true,
}, },
swipeOnDesktop: { swipeOnDesktop: {
where: "device", where: "device",
default: false, default: false,
}, },
showAdminUpdates: { woozyMode: {
where: "account", where: "device",
default: true, default: false,
}, },
woozyMode: { enableCustomKaTeXMacro: {
where: "device", where: "device",
default: false, default: false,
}, },
enableCustomKaTeXMacro: { enableEmojiReactions: {
where: "device", where: "account",
default: false, default: true,
}, },
enableEmojiReactions: { showEmojisInReactionNotifications: {
where: "account", where: "account",
default: true, default: true,
}, },
showEmojisInReactionNotifications: { showTimelineReplies: {
where: "account", where: "device",
default: true, default: true,
}, },
showTimelineReplies: { })
where: "device",
default: true,
},
}),
); );
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -344,110 +344,106 @@ export const defaultStore = markRaw(
const PREFIX = "miux:"; const PREFIX = "miux:";
type Plugin = { type Plugin = {
id: string; id: string;
name: string; name: string;
active: boolean; active: boolean;
configData: Record<string, any>; configData: Record<string, any>;
token: string; token: string;
ast: any[]; ast: any[];
}; };
/**
* ()
*/
import lightTheme from "@/themes/l-rosepinedawn.json5";
import darkTheme from "@/themes/d-rosepine.json5";
export class ColdDeviceStorage { export class ColdDeviceStorage {
public static default = { public static default = {
lightTheme, lightTheme,
darkTheme, darkTheme,
syncDeviceDarkMode: true, syncDeviceDarkMode: true,
plugins: [] as Plugin[], plugins: [] as Plugin[],
mediaVolume: 0.5, mediaVolume: 0.5,
sound_masterVolume: 0.3, sound_masterVolume: 0.3,
sound_note: { type: "none", volume: 0 }, sound_note: { type: "none", volume: 0 },
sound_noteMy: { type: "syuilo/up", volume: 1 }, sound_noteMy: { type: "syuilo/up", volume: 1 },
sound_notification: { type: "syuilo/pope2", volume: 1 }, sound_notification: { type: "syuilo/pope2", volume: 1 },
sound_chat: { type: "syuilo/pope1", volume: 1 }, sound_chat: { type: "syuilo/pope1", volume: 1 },
sound_chatBg: { type: "syuilo/waon", volume: 1 }, sound_chatBg: { type: "syuilo/waon", volume: 1 },
sound_antenna: { type: "syuilo/triple", volume: 1 }, sound_antenna: { type: "syuilo/triple", volume: 1 },
sound_channel: { type: "syuilo/square-pico", volume: 1 }, sound_channel: { type: "syuilo/square-pico", volume: 1 },
}; };
public static watchers = []; public static watchers = [];
public static get<T extends keyof typeof ColdDeviceStorage.default>( public static get<T extends keyof typeof ColdDeviceStorage.default>(
key: T, key: T
): typeof ColdDeviceStorage.default[T] { ): (typeof ColdDeviceStorage.default)[T] {
// TODO: indexedDBにする // TODO: indexedDBにする
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ // ただしその際はnullチェックではなくキー存在チェックにしないとダメ
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
const value = localStorage.getItem(PREFIX + key); const value = localStorage.getItem(PREFIX + key);
if (value == null) { if (value == null) {
return ColdDeviceStorage.default[key]; return ColdDeviceStorage.default[key];
} else { } else {
return JSON.parse(value); return JSON.parse(value);
} }
} }
public static set<T extends keyof typeof ColdDeviceStorage.default>( public static set<T extends keyof typeof ColdDeviceStorage.default>(
key: T, key: T,
value: typeof ColdDeviceStorage.default[T], value: (typeof ColdDeviceStorage.default)[T]
): void { ): void {
// 呼び出し側のバグ等で undefined が来ることがある // 呼び出し側のバグ等で undefined が来ることがある
// undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
if (value === undefined) { if (value === undefined) {
console.error(`attempt to store undefined value for key '${key}'`); console.error(`attempt to store undefined value for key '${key}'`);
return; return;
} }
localStorage.setItem(PREFIX + key, JSON.stringify(value)); localStorage.setItem(PREFIX + key, JSON.stringify(value));
for (const watcher of this.watchers) { for (const watcher of this.watchers) {
if (watcher.key === key) watcher.callback(value); if (watcher.key === key) watcher.callback(value);
} }
} }
public static watch(key, callback) { public static watch(key, callback) {
this.watchers.push({ key, callback }); this.watchers.push({ key, callback });
} }
// TODO: VueのcustomRef使うと良い感じになるかも // TODO: VueのcustomRef使うと良い感じになるかも
public static ref<T extends keyof typeof ColdDeviceStorage.default>(key: T) { public static ref<T extends keyof typeof ColdDeviceStorage.default>(
const v = ColdDeviceStorage.get(key); key: T
const r = ref(v); ) {
// TODO: このままではwatcherがリークするので開放する方法を考える const v = ColdDeviceStorage.get(key);
this.watch(key, (v) => { const r = ref(v);
r.value = v; // TODO: このままではwatcherがリークするので開放する方法を考える
}); this.watch(key, (v) => {
return r; r.value = v;
} });
return r;
}
/** /**
* getter/setterを作ります * getter/setterを作ります
* vue場で設定コントロールのmodelとして使う用 * vue場で設定コントロールのmodelとして使う用
*/ */
public static makeGetterSetter< public static makeGetterSetter<
K extends keyof typeof ColdDeviceStorage.default, K extends keyof typeof ColdDeviceStorage.default
>(key: K) { >(key: K) {
// TODO: VueのcustomRef使うと良い感じになるかも // TODO: VueのcustomRef使うと良い感じになるかも
const valueRef = ColdDeviceStorage.ref(key); const valueRef = ColdDeviceStorage.ref(key);
return { return {
get: () => { get: () => {
return valueRef.value; return valueRef.value;
}, },
set: (value: unknown) => { set: (value: unknown) => {
const val = value; const val = value;
ColdDeviceStorage.set(key, val); ColdDeviceStorage.set(key, val);
}, },
}; };
} }
} }
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない // このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
declare module "@vue/runtime-core" { declare module "@vue/runtime-core" {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$store: typeof defaultStore; $store: typeof defaultStore;
} }
} }

File diff suppressed because it is too large Load Diff