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;
res: ServerInfo;
};
"latest-version": {
req: NoParams;
res: TODO;
};
"sw/register": {
req: TODO;
res: TODO;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff