refactor: move ColdDeviceStorage into a module
This commit is contained in:
parent
93bee484bb
commit
6f324a3dcd
5 changed files with 146 additions and 115 deletions
121
packages/client/src/cold-store.ts
Normal file
121
packages/client/src/cold-store.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { ref as vueRef } from "vue";
|
||||
import type { UnwrapRef } from "vue";
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
const PREFIX = "miux:";
|
||||
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
configData: Record<string, unknown>;
|
||||
token: string;
|
||||
ast: unknown[];
|
||||
}
|
||||
|
||||
import darkTheme from "@/themes/d-rosepine.json5";
|
||||
/**
|
||||
* Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
|
||||
*/
|
||||
import lightTheme from "@/themes/l-rosepinedawn.json5";
|
||||
|
||||
const ColdStoreDefault = {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
vibrate: false,
|
||||
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 },
|
||||
};
|
||||
|
||||
const watchers: {
|
||||
key: string;
|
||||
callback: (value) => void;
|
||||
}[] = [];
|
||||
|
||||
function get<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
): (typeof ColdStoreDefault)[T] {
|
||||
// TODO: indexedDBにする
|
||||
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
|
||||
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
|
||||
const value = localStorage.getItem(PREFIX + key);
|
||||
if (value == null) {
|
||||
return ColdStoreDefault[key];
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
}
|
||||
|
||||
function set<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
value: (typeof ColdStoreDefault)[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));
|
||||
|
||||
for (const watcher of watchers) {
|
||||
if (watcher.key === key) watcher.callback(value);
|
||||
}
|
||||
}
|
||||
|
||||
function watch<T extends keyof typeof ColdStoreDefault>(
|
||||
key: T,
|
||||
callback: (value: (typeof ColdStoreDefault)[T]) => void,
|
||||
) {
|
||||
watchers.push({ key, callback });
|
||||
}
|
||||
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
function ref<T extends keyof typeof ColdStoreDefault>(key: T) {
|
||||
const v = get(key);
|
||||
const r = vueRef(v);
|
||||
// TODO: このままではwatcherがリークするので開放する方法を考える
|
||||
watch(key, (v) => {
|
||||
r.value = v as UnwrapRef<typeof v>;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||
* 主にvue場で設定コントロールのmodelとして使う用
|
||||
*/
|
||||
function makeGetterSetter<K extends keyof typeof ColdStoreDefault>(key: K) {
|
||||
// TODO: VueのcustomRef使うと良い感じになるかも
|
||||
const valueRef = ref(key);
|
||||
return {
|
||||
get: () => {
|
||||
return valueRef.value;
|
||||
},
|
||||
set: (value: (typeof ColdStoreDefault)[K]) => {
|
||||
const val = value;
|
||||
set(key, val);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
default: ColdStoreDefault,
|
||||
watchers,
|
||||
get,
|
||||
set,
|
||||
watch,
|
||||
ref,
|
||||
makeGetterSetter,
|
||||
};
|
|
@ -1,9 +1,12 @@
|
|||
import { markRaw, ref } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import type { ApiTypes, entities } from "firefish-js";
|
||||
import { isSignedIn, me } from "./me";
|
||||
import { Storage } from "./pizzax";
|
||||
import type { NoteVisibility } from "@/types/note";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
type TODO = any;
|
||||
|
||||
export const postFormActions: {
|
||||
title: string;
|
||||
handler: (from, update) => void | Promise<void>;
|
||||
|
@ -152,7 +155,7 @@ export const defaultStore = markRaw(
|
|||
type: string;
|
||||
size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
|
||||
black: boolean;
|
||||
props: Record<string, any>;
|
||||
props: Record<string, TODO>;
|
||||
}[],
|
||||
},
|
||||
widgets: {
|
||||
|
@ -161,7 +164,7 @@ export const defaultStore = markRaw(
|
|||
name: string;
|
||||
id: string;
|
||||
place: string | null;
|
||||
data: Record<string, any>;
|
||||
data: Record<string, TODO>;
|
||||
}[],
|
||||
},
|
||||
tl: {
|
||||
|
@ -453,109 +456,6 @@ export const defaultStore = markRaw(
|
|||
}),
|
||||
);
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
import ColdStore from "./cold-store";
|
||||
|
||||
const PREFIX = "miux:";
|
||||
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
configData: Record<string, any>;
|
||||
token: string;
|
||||
ast: any[];
|
||||
}
|
||||
|
||||
import darkTheme from "@/themes/d-rosepine.json5";
|
||||
/**
|
||||
* Storage for configuration information that does not need to be constantly loaded into memory (non-reactive)
|
||||
*/
|
||||
import lightTheme from "@/themes/l-rosepinedawn.json5";
|
||||
|
||||
export class ColdDeviceStorage {
|
||||
public static default = {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
syncDeviceDarkMode: true,
|
||||
plugins: [] as Plugin[],
|
||||
mediaVolume: 0.5,
|
||||
vibrate: false,
|
||||
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 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;
|
||||
}
|
||||
|
||||
localStorage.setItem(PREFIX + key, JSON.stringify(value));
|
||||
|
||||
for (const watcher of this.watchers) {
|
||||
if (watcher.key === key) watcher.callback(value);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特定のキーの、簡易的な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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
export const ColdDeviceStorage = ColdStore;
|
||||
|
|
|
@ -31,6 +31,7 @@ export function reloadStream() {
|
|||
|
||||
isReloading = true;
|
||||
stream.close();
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
|
||||
stream.once("_connected_", () => (isReloading = false));
|
||||
stream.stream.reconnect();
|
||||
isReloading = false;
|
||||
|
|
|
@ -17,7 +17,8 @@ export async function fetchThemes(): Promise<void> {
|
|||
key: "themes",
|
||||
});
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
} catch (err) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Safely any
|
||||
} catch (err: any) {
|
||||
if (err.code === "NO_SUCH_KEY") return;
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ export type APIError = {
|
|||
code: string;
|
||||
message: string;
|
||||
kind: "client" | "server";
|
||||
info: Record<string, any>;
|
||||
info: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function isAPIError(reason: any): reason is APIError {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: used it intentially
|
||||
type ExplicitlyUsedAny = any;
|
||||
export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError {
|
||||
return reason[MK_API_ERROR] === true;
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,7 @@ export type FetchLike = (
|
|||
},
|
||||
) => Promise<{
|
||||
status: number;
|
||||
json(): Promise<any>;
|
||||
json(): Promise<ExplicitlyUsedAny>;
|
||||
}>;
|
||||
|
||||
type IsNeverType<T> = [T] extends [never] ? true : false;
|
||||
|
@ -36,7 +38,10 @@ type IsCaseMatched<
|
|||
P extends Endpoints[E]["req"],
|
||||
C extends number,
|
||||
> = IsNeverType<
|
||||
StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>
|
||||
StrictExtract<
|
||||
Endpoints[E]["res"]["$switch"]["$cases"][C],
|
||||
[P, ExplicitlyUsedAny]
|
||||
>
|
||||
> extends false
|
||||
? true
|
||||
: false;
|
||||
|
@ -45,7 +50,10 @@ type GetCaseResult<
|
|||
E extends keyof Endpoints,
|
||||
P extends Endpoints[E]["req"],
|
||||
C extends number,
|
||||
> = StrictExtract<Endpoints[E]["res"]["$switch"]["$cases"][C], [P, any]>[1];
|
||||
> = StrictExtract<
|
||||
Endpoints[E]["res"]["$switch"]["$cases"][C],
|
||||
[P, ExplicitlyUsedAny]
|
||||
>[1];
|
||||
|
||||
export class APIClient {
|
||||
public origin: string;
|
||||
|
@ -70,7 +78,7 @@ export class APIClient {
|
|||
credential?: string | null | undefined,
|
||||
): Promise<
|
||||
Endpoints[E]["res"] extends {
|
||||
$switch: { $cases: [any, any][]; $default: any };
|
||||
$switch: { $cases: [unknown, unknown][]; $default: unknown };
|
||||
}
|
||||
? IsCaseMatched<E, P, 0> extends true
|
||||
? GetCaseResult<E, P, 0>
|
||||
|
|
Loading…
Reference in a new issue