Compare commits
	
		
			14 Commits
		
	
	
		
			2025.3.2-a
			...
			refine-piz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2402754dcc | ||
| 
						 | 
					2493592bd0 | ||
| 
						 | 
					eec4ab841a | ||
| 
						 | 
					7957ee5191 | ||
| 
						 | 
					d0b8ffe629 | ||
| 
						 | 
					b200743845 | ||
| 
						 | 
					cef7575b76 | ||
| 
						 | 
					9842eb2eeb | ||
| 
						 | 
					05078e9c14 | ||
| 
						 | 
					db5c6fa3c2 | ||
| 
						 | 
					8a4e2659ed | ||
| 
						 | 
					d19c094a9b | ||
| 
						 | 
					08f7e7d9b3 | ||
| 
						 | 
					a7f7ff33e7 | 
@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
 | 
			
		||||
    let fetching = ref(true);
 | 
			
		||||
    let images = ref([]);
 | 
			
		||||
    function thumbnail(image) {
 | 
			
		||||
      return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
    }
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      const image = [
 | 
			
		||||
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
 | 
			
		||||
    let fetching = ref(true);
 | 
			
		||||
    let images = ref([]);
 | 
			
		||||
    function thumbnail(image) {
 | 
			
		||||
      return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
    }
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
 | 
			
		||||
 
 | 
			
		||||
@@ -154,52 +154,52 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
 | 
			
		||||
	watch(store.reactiveState.darkMode, (darkMode) => {
 | 
			
		||||
	watch(store.r.darkMode, (darkMode) => {
 | 
			
		||||
		applyTheme(darkMode
 | 
			
		||||
			? (prefer.s.darkTheme ?? defaultDarkTheme)
 | 
			
		||||
			: (prefer.s.lightTheme ?? defaultLightTheme),
 | 
			
		||||
		);
 | 
			
		||||
	}, { immediate: miLocalStorage.getItem('theme') == null });
 | 
			
		||||
 | 
			
		||||
	document.documentElement.dataset.colorScheme = store.state.darkMode ? 'dark' : 'light';
 | 
			
		||||
	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 | 
			
		||||
 | 
			
		||||
	const darkTheme = prefer.model('darkTheme');
 | 
			
		||||
	const lightTheme = prefer.model('lightTheme');
 | 
			
		||||
 | 
			
		||||
	watch(darkTheme, (theme) => {
 | 
			
		||||
		if (store.state.darkMode) {
 | 
			
		||||
		if (store.s.darkMode) {
 | 
			
		||||
			applyTheme(theme ?? defaultDarkTheme);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	watch(lightTheme, (theme) => {
 | 
			
		||||
		if (!store.state.darkMode) {
 | 
			
		||||
		if (!store.s.darkMode) {
 | 
			
		||||
			applyTheme(theme ?? defaultLightTheme);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#region Sync dark mode
 | 
			
		||||
	if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
		store.set('darkMode', isDeviceDarkmode());
 | 
			
		||||
		store.commit('darkMode', isDeviceDarkmode());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
 | 
			
		||||
		if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
			store.set('darkMode', mql.matches);
 | 
			
		||||
			store.commit('darkMode', mql.matches);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	if (prefer.s.darkTheme && store.state.darkMode) {
 | 
			
		||||
	if (prefer.s.darkTheme && store.s.darkMode) {
 | 
			
		||||
		if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
 | 
			
		||||
	} else if (prefer.s.lightTheme && !store.state.darkMode) {
 | 
			
		||||
	} else if (prefer.s.lightTheme && !store.s.darkMode) {
 | 
			
		||||
		if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fetchInstanceMetaPromise.then(() => {
 | 
			
		||||
		// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
 | 
			
		||||
		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.set('lightTheme', JSON.parse(instance.defaultLightTheme));
 | 
			
		||||
		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
 | 
			
		||||
		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
 | 
			
		||||
		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	watch(prefer.r.overridedDeviceKind, (kind) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -138,98 +138,98 @@ export async function mainBoot() {
 | 
			
		||||
		store.loaded.then(async () => {
 | 
			
		||||
			// prefereces migration
 | 
			
		||||
			// TODO: そのうち消す
 | 
			
		||||
			if (store.state.menu.length > 0) {
 | 
			
		||||
			if (store.s.menu.length > 0) {
 | 
			
		||||
				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
 | 
			
		||||
				if (themes.length > 0) {
 | 
			
		||||
					prefer.set('themes', themes);
 | 
			
		||||
					prefer.commit('themes', themes);
 | 
			
		||||
				}
 | 
			
		||||
				const plugins = ColdDeviceStorage.get('plugins');
 | 
			
		||||
				prefer.set('plugins', plugins.map(p => ({
 | 
			
		||||
				prefer.commit('plugins', plugins.map(p => ({
 | 
			
		||||
					...p,
 | 
			
		||||
					installId: (p as any).id,
 | 
			
		||||
					id: undefined,
 | 
			
		||||
				})));
 | 
			
		||||
				prefer.set('lightTheme', ColdDeviceStorage.get('lightTheme'));
 | 
			
		||||
				prefer.set('darkTheme', ColdDeviceStorage.get('darkTheme'));
 | 
			
		||||
				prefer.set('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
 | 
			
		||||
				prefer.set('overridedDeviceKind', store.state.overridedDeviceKind);
 | 
			
		||||
				prefer.set('widgets', store.state.widgets);
 | 
			
		||||
				prefer.set('keepCw', store.state.keepCw);
 | 
			
		||||
				prefer.set('collapseRenotes', store.state.collapseRenotes);
 | 
			
		||||
				prefer.set('rememberNoteVisibility', store.state.rememberNoteVisibility);
 | 
			
		||||
				prefer.set('uploadFolder', store.state.uploadFolder);
 | 
			
		||||
				prefer.set('keepOriginalUploading', store.state.keepOriginalUploading);
 | 
			
		||||
				prefer.set('menu', store.state.menu);
 | 
			
		||||
				prefer.set('statusbars', store.state.statusbars);
 | 
			
		||||
				prefer.set('pinnedUserLists', store.state.pinnedUserLists);
 | 
			
		||||
				prefer.set('serverDisconnectedBehavior', store.state.serverDisconnectedBehavior);
 | 
			
		||||
				prefer.set('nsfw', store.state.nsfw);
 | 
			
		||||
				prefer.set('highlightSensitiveMedia', store.state.highlightSensitiveMedia);
 | 
			
		||||
				prefer.set('animation', store.state.animation);
 | 
			
		||||
				prefer.set('animatedMfm', store.state.animatedMfm);
 | 
			
		||||
				prefer.set('advancedMfm', store.state.advancedMfm);
 | 
			
		||||
				prefer.set('showReactionsCount', store.state.showReactionsCount);
 | 
			
		||||
				prefer.set('enableQuickAddMfmFunction', store.state.enableQuickAddMfmFunction);
 | 
			
		||||
				prefer.set('loadRawImages', store.state.loadRawImages);
 | 
			
		||||
				prefer.set('imageNewTab', store.state.imageNewTab);
 | 
			
		||||
				prefer.set('disableShowingAnimatedImages', store.state.disableShowingAnimatedImages);
 | 
			
		||||
				prefer.set('emojiStyle', store.state.emojiStyle);
 | 
			
		||||
				prefer.set('menuStyle', store.state.menuStyle);
 | 
			
		||||
				prefer.set('useBlurEffectForModal', store.state.useBlurEffectForModal);
 | 
			
		||||
				prefer.set('useBlurEffect', store.state.useBlurEffect);
 | 
			
		||||
				prefer.set('showFixedPostForm', store.state.showFixedPostForm);
 | 
			
		||||
				prefer.set('showFixedPostFormInChannel', store.state.showFixedPostFormInChannel);
 | 
			
		||||
				prefer.set('enableInfiniteScroll', store.state.enableInfiniteScroll);
 | 
			
		||||
				prefer.set('useReactionPickerForContextMenu', store.state.useReactionPickerForContextMenu);
 | 
			
		||||
				prefer.set('showGapBetweenNotesInTimeline', store.state.showGapBetweenNotesInTimeline);
 | 
			
		||||
				prefer.set('instanceTicker', store.state.instanceTicker);
 | 
			
		||||
				prefer.set('emojiPickerScale', store.state.emojiPickerScale);
 | 
			
		||||
				prefer.set('emojiPickerWidth', store.state.emojiPickerWidth);
 | 
			
		||||
				prefer.set('emojiPickerHeight', store.state.emojiPickerHeight);
 | 
			
		||||
				prefer.set('emojiPickerStyle', store.state.emojiPickerStyle);
 | 
			
		||||
				prefer.set('reportError', store.state.reportError);
 | 
			
		||||
				prefer.set('squareAvatars', store.state.squareAvatars);
 | 
			
		||||
				prefer.set('showAvatarDecorations', store.state.showAvatarDecorations);
 | 
			
		||||
				prefer.set('numberOfPageCache', store.state.numberOfPageCache);
 | 
			
		||||
				prefer.set('showNoteActionsOnlyHover', store.state.showNoteActionsOnlyHover);
 | 
			
		||||
				prefer.set('showClipButtonInNoteFooter', store.state.showClipButtonInNoteFooter);
 | 
			
		||||
				prefer.set('reactionsDisplaySize', store.state.reactionsDisplaySize);
 | 
			
		||||
				prefer.set('limitWidthOfReaction', store.state.limitWidthOfReaction);
 | 
			
		||||
				prefer.set('forceShowAds', store.state.forceShowAds);
 | 
			
		||||
				prefer.set('aiChanMode', store.state.aiChanMode);
 | 
			
		||||
				prefer.set('devMode', store.state.devMode);
 | 
			
		||||
				prefer.set('mediaListWithOneImageAppearance', store.state.mediaListWithOneImageAppearance);
 | 
			
		||||
				prefer.set('notificationPosition', store.state.notificationPosition);
 | 
			
		||||
				prefer.set('notificationStackAxis', store.state.notificationStackAxis);
 | 
			
		||||
				prefer.set('enableCondensedLine', store.state.enableCondensedLine);
 | 
			
		||||
				prefer.set('keepScreenOn', store.state.keepScreenOn);
 | 
			
		||||
				prefer.set('disableStreamingTimeline', store.state.disableStreamingTimeline);
 | 
			
		||||
				prefer.set('useGroupedNotifications', store.state.useGroupedNotifications);
 | 
			
		||||
				prefer.set('dataSaver', store.state.dataSaver);
 | 
			
		||||
				prefer.set('enableSeasonalScreenEffect', store.state.enableSeasonalScreenEffect);
 | 
			
		||||
				prefer.set('enableHorizontalSwipe', store.state.enableHorizontalSwipe);
 | 
			
		||||
				prefer.set('useNativeUiForVideoAudioPlayer', store.state.useNativeUIForVideoAudioPlayer);
 | 
			
		||||
				prefer.set('keepOriginalFilename', store.state.keepOriginalFilename);
 | 
			
		||||
				prefer.set('alwaysConfirmFollow', store.state.alwaysConfirmFollow);
 | 
			
		||||
				prefer.set('confirmWhenRevealingSensitiveMedia', store.state.confirmWhenRevealingSensitiveMedia);
 | 
			
		||||
				prefer.set('contextMenu', store.state.contextMenu);
 | 
			
		||||
				prefer.set('skipNoteRender', store.state.skipNoteRender);
 | 
			
		||||
				prefer.set('showSoftWordMutedWord', store.state.showSoftWordMutedWord);
 | 
			
		||||
				prefer.set('confirmOnReact', store.state.confirmOnReact);
 | 
			
		||||
				prefer.set('sound.masterVolume', store.state.sound_masterVolume);
 | 
			
		||||
				prefer.set('sound.notUseSound', store.state.sound_notUseSound);
 | 
			
		||||
				prefer.set('sound.useSoundOnlyWhenActive', store.state.sound_useSoundOnlyWhenActive);
 | 
			
		||||
				prefer.set('sound.on.note', store.state.sound_note as any);
 | 
			
		||||
				prefer.set('sound.on.noteMy', store.state.sound_noteMy as any);
 | 
			
		||||
				prefer.set('sound.on.notification', store.state.sound_notification as any);
 | 
			
		||||
				prefer.set('sound.on.reaction', store.state.sound_reaction as any);
 | 
			
		||||
				store.set('deck.profile', deckStore.state.profile);
 | 
			
		||||
				store.set('deck.columns', deckStore.state.columns);
 | 
			
		||||
				store.set('deck.layout', deckStore.state.layout);
 | 
			
		||||
				store.set('menu', []);
 | 
			
		||||
				prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
 | 
			
		||||
				prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
 | 
			
		||||
				prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
 | 
			
		||||
				prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
 | 
			
		||||
				prefer.commit('widgets', store.s.widgets);
 | 
			
		||||
				prefer.commit('keepCw', store.s.keepCw);
 | 
			
		||||
				prefer.commit('collapseRenotes', store.s.collapseRenotes);
 | 
			
		||||
				prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
 | 
			
		||||
				prefer.commit('uploadFolder', store.s.uploadFolder);
 | 
			
		||||
				prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
 | 
			
		||||
				prefer.commit('menu', store.s.menu);
 | 
			
		||||
				prefer.commit('statusbars', store.s.statusbars);
 | 
			
		||||
				prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
 | 
			
		||||
				prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
 | 
			
		||||
				prefer.commit('nsfw', store.s.nsfw);
 | 
			
		||||
				prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
 | 
			
		||||
				prefer.commit('animation', store.s.animation);
 | 
			
		||||
				prefer.commit('animatedMfm', store.s.animatedMfm);
 | 
			
		||||
				prefer.commit('advancedMfm', store.s.advancedMfm);
 | 
			
		||||
				prefer.commit('showReactionsCount', store.s.showReactionsCount);
 | 
			
		||||
				prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
 | 
			
		||||
				prefer.commit('loadRawImages', store.s.loadRawImages);
 | 
			
		||||
				prefer.commit('imageNewTab', store.s.imageNewTab);
 | 
			
		||||
				prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
 | 
			
		||||
				prefer.commit('emojiStyle', store.s.emojiStyle);
 | 
			
		||||
				prefer.commit('menuStyle', store.s.menuStyle);
 | 
			
		||||
				prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
 | 
			
		||||
				prefer.commit('useBlurEffect', store.s.useBlurEffect);
 | 
			
		||||
				prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
 | 
			
		||||
				prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
 | 
			
		||||
				prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
 | 
			
		||||
				prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
 | 
			
		||||
				prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
 | 
			
		||||
				prefer.commit('instanceTicker', store.s.instanceTicker);
 | 
			
		||||
				prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
 | 
			
		||||
				prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
 | 
			
		||||
				prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
 | 
			
		||||
				prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
 | 
			
		||||
				prefer.commit('reportError', store.s.reportError);
 | 
			
		||||
				prefer.commit('squareAvatars', store.s.squareAvatars);
 | 
			
		||||
				prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
 | 
			
		||||
				prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
 | 
			
		||||
				prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
 | 
			
		||||
				prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
 | 
			
		||||
				prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
 | 
			
		||||
				prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
 | 
			
		||||
				prefer.commit('forceShowAds', store.s.forceShowAds);
 | 
			
		||||
				prefer.commit('aiChanMode', store.s.aiChanMode);
 | 
			
		||||
				prefer.commit('devMode', store.s.devMode);
 | 
			
		||||
				prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
 | 
			
		||||
				prefer.commit('notificationPosition', store.s.notificationPosition);
 | 
			
		||||
				prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
 | 
			
		||||
				prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
 | 
			
		||||
				prefer.commit('keepScreenOn', store.s.keepScreenOn);
 | 
			
		||||
				prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
 | 
			
		||||
				prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
 | 
			
		||||
				prefer.commit('dataSaver', store.s.dataSaver);
 | 
			
		||||
				prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
 | 
			
		||||
				prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
 | 
			
		||||
				prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
 | 
			
		||||
				prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
 | 
			
		||||
				prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
 | 
			
		||||
				prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
 | 
			
		||||
				prefer.commit('contextMenu', store.s.contextMenu);
 | 
			
		||||
				prefer.commit('skipNoteRender', store.s.skipNoteRender);
 | 
			
		||||
				prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
 | 
			
		||||
				prefer.commit('confirmOnReact', store.s.confirmOnReact);
 | 
			
		||||
				prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
 | 
			
		||||
				prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
 | 
			
		||||
				prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
 | 
			
		||||
				prefer.commit('sound.on.note', store.s.sound_note as any);
 | 
			
		||||
				prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
 | 
			
		||||
				prefer.commit('sound.on.notification', store.s.sound_notification as any);
 | 
			
		||||
				prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
 | 
			
		||||
				store.commit('deck.profile', deckStore.s.profile);
 | 
			
		||||
				store.commit('deck.columns', deckStore.s.columns);
 | 
			
		||||
				store.commit('deck.layout', deckStore.s.layout);
 | 
			
		||||
				store.commit('menu', []);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (store.state.accountSetupWizard !== -1) {
 | 
			
		||||
			if (store.s.accountSetupWizard !== -1) {
 | 
			
		||||
				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
 | 
			
		||||
					closed: () => dispose(),
 | 
			
		||||
				});
 | 
			
		||||
@@ -502,7 +502,7 @@ export async function mainBoot() {
 | 
			
		||||
			post();
 | 
			
		||||
		},
 | 
			
		||||
		'd': () => {
 | 
			
		||||
			store.set('darkMode', !store.state.darkMode);
 | 
			
		||||
			store.commit('darkMode', !store.s.darkMode);
 | 
			
		||||
		},
 | 
			
		||||
		's': () => {
 | 
			
		||||
			mainRouter.push('/search');
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@ const emojiDb = computed(() => {
 | 
			
		||||
		url: char2path(x.char),
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
	for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
		for (const [emoji, keywords] of Object.entries(index)) {
 | 
			
		||||
			for (const k of keywords) {
 | 
			
		||||
				unicodeEmojiDB.push({
 | 
			
		||||
@@ -155,10 +155,10 @@ function complete(type: string, value: any) {
 | 
			
		||||
	emit('done', { type, value });
 | 
			
		||||
	emit('closed');
 | 
			
		||||
	if (type === 'emoji') {
 | 
			
		||||
		let recents = store.state.recentlyUsedEmojis;
 | 
			
		||||
		let recents = store.s.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((emoji: any) => emoji !== value);
 | 
			
		||||
		recents.unshift(value);
 | 
			
		||||
		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
		store.commit('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -238,7 +238,7 @@ function exec() {
 | 
			
		||||
	} else if (props.type === 'emoji') {
 | 
			
		||||
		if (!props.q || props.q === '') {
 | 
			
		||||
			// 最近使った絵文字をサジェスト
 | 
			
		||||
			emojis.value = store.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 | 
			
		||||
			emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -154,7 +154,7 @@ async function requestRender() {
 | 
			
		||||
 | 
			
		||||
		captchaWidgetId.value = captcha.value.render(elem, {
 | 
			
		||||
			sitekey: props.sitekey,
 | 
			
		||||
			theme: store.state.darkMode ? 'dark' : 'light',
 | 
			
		||||
			theme: store.s.darkMode ? 'dark' : 'light',
 | 
			
		||||
			callback: callback,
 | 
			
		||||
			'expired-callback': () => callback(undefined),
 | 
			
		||||
			'error-callback': () => callback(undefined),
 | 
			
		||||
 
 | 
			
		||||
@@ -161,7 +161,7 @@ const render = () => {
 | 
			
		||||
		chartInstance.destroy();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const highlighter = await getHighlighter();
 | 
			
		||||
const darkMode = store.reactiveState.darkMode;
 | 
			
		||||
const darkMode = store.r.darkMode;
 | 
			
		||||
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
 | 
			
		||||
 | 
			
		||||
const [lightThemeName, darkThemeName] = await Promise.all([
 | 
			
		||||
 
 | 
			
		||||
@@ -245,7 +245,7 @@ function deleteFolder() {
 | 
			
		||||
		folderId: props.folder.id,
 | 
			
		||||
	}).then(() => {
 | 
			
		||||
		if (prefer.s.uploadFolder === props.folder.id) {
 | 
			
		||||
			prefer.set('uploadFolder', null);
 | 
			
		||||
			prefer.commit('uploadFolder', null);
 | 
			
		||||
		}
 | 
			
		||||
	}).catch(err => {
 | 
			
		||||
		switch (err.id) {
 | 
			
		||||
@@ -266,7 +266,7 @@ function deleteFolder() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setAsUploadFolder() {
 | 
			
		||||
	prefer.set('uploadFolder', props.folder.id);
 | 
			
		||||
	prefer.commit('uploadFolder', props.folder.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -166,7 +166,7 @@ const {
 | 
			
		||||
	emojiPickerHeight,
 | 
			
		||||
} = prefer.r;
 | 
			
		||||
 | 
			
		||||
const recentlyUsedEmojis = store.reactiveState.recentlyUsedEmojis;
 | 
			
		||||
const recentlyUsedEmojis = store.r.recentlyUsedEmojis;
 | 
			
		||||
 | 
			
		||||
const recentlyUsedEmojisDef = computed(() => {
 | 
			
		||||
	return recentlyUsedEmojis.value.map(getDef);
 | 
			
		||||
@@ -319,7 +319,7 @@ watch(q, () => {
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
				for (const emoji of emojis) {
 | 
			
		||||
					if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
 | 
			
		||||
						matches.add(emoji);
 | 
			
		||||
@@ -336,7 +336,7 @@ watch(q, () => {
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
				for (const emoji of emojis) {
 | 
			
		||||
					if (index[emoji.char].some(k => k.startsWith(newQ))) {
 | 
			
		||||
						matches.add(emoji);
 | 
			
		||||
@@ -353,7 +353,7 @@ watch(q, () => {
 | 
			
		||||
			}
 | 
			
		||||
			if (matches.size >= max) return matches;
 | 
			
		||||
 | 
			
		||||
			for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
				for (const emoji of emojis) {
 | 
			
		||||
					if (index[emoji.char].some(k => k.includes(newQ))) {
 | 
			
		||||
						matches.add(emoji);
 | 
			
		||||
@@ -429,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
 | 
			
		||||
 | 
			
		||||
	// 最近使った絵文字更新
 | 
			
		||||
	if (!pinned.value?.includes(key)) {
 | 
			
		||||
		let recents = store.state.recentlyUsedEmojis;
 | 
			
		||||
		let recents = store.s.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((emoji) => emoji !== key);
 | 
			
		||||
		recents.unshift(key);
 | 
			
		||||
		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
		store.commit('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -121,11 +121,11 @@ async function onClick() {
 | 
			
		||||
			} else {
 | 
			
		||||
				await misskeyApi('following/create', {
 | 
			
		||||
					userId: props.user.id,
 | 
			
		||||
					withReplies: store.state.defaultWithReplies,
 | 
			
		||||
					withReplies: store.s.defaultWithReplies,
 | 
			
		||||
				});
 | 
			
		||||
				emit('update:user', {
 | 
			
		||||
					...props.user,
 | 
			
		||||
					withReplies: store.state.defaultWithReplies,
 | 
			
		||||
					withReplies: store.s.defaultWithReplies,
 | 
			
		||||
				});
 | 
			
		||||
				hasPendingFollowRequestFromYou.value = true;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	await nextTick();
 | 
			
		||||
 | 
			
		||||
	const color = store.state.darkMode ? '#b4e900' : '#86b300';
 | 
			
		||||
	const color = store.s.darkMode ? '#b4e900' : '#86b300';
 | 
			
		||||
 | 
			
		||||
	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
 | 
			
		||||
	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
 | 
			
		||||
 
 | 
			
		||||
@@ -176,18 +176,18 @@ const text = ref(props.initialText ?? '');
 | 
			
		||||
const files = ref(props.initialFiles ?? []);
 | 
			
		||||
const poll = ref<PollEditorModelValue | null>(null);
 | 
			
		||||
const useCw = ref<boolean>(!!props.initialCw);
 | 
			
		||||
const showPreview = ref(store.state.showPreview);
 | 
			
		||||
watch(showPreview, () => store.set('showPreview', showPreview.value));
 | 
			
		||||
const showPreview = ref(store.s.showPreview);
 | 
			
		||||
watch(showPreview, () => store.commit('showPreview', showPreview.value));
 | 
			
		||||
const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
 | 
			
		||||
watch(showAddMfmFunction, () => prefer.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
 | 
			
		||||
watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
 | 
			
		||||
const cw = ref<string | null>(props.initialCw ?? null);
 | 
			
		||||
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly));
 | 
			
		||||
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility));
 | 
			
		||||
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly));
 | 
			
		||||
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility));
 | 
			
		||||
const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
 | 
			
		||||
if (props.initialVisibleUsers) {
 | 
			
		||||
	props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
 | 
			
		||||
}
 | 
			
		||||
const reactionAcceptance = ref(store.state.reactionAcceptance);
 | 
			
		||||
const reactionAcceptance = ref(store.s.reactionAcceptance);
 | 
			
		||||
const draghover = ref(false);
 | 
			
		||||
const quoteId = ref<string | null>(null);
 | 
			
		||||
const hasNotSpecifiedMentions = ref(false);
 | 
			
		||||
@@ -270,8 +270,8 @@ const canPost = computed((): boolean => {
 | 
			
		||||
		(!poll.value || poll.value.choices.length >= 2);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
 | 
			
		||||
const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
 | 
			
		||||
const withHashtags = store.model('postFormWithHashtags');
 | 
			
		||||
const hashtags = store.model('postFormHashtags');
 | 
			
		||||
 | 
			
		||||
watch(text, () => {
 | 
			
		||||
	checkMissingMention();
 | 
			
		||||
@@ -480,7 +480,7 @@ function setVisibility() {
 | 
			
		||||
		changeVisibility: v => {
 | 
			
		||||
			visibility.value = v;
 | 
			
		||||
			if (prefer.s.rememberNoteVisibility) {
 | 
			
		||||
				store.set('visibility', visibility.value);
 | 
			
		||||
				store.commit('visibility', visibility.value);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		closed: () => dispose(),
 | 
			
		||||
@@ -528,7 +528,7 @@ async function toggleLocalOnly() {
 | 
			
		||||
 | 
			
		||||
	localOnly.value = !localOnly.value;
 | 
			
		||||
	if (prefer.s.rememberNoteVisibility) {
 | 
			
		||||
		store.set('localOnly', localOnly.value);
 | 
			
		||||
		store.commit('localOnly', localOnly.value);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	await nextTick();
 | 
			
		||||
 | 
			
		||||
	const color = store.state.darkMode ? '#b4e900' : '#86b300';
 | 
			
		||||
	const color = store.s.darkMode ? '#b4e900' : '#86b300';
 | 
			
		||||
 | 
			
		||||
	const getYYYYMMDD = (date: Date) => {
 | 
			
		||||
		const y = date.getFullYear().toString().padStart(2, '0');
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ const getDate = (ymd: string) => {
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	let raw = await misskeyApi('retention', { });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 | 
			
		||||
	const color = accent.toHex();
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
 | 
			
		||||
			scrolling="no"
 | 
			
		||||
			:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
 | 
			
		||||
			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
 | 
			
		||||
			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.s.darkMode ? 'dark' : 'light'}&id=${tweetId}`"
 | 
			
		||||
		></iframe>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.action">
 | 
			
		||||
 
 | 
			
		||||
@@ -128,10 +128,10 @@ async function ok() {
 | 
			
		||||
	dialogEl.value?.close();
 | 
			
		||||
 | 
			
		||||
	// 最近使ったユーザー更新
 | 
			
		||||
	let recents = store.state.recentlyUsedUsers;
 | 
			
		||||
	let recents = store.s.recentlyUsedUsers;
 | 
			
		||||
	recents = recents.filter(x => x !== selected.value?.id);
 | 
			
		||||
	recents.unshift(selected.value.id);
 | 
			
		||||
	store.set('recentlyUsedUsers', recents.splice(0, 16));
 | 
			
		||||
	store.commit('recentlyUsedUsers', recents.splice(0, 16));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cancel() {
 | 
			
		||||
@@ -141,7 +141,7 @@ function cancel() {
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	misskeyApi('users/show', {
 | 
			
		||||
		userIds: store.state.recentlyUsedUsers,
 | 
			
		||||
		userIds: store.s.recentlyUsedUsers,
 | 
			
		||||
	}).then(foundUsers => {
 | 
			
		||||
		let _users = foundUsers;
 | 
			
		||||
		_users = _users.filter((u) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -149,10 +149,10 @@ const emit = defineEmits<{
 | 
			
		||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 | 
			
		||||
const page = ref(store.state.accountSetupWizard);
 | 
			
		||||
const page = ref(store.s.accountSetupWizard);
 | 
			
		||||
 | 
			
		||||
watch(page, () => {
 | 
			
		||||
	store.set('accountSetupWizard', page.value);
 | 
			
		||||
	store.commit('accountSetupWizard', page.value);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function close(skip: boolean) {
 | 
			
		||||
@@ -165,11 +165,11 @@ async function close(skip: boolean) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
	store.set('accountSetupWizard', -1);
 | 
			
		||||
	store.commit('accountSetupWizard', -1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupComplete() {
 | 
			
		||||
	store.set('accountSetupWizard', -1);
 | 
			
		||||
	store.commit('accountSetupWizard', -1);
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -194,7 +194,7 @@ async function later(later: boolean) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
	store.set('accountSetupWizard', 0);
 | 
			
		||||
	store.commit('accountSetupWizard', 0);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	await nextTick();
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ const choseAd = (): Ad | null => {
 | 
			
		||||
		return props.specify;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const allAds = instance.ads.map(ad => store.state.mutedAds.includes(ad.id) ? {
 | 
			
		||||
	const allAds = instance.ads.map(ad => store.s.mutedAds.includes(ad.id) ? {
 | 
			
		||||
		...ad,
 | 
			
		||||
		ratio: 0,
 | 
			
		||||
	} : ad);
 | 
			
		||||
@@ -112,8 +112,8 @@ const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds &&
 | 
			
		||||
 | 
			
		||||
function reduceFrequency(): void {
 | 
			
		||||
	if (chosen.value == null) return;
 | 
			
		||||
	if (store.state.mutedAds.includes(chosen.value.id)) return;
 | 
			
		||||
	store.push('mutedAds', chosen.value.id);
 | 
			
		||||
	if (store.s.mutedAds.includes(chosen.value.id)) return;
 | 
			
		||||
	store.commit('mutedAds', [...store.s.mutedAds, chosen.value.id]);
 | 
			
		||||
	os.success();
 | 
			
		||||
	chosen.value = choseAd();
 | 
			
		||||
	showMenu.value = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="[$style.spacer, store.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
 | 
			
		||||
<div :class="[$style.spacer, store.r.darkMode.value ? $style.dark : $style.light]"></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
 
 | 
			
		||||
@@ -59,34 +59,34 @@ export const loadDeck = async () => {
 | 
			
		||||
	try {
 | 
			
		||||
		deck = await misskeyApi('i/registry/get', {
 | 
			
		||||
			scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
			key: store.state['deck.profile'],
 | 
			
		||||
			key: store.s['deck.profile'],
 | 
			
		||||
		});
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
 | 
			
		||||
			// 後方互換性のため
 | 
			
		||||
			if (store.state['deck.profile'] === 'default') {
 | 
			
		||||
			if (store.s['deck.profile'] === 'default') {
 | 
			
		||||
				saveDeck();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			store.set('deck.columns', []);
 | 
			
		||||
			store.set('deck.layout', []);
 | 
			
		||||
			store.commit('deck.columns', []);
 | 
			
		||||
			store.commit('deck.layout', []);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		throw err;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store.set('deck.columns', deck.columns);
 | 
			
		||||
	store.set('deck.layout', deck.layout);
 | 
			
		||||
	store.commit('deck.columns', deck.columns);
 | 
			
		||||
	store.commit('deck.layout', deck.layout);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function forceSaveDeck() {
 | 
			
		||||
	await misskeyApi('i/registry/set', {
 | 
			
		||||
		scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
		key: store.state['deck.profile'],
 | 
			
		||||
		key: store.s['deck.profile'],
 | 
			
		||||
		value: {
 | 
			
		||||
			columns: store.reactiveState['deck.columns'].value,
 | 
			
		||||
			layout: store.reactiveState['deck.layout'].value,
 | 
			
		||||
			columns: store.r['deck.columns'].value,
 | 
			
		||||
			layout: store.r['deck.layout'].value,
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -111,40 +111,40 @@ export async function deleteProfile(key: string): Promise<void> {
 | 
			
		||||
 | 
			
		||||
export function addColumn(column: Column) {
 | 
			
		||||
	if (column.name === undefined) column.name = null;
 | 
			
		||||
	store.push('deck.columns', column);
 | 
			
		||||
	store.push('deck.layout', [column.id]);
 | 
			
		||||
	store.commit('deck.columns', [...store.s['deck.columns'], column]);
 | 
			
		||||
	store.commit('deck.layout', [...store.s['deck.layout'], [column.id]]);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeColumn(id: Column['id']) {
 | 
			
		||||
	store.set('deck.columns', store.state['deck.columns'].filter(c => c.id !== id));
 | 
			
		||||
	store.set('deck.layout', store.state['deck.layout']
 | 
			
		||||
	store.commit('deck.columns', store.s['deck.columns'].filter(c => c.id !== id));
 | 
			
		||||
	store.commit('deck.layout', store.s['deck.layout']
 | 
			
		||||
		.map(ids => ids.filter(_id => _id !== id))
 | 
			
		||||
		.filter(ids => ids.length > 0));
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapColumn(a: Column['id'], b: Column['id']) {
 | 
			
		||||
	const aX = store.state['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
 | 
			
		||||
	const aY = store.state['deck.layout'][aX].findIndex(id => id === a);
 | 
			
		||||
	const bX = store.state['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
 | 
			
		||||
	const bY = store.state['deck.layout'][bX].findIndex(id => id === b);
 | 
			
		||||
	const layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	const aX = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
 | 
			
		||||
	const aY = store.s['deck.layout'][aX].findIndex(id => id === a);
 | 
			
		||||
	const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
 | 
			
		||||
	const bY = store.s['deck.layout'][bX].findIndex(id => id === b);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	layout[aX][aY] = b;
 | 
			
		||||
	layout[bX][bY] = a;
 | 
			
		||||
	store.set('deck.layout', layout);
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapLeftColumn(id: Column['id']) {
 | 
			
		||||
	const layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	store.state['deck.layout'].some((ids, i) => {
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	store.s['deck.layout'].some((ids, i) => {
 | 
			
		||||
		if (ids.includes(id)) {
 | 
			
		||||
			const left = store.state['deck.layout'][i - 1];
 | 
			
		||||
			const left = store.s['deck.layout'][i - 1];
 | 
			
		||||
			if (left) {
 | 
			
		||||
				layout[i - 1] = store.state['deck.layout'][i];
 | 
			
		||||
				layout[i - 1] = store.s['deck.layout'][i];
 | 
			
		||||
				layout[i] = left;
 | 
			
		||||
				store.set('deck.layout', layout);
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
@@ -154,14 +154,14 @@ export function swapLeftColumn(id: Column['id']) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapRightColumn(id: Column['id']) {
 | 
			
		||||
	const layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	store.state['deck.layout'].some((ids, i) => {
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	store.s['deck.layout'].some((ids, i) => {
 | 
			
		||||
		if (ids.includes(id)) {
 | 
			
		||||
			const right = store.state['deck.layout'][i + 1];
 | 
			
		||||
			const right = store.s['deck.layout'][i + 1];
 | 
			
		||||
			if (right) {
 | 
			
		||||
				layout[i + 1] = store.state['deck.layout'][i];
 | 
			
		||||
				layout[i + 1] = store.s['deck.layout'][i];
 | 
			
		||||
				layout[i] = right;
 | 
			
		||||
				store.set('deck.layout', layout);
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
@@ -171,9 +171,9 @@ export function swapRightColumn(id: Column['id']) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapUpColumn(id: Column['id']) {
 | 
			
		||||
	const layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.state['deck.layout'][idsIndex]);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.s['deck.layout'][idsIndex]);
 | 
			
		||||
	ids.some((x, i) => {
 | 
			
		||||
		if (x === id) {
 | 
			
		||||
			const up = ids[i - 1];
 | 
			
		||||
@@ -182,7 +182,7 @@ export function swapUpColumn(id: Column['id']) {
 | 
			
		||||
				ids[i] = up;
 | 
			
		||||
 | 
			
		||||
				layout[idsIndex] = ids;
 | 
			
		||||
				store.set('deck.layout', layout);
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
@@ -192,9 +192,9 @@ export function swapUpColumn(id: Column['id']) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapDownColumn(id: Column['id']) {
 | 
			
		||||
	const layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.state['deck.layout'][idsIndex]);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.s['deck.layout'][idsIndex]);
 | 
			
		||||
	ids.some((x, i) => {
 | 
			
		||||
		if (x === id) {
 | 
			
		||||
			const down = ids[i + 1];
 | 
			
		||||
@@ -203,7 +203,7 @@ export function swapDownColumn(id: Column['id']) {
 | 
			
		||||
				ids[i] = down;
 | 
			
		||||
 | 
			
		||||
				layout[idsIndex] = ids;
 | 
			
		||||
				store.set('deck.layout', layout);
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
@@ -213,74 +213,74 @@ export function swapDownColumn(id: Column['id']) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stackLeftColumn(id: Column['id']) {
 | 
			
		||||
	let layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	let layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	layout = layout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	layout[i - 1].push(id);
 | 
			
		||||
	layout = layout.filter(ids => ids.length > 0);
 | 
			
		||||
	store.set('deck.layout', layout);
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function popRightColumn(id: Column['id']) {
 | 
			
		||||
	let layout = deepClone(store.state['deck.layout']);
 | 
			
		||||
	const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	let layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const affected = layout[i];
 | 
			
		||||
	layout = layout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	layout.splice(i + 1, 0, [id]);
 | 
			
		||||
	layout = layout.filter(ids => ids.length > 0);
 | 
			
		||||
	store.set('deck.layout', layout);
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	for (const column of columns) {
 | 
			
		||||
		if (affected.includes(column.id)) {
 | 
			
		||||
			column.active = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.state['deck.columns'][columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets.unshift(widget);
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.state['deck.columns'][columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets = column.widgets.filter(w => w.id !== widget.id);
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.state['deck.columns'][columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	column.widgets = widgets;
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.state['deck.columns'][columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets = column.widgets.map(w => w.id === widgetId ? {
 | 
			
		||||
@@ -288,19 +288,19 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
 | 
			
		||||
		data: widgetData,
 | 
			
		||||
	} : w);
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
 | 
			
		||||
	const columns = deepClone(store.state['deck.columns']);
 | 
			
		||||
	const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const currentColumn = deepClone(store.state['deck.columns'][columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const currentColumn = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (currentColumn == null) return;
 | 
			
		||||
	for (const [k, v] of Object.entries(column)) {
 | 
			
		||||
		currentColumn[k] = v;
 | 
			
		||||
	}
 | 
			
		||||
	columns[columnIndex] = currentColumn;
 | 
			
		||||
	store.set('deck.columns', columns);
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
 | 
			
		||||
const containerEl = shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
function iconLoaded() {
 | 
			
		||||
	const emojis = store.state.reactions;
 | 
			
		||||
	const emojis = store.s.reactions;
 | 
			
		||||
	const containerWidth = containerEl.value.offsetWidth;
 | 
			
		||||
	for (let i = 0; i < 32; i++) {
 | 
			
		||||
		easterEggEmojis.value.push({
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<MkInfo v-if="!store.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
 | 
			
		||||
			<MkInfo v-if="!store.r.abusesTutorial.value" closable @close="closeTutorial()">
 | 
			
		||||
				{{ i18n.ts._abuseUserReport.resolveTutorial }}
 | 
			
		||||
			</MkInfo>
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +93,7 @@ function resolved(reportId) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeTutorial() {
 | 
			
		||||
	store.set('abusesTutorial', false);
 | 
			
		||||
	store.commit('abusesTutorial', false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headerActions = computed(() => []);
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const colorRead = '#3498db';
 | 
			
		||||
	const colorWrite = '#2ecc71';
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ onMounted(async () => {
 | 
			
		||||
 | 
			
		||||
	const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const succColor = '#87e000';
 | 
			
		||||
	const failColor = '#ff4400';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ const color =
 | 
			
		||||
	'?' as never;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	chartInstance = new Chart(chartEl.value, {
 | 
			
		||||
		type: 'line',
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ const color =
 | 
			
		||||
	'?' as never;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	chartInstance = new Chart(chartEl.value, {
 | 
			
		||||
		type: 'line',
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
 | 
			
		||||
				<img v-if="store.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 | 
			
		||||
				<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 | 
			
		||||
				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
 | 
			
		||||
				<canvas ref="canvasEl" :class="$style.canvas"/>
 | 
			
		||||
				<Transition
 | 
			
		||||
@@ -858,7 +858,7 @@ function updateSettings<
 | 
			
		||||
>(key: K, value: V) {
 | 
			
		||||
	const changes: { [P in K]?: V } = {};
 | 
			
		||||
	changes[key] = value;
 | 
			
		||||
	prefer.set('game.dropAndFusion', {
 | 
			
		||||
	prefer.commit('game.dropAndFusion', {
 | 
			
		||||
		...prefer.s['game.dropAndFusion'],
 | 
			
		||||
		...changes,
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -144,7 +144,7 @@ if (prefer.s.uploadFolder) {
 | 
			
		||||
 | 
			
		||||
function chooseUploadFolder() {
 | 
			
		||||
	os.selectDriveFolder(false).then(async folder => {
 | 
			
		||||
		prefer.set('uploadFolder', folder[0] ? folder[0].id : null);
 | 
			
		||||
		prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
 | 
			
		||||
		os.success();
 | 
			
		||||
		if (prefer.s.uploadFolder) {
 | 
			
		||||
			uploadFolder.value = await misskeyApi('drive/folders/show', {
 | 
			
		||||
 
 | 
			
		||||
@@ -154,8 +154,8 @@ import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 | 
			
		||||
 | 
			
		||||
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.state.reactions));
 | 
			
		||||
const pinnedEmojis: Ref<string[]> = ref(deepClone(store.state.pinnedEmojis));
 | 
			
		||||
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.s.reactions));
 | 
			
		||||
const pinnedEmojis: Ref<string[]> = ref(deepClone(store.s.pinnedEmojis));
 | 
			
		||||
 | 
			
		||||
const emojiPickerScale = prefer.model('emojiPickerScale');
 | 
			
		||||
const emojiPickerWidth = prefer.model('emojiPickerWidth');
 | 
			
		||||
@@ -240,13 +240,13 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(pinnedEmojisForReaction, () => {
 | 
			
		||||
	store.set('reactions', pinnedEmojisForReaction.value);
 | 
			
		||||
	store.commit('reactions', pinnedEmojisForReaction.value);
 | 
			
		||||
}, {
 | 
			
		||||
	deep: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(pinnedEmojis, () => {
 | 
			
		||||
	store.set('pinnedEmojis', pinnedEmojis.value);
 | 
			
		||||
	store.commit('pinnedEmojis', pinnedEmojis.value);
 | 
			
		||||
}, {
 | 
			
		||||
	deep: true,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@ import { store } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
const excludeMutingUsers = ref(false);
 | 
			
		||||
const excludeInactiveUsers = ref(false);
 | 
			
		||||
const withReplies = ref(store.state.defaultWithReplies);
 | 
			
		||||
const withReplies = ref(store.s.defaultWithReplies);
 | 
			
		||||
 | 
			
		||||
const onExportSuccess = () => {
 | 
			
		||||
	os.alert({
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				<div v-if="!narrow || currentPage?.route.name == null" class="nav">
 | 
			
		||||
					<div class="baaadecd">
 | 
			
		||||
						<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
 | 
			
		||||
						<MkInfo v-if="!store.reactiveState.enablePreferencesAutoCloudBackup.value && store.reactiveState.showPreferencesAutoCloudBackupSuggestion.value" class="info">
 | 
			
		||||
						<MkInfo v-if="!store.r.enablePreferencesAutoCloudBackup.value && store.r.showPreferencesAutoCloudBackupSuggestion.value" class="info">
 | 
			
		||||
							<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
 | 
			
		||||
							<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
 | 
			
		||||
						</MkInfo>
 | 
			
		||||
@@ -72,7 +72,7 @@ const ro = new ResizeObserver((entries, observer) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function skipAutoBackup() {
 | 
			
		||||
	store.set('showPreferencesAutoCloudBackupSuggestion', false);
 | 
			
		||||
	store.commit('showPreferencesAutoCloudBackupSuggestion', false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const menuDef = computed<SuperMenuDef[]>(() => [{
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ const items = ref(prefer.s.menu.map(x => ({
 | 
			
		||||
	type: x,
 | 
			
		||||
})));
 | 
			
		||||
 | 
			
		||||
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 | 
			
		||||
const menuDisplay = store.model('menuDisplay');
 | 
			
		||||
 | 
			
		||||
async function addItem() {
 | 
			
		||||
	const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
 | 
			
		||||
@@ -91,7 +91,7 @@ function removeItem(index: number) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function save() {
 | 
			
		||||
	prefer.set('menu', items.value.map(x => x.type));
 | 
			
		||||
	prefer.commit('menu', items.value.map(x => x.type));
 | 
			
		||||
	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -131,7 +131,7 @@ const reportError = prefer.model('reportError');
 | 
			
		||||
const enableCondensedLine = prefer.model('enableCondensedLine');
 | 
			
		||||
const skipNoteRender = prefer.model('skipNoteRender');
 | 
			
		||||
const devMode = prefer.model('devMode');
 | 
			
		||||
const defaultWithReplies = computed(store.makeGetterSetter('defaultWithReplies'));
 | 
			
		||||
const defaultWithReplies = store.model('defaultWithReplies');
 | 
			
		||||
 | 
			
		||||
watch(skipNoteRender, async () => {
 | 
			
		||||
	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 | 
			
		||||
 
 | 
			
		||||
@@ -337,8 +337,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
							<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
 | 
			
		||||
							<div class="_buttons">
 | 
			
		||||
								<template v-for="lang in emojiIndexLangs" :key="lang">
 | 
			
		||||
									<MkButton v-if="store.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
 | 
			
		||||
									<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
 | 
			
		||||
									<MkButton v-if="store.r.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
 | 
			
		||||
									<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.r.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
 | 
			
		||||
								</template>
 | 
			
		||||
							</div>
 | 
			
		||||
						</MkFolder>
 | 
			
		||||
@@ -449,7 +449,7 @@ function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
 | 
			
		||||
 | 
			
		||||
function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 | 
			
		||||
	async function main() {
 | 
			
		||||
		const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
 | 
			
		||||
		const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
 | 
			
		||||
 | 
			
		||||
		function download() {
 | 
			
		||||
			switch (lang) {
 | 
			
		||||
@@ -461,7 +461,7 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		currentIndexes[lang] = await download();
 | 
			
		||||
		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 | 
			
		||||
		await store.commit('additionalUnicodeEmojiIndexes', currentIndexes);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	os.promiseDialog(main());
 | 
			
		||||
@@ -469,9 +469,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 | 
			
		||||
 | 
			
		||||
function removeEmojiIndex(lang: string) {
 | 
			
		||||
	async function main() {
 | 
			
		||||
		const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
 | 
			
		||||
		const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
 | 
			
		||||
		delete currentIndexes[lang];
 | 
			
		||||
		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 | 
			
		||||
		await store.commit('additionalUnicodeEmojiIndexes', currentIndexes);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	os.promiseDialog(main());
 | 
			
		||||
@@ -488,11 +488,11 @@ async function setPinnedList() {
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	if (list == null) return;
 | 
			
		||||
 | 
			
		||||
	prefer.set('pinnedUserLists', [list]);
 | 
			
		||||
	prefer.commit('pinnedUserLists', [list]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removePinnedList() {
 | 
			
		||||
	prefer.set('pinnedUserLists', []);
 | 
			
		||||
	prefer.commit('pinnedUserLists', []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function enableAllDataSaver() {
 | 
			
		||||
@@ -512,7 +512,7 @@ function disableAllDataSaver() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(dataSaver, (to) => {
 | 
			
		||||
	prefer.set('dataSaver', to);
 | 
			
		||||
	prefer.commit('dataSaver', to);
 | 
			
		||||
}, {
 | 
			
		||||
	deep: true,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -177,7 +177,7 @@ const $i = signinRequired();
 | 
			
		||||
 | 
			
		||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 | 
			
		||||
 | 
			
		||||
const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
 | 
			
		||||
const reactionAcceptance = store.model('reactionAcceptance');
 | 
			
		||||
 | 
			
		||||
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
 | 
			
		||||
	return lang != null && lang in langmap;
 | 
			
		||||
 
 | 
			
		||||
@@ -101,14 +101,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
 | 
			
		||||
		volume: sound.volume,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	prefer.set(`sound.on.${type}`, v);
 | 
			
		||||
	prefer.commit(`sound.on.${type}`, v);
 | 
			
		||||
	sounds.value[type] = v;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reset() {
 | 
			
		||||
	for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
 | 
			
		||||
		const v = PREF_DEF[`sound.on.${sound}`].default;
 | 
			
		||||
		prefer.set(`sound.on.${sound}`, v);
 | 
			
		||||
		prefer.commit(`sound.on.${sound}`, v);
 | 
			
		||||
		sounds.value[sound] = v;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -137,10 +137,10 @@ async function save() {
 | 
			
		||||
	const i = prefer.s.statusbars.findIndex(x => x.id === props._id);
 | 
			
		||||
	const statusbars = deepClone(prefer.s.statusbars);
 | 
			
		||||
	statusbars[i] = deepClone(statusbar);
 | 
			
		||||
	prefer.set('statusbars', statusbars);
 | 
			
		||||
	prefer.commit('statusbars', statusbars);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function del() {
 | 
			
		||||
	prefer.set('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
 | 
			
		||||
	prefer.commit('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ onMounted(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function add() {
 | 
			
		||||
	prefer.set('statusbars', [...statusbars.value, {
 | 
			
		||||
	prefer.commit('statusbars', [...statusbars.value, {
 | 
			
		||||
		id: uuid(),
 | 
			
		||||
		type: null,
 | 
			
		||||
		black: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@ const darkThemeId = computed({
 | 
			
		||||
	set(id) {
 | 
			
		||||
		const t = themes.value.find(x => x.id === id);
 | 
			
		||||
		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
 | 
			
		||||
			prefer.set('darkTheme', t);
 | 
			
		||||
			prefer.commit('darkTheme', t);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
@@ -191,19 +191,19 @@ const lightThemeId = computed({
 | 
			
		||||
	set(id) {
 | 
			
		||||
		const t = themes.value.find(x => x.id === id);
 | 
			
		||||
		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
 | 
			
		||||
			prefer.set('lightTheme', t);
 | 
			
		||||
			prefer.commit('lightTheme', t);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const darkMode = computed(store.makeGetterSetter('darkMode'));
 | 
			
		||||
const darkMode = store.model('darkMode');
 | 
			
		||||
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
 | 
			
		||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
 | 
			
		||||
const themesCount = installedThemes.value.length;
 | 
			
		||||
 | 
			
		||||
watch(syncDeviceDarkMode, () => {
 | 
			
		||||
	if (syncDeviceDarkMode.value) {
 | 
			
		||||
		store.set('darkMode', isDeviceDarkmode());
 | 
			
		||||
		store.commit('darkMode', isDeviceDarkmode());
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,11 +44,11 @@ const pagination = {
 | 
			
		||||
const notes = ref<InstanceType<typeof MkNotes>>();
 | 
			
		||||
 | 
			
		||||
async function post() {
 | 
			
		||||
	store.set('postFormHashtags', props.tag);
 | 
			
		||||
	store.set('postFormWithHashtags', true);
 | 
			
		||||
	store.commit('postFormHashtags', props.tag);
 | 
			
		||||
	store.commit('postFormWithHashtags', true);
 | 
			
		||||
	await os.post();
 | 
			
		||||
	store.set('postFormHashtags', '');
 | 
			
		||||
	store.set('postFormWithHashtags', false);
 | 
			
		||||
	store.commit('postFormHashtags', '');
 | 
			
		||||
	store.commit('postFormWithHashtags', false);
 | 
			
		||||
	notes.value?.pagingComponent?.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -200,10 +200,10 @@ async function saveAs() {
 | 
			
		||||
	if (description.value) theme.value.desc = description.value;
 | 
			
		||||
	await addTheme(theme.value);
 | 
			
		||||
	applyTheme(theme.value);
 | 
			
		||||
	if (store.state.darkMode) {
 | 
			
		||||
		prefer.set('darkTheme', theme.value);
 | 
			
		||||
	if (store.s.darkMode) {
 | 
			
		||||
		prefer.commit('darkTheme', theme.value);
 | 
			
		||||
	} else {
 | 
			
		||||
		prefer.set('lightTheme', theme.value);
 | 
			
		||||
		prefer.commit('lightTheme', theme.value);
 | 
			
		||||
	}
 | 
			
		||||
	changed.value = false;
 | 
			
		||||
	os.alert({
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	<MkSpacer :contentMax="800">
 | 
			
		||||
		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
 | 
			
		||||
			<div :key="src" ref="rootEl">
 | 
			
		||||
				<MkInfo v-if="isBasicTimeline(src) && !store.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 | 
			
		||||
				<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 | 
			
		||||
					{{ i18n.ts._timelineDescription[src] }}
 | 
			
		||||
				</MkInfo>
 | 
			
		||||
				<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
 | 
			
		||||
@@ -67,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`;
 | 
			
		||||
const queue = ref(0);
 | 
			
		||||
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
 | 
			
		||||
const src = computed<TimelinePageSrc>({
 | 
			
		||||
	get: () => ($i ? store.reactiveState.tl.value.src : srcWhenNotSignin.value),
 | 
			
		||||
	get: () => ($i ? store.r.tl.value.src : srcWhenNotSignin.value),
 | 
			
		||||
	set: (x) => saveSrc(x),
 | 
			
		||||
});
 | 
			
		||||
const withRenotes = computed<boolean>({
 | 
			
		||||
	get: () => store.reactiveState.tl.value.filter.withRenotes,
 | 
			
		||||
	get: () => store.r.tl.value.filter.withRenotes,
 | 
			
		||||
	set: (x) => saveTlFilter('withRenotes', x),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// computed内での無限ループを防ぐためのフラグ
 | 
			
		||||
const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
 | 
			
		||||
	store.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
 | 
			
		||||
	store.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
 | 
			
		||||
	store.r.tl.value.filter.withReplies ? 'withReplies' :
 | 
			
		||||
	store.r.tl.value.filter.onlyFiles ? 'onlyFiles' :
 | 
			
		||||
	false,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +88,7 @@ const withReplies = computed<boolean>({
 | 
			
		||||
		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
 | 
			
		||||
			return false;
 | 
			
		||||
		} else {
 | 
			
		||||
			return store.reactiveState.tl.value.filter.withReplies;
 | 
			
		||||
			return store.r.tl.value.filter.withReplies;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	set: (x) => saveTlFilter('withReplies', x),
 | 
			
		||||
@@ -98,7 +98,7 @@ const onlyFiles = computed<boolean>({
 | 
			
		||||
		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
 | 
			
		||||
			return false;
 | 
			
		||||
		} else {
 | 
			
		||||
			return store.reactiveState.tl.value.filter.onlyFiles;
 | 
			
		||||
			return store.r.tl.value.filter.onlyFiles;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	set: (x) => saveTlFilter('onlyFiles', x),
 | 
			
		||||
@@ -115,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const withSensitive = computed<boolean>({
 | 
			
		||||
	get: () => store.reactiveState.tl.value.filter.withSensitive,
 | 
			
		||||
	get: () => store.r.tl.value.filter.withSensitive,
 | 
			
		||||
	set: (x) => saveTlFilter('withSensitive', x),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -196,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveSrc(newSrc: TimelinePageSrc): void {
 | 
			
		||||
	const out = deepMerge({ src: newSrc }, store.state.tl);
 | 
			
		||||
	const out = deepMerge({ src: newSrc }, store.s.tl);
 | 
			
		||||
 | 
			
		||||
	if (newSrc.startsWith('userList:')) {
 | 
			
		||||
		const id = newSrc.substring('userList:'.length);
 | 
			
		||||
		out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store.set('tl', out);
 | 
			
		||||
	store.commit('tl', out);
 | 
			
		||||
	if (['local', 'global'].includes(newSrc)) {
 | 
			
		||||
		srcWhenNotSignin.value = newSrc as 'local' | 'global';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveTlFilter(key: keyof typeof store.state.tl.filter, newValue: boolean) {
 | 
			
		||||
function saveTlFilter(key: keyof typeof store.s.tl.filter, newValue: boolean) {
 | 
			
		||||
	if (key !== 'withReplies' || $i) {
 | 
			
		||||
		const out = deepMerge({ filter: { [key]: newValue } }, store.state.tl);
 | 
			
		||||
		store.set('tl', out);
 | 
			
		||||
		const out = deepMerge({ filter: { [key]: newValue } }, store.s.tl);
 | 
			
		||||
		store.commit('tl', out);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -231,9 +231,9 @@ function focus(): void {
 | 
			
		||||
 | 
			
		||||
function closeTutorial(): void {
 | 
			
		||||
	if (!isBasicTimeline(src.value)) return;
 | 
			
		||||
	const before = store.state.timelineTutorials;
 | 
			
		||||
	const before = store.s.timelineTutorials;
 | 
			
		||||
	before[src.value] = true;
 | 
			
		||||
	store.set('timelineTutorials', before);
 | 
			
		||||
	store.commit('timelineTutorials', before);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function switchTlIfNeeded() {
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const colorFollowLocal = '#008FFB';
 | 
			
		||||
	const colorFollowRemote = '#008FFB88';
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const colorNormal = '#008FFB';
 | 
			
		||||
	const colorReply = '#FEB019';
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const colorUser = '#3498db';
 | 
			
		||||
	const colorVisitor = '#2ecc71';
 | 
			
		||||
 
 | 
			
		||||
@@ -3,252 +3,70 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// PIZZAX --- A lightweight store
 | 
			
		||||
import { computed, onUnmounted, ref, watch } from 'vue';
 | 
			
		||||
import { EventEmitter } from 'eventemitter3';
 | 
			
		||||
import type { Ref, WritableComputedRef } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { onUnmounted, ref, watch } from 'vue';
 | 
			
		||||
import { BroadcastChannel } from 'broadcast-channel';
 | 
			
		||||
import type { Ref } from 'vue';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { get, set } from '@/utility/idb-proxy.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { useStream } from '@/stream.js';
 | 
			
		||||
import { deepClone } from '@/utility/clone.js';
 | 
			
		||||
import { deepMerge } from '@/utility/merge.js';
 | 
			
		||||
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
 | 
			
		||||
 | 
			
		||||
type StateDef = Record<string, {
 | 
			
		||||
	where: 'account' | 'device' | 'deviceAccount';
 | 
			
		||||
	default: any;
 | 
			
		||||
}>;
 | 
			
		||||
//type DottedToNested<T extends Record<string, any>> = {
 | 
			
		||||
//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
 | 
			
		||||
//};
 | 
			
		||||
 | 
			
		||||
type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; };
 | 
			
		||||
type ReactiveState<T extends StateDef> = { [K in keyof T]: Ref<T[K]['default']>; };
 | 
			
		||||
 | 
			
		||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
 | 
			
		||||
 | 
			
		||||
type PizzaxChannelMessage<T extends StateDef> = {
 | 
			
		||||
	where: 'device' | 'deviceAccount';
 | 
			
		||||
	key: keyof T;
 | 
			
		||||
	value: T[keyof T]['default'];
 | 
			
		||||
	userId?: string;
 | 
			
		||||
type PizzaxEvent<Data extends Record<string, any>> = {
 | 
			
		||||
	updated: <K extends keyof Data>(ctx: {
 | 
			
		||||
		key: K;
 | 
			
		||||
		value: Data[K];
 | 
			
		||||
	}) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Storage<T extends StateDef> {
 | 
			
		||||
	public readonly ready: Promise<void>;
 | 
			
		||||
	public readonly loaded: Promise<void>;
 | 
			
		||||
export class Pizzax<Data extends Record<string, any>> extends EventEmitter<PizzaxEvent<Data>> {
 | 
			
		||||
	/**
 | 
			
		||||
	 * static / state の略 (static が予約語のため)
 | 
			
		||||
	 */
 | 
			
		||||
	public s = {} as {
 | 
			
		||||
		[K in keyof Data]: Data[K];
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public readonly key: string;
 | 
			
		||||
	public readonly deviceStateKeyName: `pizzax::${this['key']}`;
 | 
			
		||||
	public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | '';
 | 
			
		||||
	public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | '';
 | 
			
		||||
	/**
 | 
			
		||||
	 * reactive の略
 | 
			
		||||
	 */
 | 
			
		||||
	public r = {} as {
 | 
			
		||||
		[K in keyof Data]: Ref<Data[K]>;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public readonly def: T;
 | 
			
		||||
	constructor(data: { [K in keyof Data]: Data[K] }) {
 | 
			
		||||
		super();
 | 
			
		||||
 | 
			
		||||
	// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
 | 
			
		||||
	public readonly state: State<T>;
 | 
			
		||||
	public readonly reactiveState: ReactiveState<T>;
 | 
			
		||||
 | 
			
		||||
	private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
 | 
			
		||||
 | 
			
		||||
	// 簡易的にキューイングして占有ロックとする
 | 
			
		||||
	private currentIdbJob: Promise<any> = Promise.resolve();
 | 
			
		||||
	private addIdbSetJob<T>(job: () => Promise<T>) {
 | 
			
		||||
		const promise = this.currentIdbJob.then(job, err => {
 | 
			
		||||
			console.error('Pizzax failed to save data to idb!', err);
 | 
			
		||||
			return job();
 | 
			
		||||
		});
 | 
			
		||||
		this.currentIdbJob = promise;
 | 
			
		||||
		return promise;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	constructor(key: string, def: T) {
 | 
			
		||||
		this.key = key;
 | 
			
		||||
		this.deviceStateKeyName = `pizzax::${key}`;
 | 
			
		||||
		this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
 | 
			
		||||
		this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : '';
 | 
			
		||||
		this.def = def;
 | 
			
		||||
 | 
			
		||||
		this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
 | 
			
		||||
 | 
			
		||||
		this.state = {} as State<T>;
 | 
			
		||||
		this.reactiveState = {} as ReactiveState<T>;
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
			this.state[k] = v.default;
 | 
			
		||||
			this.reactiveState[k] = ref(v.default);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.ready = this.init();
 | 
			
		||||
		this.loaded = this.ready.then(() => this.load());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
 | 
			
		||||
		return typeof value === 'object' && value !== null && !Array.isArray(value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private mergeState<X>(value: X, def: X): X {
 | 
			
		||||
		if (this.isPureObject(value) && this.isPureObject(def)) {
 | 
			
		||||
			const merged = deepMerge(value, def);
 | 
			
		||||
 | 
			
		||||
			if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
 | 
			
		||||
 | 
			
		||||
			return merged as X;
 | 
			
		||||
		}
 | 
			
		||||
		return value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async init(): Promise<void> {
 | 
			
		||||
		await this.migrate();
 | 
			
		||||
 | 
			
		||||
		const deviceState: State<T> = await get(this.deviceStateKeyName) || {};
 | 
			
		||||
		const deviceAccountState = $i ? await get(this.deviceAccountStateKeyName) || {} : {};
 | 
			
		||||
		const registryCache = $i ? await get(this.registryCacheKeyName) || {} : {};
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
 | 
			
		||||
				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
 | 
			
		||||
			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
 | 
			
		||||
				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
 | 
			
		||||
			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
 | 
			
		||||
				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
 | 
			
		||||
			} else {
 | 
			
		||||
				this.reactiveState[k].value = this.state[k] = v.default;
 | 
			
		||||
		for (const key in data) {
 | 
			
		||||
			this.s[key] = data[key];
 | 
			
		||||
			this.r[key] = ref(this.s[key]);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => {
 | 
			
		||||
			// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
 | 
			
		||||
			// まずないと思うけど一応弾いておく
 | 
			
		||||
			if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
 | 
			
		||||
			this.reactiveState[key].value = this.state[key] = value;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if ($i) {
 | 
			
		||||
			const connection = useStream().useChannel('main');
 | 
			
		||||
 | 
			
		||||
			// streamingのuser storage updateイベントを監視して更新
 | 
			
		||||
			connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
 | 
			
		||||
				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
 | 
			
		||||
 | 
			
		||||
				this.reactiveState[key].value = this.state[key] = value;
 | 
			
		||||
 | 
			
		||||
				this.addIdbSetJob(async () => {
 | 
			
		||||
					const cache = await get(this.registryCacheKeyName);
 | 
			
		||||
					if (cache[key] !== value) {
 | 
			
		||||
						cache[key] = value;
 | 
			
		||||
						await set(this.registryCacheKeyName, cache);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	public commit<K extends keyof Data>(key: K, value: Data[K]) {
 | 
			
		||||
		const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
 | 
			
		||||
		this.r[key].value = this.s[key] = v;
 | 
			
		||||
		this.emit('updated', { key, value: v });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private load(): Promise<void> {
 | 
			
		||||
		return new Promise((resolve, reject) => {
 | 
			
		||||
			if ($i) {
 | 
			
		||||
				// api関数と循環参照なので一応setTimeoutしておく
 | 
			
		||||
				window.setTimeout(async () => {
 | 
			
		||||
					await store.ready;
 | 
			
		||||
 | 
			
		||||
					misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
 | 
			
		||||
						.then(kvs => {
 | 
			
		||||
							const cache: Partial<T> = {};
 | 
			
		||||
							for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
								if (v.where === 'account') {
 | 
			
		||||
									if (Object.prototype.hasOwnProperty.call(kvs, k)) {
 | 
			
		||||
										this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
 | 
			
		||||
										cache[k] = (kvs as Partial<T>)[k];
 | 
			
		||||
									} else {
 | 
			
		||||
										this.reactiveState[k].value = this.state[k] = v.default;
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							return set(this.registryCacheKeyName, cache);
 | 
			
		||||
						})
 | 
			
		||||
						.then(() => resolve());
 | 
			
		||||
				}, 1);
 | 
			
		||||
			} else {
 | 
			
		||||
				resolve();
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public set<K extends keyof T>(key: K, value: T[K]['default']): Promise<void> {
 | 
			
		||||
		// IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする
 | 
			
		||||
		// (JSON.parse(JSON.stringify(value))の代わり)
 | 
			
		||||
		const rawValue = deepClone(value);
 | 
			
		||||
 | 
			
		||||
		this.reactiveState[key].value = this.state[key] = rawValue;
 | 
			
		||||
 | 
			
		||||
		return this.addIdbSetJob(async () => {
 | 
			
		||||
			switch (this.def[key].where) {
 | 
			
		||||
				case 'device': {
 | 
			
		||||
					this.pizzaxChannel.postMessage({
 | 
			
		||||
						where: 'device',
 | 
			
		||||
						key,
 | 
			
		||||
						value: rawValue,
 | 
			
		||||
					});
 | 
			
		||||
					const deviceState = await get(this.deviceStateKeyName) || {};
 | 
			
		||||
					deviceState[key] = rawValue;
 | 
			
		||||
					await set(this.deviceStateKeyName, deviceState);
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				case 'deviceAccount': {
 | 
			
		||||
					if ($i == null) break;
 | 
			
		||||
					this.pizzaxChannel.postMessage({
 | 
			
		||||
						where: 'deviceAccount',
 | 
			
		||||
						key,
 | 
			
		||||
						value: rawValue,
 | 
			
		||||
						userId: $i.id,
 | 
			
		||||
					});
 | 
			
		||||
					const deviceAccountState = await get(this.deviceAccountStateKeyName) || {};
 | 
			
		||||
					deviceAccountState[key] = rawValue;
 | 
			
		||||
					await set(this.deviceAccountStateKeyName, deviceAccountState);
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
				case 'account': {
 | 
			
		||||
					if ($i == null) break;
 | 
			
		||||
					const cache = await get(this.registryCacheKeyName) || {};
 | 
			
		||||
					cache[key] = rawValue;
 | 
			
		||||
					await set(this.registryCacheKeyName, cache);
 | 
			
		||||
					await misskeyApi('i/registry/set', {
 | 
			
		||||
						scope: ['client', this.key],
 | 
			
		||||
						key: key.toString(),
 | 
			
		||||
						value: rawValue,
 | 
			
		||||
					});
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
 | 
			
		||||
		const currentState = this.state[key];
 | 
			
		||||
		this.set(key, [...currentState, value]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public reset(key: keyof T) {
 | 
			
		||||
		this.set(key, this.def[key].default);
 | 
			
		||||
		return this.def[key].default;
 | 
			
		||||
	public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
 | 
			
		||||
		const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
 | 
			
		||||
		this.r[key].value = this.s[key] = v;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 特定のキーの、簡易的なgetter/setterを作ります
 | 
			
		||||
	 * 特定のキーの、簡易的なcomputed refを作ります
 | 
			
		||||
	 * 主にvue上で設定コントロールのmodelとして使う用
 | 
			
		||||
	 */
 | 
			
		||||
	// TODO: 廃止
 | 
			
		||||
	public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
 | 
			
		||||
	public model<K extends keyof Data, V extends Data[K] = Data[K]>(
 | 
			
		||||
		key: K,
 | 
			
		||||
		getter?: (v: T[K]['default']) => R,
 | 
			
		||||
		setter?: (v: R) => T[K]['default'],
 | 
			
		||||
	): {
 | 
			
		||||
			get: () => R;
 | 
			
		||||
			set: (value: R) => void;
 | 
			
		||||
		} {
 | 
			
		||||
		const valueRef = ref(this.state[key]);
 | 
			
		||||
		getter?: (v: Data[K]) => V,
 | 
			
		||||
		setter?: (v: V) => Data[K],
 | 
			
		||||
	): WritableComputedRef<V> {
 | 
			
		||||
		const valueRef = ref(this.s[key]);
 | 
			
		||||
 | 
			
		||||
		const stop = watch(this.reactiveState[key], val => {
 | 
			
		||||
		const stop = watch(this.r[key], val => {
 | 
			
		||||
			valueRef.value = val;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -258,7 +76,7 @@ export class Storage<T extends StateDef> {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// TODO: VueのcustomRef使うと良い感じになるかも
 | 
			
		||||
		return {
 | 
			
		||||
		return computed({
 | 
			
		||||
			get: () => {
 | 
			
		||||
				if (getter) {
 | 
			
		||||
					return getter(valueRef.value);
 | 
			
		||||
@@ -268,30 +86,9 @@ export class Storage<T extends StateDef> {
 | 
			
		||||
			},
 | 
			
		||||
			set: (value) => {
 | 
			
		||||
				const val = setter ? setter(value) : value;
 | 
			
		||||
				this.set(key, val);
 | 
			
		||||
				this.commit(key, val);
 | 
			
		||||
				valueRef.value = val;
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// localStorage => indexedDBのマイグレーション
 | 
			
		||||
	private async migrate() {
 | 
			
		||||
		const deviceState = localStorage.getItem(this.deviceStateKeyName);
 | 
			
		||||
		if (deviceState) {
 | 
			
		||||
			await set(this.deviceStateKeyName, JSON.parse(deviceState));
 | 
			
		||||
			localStorage.removeItem(this.deviceStateKeyName);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const deviceAccountState = $i && localStorage.getItem(this.deviceAccountStateKeyName);
 | 
			
		||||
		if ($i && deviceAccountState) {
 | 
			
		||||
			await set(this.deviceAccountStateKeyName, JSON.parse(deviceAccountState));
 | 
			
		||||
			localStorage.removeItem(this.deviceAccountStateKeyName);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const registryCache = $i && localStorage.getItem(this.registryCacheKeyName);
 | 
			
		||||
		if ($i && registryCache) {
 | 
			
		||||
			await set(this.registryCacheKeyName, JSON.parse(registryCache));
 | 
			
		||||
			localStorage.removeItem(this.registryCacheKeyName);
 | 
			
		||||
		}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
 | 
			
		||||
 | 
			
		||||
export async function authorizePlugin(plugin: Plugin) {
 | 
			
		||||
	if (plugin.permissions == null || plugin.permissions.length === 0) return;
 | 
			
		||||
	if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) return;
 | 
			
		||||
	if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) return;
 | 
			
		||||
 | 
			
		||||
	const token = await new Promise<string>((res, rej) => {
 | 
			
		||||
		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
 | 
			
		||||
@@ -115,8 +115,8 @@ export async function authorizePlugin(plugin: Plugin) {
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	store.set('pluginTokens', {
 | 
			
		||||
		...store.state.pluginTokens,
 | 
			
		||||
	store.commit('pluginTokens', {
 | 
			
		||||
		...store.s.pluginTokens,
 | 
			
		||||
		[plugin.installId]: token,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -145,7 +145,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
 | 
			
		||||
		src: code,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	prefer.set('plugins', prefer.s.plugins.concat(plugin));
 | 
			
		||||
	prefer.commit('plugins', prefer.s.plugins.concat(plugin));
 | 
			
		||||
 | 
			
		||||
	await authorizePlugin(plugin);
 | 
			
		||||
 | 
			
		||||
@@ -154,14 +154,14 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
 | 
			
		||||
 | 
			
		||||
export async function uninstallPlugin(plugin: Plugin) {
 | 
			
		||||
	abortPlugin(plugin);
 | 
			
		||||
	prefer.set('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
 | 
			
		||||
	if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) {
 | 
			
		||||
	prefer.commit('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
 | 
			
		||||
	if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) {
 | 
			
		||||
		await os.apiWithDialog('i/revoke-token', {
 | 
			
		||||
			token: store.state.pluginTokens[plugin.installId],
 | 
			
		||||
			token: store.s.pluginTokens[plugin.installId],
 | 
			
		||||
		});
 | 
			
		||||
		const pluginTokens = { ...store.state.pluginTokens };
 | 
			
		||||
		const pluginTokens = { ...store.s.pluginTokens };
 | 
			
		||||
		delete pluginTokens[plugin.installId];
 | 
			
		||||
		store.set('pluginTokens', pluginTokens);
 | 
			
		||||
		store.commit('pluginTokens', pluginTokens);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -311,13 +311,13 @@ export async function configPlugin(plugin: Plugin) {
 | 
			
		||||
	const { canceled, result } = await os.form(plugin.name, config);
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
	prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
 | 
			
		||||
	prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
 | 
			
		||||
 | 
			
		||||
	reloadPlugin(plugin);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function changePluginActive(plugin: Plugin, active: boolean) {
 | 
			
		||||
	prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
 | 
			
		||||
	prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
 | 
			
		||||
 | 
			
		||||
	if (active) {
 | 
			
		||||
		launchPlugin(plugin.installId);
 | 
			
		||||
@@ -341,7 +341,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const env: Record<string, values.Value> = {
 | 
			
		||||
		...createAiScriptEnv({ ...opts, token: store.state.pluginTokens[id] }),
 | 
			
		||||
		...createAiScriptEnv({ ...opts, token: store.s.pluginTokens[id] }),
 | 
			
		||||
 | 
			
		||||
		'Plugin:register:post_form_action': values.FN_NATIVE(([title, handler]) => {
 | 
			
		||||
			utils.assertString(title);
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ let latestBackupAt = 0;
 | 
			
		||||
 | 
			
		||||
window.setInterval(() => {
 | 
			
		||||
	if ($i == null) return;
 | 
			
		||||
	if (!store.state.enablePreferencesAutoCloudBackup) return;
 | 
			
		||||
	if (!store.s.enablePreferencesAutoCloudBackup) return;
 | 
			
		||||
	if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
 | 
			
		||||
	if (profileManager.profile.modifiedAt <= latestBackupAt) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,11 @@ import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { host, version } from '@@/js/config.js';
 | 
			
		||||
import { EventEmitter } from 'eventemitter3';
 | 
			
		||||
import { PREF_DEF } from './def.js';
 | 
			
		||||
import { Store } from './store.js';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { Pizzax } from '@/pizzax.js';
 | 
			
		||||
 | 
			
		||||
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ export class ProfileManager extends EventEmitter<{
 | 
			
		||||
	}) => void;
 | 
			
		||||
}> {
 | 
			
		||||
	public profile: PreferencesProfile;
 | 
			
		||||
	public store: Store<{
 | 
			
		||||
	public store: Pizzax<{
 | 
			
		||||
		[K in keyof PREF]: ValueOf<K>;
 | 
			
		||||
	}>;
 | 
			
		||||
 | 
			
		||||
@@ -58,7 +58,7 @@ export class ProfileManager extends EventEmitter<{
 | 
			
		||||
 | 
			
		||||
		const states = this.genStates();
 | 
			
		||||
 | 
			
		||||
		this.store = new Store(states);
 | 
			
		||||
		this.store = new Pizzax(states);
 | 
			
		||||
		this.store.addListener('updated', ({ key, value }) => {
 | 
			
		||||
			console.log('prefer:set', key, value);
 | 
			
		||||
 | 
			
		||||
@@ -222,7 +222,7 @@ export class ProfileManager extends EventEmitter<{
 | 
			
		||||
			text: i18n.ts.resetToDefaultValue,
 | 
			
		||||
			danger: true,
 | 
			
		||||
			action: () => {
 | 
			
		||||
				this.store.set(key, PREF_DEF[key].default);
 | 
			
		||||
				this.store.commit(key, PREF_DEF[key].default);
 | 
			
		||||
			},
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'divider',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,92 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { computed, onUnmounted, ref, watch } from 'vue';
 | 
			
		||||
import { EventEmitter } from 'eventemitter3';
 | 
			
		||||
import type { Ref, WritableComputedRef } from 'vue';
 | 
			
		||||
 | 
			
		||||
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
 | 
			
		||||
 | 
			
		||||
//type DottedToNested<T extends Record<string, any>> = {
 | 
			
		||||
//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
 | 
			
		||||
//};
 | 
			
		||||
 | 
			
		||||
type StoreEvent<Data extends Record<string, any>> = {
 | 
			
		||||
	updated: <K extends keyof Data>(ctx: {
 | 
			
		||||
		key: K;
 | 
			
		||||
		value: Data[K];
 | 
			
		||||
	}) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Store<Data extends Record<string, any>> extends EventEmitter<StoreEvent<Data>> {
 | 
			
		||||
	/**
 | 
			
		||||
	 * static の略 (static が予約語のため)
 | 
			
		||||
	 */
 | 
			
		||||
	public s = {} as {
 | 
			
		||||
		[K in keyof Data]: Data[K];
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * reactive の略
 | 
			
		||||
	 */
 | 
			
		||||
	public r = {} as {
 | 
			
		||||
		[K in keyof Data]: Ref<Data[K]>;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	constructor(data: { [K in keyof Data]: Data[K] }) {
 | 
			
		||||
		super();
 | 
			
		||||
 | 
			
		||||
		for (const key in data) {
 | 
			
		||||
			this.s[key] = data[key];
 | 
			
		||||
			this.r[key] = ref(this.s[key]);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public set<K extends keyof Data>(key: K, value: Data[K]) {
 | 
			
		||||
		this.r[key].value = this.s[key] = value;
 | 
			
		||||
		this.emit('updated', { key, value });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
 | 
			
		||||
		this.r[key].value = this.s[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 特定のキーの、簡易的なcomputed refを作ります
 | 
			
		||||
	 * 主にvue上で設定コントロールのmodelとして使う用
 | 
			
		||||
	 */
 | 
			
		||||
	public model<K extends keyof Data, V extends Data[K] = Data[K]>(
 | 
			
		||||
		key: K,
 | 
			
		||||
		getter?: (v: Data[K]) => V,
 | 
			
		||||
		setter?: (v: V) => Data[K],
 | 
			
		||||
	): WritableComputedRef<V> {
 | 
			
		||||
		const valueRef = ref(this.s[key]);
 | 
			
		||||
 | 
			
		||||
		const stop = watch(this.r[key], val => {
 | 
			
		||||
			valueRef.value = val;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
 | 
			
		||||
		onUnmounted(() => {
 | 
			
		||||
			stop();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// TODO: VueのcustomRef使うと良い感じになるかも
 | 
			
		||||
		return computed({
 | 
			
		||||
			get: () => {
 | 
			
		||||
				if (getter) {
 | 
			
		||||
					return getter(valueRef.value);
 | 
			
		||||
				} else {
 | 
			
		||||
					return valueRef.value;
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			set: (value) => {
 | 
			
		||||
				const val = setter ? setter(value) : value;
 | 
			
		||||
				this.set(key, val);
 | 
			
		||||
				valueRef.value = val;
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -21,7 +21,7 @@ function canAutoBackup() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPreferencesProfileMenu(): MenuItem[] {
 | 
			
		||||
	const autoBackupEnabled = ref(store.state.enablePreferencesAutoCloudBackup);
 | 
			
		||||
	const autoBackupEnabled = ref(store.s.enablePreferencesAutoCloudBackup);
 | 
			
		||||
 | 
			
		||||
	watch(autoBackupEnabled, () => {
 | 
			
		||||
		if (autoBackupEnabled.value) {
 | 
			
		||||
@@ -34,9 +34,9 @@ export function getPreferencesProfileMenu(): MenuItem[] {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			store.set('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
			store.commit('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
		} else {
 | 
			
		||||
			store.set('enablePreferencesAutoCloudBackup', false);
 | 
			
		||||
			store.commit('enablePreferencesAutoCloudBackup', false);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -183,7 +183,7 @@ export async function restoreFromCloudBackup() {
 | 
			
		||||
 | 
			
		||||
	miLocalStorage.setItem('preferences', JSON.stringify(profile));
 | 
			
		||||
	miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
 | 
			
		||||
	store.set('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
	store.commit('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
	shouldSuggestRestoreBackup.value = false;
 | 
			
		||||
	unisonReload();
 | 
			
		||||
}
 | 
			
		||||
@@ -197,7 +197,7 @@ export async function enableAutoBackup() {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store.set('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
	store.commit('enablePreferencesAutoCloudBackup', true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const shouldSuggestRestoreBackup = ref(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,219 @@ import * as Misskey from 'misskey-js';
 | 
			
		||||
import lightTheme from '@@/themes/l-light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/d-green-lime.json5';
 | 
			
		||||
import { hemisphere } from '@@/js/intl-const.js';
 | 
			
		||||
import { BroadcastChannel } from 'broadcast-channel';
 | 
			
		||||
import type { DeviceKind } from '@/utility/device-kind.js';
 | 
			
		||||
import type { Plugin } from '@/plugin.js';
 | 
			
		||||
import type { Column } from '@/deck.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { Storage } from '@/pizzax.js';
 | 
			
		||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
 | 
			
		||||
import { Pizzax } from '@/pizzax.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import * as idb from '@/utility/idb-proxy.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { useStream } from '@/stream.js';
 | 
			
		||||
import { deepMerge } from '@/utility/merge.js';
 | 
			
		||||
import { deepClone } from '@/utility/clone.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 「状態」を管理するストア(not「設定」)
 | 
			
		||||
 */
 | 
			
		||||
export const store = markRaw(new Storage('base', {
 | 
			
		||||
type StateDef = Record<string, {
 | 
			
		||||
	where: 'account' | 'device' | 'deviceAccount';
 | 
			
		||||
	default: any;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; };
 | 
			
		||||
 | 
			
		||||
type PizzaxChannelMessage<T extends StateDef> = {
 | 
			
		||||
	where: 'device' | 'deviceAccount';
 | 
			
		||||
	key: keyof T;
 | 
			
		||||
	value: T[keyof T]['default'];
 | 
			
		||||
	userId?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// TODO: export消す
 | 
			
		||||
export class Store<T extends StateDef> extends Pizzax<State<T>> {
 | 
			
		||||
	public readonly def: T;
 | 
			
		||||
 | 
			
		||||
	public readonly ready: Promise<void>;
 | 
			
		||||
	public readonly loaded: Promise<void>;
 | 
			
		||||
 | 
			
		||||
	public readonly key: string;
 | 
			
		||||
	public readonly deviceStateKeyName: `pizzax::${this['key']}`;
 | 
			
		||||
	public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | '';
 | 
			
		||||
	public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | '';
 | 
			
		||||
 | 
			
		||||
	private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
 | 
			
		||||
 | 
			
		||||
	// 簡易的にキューイングして占有ロックとする
 | 
			
		||||
	private currentIdbJob: Promise<any> = Promise.resolve();
 | 
			
		||||
	private addIdbSetJob<T>(job: () => Promise<T>) {
 | 
			
		||||
		const promise = this.currentIdbJob.then(job, err => {
 | 
			
		||||
			console.error('Pizzax failed to save data to idb!', err);
 | 
			
		||||
			return job();
 | 
			
		||||
		});
 | 
			
		||||
		this.currentIdbJob = promise;
 | 
			
		||||
		return promise;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	constructor(def: T, key = 'base') {
 | 
			
		||||
		const data = {} as State<T>;
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
			data[k] = v.default;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		super(data);
 | 
			
		||||
 | 
			
		||||
		this.key = key;
 | 
			
		||||
		this.deviceStateKeyName = `pizzax::${key}`;
 | 
			
		||||
		this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
 | 
			
		||||
		this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : '';
 | 
			
		||||
		this.def = def;
 | 
			
		||||
		this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
 | 
			
		||||
		this.ready = this.init();
 | 
			
		||||
		this.loaded = this.ready.then(() => this.load());
 | 
			
		||||
 | 
			
		||||
		this.addListener('updated', ({ key, value }) => {
 | 
			
		||||
			// IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする
 | 
			
		||||
			// (JSON.parse(JSON.stringify(value))の代わり)
 | 
			
		||||
			const rawValue = deepClone(value);
 | 
			
		||||
 | 
			
		||||
			this.r[key].value = this.s[key] = rawValue;
 | 
			
		||||
 | 
			
		||||
			return this.addIdbSetJob(async () => {
 | 
			
		||||
				switch (this.def[key].where) {
 | 
			
		||||
					case 'device': {
 | 
			
		||||
						this.pizzaxChannel.postMessage({
 | 
			
		||||
							where: 'device',
 | 
			
		||||
							key,
 | 
			
		||||
							value: rawValue,
 | 
			
		||||
						});
 | 
			
		||||
						const deviceState = await idb.get(this.deviceStateKeyName) || {};
 | 
			
		||||
						deviceState[key] = rawValue;
 | 
			
		||||
						await idb.set(this.deviceStateKeyName, deviceState);
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
					case 'deviceAccount': {
 | 
			
		||||
						if ($i == null) break;
 | 
			
		||||
						this.pizzaxChannel.postMessage({
 | 
			
		||||
							where: 'deviceAccount',
 | 
			
		||||
							key,
 | 
			
		||||
							value: rawValue,
 | 
			
		||||
							userId: $i.id,
 | 
			
		||||
						});
 | 
			
		||||
						const deviceAccountState = await idb.get(this.deviceAccountStateKeyName) || {};
 | 
			
		||||
						deviceAccountState[key] = rawValue;
 | 
			
		||||
						await idb.set(this.deviceAccountStateKeyName, deviceAccountState);
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
					case 'account': {
 | 
			
		||||
						if ($i == null) break;
 | 
			
		||||
						const cache = await idb.get(this.registryCacheKeyName) || {};
 | 
			
		||||
						cache[key] = rawValue;
 | 
			
		||||
						await idb.set(this.registryCacheKeyName, cache);
 | 
			
		||||
						await misskeyApi('i/registry/set', {
 | 
			
		||||
							scope: ['client', this.key],
 | 
			
		||||
							key: key.toString(),
 | 
			
		||||
							value: rawValue,
 | 
			
		||||
						});
 | 
			
		||||
						break;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async init(): Promise<void> {
 | 
			
		||||
		const deviceState: State<T> = await idb.get(this.deviceStateKeyName) || {};
 | 
			
		||||
		const deviceAccountState = $i ? await idb.get(this.deviceAccountStateKeyName) || {} : {};
 | 
			
		||||
		const registryCache = $i ? await idb.get(this.registryCacheKeyName) || {} : {};
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
 | 
			
		||||
				this.rewrite(k, this.mergeState<T[keyof T]['default']>(deviceState[k], v.default));
 | 
			
		||||
			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
 | 
			
		||||
				this.rewrite(k, this.mergeState<T[keyof T]['default']>(registryCache[k], v.default));
 | 
			
		||||
			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
 | 
			
		||||
				this.rewrite(k, this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default));
 | 
			
		||||
			} else {
 | 
			
		||||
				this.rewrite(k, v.default);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => {
 | 
			
		||||
			// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
 | 
			
		||||
			// まずないと思うけど一応弾いておく
 | 
			
		||||
			if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
 | 
			
		||||
			this.r[key].value = this.s[key] = value;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if ($i) {
 | 
			
		||||
			const connection = useStream().useChannel('main');
 | 
			
		||||
 | 
			
		||||
			// streamingのuser storage updateイベントを監視して更新
 | 
			
		||||
			connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
 | 
			
		||||
				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.s[key] === value) return;
 | 
			
		||||
 | 
			
		||||
				this.rewrite(key, value);
 | 
			
		||||
 | 
			
		||||
				this.addIdbSetJob(async () => {
 | 
			
		||||
					const cache = await idb.get(this.registryCacheKeyName);
 | 
			
		||||
					if (cache[key] !== value) {
 | 
			
		||||
						cache[key] = value;
 | 
			
		||||
						await idb.set(this.registryCacheKeyName, cache);
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private load(): Promise<void> {
 | 
			
		||||
		return new Promise((resolve, reject) => {
 | 
			
		||||
			if ($i) {
 | 
			
		||||
				// api関数と循環参照なので一応setTimeoutしておく
 | 
			
		||||
				window.setTimeout(async () => {
 | 
			
		||||
					await store.ready;
 | 
			
		||||
 | 
			
		||||
					misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
 | 
			
		||||
						.then(kvs => {
 | 
			
		||||
							const cache: Partial<T> = {};
 | 
			
		||||
							for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 | 
			
		||||
								if (v.where === 'account') {
 | 
			
		||||
									if (Object.prototype.hasOwnProperty.call(kvs, k)) {
 | 
			
		||||
										this.rewrite(k, (kvs as Partial<T>)[k]);
 | 
			
		||||
										cache[k] = (kvs as Partial<T>)[k];
 | 
			
		||||
									} else {
 | 
			
		||||
										this.r[k].value = this.s[k] = v.default;
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							return idb.set(this.registryCacheKeyName, cache);
 | 
			
		||||
						})
 | 
			
		||||
						.then(() => resolve());
 | 
			
		||||
				}, 1);
 | 
			
		||||
			} else {
 | 
			
		||||
				resolve();
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
 | 
			
		||||
		return typeof value === 'object' && value !== null && !Array.isArray(value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private mergeState<X>(value: X, def: X): X {
 | 
			
		||||
		if (this.isPureObject(value) && this.isPureObject(def)) {
 | 
			
		||||
			const merged = deepMerge(value, def);
 | 
			
		||||
 | 
			
		||||
			if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
 | 
			
		||||
 | 
			
		||||
			return merged as X;
 | 
			
		||||
		}
 | 
			
		||||
		return value;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const STORE_DEF = {
 | 
			
		||||
	accountSetupWizard: {
 | 
			
		||||
		where: 'account',
 | 
			
		||||
		default: 0,
 | 
			
		||||
@@ -465,9 +667,12 @@ export const store = markRaw(new Storage('base', {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	//#endregion
 | 
			
		||||
}));
 | 
			
		||||
} satisfies StateDef;
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
/**
 | 
			
		||||
 * 「状態」を管理するストア(not「設定」)
 | 
			
		||||
 */
 | 
			
		||||
export const store = markRaw(new Store(STORE_DEF));
 | 
			
		||||
 | 
			
		||||
const PREFIX = 'miux:' as const;
 | 
			
		||||
 | 
			
		||||
@@ -547,7 +752,7 @@ export class ColdDeviceStorage {
 | 
			
		||||
	 * 特定のキーの、簡易的なgetter/setterを作ります
 | 
			
		||||
	 * 主にvue場で設定コントロールのmodelとして使う用
 | 
			
		||||
	 */
 | 
			
		||||
	public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
 | 
			
		||||
	public static model<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
 | 
			
		||||
		// TODO: VueのcustomRef使うと良い感じになるかも
 | 
			
		||||
		const valueRef = ColdDeviceStorage.ref(key);
 | 
			
		||||
		return {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,11 @@ export async function addTheme(theme: Theme): Promise<void> {
 | 
			
		||||
	if (themes.some(t => t.id === theme.id)) {
 | 
			
		||||
		throw new Error('already exists');
 | 
			
		||||
	}
 | 
			
		||||
	prefer.set('themes', [...themes, theme]);
 | 
			
		||||
	prefer.commit('themes', [...themes, theme]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeTheme(theme: Theme): Promise<void> {
 | 
			
		||||
	if ($i == null) return;
 | 
			
		||||
	const themes = getThemes().filter(t => t.id !== theme.id);
 | 
			
		||||
	prefer.set('themes', themes);
 | 
			
		||||
	prefer.commit('themes', themes);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -105,7 +105,7 @@ const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const forceIconOnly = ref(window.innerWidth <= 1279);
 | 
			
		||||
const iconOnly = computed(() => {
 | 
			
		||||
	return forceIconOnly.value || (store.reactiveState.menuDisplay.value === 'sideIcon');
 | 
			
		||||
	return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const menu = computed(() => prefer.s.menu);
 | 
			
		||||
@@ -123,12 +123,12 @@ function calcViewState() {
 | 
			
		||||
 | 
			
		||||
window.addEventListener('resize', calcViewState);
 | 
			
		||||
 | 
			
		||||
watch(store.reactiveState.menuDisplay, () => {
 | 
			
		||||
watch(store.r.menuDisplay, () => {
 | 
			
		||||
	calcViewState();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function toggleIconOnly() {
 | 
			
		||||
	store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
 | 
			
		||||
	store.commit('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openAccountMenu(ev: MouseEvent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ const WINDOW_THRESHOLD = 1400;
 | 
			
		||||
 | 
			
		||||
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
 | 
			
		||||
const menu = ref(prefer.s.menu);
 | 
			
		||||
// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 | 
			
		||||
// const menuDisplay = store.model('menuDisplay');
 | 
			
		||||
const otherNavItemIndicated = computed<boolean>(() => {
 | 
			
		||||
	for (const def in navbarItemDef) {
 | 
			
		||||
		if (menu.value.includes(def)) continue;
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ import { prefer } from '@/preferences.js';
 | 
			
		||||
const WINDOW_THRESHOLD = 1400;
 | 
			
		||||
 | 
			
		||||
const menu = ref(prefer.s.menu);
 | 
			
		||||
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 | 
			
		||||
const menuDisplay = store.model('menuDisplay');
 | 
			
		||||
const otherNavItemIndicated = computed<boolean>(() => {
 | 
			
		||||
	for (const def in navbarItemDef) {
 | 
			
		||||
		if (menu.value.includes(def)) continue;
 | 
			
		||||
@@ -100,7 +100,7 @@ function openAccountMenu(ev: MouseEvent) {
 | 
			
		||||
	}, ev);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(store.reactiveState.menuDisplay, () => {
 | 
			
		||||
watch(store.r.menuDisplay, () => {
 | 
			
		||||
	calcViewState();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ const widgetsShowing = ref(false);
 | 
			
		||||
const fullView = ref(false);
 | 
			
		||||
const globalHeaderHeight = ref(0);
 | 
			
		||||
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
 | 
			
		||||
const showMenuOnTop = computed(() => store.state.menuDisplay === 'top');
 | 
			
		||||
const showMenuOnTop = computed(() => store.s.menuDisplay === 'top');
 | 
			
		||||
const live2d = shallowRef<HTMLIFrameElement>();
 | 
			
		||||
const widgetsLeft = ref<HTMLElement>();
 | 
			
		||||
const widgetsRight = ref<HTMLElement>();
 | 
			
		||||
@@ -97,7 +97,7 @@ provide('shouldHeaderThin', showMenuOnTop.value);
 | 
			
		||||
provide('forceSpacerMin', true);
 | 
			
		||||
 | 
			
		||||
function attachSticky(el: HTMLElement) {
 | 
			
		||||
	const sticky = new StickySidebar(el, 0, store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
 | 
			
		||||
	const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
 | 
			
		||||
	window.addEventListener('scroll', () => {
 | 
			
		||||
		sticky.calc(window.scrollY);
 | 
			
		||||
	}, { passive: true });
 | 
			
		||||
@@ -144,7 +144,7 @@ if (window.innerWidth < 1024) {
 | 
			
		||||
document.documentElement.style.overflowY = 'scroll';
 | 
			
		||||
 | 
			
		||||
if (prefer.s.widgets.length === 0) {
 | 
			
		||||
	prefer.set('widgets', [{
 | 
			
		||||
	prefer.commit('widgets', [{
 | 
			
		||||
		name: 'calendar',
 | 
			
		||||
		id: 'a', place: null, data: {},
 | 
			
		||||
	}, {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			</div>
 | 
			
		||||
			<div :class="$style.sideMenu">
 | 
			
		||||
				<div :class="$style.sideMenuTop">
 | 
			
		||||
					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.state['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
 | 
			
		||||
					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
 | 
			
		||||
					<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div :class="$style.sideMenuMiddle">
 | 
			
		||||
@@ -137,7 +137,7 @@ const columnComponents = {
 | 
			
		||||
 | 
			
		||||
mainRouter.navHook = (path, flag): boolean => {
 | 
			
		||||
	if (flag === 'forcePage') return false;
 | 
			
		||||
	const noMainColumn = !store.state['deck.columns'].some(x => x.type === 'main');
 | 
			
		||||
	const noMainColumn = !store.s['deck.columns'].some(x => x.type === 'main');
 | 
			
		||||
	if (prefer.s['deck.navWindow'] || noMainColumn) {
 | 
			
		||||
		os.pageWindow(path);
 | 
			
		||||
		return true;
 | 
			
		||||
@@ -160,8 +160,8 @@ watch(route, () => {
 | 
			
		||||
});
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const columns = store.reactiveState['deck.columns'];
 | 
			
		||||
const layout = store.reactiveState['deck.layout'];
 | 
			
		||||
const columns = store.r['deck.columns'];
 | 
			
		||||
const layout = store.r['deck.layout'];
 | 
			
		||||
const menuIndicated = computed(() => {
 | 
			
		||||
	if ($i == null) return false;
 | 
			
		||||
	for (const def in navbarItemDef) {
 | 
			
		||||
@@ -214,15 +214,15 @@ loadDeck();
 | 
			
		||||
 | 
			
		||||
function changeProfile(ev: MouseEvent) {
 | 
			
		||||
	let items: MenuItem[] = [{
 | 
			
		||||
		text: store.state['deck.profile'],
 | 
			
		||||
		text: store.s['deck.profile'],
 | 
			
		||||
		active: true,
 | 
			
		||||
		action: () => {},
 | 
			
		||||
	}];
 | 
			
		||||
	getProfiles().then(profiles => {
 | 
			
		||||
		items.push(...(profiles.filter(k => k !== store.state['deck.profile']).map(k => ({
 | 
			
		||||
		items.push(...(profiles.filter(k => k !== store.s['deck.profile']).map(k => ({
 | 
			
		||||
			text: k,
 | 
			
		||||
			action: () => {
 | 
			
		||||
				store.set('deck.profile', k);
 | 
			
		||||
				store.commit('deck.profile', k);
 | 
			
		||||
				unisonReload();
 | 
			
		||||
			},
 | 
			
		||||
		}))), { type: 'divider' as const }, {
 | 
			
		||||
@@ -237,7 +237,7 @@ function changeProfile(ev: MouseEvent) {
 | 
			
		||||
				if (canceled || name == null) return;
 | 
			
		||||
 | 
			
		||||
				os.promiseDialog((async () => {
 | 
			
		||||
					await store.set('deck.profile', name);
 | 
			
		||||
					await store.commit('deck.profile', name);
 | 
			
		||||
					await forceSaveDeck();
 | 
			
		||||
				})(), () => {
 | 
			
		||||
					unisonReload();
 | 
			
		||||
@@ -252,19 +252,19 @@ function changeProfile(ev: MouseEvent) {
 | 
			
		||||
async function deleteProfile() {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: store.state['deck.profile'] }),
 | 
			
		||||
		text: i18n.tsx.deleteAreYouSure({ x: store.s['deck.profile'] }),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
 | 
			
		||||
	os.promiseDialog((async () => {
 | 
			
		||||
		if (store.state['deck.profile'] === 'default') {
 | 
			
		||||
			await store.set('deck.columns', []);
 | 
			
		||||
			await store.set('deck.layout', []);
 | 
			
		||||
		if (store.s['deck.profile'] === 'default') {
 | 
			
		||||
			await store.commit('deck.columns', []);
 | 
			
		||||
			await store.commit('deck.layout', []);
 | 
			
		||||
			await forceSaveDeck();
 | 
			
		||||
		} else {
 | 
			
		||||
			await deleteProfile_(store.state['deck.profile']);
 | 
			
		||||
			await deleteProfile_(store.s['deck.profile']);
 | 
			
		||||
		}
 | 
			
		||||
		await store.set('deck.profile', 'default');
 | 
			
		||||
		await store.commit('deck.profile', 'default');
 | 
			
		||||
	})(), () => {
 | 
			
		||||
		unisonReload();
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,10 @@
 | 
			
		||||
 | 
			
		||||
import { markRaw } from 'vue';
 | 
			
		||||
import type { Column } from '@/deck.js';
 | 
			
		||||
import { Storage } from '@/pizzax.js';
 | 
			
		||||
import { Store } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
// TODO: 消す(移行済みのため)
 | 
			
		||||
export const deckStore = markRaw(new Storage('deck', {
 | 
			
		||||
export const deckStore = markRaw(new Store({
 | 
			
		||||
	profile: {
 | 
			
		||||
		where: 'deviceAccount',
 | 
			
		||||
		default: 'default',
 | 
			
		||||
@@ -21,4 +21,4 @@ export const deckStore = markRaw(new Storage('deck', {
 | 
			
		||||
		where: 'deviceAccount',
 | 
			
		||||
		default: [] as Column['id'][][],
 | 
			
		||||
	},
 | 
			
		||||
}));
 | 
			
		||||
}, 'deck'));
 | 
			
		||||
 
 | 
			
		||||
@@ -178,7 +178,7 @@ if (window.innerWidth > 1024) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (prefer.s.widgets.length === 0) {
 | 
			
		||||
	prefer.set('widgets', [{
 | 
			
		||||
	prefer.commit('widgets', [{
 | 
			
		||||
		name: 'calendar',
 | 
			
		||||
		id: 'a', place: 'right', data: {},
 | 
			
		||||
	}, {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,18 +37,18 @@ const widgets = computed(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function addWidget(widget) {
 | 
			
		||||
	prefer.set('widgets', [{
 | 
			
		||||
	prefer.commit('widgets', [{
 | 
			
		||||
		...widget,
 | 
			
		||||
		place: props.place,
 | 
			
		||||
	}, ...prefer.s.widgets]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeWidget(widget) {
 | 
			
		||||
	prefer.set('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
 | 
			
		||||
	prefer.commit('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateWidget({ id, data }) {
 | 
			
		||||
	prefer.set('widgets', prefer.s.widgets.map(w => w.id === id ? {
 | 
			
		||||
	prefer.commit('widgets', prefer.s.widgets.map(w => w.id === id ? {
 | 
			
		||||
		...w,
 | 
			
		||||
		data,
 | 
			
		||||
		place: props.place,
 | 
			
		||||
@@ -57,17 +57,17 @@ function updateWidget({ id, data }) {
 | 
			
		||||
 | 
			
		||||
function updateWidgets(thisWidgets) {
 | 
			
		||||
	if (props.place === null) {
 | 
			
		||||
		prefer.set('widgets', thisWidgets);
 | 
			
		||||
		prefer.commit('widgets', thisWidgets);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	if (props.place === 'left') {
 | 
			
		||||
		prefer.set('widgets', [
 | 
			
		||||
		prefer.commit('widgets', [
 | 
			
		||||
			...thisWidgets.map(w => ({ ...w, place: 'left' })),
 | 
			
		||||
			...prefer.s.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
 | 
			
		||||
		]);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	prefer.set('widgets', [
 | 
			
		||||
	prefer.commit('widgets', [
 | 
			
		||||
		...prefer.s.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
 | 
			
		||||
		...thisWidgets.map(w => ({ ...w, place: 'right' })),
 | 
			
		||||
	]);
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ class EmojiPicker {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async init() {
 | 
			
		||||
		const emojisRef = store.reactiveState.pinnedEmojis;
 | 
			
		||||
		const emojisRef = store.r.pinnedEmojis;
 | 
			
		||||
		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 | 
			
		||||
			src: this.src,
 | 
			
		||||
			pinnedEmojis: emojisRef,
 | 
			
		||||
 
 | 
			
		||||
@@ -608,8 +608,8 @@ export function getRenoteMenu(props: {
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility;
 | 
			
		||||
				const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly;
 | 
			
		||||
				const configuredVisibility = prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility;
 | 
			
		||||
				const localOnly = prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly;
 | 
			
		||||
 | 
			
		||||
				let visibility = appearNote.visibility;
 | 
			
		||||
				visibility = smallerVisibility(visibility, configuredVisibility);
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ export function initChart() {
 | 
			
		||||
	// フォントカラー
 | 
			
		||||
	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
 | 
			
		||||
 | 
			
		||||
	Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
	Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
 | 
			
		||||
	Chart.defaults.animation = false;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class ReactionPicker {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async init() {
 | 
			
		||||
		const reactionsRef = store.reactiveState.reactions;
 | 
			
		||||
		const reactionsRef = store.r.reactions;
 | 
			
		||||
		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 | 
			
		||||
			src: this.src,
 | 
			
		||||
			pinnedEmojis: reactionsRef,
 | 
			
		||||
 
 | 
			
		||||
@@ -48,12 +48,12 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 | 
			
		||||
	emit,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const text = ref<string | null>(store.state.memo);
 | 
			
		||||
const text = ref<string | null>(store.s.memo);
 | 
			
		||||
const changed = ref(false);
 | 
			
		||||
let timeoutId;
 | 
			
		||||
 | 
			
		||||
const saveMemo = () => {
 | 
			
		||||
	store.set('memo', text.value);
 | 
			
		||||
	store.commit('memo', text.value);
 | 
			
		||||
	changed.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -63,7 +63,7 @@ const onChange = () => {
 | 
			
		||||
	timeoutId = window.setTimeout(saveMemo, 1000);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(() => store.reactiveState.memo, newText => {
 | 
			
		||||
watch(() => store.r.memo, newText => {
 | 
			
		||||
	text.value = newText.value;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user