/* * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { markRaw, ref } from 'vue'; import { Storage } from './pizzax'; interface PostFormAction { title: string, handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void; } interface UserAction { title: string, handler: (user: UserDetailed) => void; } interface NoteAction { title: string, handler: (note: Note) => void; } interface NoteViewInterruptor { handler: (note: Note) => unknown; } interface NotePostInterruptor { handler: (note: FIXME) => unknown; } interface PageViewInterruptor { handler: (page: Page) => unknown; } export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; export const noteViewInterruptors: NoteViewInterruptor[] = []; export const notePostInterruptors: NotePostInterruptor[] = []; export const pageViewInterruptors: PageViewInterruptor[] = []; // TODO: ãれãžã‚Œã„ã¡ã„ã¡whereã¨ã‹defaultã¨ã„ã†ã‚ーを付ã‘ãªãゃã„ã‘ãªã„ã®å†—é•·ãªã®ã§ãªã‚“ã¨ã‹ã™ã‚‹(ãŸã 型定義ãŒé¢å€’ã«ãªã‚Šãã†) // ã‚ã¨ã€ç¾è¡Œã®å®šç¾©ã®ä»•æ–¹ãªã‚‰ã€ŒwhereãŒä½•ã§ã‚ã‚‹ã‹ã«é–¢ã‚らãšã‚ーåã®é‡è¤‡ä¸å¯ã€ã¨ã„ã†åˆ¶ç´„を付ã‘られるメリットもã‚ã‚‹ã‹ã‚‰ãã®ãƒ¡ãƒªãƒƒãƒˆã‚’引ãç¶™ãæ–¹æ³•も考ãˆãªã„ã¨ã„ã‘ãªã„ export const defaultStore = markRaw(new Storage('base', { accountSetupWizard: { where: 'account', default: 0, }, timelineTutorial: { where: 'account', default: 0, }, keepCw: { where: 'account', default: true, }, showFullAcct: { where: 'account', default: false, }, collapseRenotes: { where: 'account', default: true, }, 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: ['ðŸ‘', 'â¤ï¸', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', 'ðŸ®'], }, reactionAcceptance: { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, }, mutedWords: { where: 'account', default: [], }, mutedAds: { where: 'account', default: [] as string[], }, showTimelineReplies: { where: 'account', default: false, }, menu: { where: 'deviceAccount', default: [ 'notifications', 'favorites', 'drive', 'followRequests', '-', 'explore', 'announcements', 'search', '-', 'ui', ], }, 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: 'account', 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: 'quiet' as 'quiet' | 'reload' | 'dialog', }, nsfw: { where: 'device', default: 'respect' as 'respect' | 'force' | 'ignore', }, animation: { where: 'device', default: !window.matchMedia('(prefers-reduced-motion)').matches, }, animatedMfm: { where: 'device', default: false, }, advancedMfm: { where: 'device', default: true, }, loadRawImages: { where: 'device', default: false, }, imageNewTab: { where: 'device', default: false, }, enableDataSaverMode: { where: 'device', default: false, }, disableShowingAnimatedImages: { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, }, emojiStyle: { where: 'device', default: 'twemoji', // twemoji / fluentEmoji / native }, disableDrawer: { where: 'device', default: false, }, useBlurEffectForModal: { where: 'device', default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環å‚ç…§ã™ã‚‹ã®ã§device-kind.tsã¯å‚ç…§ã§ããªã„ }, useBlurEffect: { where: 'device', default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環å‚ç…§ã™ã‚‹ã®ã§device-kind.tsã¯å‚ç…§ã§ããªã„ }, showFixedPostForm: { where: 'device', default: false, }, showFixedPostFormInChannel: { where: 'device', default: false, }, enableInfiniteScroll: { where: 'device', default: true, }, useReactionPickerForContextMenu: { where: 'device', default: false, }, showGapBetweenNotesInTimeline: { where: 'device', default: false, }, darkMode: { where: 'device', default: false, }, instanceTicker: { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', }, reactionPickerSize: { where: 'device', default: 1, }, reactionPickerWidth: { where: 'device', default: 1, }, reactionPickerHeight: { where: 'device', default: 2, }, 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: false, }, postFormWithHashtags: { where: 'device', default: false, }, postFormHashtags: { where: 'device', default: '', }, themeInitial: { where: 'device', default: true, }, numberOfPageCache: { where: 'device', default: 3, }, showNoteActionsOnlyHover: { where: 'device', default: false, }, showClipButtonInNoteFooter: { where: 'device', default: false, }, largeNoteReactions: { where: 'device', default: false, }, forceShowAds: { where: 'device', default: false, }, aiChanMode: { where: 'device', default: false, }, devMode: { where: 'device', default: false, }, mediaListWithOneImageAppearance: { where: 'device', default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3', }, notificationPosition: { where: 'device', default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom', }, notificationStackAxis: { where: 'device', default: 'horizontal' as 'vertical' | 'horizontal', }, enableCondensedLineForAcct: { where: 'device', default: false, }, additionalUnicodeEmojiIndexes: { where: 'device', default: {} as Record<string, Record<string, string[]>>, }, })); // TODO: ä»–ã®ã‚¿ãƒ–ã¨æ°¸ç¶šåŒ–ã•れãŸstateã‚’åŒæœŸ const PREFIX = 'miux:' as const; export type Plugin = { id: string; name: string; active: boolean; config?: Record<string, { default: any }>; configData: Record<string, any>; token: string; src: string | null; version: string; ast: any[]; }; interface Watcher { key: string; callback: (value: unknown) => void; } /** * 常ã«ãƒ¡ãƒ¢ãƒªã«ãƒãƒ¼ãƒ‰ã—ã¦ãŠãå¿…è¦ãŒãªã„よã†ãªè¨å®šæƒ…å ±ã‚’ä¿ç®¡ã™ã‚‹ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸(éžãƒªã‚¢ã‚¯ãƒ†ã‚£ãƒ–) */ import { miLocalStorage } from './local-storage'; import lightTheme from '@/themes/l-light.json5'; import darkTheme from '@/themes/d-green-lime.json5'; import { Note, UserDetailed, Page } from 'misskey-js/built/entities'; export class ColdDeviceStorage { public static default = { lightTheme, darkTheme, syncDeviceDarkMode: true, plugins: [] as Plugin[], }; public static watchers: Watcher[] = []; public static get<T extends keyof typeof ColdDeviceStorage.default>(key: T): typeof ColdDeviceStorage.default[T] { // TODO: indexedDBã«ã™ã‚‹ // ãŸã ã—ãã®éš›ã¯nullãƒã‚§ãƒƒã‚¯ã§ã¯ãªãã‚ーå˜åœ¨ãƒã‚§ãƒƒã‚¯ã«ã—ãªã„ã¨ãƒ€ãƒ¡ // (indexedDBã¯nullã‚’ä¿å˜ã§ãã‚‹ãŸã‚ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒæ„図ã—ã¦nullã‚’æ ¼ç´ã—ãŸå¯èƒ½æ€§ãŒã‚ã‚‹) const value = miLocalStorage.getItem(`${PREFIX}${key}`); if (value == null) { return ColdDeviceStorage.default[key]; } else { return JSON.parse(value); } } public static getAll(): Partial<typeof this.default> { return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => { const value = localStorage.getItem(PREFIX + key); if (value != null) { acc[key] = JSON.parse(value); } return acc; }, {} as any); } public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼ã³å‡ºã—å´ã®ãƒã‚°ç‰ã§ undefined ãŒæ¥ã‚‹ã“ã¨ãŒã‚ã‚‹ // undefined ã‚’æ–‡å—列ã¨ã—㦠miLocalStorage ã«å…¥ã‚Œã‚‹ã¨å‚ç…§ã™ã‚‹éš›ã® JSON.parse ã§ã‚³ã‚±ã¦ä¸å…·åˆã®å…ƒã«ãªã‚‹ãŸã‚無視 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; } miLocalStorage.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); }, }; } }