{{ i18n.ts.notificationRecieveConfig }}
@@ -63,6 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 7e0efa2e89..93dd081127 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -134,33 +134,29 @@ const routes: RouteDef[] = [{
name: 'plugin',
component: page(() => import('@/pages/settings/plugin.vue')),
}, {
- path: '/import-export',
- name: 'import-export',
- component: page(() => import('@/pages/settings/import-export.vue')),
+ path: '/account-data',
+ name: 'account-data',
+ component: page(() => import('@/pages/settings/account-data.vue')),
}, {
path: '/mute-block',
name: 'mute-block',
component: page(() => import('@/pages/settings/mute-block.vue')),
}, {
- path: '/api',
- name: 'api',
- component: page(() => import('@/pages/settings/api.vue')),
+ path: '/connect',
+ name: 'connect',
+ component: page(() => import('@/pages/settings/connect.vue')),
}, {
path: '/apps',
- name: 'api',
+ name: 'connect',
component: page(() => import('@/pages/settings/apps.vue')),
}, {
path: '/webhook/edit/:webhookId',
- name: 'webhook',
+ name: 'connect',
component: page(() => import('@/pages/settings/webhook.edit.vue')),
}, {
path: '/webhook/new',
- name: 'webhook',
+ name: 'connect',
component: page(() => import('@/pages/settings/webhook.new.vue')),
- }, {
- path: '/webhook',
- name: 'webhook',
- component: page(() => import('@/pages/settings/webhook.vue')),
}, {
path: '/deck',
name: 'deck',
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index db4459bf06..79310716e0 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -52,23 +52,23 @@ export const searchIndexes: SearchIndexItem[] = [
id: '6fFIRXUww',
children: [
{
- id: 'nO7NnzqiC',
+ id: 'EcwZE7dCl',
label: i18n.ts.notUseSound,
keywords: ['mute'],
},
{
- id: 'oALW4ja7U',
+ id: '9MxYVIf7k',
label: i18n.ts.useSoundOnlyWhenActive,
keywords: ['active', 'mute'],
},
{
- id: 'BbJK2SKT2',
+ id: '94afQxKat',
label: i18n.ts.masterVolume,
keywords: ['volume', 'master'],
},
],
label: i18n.ts.sounds,
- keywords: ['sounds'],
+ keywords: ['sounds', i18n.ts._settings.soundsBanner],
path: '/settings/sounds',
icon: 'ti ti-music',
},
@@ -76,10 +76,10 @@ export const searchIndexes: SearchIndexItem[] = [
id: '5BjnxMfYV',
children: [
{
- id: '3UqdSCaFw',
+ id: '75QPEg57v',
children: [
{
- id: '75QPEg57v',
+ id: 'CiHijRkGG',
label: i18n.ts.changePassword,
keywords: [],
},
@@ -111,7 +111,7 @@ export const searchIndexes: SearchIndexItem[] = [
},
],
label: i18n.ts.security,
- keywords: ['security'],
+ keywords: ['security', i18n.ts._settings.securityBanner],
path: '/settings/security',
icon: 'ti ti-lock',
},
@@ -195,65 +195,65 @@ export const searchIndexes: SearchIndexItem[] = [
id: '2rp9ka5Ht',
children: [
{
- id: 'qBUSKPxLW',
+ id: 'BhAQiHogN',
label: i18n.ts.makeFollowManuallyApprove,
keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo],
},
{
- id: '3LZBlZCej',
+ id: '4DeWGsPaD',
label: i18n.ts.autoAcceptFollowed,
keywords: ['follow', 'auto', 'accept'],
},
{
- id: '9gOp28wKG',
+ id: 'iaM6zUmO9',
label: i18n.ts.makeReactionsPublic,
keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription],
},
{
- id: 'CjAkqMhct',
+ id: '5Q6uhghzV',
label: i18n.ts.followingVisibility,
keywords: ['following', 'visibility'],
},
{
- id: '4nEwI6LYt',
+ id: 'pZ9q65FX5',
label: i18n.ts.followersVisibility,
keywords: ['follower', 'visibility'],
},
{
- id: 'naMp37wTL',
+ id: 'DMS4yvAGg',
label: i18n.ts.hideOnlineStatus,
keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription],
},
{
- id: 'p0dCVR0UP',
+ id: '8rEsGuN8w',
label: i18n.ts.noCrawle,
keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription],
},
{
- id: 'aceURmNPq',
+ id: 's7LdSpiLn',
label: i18n.ts.preventAiLearning,
keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription],
},
{
- id: 'ahABA0j7u',
+ id: 'l2Wf1s2ad',
label: i18n.ts.makeExplorable,
keywords: ['explore', i18n.ts.makeExplorableDescription],
},
{
- id: 'cyeDbLN8N',
+ id: '7vr04wKol',
children: [
{
- id: 'xEYlOghao',
+ id: 'Av7fAaHv8',
label: i18n.ts._accountSettings.requireSigninToViewContents,
keywords: ['login', 'signin'],
},
{
- id: 'sMmYFCS60',
+ id: 'lUtOQbnwi',
label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore,
keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
},
{
- id: 'ebJ9IUbik',
+ id: '83WWcjwS9',
label: i18n.ts._accountSettings.makeNotesHiddenBefore,
keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
},
@@ -263,7 +263,7 @@ export const searchIndexes: SearchIndexItem[] = [
},
],
label: i18n.ts.privacy,
- keywords: ['privacy'],
+ keywords: ['privacy', i18n.ts._settings.privacyBanner],
path: '/settings/privacy',
icon: 'ti ti-lock-open',
},
@@ -271,75 +271,75 @@ export const searchIndexes: SearchIndexItem[] = [
id: '3yCAv0IsZ',
children: [
{
- id: 'x1GWSQnPw',
+ id: 'kMJ5laK3n',
label: i18n.ts.uiLanguage,
keywords: ['language'],
},
{
- id: 'EOSa4rtt3',
+ id: 'dlKebHH6k',
label: i18n.ts.overridedDeviceKind,
keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
},
{
- id: 'm9LhX8BG8',
+ id: 'nxvMUir3T',
label: i18n.ts.showFixedPostForm,
keywords: ['post', 'form', 'timeline'],
},
{
- id: 'snyCQ5oKE',
+ id: '84MdeDWL1',
label: i18n.ts.showFixedPostFormInChannel,
keywords: ['post', 'form', 'timeline', 'channel'],
},
{
- id: '8j36S4Ev6',
+ id: 'dOig3ye4Z',
label: i18n.ts.pinnedList,
keywords: ['pinned', 'list'],
},
{
- id: 'CWpyT9vLK',
+ id: '4huRldNp5',
label: i18n.ts.enableQuickAddMfmFunction,
keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
},
{
- id: '1yhown1Xc',
+ id: '1x3JNXj8N',
label: i18n.ts.rememberNoteVisibility,
keywords: ['remember', 'keep', 'note', 'visibility'],
},
{
- id: 'wUeAI5QBV',
+ id: 'CfAg0Qekq',
label: i18n.ts.defaultNoteVisibility,
keywords: ['default', 'note', 'visibility'],
},
{
- id: '6kMj4HVOg',
+ id: 'tMm9kH9gy',
children: [
{
- id: 'DQIcvf64G',
+ id: 'hDdVkBFJP',
label: i18n.ts.collapseRenotes,
keywords: ['renote', i18n.ts.collapseRenotesDescription],
},
{
- id: 'igFN7RIUa',
+ id: 'uJJyDABGu',
label: i18n.ts.showNoteActionsOnlyHover,
keywords: ['hover', 'show', 'footer', 'action'],
},
{
- id: '9uxocbLO0',
+ id: 'ufc2X9voy',
label: i18n.ts.showClipButtonInNoteFooter,
keywords: ['footer', 'action', 'clip', 'show'],
},
{
- id: 'eaT1O1Fao',
+ id: '7Jwvu8bK6',
label: i18n.ts.enableAdvancedMfm,
keywords: ['mfm', 'enable', 'show', 'advanced'],
},
{
- id: 'omxZk3eET',
+ id: 'yb11lSY1G',
label: i18n.ts.showReactionsCount,
keywords: ['reaction', 'count', 'show'],
},
{
- id: 'epvi2Nv2G',
+ id: 'fL49Zxe9i',
label: i18n.ts.loadRawImages,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
},
@@ -348,10 +348,10 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['note'],
},
{
- id: 'jb3HUeyrx',
+ id: 'bUOs2UKY4',
children: [
{
- id: 'ykifk3NHS',
+ id: 'c8gA9Xj2a',
label: i18n.ts.useGroupedNotifications,
keywords: ['group'],
},
@@ -360,60 +360,60 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['notification'],
},
{
- id: 'abEAdSpYY',
+ id: 'tjGzqy3qa',
children: [
{
- id: 'lBbtAg0Hm',
+ id: '3OeHscv45',
label: i18n.ts.openImageInNewTab,
keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
},
{
- id: 'E9whefUtX',
+ id: 'bFsNusspF',
label: i18n.ts.useReactionPickerForContextMenu,
keywords: ['reaction', 'picker', 'contextmenu', 'open'],
},
{
- id: 'iQaBbJBva',
+ id: '2h3rY1izt',
label: i18n.ts.enableInfiniteScroll,
keywords: ['load', 'auto', 'more'],
},
{
- id: 'hgEVGgJa1',
+ id: 'pkK3eeFKm',
label: i18n.ts.disableStreamingTimeline,
keywords: ['disable', 'streaming', 'timeline'],
},
{
- id: 'yxehrHZ6x',
+ id: 'y2v7CV9zs',
label: i18n.ts.alwaysConfirmFollow,
keywords: ['follow', 'confirm', 'always'],
},
{
- id: 'DdoFLaSG8',
+ id: 'A8a5hcLce',
label: i18n.ts.confirmWhenRevealingSensitiveMedia,
keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
},
{
- id: 'uIMCIK7kG',
+ id: 'utFrfuW7X',
label: i18n.ts.confirmOnReact,
keywords: ['reaction', 'confirm'],
},
{
- id: 'zvM13vl26',
+ id: 'kmdsnVIQX',
label: i18n.ts.keepCw,
keywords: ['remember', 'keep', 'note', 'cw'],
},
{
- id: 'm75VEWI3S',
+ id: 'mNRK0pt8L',
label: i18n.ts.whenServerDisconnected,
keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
},
{
- id: 'bLO9vCyKW',
+ id: 'vE7KeV4U4',
label: i18n.ts.numberOfPageCache,
keywords: ['cache', 'page'],
},
{
- id: 'iQ7Er89l5',
+ id: 'eJ2jme16W',
label: i18n.ts.dataSaver,
keywords: ['datasaver'],
},
@@ -422,20 +422,20 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['behavior'],
},
{
- id: 'C2WYcVM1d',
+ id: 'F3kpUNvSQ',
children: [
{
- id: 'Cu7ErCM7C',
+ id: '4bfFRM0UD',
label: i18n.ts.forceShowAds,
keywords: ['ad', 'show'],
},
{
- id: 'BBxwy4F6E',
+ id: '2pB0jWBHo',
label: i18n.ts.hemisphere,
keywords: [],
},
{
- id: '9YdUwDC8d',
+ id: 'eIvnR6Xxo',
label: i18n.ts.additionalEmojiDictionary,
keywords: ['emoji', 'dictionary', 'additional', 'extra'],
},
@@ -445,14 +445,14 @@ export const searchIndexes: SearchIndexItem[] = [
},
],
label: i18n.ts.preferences,
- keywords: ['general', 'preferences'],
+ keywords: ['general', 'preferences', i18n.ts._settings.preferencesBanner],
path: '/settings/preferences',
icon: 'ti ti-adjustments',
},
{
id: 'mwkwtw83Y',
label: i18n.ts.plugins,
- keywords: ['plugin'],
+ keywords: ['plugin', 'addon', 'extension', i18n.ts._settings.pluginBanner],
path: '/settings/plugin',
icon: 'ti ti-plug',
},
@@ -494,10 +494,10 @@ export const searchIndexes: SearchIndexItem[] = [
id: '3icEvyv2D',
children: [
{
- id: 'Tyt3gZTy',
+ id: 'lO3uFTkPN',
children: [
{
- id: '9b7ZURyAt',
+ id: '5JKaXRqyt',
label: i18n.ts.showMutedWord,
keywords: ['show'],
},
@@ -506,85 +506,36 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['note', 'word', 'soft', 'mute', 'hide'],
},
{
- id: 'kdMk41II0',
+ id: 'fMkjL3dK4',
label: i18n.ts.hardWordMute,
keywords: ['note', 'word', 'hard', 'mute', 'hide'],
},
{
- id: 'mjORQamAK',
+ id: 'cimSzQXN0',
label: i18n.ts.instanceMute,
keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'],
},
{
- id: '1ZT7S9FZd',
+ id: 'gq8rPy3Du',
label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`,
keywords: ['renote', 'mute', 'hide', 'user'],
},
{
- id: 'ANrPit3kQ',
+ id: 'mh2r7EUbF',
label: i18n.ts.mutedUsers,
keywords: ['note', 'mute', 'hide', 'user'],
},
{
- id: 'bPAE4lfno',
+ id: 'AUS1OgHrn',
label: i18n.ts.blockedUsers,
keywords: ['block', 'user'],
},
],
label: i18n.ts.muteAndBlock,
- keywords: ['mute', 'block'],
+ keywords: ['mute', 'block', i18n.ts._settings.muteAndBlockBanner],
path: '/settings/mute-block',
icon: 'ti ti-ban',
},
- {
- id: 'qE2vLlMkF',
- children: [
- {
- id: 'hPPEzjvZC',
- label: i18n.ts._exportOrImport.allNotes,
- keywords: ['notes'],
- },
- {
- id: 'AFaeHsCUB',
- label: i18n.ts._exportOrImport.favoritedNotes,
- keywords: ['favorite', 'notes'],
- },
- {
- id: 'xyCPmQiRo',
- label: i18n.ts._exportOrImport.clips,
- keywords: ['clip', 'notes'],
- },
- {
- id: 'Ch7hWAGUy',
- label: i18n.ts._exportOrImport.followingList,
- keywords: ['following', 'users'],
- },
- {
- id: 'AwPgFboEx',
- label: i18n.ts._exportOrImport.userLists,
- keywords: ['user', 'lists'],
- },
- {
- id: 'nporiHshC',
- label: i18n.ts._exportOrImport.muteList,
- keywords: ['mute', 'users'],
- },
- {
- id: 'BsCzR7vNw',
- label: i18n.ts._exportOrImport.blockingList,
- keywords: ['block', 'users'],
- },
- {
- id: 'dvf4IgYrQ',
- label: i18n.ts.antennas,
- keywords: ['antennas'],
- },
- ],
- label: i18n.ts.importAndExport,
- keywords: ['import', 'export', 'data'],
- path: '/settings/import-export',
- icon: 'ti ti-package',
- },
{
id: '3Tcxw4Fwl',
children: [
@@ -613,46 +564,65 @@ export const searchIndexes: SearchIndexItem[] = [
id: 'tnYoppRiv',
children: [
{
- id: 'ncIq6TAR2',
+ id: 'cN3dsGNxu',
label: i18n.ts.usageAmount,
keywords: ['capacity', 'usage'],
},
{
- id: '2c4CQSvSr',
+ id: 'rOAOU2P6C',
label: i18n.ts.statistics,
keywords: ['statistics', 'usage'],
},
{
- id: 'pepHELHMt',
+ id: 'uXGlQXATx',
label: i18n.ts.uploadFolder,
keywords: ['default', 'upload', 'folder'],
},
{
- id: 'xqOWrABxV',
+ id: 'goQdtf3dD',
label: i18n.ts.keepOriginalUploading,
keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
},
{
- id: 'D8HUTGWE1',
+ id: '83xRo0XJl',
label: i18n.ts.keepOriginalFilename,
keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
},
{
- id: '6xAvsWSZi',
+ id: 'wf77yRQQq',
label: i18n.ts.alwaysMarkSensitive,
keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
},
{
- id: 'csNNPF1KX',
+ id: '3pxwNB8e4',
label: i18n.ts.enableAutoSensitive,
keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
},
],
label: i18n.ts.drive,
- keywords: ['drive'],
+ keywords: ['drive', i18n.ts._settings.driveBanner],
path: '/settings/drive',
icon: 'ti ti-cloud',
},
+ {
+ id: 'BlJ2rsw9h',
+ children: [
+ {
+ id: '9bLU1nIjt',
+ label: i18n.ts._settings.api,
+ keywords: ['api', 'app', 'token', 'accessToken'],
+ },
+ {
+ id: '5VSGOVYR0',
+ label: i18n.ts.manage,
+ keywords: ['webhook'],
+ },
+ ],
+ label: i18n.ts._settings.serviceConnection,
+ keywords: ['app', 'service', 'connect', 'webhook', 'api', 'token', i18n.ts._settings.serviceConnectionBanner],
+ path: '/settings/connect',
+ icon: 'ti ti-link',
+ },
{
id: 'gtaOSdIJB',
label: i18n.ts.avatarDecorations,
@@ -664,85 +634,85 @@ export const searchIndexes: SearchIndexItem[] = [
id: 'AqPvMgn3A',
children: [
{
- id: 'j5gTtuMWP',
+ id: '1wtOIwAdm',
label: i18n.ts.useBlurEffect,
keywords: ['blur'],
},
{
- id: 'C05WQNSIJ',
+ id: '6fLNMTwNt',
label: i18n.ts.useBlurEffectForModal,
keywords: ['blur', 'modal'],
},
{
- id: 'snVKNr7Bw',
+ id: 'E0WXhhRB1',
label: i18n.ts.highlightSensitiveMedia,
keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
},
{
- id: 'DsS2CwjYE',
+ id: '7iZsGkplG',
label: i18n.ts.squareAvatars,
keywords: ['avatar', 'icon', 'square'],
},
{
- id: 'xCcTDl651',
+ id: 'AfRMcC6IM',
label: i18n.ts.showAvatarDecorations,
keywords: ['avatar', 'icon', 'decoration', 'show'],
},
{
- id: '3dHw723VD',
+ id: 'i7aSaEWaT',
label: i18n.ts.showGapBetweenNotesInTimeline,
keywords: ['note', 'timeline', 'gap'],
},
{
- id: 'AWi72xbrl',
+ id: 'knj98Mx84',
label: i18n.ts.seasonalScreenEffect,
keywords: ['effect', 'show'],
},
{
- id: 'Ces8FsJws',
+ id: 'Bzg77rYNd',
label: i18n.ts.menuStyle,
keywords: ['menu', 'style', 'popup', 'drawer'],
},
{
- id: 'wDr9xSXCv',
+ id: '7AOZ1ZgDv',
label: i18n.ts.emojiStyle,
keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
},
{
- id: 'vFB0pLzck',
+ id: 'fDelHUrBi',
label: i18n.ts.fontSize,
keywords: ['font', 'size'],
},
{
- id: '23BhvYXPC',
+ id: 'siOW5aSwp',
label: i18n.ts.useSystemFont,
keywords: ['font', 'system', 'native'],
},
{
- id: 'EeNLndAOa',
+ id: 's05dHQ1dW',
children: [
{
- id: 'rAAPoaodS',
+ id: 'zoMbYCvP0',
label: i18n.ts.reactionsDisplaySize,
keywords: ['reaction', 'size', 'scale', 'display'],
},
{
- id: 'qTLAvNWsc',
+ id: 'lGFzLnWfB',
label: i18n.ts.limitWidthOfReaction,
keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
},
{
- id: '2lWgzAm13',
+ id: '9E0v8VKIY',
label: i18n.ts.mediaListWithOneImageAppearance,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
},
{
- id: 'EU7HbxOR5',
+ id: 'xB7MPEF4Q',
label: i18n.ts.instanceTicker,
keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
},
{
- id: 'AEtM0FAp1',
+ id: '7siYCSodm',
label: i18n.ts.displayOfSensitiveMedia,
keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
},
@@ -751,15 +721,15 @@ export const searchIndexes: SearchIndexItem[] = [
keywords: ['note', 'display'],
},
{
- id: 'A1FMC2Zon',
+ id: 'uQfyiHMSs',
children: [
{
- id: 'CB37G5ZDo',
+ id: 'y3uTXsSQ6',
label: i18n.ts.position,
keywords: ['position'],
},
{
- id: 'gGS2i19hS',
+ id: 'PILAdkVM',
label: i18n.ts.stackAxis,
keywords: ['stack', 'axis', 'direction'],
},
@@ -769,51 +739,100 @@ export const searchIndexes: SearchIndexItem[] = [
},
],
label: i18n.ts.appearance,
- keywords: ['appearance'],
+ keywords: ['appearance', i18n.ts._settings.appearanceBanner],
path: '/settings/appearance',
icon: 'ti ti-device-desktop',
},
+ {
+ id: '330Q4mf8E',
+ children: [
+ {
+ id: 'eGSjUDIKu',
+ label: i18n.ts._exportOrImport.allNotes,
+ keywords: ['notes'],
+ },
+ {
+ id: 'iMDgUVgRu',
+ label: i18n.ts._exportOrImport.favoritedNotes,
+ keywords: ['favorite', 'notes'],
+ },
+ {
+ id: '3y6KgkVbT',
+ label: i18n.ts._exportOrImport.clips,
+ keywords: ['clip', 'notes'],
+ },
+ {
+ id: 'cKiHkj8HE',
+ label: i18n.ts._exportOrImport.followingList,
+ keywords: ['following', 'users'],
+ },
+ {
+ id: '3zzmQXn0t',
+ label: i18n.ts._exportOrImport.userLists,
+ keywords: ['user', 'lists'],
+ },
+ {
+ id: '3ZGXcEqWZ',
+ label: i18n.ts._exportOrImport.muteList,
+ keywords: ['mute', 'users'],
+ },
+ {
+ id: '84oL7B1Dr',
+ label: i18n.ts._exportOrImport.blockingList,
+ keywords: ['block', 'users'],
+ },
+ {
+ id: 'ckqi48Kbl',
+ label: i18n.ts.antennas,
+ keywords: ['antennas'],
+ },
+ ],
+ label: i18n.ts._settings.accountData,
+ keywords: ['import', 'export', 'data', i18n.ts._settings.accountDataBanner],
+ path: '/settings/account-data',
+ icon: 'ti ti-package',
+ },
{
id: 'f08Mi1Uwn',
children: [
{
- id: '7ov7ceoij',
+ id: 'C5dRH2Ypy',
label: i18n.ts.reduceUiAnimation,
keywords: ['animation', 'motion', 'reduce'],
},
{
- id: 'cXr3tFdpa',
+ id: '5mZxz2cru',
label: i18n.ts.disableShowingAnimatedImages,
keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
},
{
- id: 'Ok1UBwtP',
+ id: 'c0Iy5hL5o',
label: i18n.ts.enableAnimatedMfm,
keywords: ['mfm', 'enable', 'show', 'animated'],
},
{
- id: 'yPEpJigqY',
+ id: '4HYFjs2Nv',
label: i18n.ts.enableHorizontalSwipe,
keywords: ['swipe', 'horizontal', 'tab'],
},
{
- id: 'h7iZtdTU3',
+ id: 'kYVJ3SVNq',
label: i18n.ts.keepScreenOn,
keywords: ['keep', 'screen', 'display', 'on'],
},
{
- id: 'gP1BY3PDy',
+ id: 'w4Bv0meAt',
label: i18n.ts.useNativeUIForVideoAudioPlayer,
keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
},
{
- id: 'jnMK3M6rs',
+ id: '1fV9WINCQ',
label: i18n.ts._contextMenu.title,
keywords: ['contextmenu', 'system', 'native'],
},
],
label: i18n.ts.accessibility,
- keywords: ['accessibility'],
+ keywords: ['accessibility', i18n.ts._settings.accessibilityBanner],
path: '/settings/accessibility',
icon: 'ti ti-accessible',
},
From ddbc83b2e46de5c2931ea3ea421408b8182b25ba Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 11 Mar 2025 20:42:06 +0900
Subject: [PATCH 087/348] chore(frontend): tweak settings page
---
packages/frontend/src/pages/settings/index.vue | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 26677a188f..7bbec82757 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -91,11 +91,6 @@ const menuDef = computed(() => [{
text: i18n.ts.emojiPicker,
to: '/settings/emoji-picker',
active: currentPage.value?.route.name === 'emojiPicker',
- }, {
- icon: 'ti ti-cloud',
- text: i18n.ts.drive,
- to: '/settings/drive',
- active: currentPage.value?.route.name === 'drive',
}, {
icon: 'ti ti-bell',
text: i18n.ts.notifications,
@@ -146,6 +141,11 @@ const menuDef = computed(() => [{
}],
}, {
items: [{
+ icon: 'ti ti-cloud',
+ text: i18n.ts.drive,
+ to: '/settings/drive',
+ active: currentPage.value?.route.name === 'drive',
+ }, {
icon: 'ti ti-badges',
text: i18n.ts.roles,
to: '/settings/roles',
From b03bcf26cd89183c86dea73dc8ef30ae68fe2eb2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 11:39:05 +0900
Subject: [PATCH 088/348] =?UTF-8?q?enhance(frontend):=20=E8=A8=AD=E5=AE=9A?=
=?UTF-8?q?=E5=80=A4=E3=81=AE=E5=90=8C=E6=9C=9F=E3=82=92=E5=AE=9F=E8=A3=85?=
=?UTF-8?q?(=E5=AE=9F=E9=A8=93=E7=9A=84)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 1 +
locales/index.d.ts | 24 ++
locales/ja-JP.yml | 6 +
.../src/components/MkPreferenceContainer.vue | 10 +-
packages/frontend/src/preferences.ts | 53 ++-
packages/frontend/src/preferences/def.ts | 1 +
packages/frontend/src/preferences/profile.ts | 321 ++++++++++++++----
packages/frontend/src/preferences/store.ts | 94 -----
packages/frontend/src/preferences/utility.ts | 20 +-
9 files changed, 343 insertions(+), 187 deletions(-)
delete mode 100644 packages/frontend/src/preferences/store.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 748f3aa8eb..d02b37cbdb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
### Client
- Feat: 設定の管理が強化されました
- 自動でバックアップされるように
+ - 任意の設定項目をデバイス間で同期できるように(実験的)
- Enhance: プラグインの管理が強化されました
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 409ad3835b..297b56e289 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5310,6 +5310,30 @@ export interface Locale extends ILocale {
* 復元
*/
"restore": string;
+ /**
+ * デバイス間で同期
+ */
+ "syncBetweenDevices": string;
+ /**
+ * サーバーに設定値が存在します
+ */
+ "preferenceSyncConflictTitle": string;
+ /**
+ * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?
+ */
+ "preferenceSyncConflictText": string;
+ /**
+ * サーバーの設定値
+ */
+ "preferenceSyncConflictChoiceServer": string;
+ /**
+ * デバイスの設定値
+ */
+ "preferenceSyncConflictChoiceDevice": string;
+ /**
+ * 同期の有効化をキャンセル
+ */
+ "preferenceSyncConflictChoiceCancel": string;
"_settings": {
/**
* ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5d24282849..23aeb59863 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1323,6 +1323,12 @@ untitled: "無題"
noName: "名前はありません"
skip: "スキップ"
restore: "復元"
+syncBetweenDevices: "デバイス間で同期"
+preferenceSyncConflictTitle: "サーバーに設定値が存在します"
+preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
+preferenceSyncConflictChoiceServer: "サーバーの設定値"
+preferenceSyncConflictChoiceDevice: "デバイスの設定値"
+preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
_settings:
driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue
index 85fab462cd..7c9484a88c 100644
--- a/packages/frontend/src/components/MkPreferenceContainer.vue
+++ b/packages/frontend/src/components/MkPreferenceContainer.vue
@@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -21,20 +22,21 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import type { PREF_DEF } from '@/preferences/def.js';
import * as os from '@/os.js';
-import { profileManager } from '@/preferences.js';
+import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
k: keyof typeof PREF_DEF;
}>(), {
});
-const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k));
+const isAccountOverrided = ref(prefer.isAccountOverrided(props.k));
+const isSyncEnabled = ref(prefer.isSyncEnabled(props.k));
function showMenu(ev: MouseEvent) {
const i = window.setInterval(() => {
- isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
+ isAccountOverrided.value = prefer.isAccountOverrided(props.k);
}, 100);
- os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
+ os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
onClosing: () => {
window.clearInterval(i);
},
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index a38f1a2a33..ab234a926a 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -4,16 +4,17 @@
*/
import { v4 as uuid } from 'uuid';
-import type { PreferencesProfile } from '@/preferences/profile.js';
+import type { PreferencesProfile, StorageProvider } from '@/preferences/profile.js';
import { cloudBackup } from '@/preferences/utility.js';
import { miLocalStorage } from '@/local-storage.js';
import { ProfileManager } from '@/preferences/profile.js';
import { store } from '@/store.js';
import { $i } from '@/account.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
const TAB_ID = uuid();
-function createProfileManager() {
+function createProfileManager(storageProvider: StorageProvider) {
let profile: PreferencesProfile;
const savedProfileRaw = miLocalStorage.getItem('preferences');
@@ -24,15 +25,44 @@ function createProfileManager() {
profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw));
}
- return new ProfileManager(profile);
+ return new ProfileManager(profile, storageProvider);
}
-export const profileManager = createProfileManager();
-profileManager.addListener('updated', ({ profile: p }) => {
- miLocalStorage.setItem('preferences', JSON.stringify(p));
- miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
-});
-export const prefer = profileManager.store;
+const storageProvider: StorageProvider = {
+ save: (ctx) => {
+ miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile));
+ miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
+ },
+ cloudGet: async (ctx) => {
+ // TODO: この取得方法だとアカウントが変わると保存場所も変わってしまうので改修する
+ // 例えば複数アカウントある場合でも設定値を保存するための「プライマリアカウント」を設定できるようにするとか
+ // TODO: keyのcondに応じた取得
+ try {
+ const value = await misskeyApi('i/registry/get', {
+ scope: ['client', 'preferences', 'sync'],
+ key: ctx.key,
+ });
+ return {
+ value,
+ };
+ } catch (err: any) {
+ if (err.code === 'NO_SUCH_KEY') {
+ return null;
+ } else {
+ throw err;
+ }
+ }
+ },
+ cloudSet: async (ctx) => {
+ await misskeyApi('i/registry/set', {
+ scope: ['client', 'preferences', 'sync'],
+ key: ctx.key,
+ value: ctx.value,
+ });
+ },
+};
+
+export const prefer = createProfileManager(storageProvider);
let latestSyncedAt = Date.now();
@@ -46,7 +76,7 @@ function syncBetweenTabs() {
if (latestTab === TAB_ID) return;
if (latestAt <= latestSyncedAt) return;
- profileManager.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
+ prefer.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
latestSyncedAt = Date.now();
@@ -67,7 +97,7 @@ window.setInterval(() => {
if ($i == null) return;
if (!store.s.enablePreferencesAutoCloudBackup) return;
if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
- if (profileManager.profile.modifiedAt <= latestBackupAt) return;
+ if (prefer.profile.modifiedAt <= latestBackupAt) return;
cloudBackup().then(() => {
latestBackupAt = Date.now();
@@ -75,7 +105,6 @@ window.setInterval(() => {
}, 1000 * 60 * 3);
if (_DEV_) {
- (window as any).profileManager = profileManager;
(window as any).prefer = prefer;
(window as any).cloudBackup = cloudBackup;
}
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 68e0b08f92..b75b99d6b5 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -327,4 +327,5 @@ export const PREF_DEF = {
} satisfies Record
;
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
index defa2747eb..fc8057540a 100644
--- a/packages/frontend/src/preferences/profile.ts
+++ b/packages/frontend/src/preferences/profile.ts
@@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ref, watch } from 'vue';
+import { computed, onUnmounted, ref, watch } from 'vue';
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 { Ref, WritableComputedRef } from 'vue';
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 * as os from '@/os.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@@ -24,11 +24,41 @@ type PREF = typeof PREF_DEF;
type ValueOf = PREF[K]['default'];
type Account = string; // /
-type Cond = {
+type Cond = Partial<{
server: string | null; // 将来のため
account: Account | null;
device: string | null; // 将来のため
-};
+}>;
+
+type ValueMeta = Partial<{
+ sync: boolean;
+}>;
+
+type PrefRecord = [cond: Cond, value: ValueOf, meta: ValueMeta];
+
+function parseCond(cond: Cond): {
+ server: string | null;
+ account: Account | null;
+ device: string | null;
+} {
+ return {
+ server: cond.server ?? null,
+ account: cond.account ?? null,
+ device: cond.device ?? null,
+ };
+}
+
+function makeCond(cond: Partial<{
+ server: string | null;
+ account: Account | null;
+ device: string | null;
+}>): Cond {
+ const c = {} as Cond;
+ if (cond.server != null) c.server = cond.server;
+ if (cond.account != null) c.account = cond.account;
+ if (cond.device != null) c.device = cond.device;
+ return c;
+}
export type PreferencesProfile = {
id: string;
@@ -37,53 +67,119 @@ export type PreferencesProfile = {
modifiedAt: number;
name: string;
preferences: {
- [K in keyof PREF]: [Cond, ValueOf][];
+ [K in keyof PREF]: PrefRecord[];
};
- syncByAccount: [Account, keyof PREF][],
};
-// TODO: 任意のプロパティをデバイス間で同期できるようにする?
+export type StorageProvider = {
+ save: (ctx: { profile: PreferencesProfile; }) => void;
+ cloudGet: (ctx: { key: K; }) => Promise<{ value: ValueOf; } | null>;
+ cloudSet: (ctx: { key: K; value: ValueOf; }) => Promise;
+};
-export class ProfileManager extends EventEmitter<{
- updated: (ctx: {
- profile: PreferencesProfile
- }) => void;
-}> {
+export class ProfileManager {
+ private storageProvider: StorageProvider;
public profile: PreferencesProfile;
- public store: Store<{
- [K in keyof PREF]: ValueOf;
- }>;
- constructor(profile: PreferencesProfile) {
- super();
+ /**
+ * static / state の略 (static が予約語のため)
+ */
+ public s = {} as {
+ [K in keyof PREF]: ValueOf;
+ };
+
+ /**
+ * reactive の略
+ */
+ public r = {} as {
+ [K in keyof PREF]: Ref>;
+ };
+
+ constructor(profile: PreferencesProfile, storageProvider: StorageProvider) {
this.profile = profile;
+ this.storageProvider = storageProvider;
const states = this.genStates();
- this.store = new Store(states);
- this.store.addListener('updated', ({ key, value }) => {
- console.log('prefer:set', key, value);
+ for (const key in states) {
+ this.s[key] = states[key];
+ this.r[key] = ref(this.s[key]);
+ }
- const record = this.getMatchedRecord(key);
- if (record[0].account == null && PREF_DEF[key].accountDependent) {
- this.profile.preferences[key].push([{
- server: null,
- account: `${host}/${$i!.id}`,
- device: null,
- }, value]);
- this.save();
- return;
- }
+ this.fetchCloudValues();
- record[1] = value;
+ // TODO: 定期的にクラウドの値をフェッチ
+ }
+
+ private rewriteRawState(key: K, value: ValueOf) {
+ const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
+ this.r[key].value = this.s[key] = v;
+ }
+
+ public commit(key: K, value: ValueOf) {
+ console.log('prefer:commit', key, value);
+
+ this.rewriteRawState(key, value);
+
+ const record = this.getMatchedRecord(key);
+ if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) {
+ this.profile.preferences[key].push([makeCond({
+ account: `${host}/${$i!.id}`,
+ }), value, {}]);
this.save();
+ return;
+ }
+
+ if (record[2].sync) {
+ // awaitの必要なし
+ // TODO: リクエストを間引く
+ this.storageProvider.cloudSet({ key, value });
+ }
+
+ record[1] = value;
+ this.save();
+ }
+
+ /**
+ * 特定のキーの、簡易的なcomputed refを作ります
+ * 主にvue上で設定コントロールのmodelとして使う用
+ */
+ public model = ValueOf>(
+ key: K,
+ getter?: (v: ValueOf) => V,
+ setter?: (v: V) => ValueOf,
+ ): WritableComputedRef {
+ 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.commit(key, val);
+ valueRef.value = val;
+ },
});
}
private genStates() {
const states = {} as { [K in keyof PREF]: ValueOf };
- let key: keyof PREF;
- for (key in PREF_DEF) {
+ for (const key in PREF_DEF) {
const record = this.getMatchedRecord(key);
states[key] = record[1];
}
@@ -91,15 +187,37 @@ export class ProfileManager extends EventEmitter<{
return states;
}
+ private fetchCloudValues() {
+ // TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
+
+ const promises: Promise[] = [];
+ for (const key in PREF_DEF) {
+ const record = this.getMatchedRecord(key);
+ if (record[2].sync) {
+ const getting = this.storageProvider.cloudGet({ key });
+ promises.push(getting.then((res) => {
+ if (res == null) return;
+ const value = res.value;
+ if (value !== this.s[key]) {
+ this.rewriteRawState(key, value);
+ record[1] = value;
+ console.log('cloud fetched', key, value);
+ }
+ }));
+ }
+ }
+ Promise.all(promises).then(() => {
+ console.log('cloud fetched all');
+ this.save();
+
+ console.log(this.s.showFixedPostForm, this.r.showFixedPostForm.value);
+ });
+ }
+
public static newProfile(): PreferencesProfile {
const data = {} as PreferencesProfile['preferences'];
- let key: keyof PREF;
- for (key in PREF_DEF) {
- data[key] = [[{
- server: null,
- account: null,
- device: null,
- }, PREF_DEF[key].default]];
+ for (const key in PREF_DEF) {
+ data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
}
return {
id: uuid(),
@@ -108,29 +226,31 @@ export class ProfileManager extends EventEmitter<{
modifiedAt: Date.now(),
name: '',
preferences: data,
- syncByAccount: [],
};
}
- public static normalizeProfile(profile: any): PreferencesProfile {
+ public static normalizeProfile(profileLike: any): PreferencesProfile {
const data = {} as PreferencesProfile['preferences'];
- let key: keyof PREF;
- for (key in PREF_DEF) {
- const records = profile.preferences[key];
+ for (const key in PREF_DEF) {
+ const records = profileLike.preferences[key];
if (records == null || records.length === 0) {
- data[key] = [[{
- server: null,
- account: null,
- device: null,
- }, PREF_DEF[key].default]];
+ data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
continue;
} else {
data[key] = records;
+
+ // alpha段階ではmetaが無かったのでマイグレート
+ // TODO: そのうち消す
+ for (const record of data[key] as any[][]) {
+ if (record.length === 2) {
+ record.push({});
+ }
+ }
}
}
return {
- ...profile,
+ ...profileLike,
preferences: data,
};
}
@@ -138,24 +258,24 @@ export class ProfileManager extends EventEmitter<{
public save() {
this.profile.modifiedAt = Date.now();
this.profile.version = version;
- this.emit('updated', { profile: this.profile });
+ this.storageProvider.save({ profile: this.profile });
}
- public getMatchedRecord(key: K): [Cond, ValueOf] {
+ public getMatchedRecord(key: K): PrefRecord {
const records = this.profile.preferences[key];
- if ($i == null) return records.find(([cond, v]) => cond.account == null)!;
+ if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!;
- const accountOverrideRecord = records.find(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+ const accountOverrideRecord = records.find(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
if (accountOverrideRecord) return accountOverrideRecord;
- const record = records.find(([cond, v]) => cond.account == null);
+ const record = records.find(([cond, v]) => parseCond(cond).account == null);
return record!;
}
public isAccountOverrided(key: K): boolean {
if ($i == null) return false;
- return this.profile.preferences[key].some(([cond, v]) => cond.account === `${host}/${$i!.id}`) ?? false;
+ return this.profile.preferences[key].some(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`) ?? false;
}
public setAccountOverride(key: K) {
@@ -164,11 +284,9 @@ export class ProfileManager extends EventEmitter<{
if (this.isAccountOverrided(key)) return;
const records = this.profile.preferences[key];
- records.push([{
- server: null,
+ records.push([makeCond({
account: `${host}/${$i!.id}`,
- device: null,
- }, this.store.s[key]]);
+ }), this.s[key], {}]);
this.save();
}
@@ -179,16 +297,67 @@ export class ProfileManager extends EventEmitter<{
const records = this.profile.preferences[key];
- const index = records.findIndex(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+ const index = records.findIndex(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
if (index === -1) return;
records.splice(index, 1);
- this.store.rewrite(key, this.getMatchedRecord(key)[1]);
+ this.rewriteRawState(key, this.getMatchedRecord(key)[1]);
this.save();
}
+ public isSyncEnabled(key: K): boolean {
+ return this.getMatchedRecord(key)[2].sync ?? false;
+ }
+
+ public async enableSync(key: K): Promise<{ enabled: boolean; } | null> {
+ if (this.isSyncEnabled(key)) return Promise.resolve(null);
+
+ const existing = await this.storageProvider.cloudGet({ key });
+ if (existing != null) {
+ const { canceled, result } = await os.select({
+ title: i18n.ts.preferenceSyncConflictTitle,
+ text: i18n.ts.preferenceSyncConflictText,
+ items: [{
+ text: i18n.ts.preferenceSyncConflictChoiceServer,
+ value: 'remote',
+ }, {
+ text: i18n.ts.preferenceSyncConflictChoiceDevice,
+ value: 'local',
+ }, {
+ text: i18n.ts.preferenceSyncConflictChoiceCancel,
+ value: null,
+ }],
+ default: 'remote',
+ });
+ if (canceled || result == null) return { enabled: false };
+
+ if (result === 'remote') {
+ this.commit(key, existing.value);
+ } else if (result === 'local') {
+ // nop
+ }
+ }
+
+ const record = this.getMatchedRecord(key);
+ record[2].sync = true;
+ this.save();
+
+ // awaitの必要性は無い
+ this.storageProvider.cloudSet({ key, value: this.s[key] });
+
+ return { enabled: true };
+ }
+
+ public disableSync(key: K) {
+ if (!this.isSyncEnabled(key)) return;
+
+ const record = this.getMatchedRecord(key);
+ delete record[2].sync;
+ this.save();
+ }
+
public renameProfile(name: string) {
this.profile.name = name;
this.save();
@@ -198,13 +367,14 @@ export class ProfileManager extends EventEmitter<{
this.profile = profile;
const states = this.genStates();
for (const key in states) {
- this.store.rewrite(key, states[key]);
+ this.rewriteRawState(key, states[key]);
}
+
+ this.fetchCloudValues();
}
public getPerPrefMenu(key: K): MenuItem[] {
const overrideByAccount = ref(this.isAccountOverrided(key));
-
watch(overrideByAccount, () => {
if (overrideByAccount.value) {
this.setAccountOverride(key);
@@ -213,6 +383,18 @@ export class ProfileManager extends EventEmitter<{
}
});
+ const sync = ref(this.isSyncEnabled(key));
+ watch(sync, () => {
+ if (sync.value) {
+ this.enableSync(key).then((res) => {
+ if (res == null) return;
+ if (!res.enabled) sync.value = false;
+ });
+ } else {
+ this.disableSync(key);
+ }
+ });
+
return [{
icon: 'ti ti-copy',
text: i18n.ts.copyPreferenceId,
@@ -224,7 +406,7 @@ export class ProfileManager extends EventEmitter<{
text: i18n.ts.resetToDefaultValue,
danger: true,
action: () => {
- this.store.commit(key, PREF_DEF[key].default);
+ this.commit(key, PREF_DEF[key].default);
},
}, {
type: 'divider',
@@ -233,6 +415,11 @@ export class ProfileManager extends EventEmitter<{
icon: 'ti ti-user-cog',
text: i18n.ts.overrideByAccount,
ref: overrideByAccount,
+ }, {
+ type: 'switch',
+ icon: 'ti ti-cloud-cog',
+ text: i18n.ts.syncBetweenDevices,
+ ref: sync,
}];
}
}
diff --git a/packages/frontend/src/preferences/store.ts b/packages/frontend/src/preferences/store.ts
deleted file mode 100644
index e061021be3..0000000000
--- a/packages/frontend/src/preferences/store.ts
+++ /dev/null
@@ -1,94 +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> = {
-// [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> = {
- updated: (ctx: {
- key: K;
- value: Data[K];
- }) => void;
-};
-
-export class Store> extends EventEmitter> {
- /**
- * static / state の略 (static が予約語のため)
- */
- public s = {} as {
- [K in keyof Data]: Data[K];
- };
-
- /**
- * reactive の略
- */
- public r = {} as {
- [K in keyof Data]: Ref;
- };
-
- 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 commit(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 });
- }
-
- public rewrite(key: K, value: Data[K]) {
- const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
- this.r[key].value = this.s[key] = v;
- }
-
- /**
- * 特定のキーの、簡易的なcomputed refを作ります
- * 主にvue上で設定コントロールのmodelとして使う用
- */
- public model(
- key: K,
- getter?: (v: Data[K]) => V,
- setter?: (v: V) => Data[K],
- ): WritableComputedRef {
- 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.commit(key, val);
- valueRef.value = val;
- },
- });
- }
-}
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index 64b2bde4de..fc6eff5f49 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -9,7 +9,7 @@ import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
-import { prefer, profileManager } from '@/preferences.js';
+import { prefer } from '@/preferences.js';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { $i } from '@/account.js';
@@ -17,7 +17,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { unisonReload } from '@/utility/unison-reload.js';
function canAutoBackup() {
- return profileManager.profile.name != null && profileManager.profile.name.trim() !== '';
+ return prefer.profile.name != null && prefer.profile.name.trim() !== '';
}
export function getPreferencesProfileMenu(): MenuItem[] {
@@ -42,7 +42,7 @@ export function getPreferencesProfileMenu(): MenuItem[] {
const menu: MenuItem[] = [{
type: 'label',
- text: profileManager.profile.name || `(${i18n.ts.noName})`,
+ text: prefer.profile.name || `(${i18n.ts.noName})`,
}, {
text: i18n.ts.rename,
icon: 'ti ti-pencil',
@@ -83,7 +83,7 @@ export function getPreferencesProfileMenu(): MenuItem[] {
text: 'Copy profile as text',
icon: 'ti ti-clipboard',
action: () => {
- copyToClipboard(JSON.stringify(profileManager.profile, null, '\t'));
+ copyToClipboard(JSON.stringify(prefer.profile, null, '\t'));
},
});
}
@@ -95,16 +95,16 @@ async function renameProfile() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._preferencesProfile.profileName,
text: i18n.ts._preferencesProfile.profileNameDescription + '\n' + i18n.ts._preferencesProfile.profileNameDescription2,
- placeholder: profileManager.profile.name || null,
- default: profileManager.profile.name || null,
+ placeholder: prefer.profile.name || null,
+ default: prefer.profile.name || null,
});
if (canceled || name == null || name.trim() === '') return;
- profileManager.renameProfile(name);
+ prefer.renameProfile(name);
}
function exportCurrentProfile() {
- const p = profileManager.profile;
+ const p = prefer.profile;
const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' });
const dummya = document.createElement('a');
dummya.href = URL.createObjectURL(txtBlob);
@@ -140,8 +140,8 @@ export async function cloudBackup() {
await misskeyApi('i/registry/set', {
scope: ['client', 'preferences', 'backups'],
- key: profileManager.profile.name,
- value: profileManager.profile,
+ key: prefer.profile.name,
+ value: prefer.profile,
});
}
From ffade9740ea759150e4a0f6c50d7928331f358e9 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 12 Mar 2025 03:03:37 +0000
Subject: [PATCH 089/348] Bump version to 2025.3.2-alpha.7
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index d6324ca38c..591aa2a573 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2025.3.2-alpha.6",
+ "version": "2025.3.2-alpha.7",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index d9d7429e9a..d44e70c4f9 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.3.2-alpha.6",
+ "version": "2025.3.2-alpha.7",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
From caab1ec7c3fbd6b8924a23afb1d934838b28e8ad Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 13:04:41 +0900
Subject: [PATCH 090/348] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/components/MkButton.vue | 12 ++++++------
packages/frontend/src/components/MkMenu.vue | 10 +++++-----
.../src/components/MkPreferenceContainer.vue | 1 +
3 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 311facb4aa..667e624853 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -220,28 +220,28 @@ function onMousedown(evt: MouseEvent): void {
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
- background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
}
&:not(:disabled):active {
- background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
}
}
&.danger {
font-weight: bold;
- color: #ff2a2a;
+ color: var(--MI_THEME-error);
&.primary {
color: #fff;
- background: #ff2a2a;
+ background: var(--MI_THEME-error);
&:not(:disabled):hover {
- background: #ff4242;
+ background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
}
&:not(:disabled):active {
- background: #d42e2e;
+ background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
}
}
}
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 61b3fa2fee..aa53c19c33 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -177,12 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index ab234a926a..474abe22ab 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';
import type { PreferencesProfile, StorageProvider } from '@/preferences/profile.js';
import { cloudBackup } from '@/preferences/utility.js';
import { miLocalStorage } from '@/local-storage.js';
-import { ProfileManager } from '@/preferences/profile.js';
+import { isSameCond, ProfileManager } from '@/preferences/profile.js';
import { store } from '@/store.js';
import { $i } from '@/account.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -28,22 +28,27 @@ function createProfileManager(storageProvider: StorageProvider) {
return new ProfileManager(profile, storageProvider);
}
+const syncGroup = 'default';
+
const storageProvider: StorageProvider = {
save: (ctx) => {
miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile));
miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
},
+
cloudGet: async (ctx) => {
// TODO: この取得方法だとアカウントが変わると保存場所も変わってしまうので改修する
// 例えば複数アカウントある場合でも設定値を保存するための「プライマリアカウント」を設定できるようにするとか
// TODO: keyのcondに応じた取得
try {
- const value = await misskeyApi('i/registry/get', {
+ const cloudData = await misskeyApi('i/registry/get', {
scope: ['client', 'preferences', 'sync'],
- key: ctx.key,
- });
+ key: syncGroup + ':' + ctx.key,
+ }) as [any, any][];
+ const target = cloudData.find(([cond]) => isSameCond(cond, ctx.cond));
+ if (target == null) return null;
return {
- value,
+ value: target[1],
};
} catch (err: any) {
if (err.code === 'NO_SUCH_KEY') {
@@ -53,11 +58,34 @@ const storageProvider: StorageProvider = {
}
}
},
+
cloudSet: async (ctx) => {
+ let cloudData: [any, any][] = [];
+ try {
+ cloudData = await misskeyApi('i/registry/get', {
+ scope: ['client', 'preferences', 'sync'],
+ key: syncGroup + ':' + ctx.key,
+ }) as [any, any][];
+ } catch (err: any) {
+ if (err.code === 'NO_SUCH_KEY') {
+ cloudData = [];
+ } else {
+ throw err;
+ }
+ }
+
+ const i = cloudData.findIndex(([cond]) => isSameCond(cond, ctx.cond));
+
+ if (i === -1) {
+ cloudData.push([ctx.cond, ctx.value]);
+ } else {
+ cloudData[i] = [ctx.cond, ctx.value];
+ }
+
await misskeyApi('i/registry/set', {
scope: ['client', 'preferences', 'sync'],
- key: ctx.key,
- value: ctx.value,
+ key: syncGroup + ':' + ctx.key,
+ value: cloudData,
});
},
};
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
index fc8057540a..de1c674e5c 100644
--- a/packages/frontend/src/preferences/profile.ts
+++ b/packages/frontend/src/preferences/profile.ts
@@ -60,6 +60,12 @@ function makeCond(cond: Partial<{
return c;
}
+export function isSameCond(a: Cond, b: Cond): boolean {
+ // null と undefined (キー無し) は区別したくないので == で比較
+ // eslint-disable-next-line eqeqeq
+ return a.server == b.server && a.account == b.account && a.device == b.device;
+}
+
export type PreferencesProfile = {
id: string;
version: string;
@@ -73,8 +79,8 @@ export type PreferencesProfile = {
export type StorageProvider = {
save: (ctx: { profile: PreferencesProfile; }) => void;
- cloudGet: (ctx: { key: K; }) => Promise<{ value: ValueOf; } | null>;
- cloudSet: (ctx: { key: K; value: ValueOf; }) => Promise;
+ cloudGet: (ctx: { key: K; cond: Cond; }) => Promise<{ value: ValueOf; } | null>;
+ cloudSet: (ctx: { key: K; cond: Cond; value: ValueOf; }) => Promise;
};
export class ProfileManager {
@@ -121,7 +127,7 @@ export class ProfileManager {
this.rewriteRawState(key, value);
- const record = this.getMatchedRecord(key);
+ const record = this.getMatchedRecordOf(key);
if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) {
this.profile.preferences[key].push([makeCond({
account: `${host}/${$i!.id}`,
@@ -130,14 +136,14 @@ export class ProfileManager {
return;
}
+ record[1] = value;
+ this.save();
+
if (record[2].sync) {
// awaitの必要なし
// TODO: リクエストを間引く
- this.storageProvider.cloudSet({ key, value });
+ this.storageProvider.cloudSet({ key, cond: record[0], value: record[1] });
}
-
- record[1] = value;
- this.save();
}
/**
@@ -180,7 +186,7 @@ export class ProfileManager {
private genStates() {
const states = {} as { [K in keyof PREF]: ValueOf };
for (const key in PREF_DEF) {
- const record = this.getMatchedRecord(key);
+ const record = this.getMatchedRecordOf(key);
states[key] = record[1];
}
@@ -192,9 +198,9 @@ export class ProfileManager {
const promises: Promise[] = [];
for (const key in PREF_DEF) {
- const record = this.getMatchedRecord(key);
+ const record = this.getMatchedRecordOf(key);
if (record[2].sync) {
- const getting = this.storageProvider.cloudGet({ key });
+ const getting = this.storageProvider.cloudGet({ key, cond: record[0] });
promises.push(getting.then((res) => {
if (res == null) return;
const value = res.value;
@@ -261,7 +267,7 @@ export class ProfileManager {
this.storageProvider.save({ profile: this.profile });
}
- public getMatchedRecord(key: K): PrefRecord {
+ public getMatchedRecordOf(key: K): PrefRecord {
const records = this.profile.preferences[key];
if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!;
@@ -302,19 +308,21 @@ export class ProfileManager {
records.splice(index, 1);
- this.rewriteRawState(key, this.getMatchedRecord(key)[1]);
+ this.rewriteRawState(key, this.getMatchedRecordOf(key)[1]);
this.save();
}
public isSyncEnabled(key: K): boolean {
- return this.getMatchedRecord(key)[2].sync ?? false;
+ return this.getMatchedRecordOf(key)[2].sync ?? false;
}
public async enableSync(key: K): Promise<{ enabled: boolean; } | null> {
if (this.isSyncEnabled(key)) return Promise.resolve(null);
- const existing = await this.storageProvider.cloudGet({ key });
+ const record = this.getMatchedRecordOf(key);
+
+ const existing = await this.storageProvider.cloudGet({ key, cond: record[0] });
if (existing != null) {
const { canceled, result } = await os.select({
title: i18n.ts.preferenceSyncConflictTitle,
@@ -340,12 +348,11 @@ export class ProfileManager {
}
}
- const record = this.getMatchedRecord(key);
record[2].sync = true;
this.save();
// awaitの必要性は無い
- this.storageProvider.cloudSet({ key, value: this.s[key] });
+ this.storageProvider.cloudSet({ key, cond: record[0], value: this.s[key] });
return { enabled: true };
}
@@ -353,7 +360,7 @@ export class ProfileManager {
public disableSync(key: K) {
if (!this.isSyncEnabled(key)) return;
- const record = this.getMatchedRecord(key);
+ const record = this.getMatchedRecordOf(key);
delete record[2].sync;
this.save();
}
From a36972179101b2132a27b836d87ff73ae8cc2f87 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 14:35:22 +0900
Subject: [PATCH 092/348] remove todo
---
packages/frontend/src/preferences.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 474abe22ab..27c61349a4 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -39,7 +39,6 @@ const storageProvider: StorageProvider = {
cloudGet: async (ctx) => {
// TODO: この取得方法だとアカウントが変わると保存場所も変わってしまうので改修する
// 例えば複数アカウントある場合でも設定値を保存するための「プライマリアカウント」を設定できるようにするとか
- // TODO: keyのcondに応じた取得
try {
const cloudData = await misskeyApi('i/registry/get', {
scope: ['client', 'preferences', 'sync'],
From e594fb0037f440f589c36c01e0108f011545a681 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 12 Mar 2025 14:37:57 +0900
Subject: [PATCH 093/348] =?UTF-8?q?enhance(dev):=20frontend=E3=81=AE?=
=?UTF-8?q?=E6=A4=9C=E7=B4=A2=E3=82=A4=E3=83=B3=E3=83=87=E3=83=83=E3=82=AF?=
=?UTF-8?q?=E3=82=B9=E4=BD=9C=E6=88=90=E3=82=92=E5=8D=98=E7=8B=AC=E3=81=AE?=
=?UTF-8?q?=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=81=A7=E8=A1=8C=E3=81=88?=
=?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#15653)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 1 +
.../lib/vite-plugin-create-search-index.ts | 31 +++++++++++--------
packages/frontend/package.json | 2 ++
.../frontend/scripts/generate-search-index.ts | 15 +++++++++
packages/frontend/vite-node.config.ts | 3 ++
packages/frontend/vite.config.ts | 19 ++++++++----
pnpm-lock.yaml | 3 ++
7 files changed, 55 insertions(+), 19 deletions(-)
create mode 100644 packages/frontend/scripts/generate-search-index.ts
create mode 100644 packages/frontend/vite-node.config.ts
diff --git a/package.json b/package.json
index 591aa2a573..7142e1f660 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
+ "build-frontend-search-index": "pnpm --filter frontend build-search-index",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
"init": "pnpm migrate",
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index e194872640..d506e84bb6 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -1428,6 +1428,23 @@ async function processVueFile(
};
}
+export async function generateSearchIndex(options: Options, transformedCodeCache: Record = {}) {
+ const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => {
+ const matchedFiles = glob.sync(filePathPattern);
+ return [...acc, ...matchedFiles];
+ }, []);
+
+ for (const filePath of filePaths) {
+ const id = path.resolve(filePath); // 絶対パスに変換
+ const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
+ const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
+ transformedCodeCache = newCache; // キャッシュを更新
+ }
+
+ await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
+
+ return transformedCodeCache; // キャッシュを返す
+}
// Rollup プラグインとして export
export default function pluginCreateSearchIndex(options: Options): Plugin {
@@ -1445,19 +1462,7 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
return;
}
- const filePaths = options.targetFilePaths.reduce((acc, filePathPattern) => {
- const matchedFiles = glob.sync(filePathPattern);
- return [...acc, ...matchedFiles];
- }, []);
-
- for (const filePath of filePaths) {
- const id = path.resolve(filePath); // 絶対パスに変換
- const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
- const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
- transformedCodeCache = newCache; // キャッシュを更新
- }
-
- await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
+ transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
},
async transform(code, id) {
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 946a0be48f..d829a34804 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -5,6 +5,7 @@
"scripts": {
"watch": "vite",
"build": "vite build",
+ "build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
@@ -133,6 +134,7 @@
"start-server-and-test": "2.0.10",
"storybook": "8.6.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
+ "vite-node": "3.0.8",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.0.8",
"vitest-fetch-mock": "0.4.5",
diff --git a/packages/frontend/scripts/generate-search-index.ts b/packages/frontend/scripts/generate-search-index.ts
new file mode 100644
index 0000000000..cbb4bb8c51
--- /dev/null
+++ b/packages/frontend/scripts/generate-search-index.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { searchIndexes } from '../vite.config.js';
+import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
+
+async function main() {
+ for (const searchIndex of searchIndexes) {
+ await generateSearchIndex(searchIndex);
+ }
+}
+
+main();
diff --git a/packages/frontend/vite-node.config.ts b/packages/frontend/vite-node.config.ts
new file mode 100644
index 0000000000..c049f46e10
--- /dev/null
+++ b/packages/frontend/vite-node.config.ts
@@ -0,0 +1,3 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({});
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index a28fc553f4..ec80e71ae4 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -1,7 +1,8 @@
import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
-import { type UserConfig, defineConfig } from 'vite';
+import { defineConfig } from 'vite';
+import type { UserConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
@@ -11,12 +12,22 @@ import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
+import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
+/**
+ * 検索インデックスの生成設定
+ */
+export const searchIndexes = [{
+ targetFilePaths: ['src/pages/settings/*.vue'],
+ exportFilePath: './src/utility/autogen/settings-search-index.ts',
+ verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
+}] satisfies SearchIndexOptions[];
+
/**
* Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。
* CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK
@@ -84,11 +95,7 @@ export function getConfig(): UserConfig {
},
plugins: [
- pluginCreateSearchIndex({
- targetFilePaths: ['src/pages/settings/*.vue'],
- exportFilePath: './src/utility/autogen/settings-search-index.ts',
- verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
- }),
+ ...searchIndexes.map(options => pluginCreateSearchIndex(options)),
pluginVue(),
pluginUnwindCssModuleClassName(),
pluginJson5(),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4b70b273d0..681cf6fb18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1040,6 +1040,9 @@ importers:
storybook-addon-misskey-theme:
specifier: github:misskey-dev/storybook-addon-misskey-theme
version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.6.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/components@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/core-events@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/manager-api@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/preview-api@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/theming@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(@storybook/types@8.6.4(storybook@8.6.4(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ vite-node:
+ specifier: 3.0.8
+ version: 3.0.8(@types/node@22.13.9)(sass@1.85.1)(terser@5.39.0)(tsx@4.19.3)
vite-plugin-turbosnap:
specifier: 1.0.3
version: 1.0.3
From 8508c4dadc51fa597884655195b6fe80ca2d4e08 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 15:07:45 +0900
Subject: [PATCH 094/348] refactor
---
packages/frontend/src/preferences.ts | 15 +++++++
packages/frontend/src/preferences/profile.ts | 42 +++++++++++---------
2 files changed, 38 insertions(+), 19 deletions(-)
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 27c61349a4..bfcd8b8dd7 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -87,6 +87,21 @@ const storageProvider: StorageProvider = {
value: cloudData,
});
},
+
+ cloudGets: async (ctx) => {
+ // TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
+ const fetchings = ctx.needs.map(need => storageProvider.cloudGet(need).then(res => [need.key, res] as const));
+ const cloudDatas = await Promise.all(fetchings);
+
+ const res = {} as Partial>;
+ for (const cloudData of cloudDatas) {
+ if (cloudData[1] != null) {
+ res[cloudData[0]] = cloudData[1].value;
+ }
+ }
+
+ return res;
+ },
};
export const prefer = createProfileManager(storageProvider);
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
index de1c674e5c..2ac4e58d14 100644
--- a/packages/frontend/src/preferences/profile.ts
+++ b/packages/frontend/src/preferences/profile.ts
@@ -79,6 +79,7 @@ export type PreferencesProfile = {
export type StorageProvider = {
save: (ctx: { profile: PreferencesProfile; }) => void;
+ cloudGets: (ctx: { needs: { key: K; cond: Cond; }[] }) => Promise>>>;
cloudGet: (ctx: { key: K; cond: Cond; }) => Promise<{ value: ValueOf; } | null>;
cloudSet: (ctx: { key: K; cond: Cond; value: ValueOf; }) => Promise;
};
@@ -193,31 +194,34 @@ export class ProfileManager {
return states;
}
- private fetchCloudValues() {
- // TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
-
- const promises: Promise[] = [];
+ private async fetchCloudValues() {
+ const needs = [] as { key: keyof PREF; cond: Cond; }[];
for (const key in PREF_DEF) {
const record = this.getMatchedRecordOf(key);
if (record[2].sync) {
- const getting = this.storageProvider.cloudGet({ key, cond: record[0] });
- promises.push(getting.then((res) => {
- if (res == null) return;
- const value = res.value;
- if (value !== this.s[key]) {
- this.rewriteRawState(key, value);
- record[1] = value;
- console.log('cloud fetched', key, value);
- }
- }));
+ needs.push({
+ key,
+ cond: record[0],
+ });
}
}
- Promise.all(promises).then(() => {
- console.log('cloud fetched all');
- this.save();
- console.log(this.s.showFixedPostForm, this.r.showFixedPostForm.value);
- });
+ const cloudValues = await this.storageProvider.cloudGets({ needs });
+
+ for (const key in PREF_DEF) {
+ const record = this.getMatchedRecordOf(key);
+ if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) {
+ const cloudValue = cloudValues[key];
+ if (cloudValue !== this.s[key]) {
+ this.rewriteRawState(key, cloudValue);
+ record[1] = cloudValue;
+ console.log('cloud fetched', key, cloudValue);
+ }
+ }
+ }
+
+ this.save();
+ console.log('cloud fetch completed');
}
public static newProfile(): PreferencesProfile {
From 15685be4cc02a7bc1b15028cdd498de52e35a5ba Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 12 Mar 2025 06:10:35 +0000
Subject: [PATCH 095/348] Bump version to 2025.3.2-alpha.8
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 7142e1f660..8f89108665 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2025.3.2-alpha.7",
+ "version": "2025.3.2-alpha.8",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index d44e70c4f9..021c5a54bd 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.3.2-alpha.7",
+ "version": "2025.3.2-alpha.8",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
From aa1cc2f817b0980609ca44715d72bf341f3c2e91 Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Wed, 12 Mar 2025 16:51:10 +0900
Subject: [PATCH 096/348] fix(storybook): use type-only imports in generated
stories (#15654)
---
packages/frontend/.storybook/generate.tsx | 61 ++++++++++++++++++++---
1 file changed, 53 insertions(+), 8 deletions(-)
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 3cd08191f5..89d4214141 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -17,8 +17,52 @@ interface SatisfiesExpression extends estree.BaseExpression {
reference: estree.Identifier;
}
+interface ImportDeclaration extends estree.ImportDeclaration {
+ kind?: 'type';
+}
+
const generator = {
...GENERATOR,
+ ImportDeclaration(node: ImportDeclaration, state: State) {
+ state.write('import ');
+ if (node.kind === 'type') state.write('type ');
+ const { specifiers } = node;
+ if (specifiers.length > 0) {
+ let i = 0;
+ for (; i < specifiers.length; i++) {
+ if (i > 0) {
+ state.write(', ');
+ }
+ const specifier = specifiers[i]!;
+ if (specifier.type === 'ImportDefaultSpecifier') {
+ state.write(specifier.local.name, specifier);
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
+ state.write(`* as ${specifier.local.name}`, specifier);
+ } else {
+ break;
+ }
+ }
+ if (i < specifiers.length) {
+ state.write('{');
+ for (; i < specifiers.length; i++) {
+ const specifier = specifiers[i]! as estree.ImportSpecifier;
+ const { name } = specifier.imported as estree.Identifier;
+ state.write(name, specifier);
+ if (name !== specifier.local.name) {
+ state.write(` as ${specifier.local.name}`);
+ }
+ if (i < specifiers.length - 1) {
+ state.write(', ');
+ }
+ }
+ state.write('}');
+ }
+ state.write(' from ');
+ }
+ this.Literal(node.source, state);
+
+ state.write(';');
+ },
SatisfiesExpression(node: SatisfiesExpression, state: State) {
switch (node.expression.type) {
case 'ArrowFunctionExpression': {
@@ -62,7 +106,7 @@ type ToKebab = T extends readonly [
: T extends readonly [
infer XH extends string,
...infer XR extends readonly string[]
- ]
+ ]
? `${XH}${XR extends readonly string[] ? `-${ToKebab}` : ''}`
: '';
@@ -132,7 +176,7 @@ function toStories(component: string): Promise {
kind={'init' as const}
shorthand
/> as estree.Property,
- ]
+ ]
: []),
]}
/> as estree.ObjectExpression;
@@ -155,7 +199,8 @@ function toStories(component: string): Promise {
/> as estree.ImportSpecifier,
]),
]}
- /> as estree.ImportDeclaration,
+ kind={'type'}
+ /> as ImportDeclaration,
...(hasMsw
? [
{
local={ as estree.Identifier}
/> as estree.ImportNamespaceSpecifier,
]}
- /> as estree.ImportDeclaration,
- ]
+ /> as ImportDeclaration,
+ ]
: []),
...(hasImplStories
? []
@@ -176,8 +221,8 @@ function toStories(component: string): Promise {
specifiers={[
as estree.ImportDefaultSpecifier,
]}
- /> as estree.ImportDeclaration,
- ]),
+ /> as ImportDeclaration,
+ ]),
...(hasMetaStories
? [
{
local={ as estree.Identifier}
/> as estree.ImportNamespaceSpecifier,
]}
- /> as estree.ImportDeclaration,
+ /> as ImportDeclaration,
]
: []),
Date: Wed, 12 Mar 2025 18:54:36 +0900
Subject: [PATCH 097/348] add todo
---
packages/frontend/src/preferences.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index bfcd8b8dd7..ab73fe2b77 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -50,7 +50,7 @@ const storageProvider: StorageProvider = {
value: target[1],
};
} catch (err: any) {
- if (err.code === 'NO_SUCH_KEY') {
+ if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する
return null;
} else {
throw err;
@@ -66,7 +66,7 @@ const storageProvider: StorageProvider = {
key: syncGroup + ':' + ctx.key,
}) as [any, any][];
} catch (err: any) {
- if (err.code === 'NO_SUCH_KEY') {
+ if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する
cloudData = [];
} else {
throw err;
From 3129fcf164369ecd1ed7fd26e17e0806ada4e435 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 20:17:54 +0900
Subject: [PATCH 098/348] fix(frontend): fix type errors
---
packages/frontend/src/preferences.ts | 4 +--
packages/frontend/src/preferences/def.ts | 9 ++----
.../preferences/{profile.ts => manager.ts} | 30 ++++++++++++++-----
packages/frontend/src/preferences/utility.ts | 2 +-
4 files changed, 28 insertions(+), 17 deletions(-)
rename packages/frontend/src/preferences/{profile.ts => manager.ts} (92%)
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index ab73fe2b77..7d1821b72b 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -4,10 +4,10 @@
*/
import { v4 as uuid } from 'uuid';
-import type { PreferencesProfile, StorageProvider } from '@/preferences/profile.js';
+import type { PreferencesProfile, StorageProvider } from '@/preferences/manager.js';
import { cloudBackup } from '@/preferences/utility.js';
import { miLocalStorage } from '@/local-storage.js';
-import { isSameCond, ProfileManager } from '@/preferences/profile.js';
+import { isSameCond, ProfileManager } from '@/preferences/manager.js';
import { store } from '@/store.js';
import { $i } from '@/account.js';
import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index b75b99d6b5..47d0ab5cbc 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -9,7 +9,8 @@ import type { Theme } from '@/theme.js';
import type { SoundType } from '@/utility/sound.js';
import type { Plugin } from '@/plugin.js';
import type { DeviceKind } from '@/utility/device-kind.js';
-import type { Column, DeckProfile } from '@/deck.js';
+import type { DeckProfile } from '@/deck.js';
+import type { PreferencesDefinition } from './manager.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
/** サウンド設定 */
@@ -324,8 +325,4 @@ export const PREF_DEF = {
sfxVolume: 1,
},
},
-} satisfies Record;
+} satisfies PreferencesDefinition;
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/manager.ts
similarity index 92%
rename from packages/frontend/src/preferences/profile.ts
rename to packages/frontend/src/preferences/manager.ts
index 2ac4e58d14..9866227d93 100644
--- a/packages/frontend/src/preferences/profile.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -84,6 +84,12 @@ export type StorageProvider = {
cloudSet: (ctx: { key: K; cond: Cond; value: ValueOf; }) => Promise;
};
+export type PreferencesDefinition = Record;
+
export class ProfileManager {
private storageProvider: StorageProvider;
public profile: PreferencesProfile;
@@ -118,6 +124,10 @@ export class ProfileManager {
// TODO: 定期的にクラウドの値をフェッチ
}
+ private isAccountDependentKey(key: K): boolean {
+ return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true;
+ }
+
private rewriteRawState(key: K, value: ValueOf) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
@@ -129,7 +139,7 @@ export class ProfileManager {
this.rewriteRawState(key, value);
const record = this.getMatchedRecordOf(key);
- if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) {
+ if (parseCond(record[0]).account == null && this.isAccountDependentKey(key)) {
this.profile.preferences[key].push([makeCond({
account: `${host}/${$i!.id}`,
}), value, {}]);
@@ -186,9 +196,10 @@ export class ProfileManager {
private genStates() {
const states = {} as { [K in keyof PREF]: ValueOf };
- for (const key in PREF_DEF) {
+ for (const _key in PREF_DEF) {
+ const key = _key as keyof PREF;
const record = this.getMatchedRecordOf(key);
- states[key] = record[1];
+ (states[key] as any) = record[1];
}
return states;
@@ -196,7 +207,8 @@ export class ProfileManager {
private async fetchCloudValues() {
const needs = [] as { key: keyof PREF; cond: Cond; }[];
- for (const key in PREF_DEF) {
+ for (const _key in PREF_DEF) {
+ const key = _key as keyof PREF;
const record = this.getMatchedRecordOf(key);
if (record[2].sync) {
needs.push({
@@ -208,7 +220,8 @@ export class ProfileManager {
const cloudValues = await this.storageProvider.cloudGets({ needs });
- for (const key in PREF_DEF) {
+ for (const _key in PREF_DEF) {
+ const key = _key as keyof PREF;
const record = this.getMatchedRecordOf(key);
if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) {
const cloudValue = cloudValues[key];
@@ -290,7 +303,7 @@ export class ProfileManager {
public setAccountOverride(key: K) {
if ($i == null) return;
- if (PREF_DEF[key].accountDependent) throw new Error('already account-dependent');
+ if (this.isAccountDependentKey(key)) throw new Error('already account-dependent');
if (this.isAccountOverrided(key)) return;
const records = this.profile.preferences[key];
@@ -303,7 +316,7 @@ export class ProfileManager {
public clearAccountOverride(key: K) {
if ($i == null) return;
- if (PREF_DEF[key].accountDependent) throw new Error('cannot clear override for this account-dependent property');
+ if (this.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property');
const records = this.profile.preferences[key];
@@ -377,7 +390,8 @@ export class ProfileManager {
public rewriteProfile(profile: PreferencesProfile) {
this.profile = profile;
const states = this.genStates();
- for (const key in states) {
+ for (const _key in states) {
+ const key = _key as keyof PREF;
this.rewriteRawState(key, states[key]);
}
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index fc6eff5f49..c37dbcf96b 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -4,7 +4,7 @@
*/
import { ref, watch } from 'vue';
-import type { PreferencesProfile } from './profile.js';
+import type { PreferencesProfile } from './manager.js';
import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
From a06b9eefaa550b2fa67ad661384d431cc842bfc2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 21:05:39 +0900
Subject: [PATCH 099/348] enhance(frontend): suppress needless confirmation
when turn on pref sync
---
packages/frontend/src/preferences/manager.ts | 3 +-
packages/frontend/src/utility/deep-equal.ts | 29 ++++++++++++++++++++
2 files changed, 31 insertions(+), 1 deletion(-)
create mode 100644 packages/frontend/src/utility/deep-equal.ts
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index 9866227d93..3f3eba6389 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -13,6 +13,7 @@ import { $i } from '@/account.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { deepEqual } from '@/utility/deep-equal.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@@ -340,7 +341,7 @@ export class ProfileManager {
const record = this.getMatchedRecordOf(key);
const existing = await this.storageProvider.cloudGet({ key, cond: record[0] });
- if (existing != null) {
+ if (existing != null && !deepEqual(existing.value, record[1])) {
const { canceled, result } = await os.select({
title: i18n.ts.preferenceSyncConflictTitle,
text: i18n.ts.preferenceSyncConflictText,
diff --git a/packages/frontend/src/utility/deep-equal.ts b/packages/frontend/src/utility/deep-equal.ts
new file mode 100644
index 0000000000..c64d82c6cc
--- /dev/null
+++ b/packages/frontend/src/utility/deep-equal.ts
@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function deepEqual(a: any, b: any): boolean {
+ if (a === b) return true;
+
+ if (a === null) return b === null;
+
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (!deepEqual(a[i], b[i])) return false;
+ }
+ return true;
+ } else if (((typeof a) === 'object') && ((typeof b) === 'object')) {
+ const aks = Object.keys(a);
+ const bks = Object.keys(b);
+ if (aks.length !== bks.length) return false;
+ for (let i = 0; i < aks.length; i++) {
+ const k = aks[i];
+ if (!deepEqual(a[k], b[k])) return false;
+ }
+ return true;
+ }
+
+ return false;
+}
From 4a73feb041a554e813e490cc5cd63c105e142623 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 12 Mar 2025 21:12:08 +0900
Subject: [PATCH 100/348] enhance(frontend): make deck profiles syncable
---
locales/index.d.ts | 4 ++
locales/ja-JP.yml | 1 +
packages/frontend/src/pages/settings/deck.vue | 40 ++++++++++++++-----
.../utility/autogen/settings-search-index.ts | 7 ++++
4 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 297b56e289..a4233cf7c8 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9848,6 +9848,10 @@ export interface Locale extends ILocale {
* 幅を自動調整
*/
"flexible": string;
+ /**
+ * プロファイル情報のデバイス間同期を有効にする
+ */
+ "enableSyncBetweenDevicesForProfiles": string;
"_columns": {
/**
* メイン
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 23aeb59863..c45553817d 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2603,6 +2603,7 @@ _deck:
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
flexible: "幅を自動調整"
+ enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
_columns:
main: "メイン"
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index e7c5c942e9..2c4ec01344 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -4,23 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
-
{{ i18n.ts._deck.useSimpleUiForNonRootPages }}
+
+
+ {{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}
- {{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}
+ {{ i18n.ts._deck.useSimpleUiForNonRootPages }}
- {{ i18n.ts._deck.alwaysShowMainColumn }}
+ {{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}
-
- {{ i18n.ts._deck.columnAlign }}
-
-
-
-
+ {{ i18n.ts._deck.alwaysShowMainColumn }}
+
+
+ {{ i18n.ts._deck.columnAlign }}
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
new file mode 100644
index 0000000000..398228e226
--- /dev/null
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+ {{ i18n.ts._emojiPalette.palettes }}
+
+
+ updatePaletteEmojis(palette.id, emojis)"
+ @updateName="name => updatePaletteName(palette.id, name)"
+ @del="delPalette(palette.id)"
+ />
+
+
+
+
+
+
+
+
+ {{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._emojiPalette.paletteForMain }}
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._emojiPalette.paletteForReaction }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.emojiPickerDisplay }}
+
+
+
+
+
+ {{ i18n.ts.size }}
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.numberOfColumn }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.height }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.style }}
+ {{ i18n.ts.needReloadToApply }}
+
+
+
+
+
+
+
+ {{ i18n.ts.preview }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
deleted file mode 100644
index d8f27078ae..0000000000
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ /dev/null
@@ -1,288 +0,0 @@
-
-
-
-
-
-
- {{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})
- {{ i18n.ts.pinnedEmojisForReactionSettingDescription }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ i18n.ts.reactionSettingDescription2 }}
-
-
-
- {{ i18n.ts.preview }}
- {{ i18n.ts.default }}
- {{ i18n.ts.overwriteFromPinnedEmojis }}
-
-
-
-
-
-
- {{ i18n.ts.pinned }} ({{ i18n.ts.general }})
- {{ i18n.ts.pinnedEmojisSettingDescription }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ i18n.ts.reactionSettingDescription2 }}
-
-
-
- {{ i18n.ts.preview }}
- {{ i18n.ts.default }}
- {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}
-
-
-
-
-
- {{ i18n.ts.emojiPickerDisplay }}
-
-
-
-
- {{ i18n.ts.size }}
-
-
-
-
-
-
-
-
- {{ i18n.ts.numberOfColumn }}
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.height }}
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.style }}
- {{ i18n.ts.needReloadToApply }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 7bbec82757..debcd4bd3e 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -86,11 +86,6 @@ const menuDef = computed(() => [{
text: i18n.ts.privacy,
to: '/settings/privacy',
active: currentPage.value?.route.name === 'privacy',
- }, {
- icon: 'ti ti-mood-happy',
- text: i18n.ts.emojiPicker,
- to: '/settings/emoji-picker',
- active: currentPage.value?.route.name === 'emojiPicker',
}, {
icon: 'ti ti-bell',
text: i18n.ts.notifications,
@@ -118,6 +113,11 @@ const menuDef = computed(() => [{
text: i18n.ts.theme,
to: '/settings/theme',
active: currentPage.value?.route.name === 'theme',
+ }, {
+ icon: 'ti ti-mood-happy',
+ text: i18n.ts.emojiPalette,
+ to: '/settings/emoji-palette',
+ active: currentPage.value?.route.name === 'emoji-palette',
}, {
icon: 'ti ti-device-desktop',
text: i18n.ts.appearance,
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 47d0ab5cbc..6a926c4b26 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -29,6 +29,8 @@ export type SoundStore = {
volume: number;
};
+// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
+
export const PREF_DEF = {
pinnedUserLists: {
accountDependent: true,
@@ -56,6 +58,27 @@ export const PREF_DEF = {
default: [] as DeckProfile[],
},
+ emojiPalettes: {
+ serverDependent: true,
+ default: [{
+ id: 'a',
+ name: '',
+ emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+ }] as {
+ id: string;
+ name: string;
+ emojis: string[];
+ }[],
+ },
+ emojiPaletteForReaction: {
+ serverDependent: true,
+ default: null as string | null,
+ },
+ emojiPaletteForMain: {
+ serverDependent: true,
+ default: null as string | null,
+ },
+
overridedDeviceKind: {
default: null as DeviceKind | null,
},
@@ -180,13 +203,13 @@ export const PREF_DEF = {
default: 'remote' as 'none' | 'remote' | 'always',
},
emojiPickerScale: {
- default: 1,
+ default: 2,
},
emojiPickerWidth: {
- default: 1,
+ default: 2,
},
emojiPickerHeight: {
- default: 2,
+ default: 3,
},
emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 93dd081127..752356497e 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -66,9 +66,9 @@ const routes: RouteDef[] = [{
name: 'privacy',
component: page(() => import('@/pages/settings/privacy.vue')),
}, {
- path: '/emoji-picker',
- name: 'emojiPicker',
- component: page(() => import('@/pages/settings/emoji-picker.vue')),
+ path: '/emoji-palette',
+ name: 'emoji-palette',
+ component: page(() => import('@/pages/settings/emoji-palette.vue')),
}, {
path: '/drive',
name: 'drive',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 738a57d233..6eebcd1ead 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -39,14 +39,6 @@ export const store = markRaw(new Storage('base', {
where: 'account',
default: null,
},
- reactions: {
- where: 'account',
- default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
- },
- pinnedEmojis: {
- where: 'account',
- default: [],
- },
reactionAcceptance: {
where: 'account',
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
@@ -127,6 +119,14 @@ export const store = markRaw(new Storage('base', {
},
//#region TODO: そのうち消す (preferに移行済み)
+ reactions: {
+ where: 'account',
+ default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+ },
+ pinnedEmojis: {
+ where: 'account',
+ default: [],
+ },
widgets: {
where: 'account',
default: [] as {
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 52100ab639..4f1a94f266 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -536,6 +536,57 @@ export const searchIndexes: SearchIndexItem[] = [
path: '/settings/mute-block',
icon: 'ti ti-ban',
},
+ {
+ id: 'yR1OSyLiT',
+ children: [
+ {
+ id: 'yMJzyzOUk',
+ label: i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes,
+ keywords: ['sync', 'palettes', 'devices'],
+ },
+ {
+ id: 'wCE09vgZr',
+ label: i18n.ts._emojiPalette.paletteForMain,
+ keywords: ['main', 'palette'],
+ },
+ {
+ id: 'uCzRPrSNx',
+ label: i18n.ts._emojiPalette.paletteForReaction,
+ keywords: ['reaction', 'palette'],
+ },
+ {
+ id: 'hgQr28WUk',
+ children: [
+ {
+ id: 'fY04NIHSQ',
+ label: i18n.ts.size,
+ keywords: ['emoji', 'picker', 'scale', 'size'],
+ },
+ {
+ id: '3j7vlaL7t',
+ label: i18n.ts.numberOfColumn,
+ keywords: ['emoji', 'picker', 'width', 'column', 'size'],
+ },
+ {
+ id: 'zPX8z1Bcy',
+ label: i18n.ts.height,
+ keywords: ['emoji', 'picker', 'height', 'size'],
+ },
+ {
+ id: '2CSkZa4tl',
+ label: i18n.ts.style,
+ keywords: ['emoji', 'picker', 'style'],
+ },
+ ],
+ label: i18n.ts.emojiPickerDisplay,
+ keywords: ['emoji', 'picker', 'display'],
+ },
+ ],
+ label: i18n.ts.emojiPalette,
+ keywords: ['emoji', 'palette'],
+ path: '/settings/emoji-palette',
+ icon: 'ti ti-mood-happy',
+ },
{
id: '3Tcxw4Fwl',
children: [
@@ -608,23 +659,28 @@ export const searchIndexes: SearchIndexItem[] = [
id: 'FfZdOs8y',
children: [
{
- id: 'lVlkdP4zN',
+ id: 'B1ZU6Ur54',
+ label: i18n.ts._deck.enableSyncBetweenDevicesForProfiles,
+ keywords: ['sync', 'profiles', 'devices'],
+ },
+ {
+ id: 'iEF0gqNAo',
label: i18n.ts._deck.useSimpleUiForNonRootPages,
keywords: ['ui', 'root', 'page'],
},
{
- id: 'avgxEYgsi',
+ id: 'BNdSeWxZn',
label: i18n.ts.defaultNavigationBehaviour,
keywords: ['default', 'navigation', 'behaviour', 'window'],
},
{
- id: 'ma7OSw5JK',
+ id: 'zT9pGm8DF',
label: i18n.ts._deck.alwaysShowMainColumn,
keywords: ['always', 'show', 'main', 'column'],
},
{
- id: 'jjTlUDhJH',
- label: 'Unnamed marker',
+ id: '5dk2xv1vc',
+ label: i18n.ts._deck.columnAlign,
keywords: ['column', 'align'],
},
],
diff --git a/packages/frontend/src/utility/emoji-picker.ts b/packages/frontend/src/utility/emoji-picker.ts
index e7275b86f2..6279786b2d 100644
--- a/packages/frontend/src/utility/emoji-picker.ts
+++ b/packages/frontend/src/utility/emoji-picker.ts
@@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { popup } from '@/os.js';
-import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
/**
* 絵文字ピッカーを表示する。
@@ -25,7 +25,14 @@ class EmojiPicker {
}
public async init() {
- const emojisRef = store.r.pinnedEmojis;
+ const emojisRef = ref([]);
+
+ watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
+ emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
+ }, {
+ immediate: true,
+ });
+
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: emojisRef,
diff --git a/packages/frontend/src/utility/reaction-picker.ts b/packages/frontend/src/utility/reaction-picker.ts
index 200fb0b686..7c159fa2da 100644
--- a/packages/frontend/src/utility/reaction-picker.ts
+++ b/packages/frontend/src/utility/reaction-picker.ts
@@ -4,10 +4,10 @@
*/
import * as Misskey from 'misskey-js';
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { popup } from '@/os.js';
-import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
class ReactionPicker {
private src: Ref = ref(null);
@@ -21,7 +21,14 @@ class ReactionPicker {
}
public async init() {
- const reactionsRef = store.r.reactions;
+ const reactionsRef = ref([]);
+
+ watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], () => {
+ reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? [];
+ }, {
+ immediate: true,
+ });
+
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: reactionsRef,
From 5d228fb0f32aca9337c8b8a9ea9544f28d981f34 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 17:39:53 +0900
Subject: [PATCH 112/348] enhance(frontend): re-organize settings page
---
locales/index.d.ts | 4 +
locales/ja-JP.yml | 1 +
.../src/pages/settings/accessibility.vue | 51 ++
.../src/pages/settings/appearance.vue | 325 ---------
.../frontend/src/pages/settings/index.vue | 5 -
.../src/pages/settings/preferences.vue | 668 ++++++++++++------
packages/frontend/src/router/definition.ts | 4 -
.../utility/autogen/settings-search-index.ts | 423 ++++++-----
8 files changed, 702 insertions(+), 779 deletions(-)
delete mode 100644 packages/frontend/src/pages/settings/appearance.vue
diff --git a/locales/index.d.ts b/locales/index.d.ts
index b814bb70e1..f579aadb5d 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5342,6 +5342,10 @@ export interface Locale extends ILocale {
* 絵文字パレット
*/
"emojiPalette": string;
+ /**
+ * 投稿フォーム
+ */
+ "postForm": string;
"_emojiPalette": {
/**
* パレット
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b51a839715..2151a06611 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1331,6 +1331,7 @@ preferenceSyncConflictChoiceDevice: "デバイスの設定値"
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
paste: "ペースト"
emojiPalette: "絵文字パレット"
+postForm: "投稿フォーム"
_emojiPalette:
palettes: "パレット"
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
index 3dbb039a17..f7b1e7d2a0 100644
--- a/packages/frontend/src/pages/settings/accessibility.vue
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -60,6 +60,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+ {{ i18n.ts.menuStyle }}
+
+
+
+
+
+
+
@@ -70,6 +81,22 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+ {{ i18n.ts.fontSize }}
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.useSystemFont }}
+
+
@@ -84,6 +111,8 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
+import { miLocalStorage } from '@/local-storage.js';
+import MkRadios from '@/components/MkRadios.vue';
const reduceAnimation = prefer.model('animation', v => !v, v => !v);
const animatedMfm = prefer.model('animatedMfm');
@@ -92,10 +121,32 @@ const keepScreenOn = prefer.model('keepScreenOn');
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
const contextMenu = prefer.model('contextMenu');
+const menuStyle = prefer.model('menuStyle');
+
+const fontSize = ref(miLocalStorage.getItem('fontSize'));
+const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
+
+watch(fontSize, () => {
+ if (fontSize.value == null) {
+ miLocalStorage.removeItem('fontSize');
+ } else {
+ miLocalStorage.setItem('fontSize', fontSize.value);
+ }
+});
+
+watch(useSystemFont, () => {
+ if (useSystemFont.value) {
+ miLocalStorage.setItem('useSystemFont', 't');
+ } else {
+ miLocalStorage.removeItem('useSystemFont');
+ }
+});
watch([
keepScreenOn,
contextMenu,
+ fontSize,
+ useSystemFont,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
deleted file mode 100644
index 3fda5bc4c8..0000000000
--- a/packages/frontend/src/pages/settings/appearance.vue
+++ /dev/null
@@ -1,325 +0,0 @@
-
-
-
-
-
-
- {{ i18n.ts._settings.appearanceBanner }}
-
-
-
-
-
-
-
-
- {{ i18n.ts.useBlurEffect }}
-
-
-
-
-
-
-
- {{ i18n.ts.useBlurEffectForModal }}
-
-
-
-
-
-
-
- {{ i18n.ts.highlightSensitiveMedia }}
-
-
-
-
-
-
-
- {{ i18n.ts.squareAvatars }}
-
-
-
-
-
-
-
- {{ i18n.ts.showAvatarDecorations }}
-
-
-
-
-
-
-
- {{ i18n.ts.showGapBetweenNotesInTimeline }}
-
-
-
-
-
-
-
- {{ i18n.ts.seasonalScreenEffect }}
-
-
-
-
-
-
-
-
- {{ i18n.ts.menuStyle }}
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.emojiStyle }}
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.fontSize }}
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.useSystemFont }}
-
-
-
-
-
-
-
- {{ i18n.ts.displayOfNote }}
-
-
-
-
-
- {{ i18n.ts.reactionsDisplaySize }}
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.limitWidthOfReaction }}
-
-
-
-
-
-
-
- {{ i18n.ts.mediaListWithOneImageAppearance }}
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.instanceTicker }}
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.displayOfSensitiveMedia }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.notificationDisplay }}
-
-
-
-
-
- {{ i18n.ts.position }}
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.stackAxis }}
-
-
-
-
-
-
- {{ i18n.ts._notification.checkNotificationBehavior }}
-
-
-
-
-
- {{ i18n.ts.customCss }}
-
-
-
-
-
-
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index debcd4bd3e..3b7c44fbfe 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -118,11 +118,6 @@ const menuDef = computed(() => [{
text: i18n.ts.emojiPalette,
to: '/settings/emoji-palette',
active: currentPage.value?.route.name === 'emoji-palette',
- }, {
- icon: 'ti ti-device-desktop',
- text: i18n.ts.appearance,
- to: '/settings/appearance',
- active: currentPage.value?.route.name === 'appearance',
}, {
icon: 'ti ti-music',
text: i18n.ts.sounds,
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 374477c510..b9a596067c 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -10,121 +10,174 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._settings.preferencesBanner }}
-
-
- {{ i18n.ts.uiLanguage }}
-
-
-
-
- Crowdin
-
-
-
-
-
-
-
-
- {{ i18n.ts.overridedDeviceKind }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.showFixedPostForm }}
-
-
-
-
-
-
-
- {{ i18n.ts.showFixedPostFormInChannel }}
-
-
-
-
-
-
- {{ i18n.ts.pinnedList }}
-
- {{ i18n.ts.add }}
- {{ i18n.ts.remove }}
-
-
-
-
-
-
- {{ i18n.ts.enableQuickAddMfmFunction }}
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.rememberNoteVisibility }}
-
-
-
-
-
-
-
- {{ i18n.ts.defaultNoteVisibility }}
- {{ i18n.ts._visibility.public }}
- {{ i18n.ts._visibility.home }}
- {{ i18n.ts._visibility.followers }}
- {{ i18n.ts._visibility.specified }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts._visibility.disableFederation }}
-
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.note }}
+
+
+ {{ i18n.ts.general }}
+
+
+ {{ i18n.ts.uiLanguage }}
+
+
+
+
+ Crowdin
+
+
+
+
+
+
+
+
+ {{ i18n.ts.overridedDeviceKind }}
+
+
+
+
+
+
+
-
-
-
- {{ i18n.ts.collapseRenotes }}
- {{ i18n.ts.collapseRenotesDescription }}
+
+
+
+ {{ i18n.ts.useBlurEffect }}
+
+
+
+ {{ i18n.ts.useBlurEffectForModal }}
+
+
+
+
+
+
+
+ {{ i18n.ts.showAvatarDecorations }}
+
+
+
+
+
+
+
+ {{ i18n.ts.alwaysConfirmFollow }}
+
+
+
+
+
+
+
+ {{ i18n.ts.highlightSensitiveMedia }}
+
+
+
+
+
+
+
+ {{ i18n.ts.confirmWhenRevealingSensitiveMedia }}
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.emojiStyle }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.pinnedList }}
+
+ {{ i18n.ts.add }}
+ {{ i18n.ts.remove }}
+
+
+
+
+
+
+
+
+ {{ i18n.ts.timeline }}
+
+
+
+
+
+ {{ i18n.ts.showFixedPostForm }}
+
+
+
+
+
+
+
+ {{ i18n.ts.showFixedPostFormInChannel }}
+
+
+
+
+
+
+
+ {{ i18n.ts.collapseRenotes }}
+ {{ i18n.ts.collapseRenotesDescription }}
+
+
+
+
+
+
+
+ {{ i18n.ts.showGapBetweenNotesInTimeline }}
+
+
+
+
+
+
+
+ {{ i18n.ts.enableInfiniteScroll }}
+
+
+
+
+
+
+
+ {{ i18n.ts.disableStreamingTimeline }}
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.note }}
+
+
+
@@ -157,6 +210,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+ {{ i18n.ts.confirmOnReact }}
+
+
+
+
@@ -164,40 +225,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
-
-
-
-
- {{ i18n.ts.notifications }}
-
-
-
-
-
- {{ i18n.ts.useGroupedNotifications }}
-
-
-
-
-
-
-
-
-
- {{ i18n.ts.behavior }}
-
-
-
-
-
-
- {{ i18n.ts.openImageInNewTab }}
-
-
-
@@ -206,47 +233,70 @@ SPDX-License-Identifier: AGPL-3.0-only
+
-
-
-
- {{ i18n.ts.enableInfiniteScroll }}
-
-
-
+
+
+
+ {{ i18n.ts.reactionsDisplaySize }}
+
+
+
+
+
+
-
-
-
- {{ i18n.ts.disableStreamingTimeline }}
-
-
-
+
+
+
+ {{ i18n.ts.limitWidthOfReaction }}
+
+
+
-
-
-
- {{ i18n.ts.alwaysConfirmFollow }}
-
-
-
+
+
+
+ {{ i18n.ts.mediaListWithOneImageAppearance }}
+
+
+
+
+
+
+
-
-
-
- {{ i18n.ts.confirmWhenRevealingSensitiveMedia }}
-
-
-
+
+
+
+ {{ i18n.ts.instanceTicker }}
+
+
+
+
+
+
-
-
-
- {{ i18n.ts.confirmOnReact }}
-
-
-
+
+
+
+ {{ i18n.ts.displayOfSensitiveMedia }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.postForm }}
+
+
+
@@ -254,6 +304,123 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
+
+ {{ i18n.ts.rememberNoteVisibility }}
+
+
+
+
+
+
+
+ {{ i18n.ts.enableQuickAddMfmFunction }}
+
+
+
+
+
+
+
+
+ {{ i18n.ts.defaultNoteVisibility }}
+ {{ i18n.ts._visibility.public }}
+ {{ i18n.ts._visibility.home }}
+ {{ i18n.ts._visibility.followers }}
+ {{ i18n.ts._visibility.specified }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts._visibility.disableFederation }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.notifications }}
+
+
+
+
+
+ {{ i18n.ts.useGroupedNotifications }}
+
+
+
+
+
+
+
+ {{ i18n.ts.position }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.stackAxis }}
+
+
+
+
+
+
+ {{ i18n.ts._notification.checkNotificationBehavior }}
+
+
+
+
+
+
+ {{ i18n.ts.other }}
+
+
+
+
+
+
+ {{ i18n.ts.squareAvatars }}
+
+
+
+
+
+
+
+ {{ i18n.ts.seasonalScreenEffect }}
+
+
+
+
+
+
+
+ {{ i18n.ts.openImageInNewTab }}
+
+
+
@@ -276,47 +443,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
- {{ i18n.ts.dataSaver }}
-
-
-
{{ i18n.ts.reloadRequiredToApplySettings }}
-
-
- {{ i18n.ts.enableAll }}
- {{ i18n.ts.disableAll }}
-
-
-
- {{ i18n.ts._dataSaver._media.title }}
- {{ i18n.ts._dataSaver._media.description }}
-
-
- {{ i18n.ts._dataSaver._avatar.title }}
- {{ i18n.ts._dataSaver._avatar.description }}
-
-
- {{ i18n.ts._dataSaver._urlPreview.title }}
- {{ i18n.ts._dataSaver._urlPreview.description }}
-
-
- {{ i18n.ts._dataSaver._code.title }}
- {{ i18n.ts._dataSaver._code.description }}
-
-
-
-
-
-
-