Compare commits

...

27 Commits

Author SHA1 Message Date
syuilo
2402754dcc Update pizzax.ts 2025-03-10 21:44:23 +09:00
syuilo
2493592bd0 wip 2025-03-10 12:08:42 +09:00
syuilo
eec4ab841a Merge branch 'develop' into refine-pizzax 2025-03-10 11:29:16 +09:00
syuilo
7957ee5191 fix(frontend): rename pizzax fields 2025-03-10 11:28:54 +09:00
syuilo
d0b8ffe629 Merge branch 'develop' into refine-pizzax 2025-03-10 11:28:13 +09:00
syuilo
b200743845 refactor(frontend): rename store.set -> store.commit 2025-03-10 11:27:07 +09:00
syuilo
cef7575b76 commit 2025-03-10 11:23:15 +09:00
syuilo
9842eb2eeb wip 2025-03-10 11:21:17 +09:00
syuilo
05078e9c14 wip 2025-03-10 11:17:08 +09:00
syuilo
db5c6fa3c2 wip 2025-03-10 11:13:33 +09:00
syuilo
8a4e2659ed Merge branch 'develop' into refine-pizzax 2025-03-10 11:10:32 +09:00
syuilo
d19c094a9b refactor(frontend): rename pizzax fields 2025-03-10 11:09:59 +09:00
syuilo
08f7e7d9b3 refactor(frontend): rename pizzax fields 2025-03-10 10:51:54 +09:00
syuilo
a7f7ff33e7 wip 2025-03-10 10:47:50 +09:00
github-actions[bot]
16ad6b3f6c Bump version to 2025.3.2-alpha.4 2025-03-10 01:09:42 +00:00
syuilo
4df9083bf0 fix(frontend): テーマ切り替え時に一部の色が変わらない問題を修正 2025-03-10 10:05:50 +09:00
taichan
6419af2179 fix(frontend, dev): storybookのビルドエラー修正のため、as構文にリファクタ (#15640) 2025-03-10 09:34:45 +09:00
syuilo
d9858b03c9 enhance(frontend): improve plugin management 2025-03-10 09:28:07 +09:00
taichan
88efc0a3be fix(dev): 検索インデックス対象ファイルでHMRが効かない問題を修正 (#15638) 2025-03-09 22:45:17 +00:00
github-actions[bot]
ac21fa7194 Bump version to 2025.3.2-alpha.3 2025-03-09 13:01:46 +00:00
syuilo
c76afce9a7 enhance(frontend): improve plugin management 2025-03-09 21:57:56 +09:00
github-actions[bot]
8e3304344f Bump version to 2025.3.2-alpha.2 2025-03-09 12:32:54 +00:00
饺子w (Yumechi)
db5c127cdd fix(backend): fix handling of invalid urls in user profile (#15635)
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-09 12:28:47 +00:00
syuilo
0402866b43 enhance(frontend): improve plugin management 2025-03-09 21:23:36 +09:00
syuilo
6cefabc6b6 chore(frontend): remove unused binding 2025-03-09 17:19:21 +09:00
syuilo
c9c04d8391 enhance(frontend): migrate overridedDeviceKind to preference 2025-03-09 17:14:48 +09:00
syuilo
27e8805dcb refactor(frontend): relocate plugin consts 2025-03-09 17:02:46 +09:00
86 changed files with 997 additions and 946 deletions

View File

@@ -6,9 +6,11 @@
### Client
- Feat: 設定の管理が強化されました
- 自動でバックアップされるように
- Enhance: プラグインの管理が強化されました
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
### Server
-
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
## 2025.3.1

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.3.2-alpha.1",
"version": "2025.3.2-alpha.4",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -499,11 +499,28 @@ export class ApRendererService {
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
]);
const tryRewriteUrl = (maybeUrl: string) => {
const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/;
try {
const match = maybeUrl.match(urlSafeRegex);
if (!match) {
return maybeUrl;
}
const urlPart = match[0];
const urlPartParsed = new URL(urlPart);
const restPart = maybeUrl.slice(match[0].length);
return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
} catch (e) {
return maybeUrl;
}
};
const attachment = profile.fields.map(field => ({
type: 'PropertyValue',
name: field.name,
value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
? tryRewriteUrl(field.value)
: field.value,
}));

View File

@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = [
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return store.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];

View File

@@ -1213,22 +1213,37 @@ async function processVueFile(
transformedCodeCache: Record<string, string>
}> {
const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化
// すでにキャッシュに存在する場合は、そのまま返す
if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
// 開発モード時はコード内容に変更があれば常に再処理する
// コード内容が同じ場合のみキャッシュを使用
const isDevMode = process.env.NODE_ENV === 'development';
const s = new MagicString(code); // magic-string のインスタンスを作成
if (!isDevMode && transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
logger.info(`Using cached version for ${id}`);
return {
code: transformedCodeCache[normalizedId],
map: null,
map: s.generateMap({ source: id, includeContent: true }),
transformedCodeCache
};
}
// すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す
if (transformedCodeCache[normalizedId] === code) {
logger.info(`Code unchanged for ${id}, using cached version`);
return {
code: transformedCodeCache[normalizedId],
map: s.generateMap({ source: id, includeContent: true }),
transformedCodeCache
};
}
const s = new MagicString(code); // magic-string のインスタンスを作成
const parsed = vueSfcParse(code, { filename: id });
if (!parsed.descriptor.template) {
return {
code,
map: null,
map: s.generateMap({ source: id, includeContent: true }),
transformedCodeCache
};
}
@@ -1466,16 +1481,21 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける
}
if (!isMatch) {
return;
}
// ファイルの内容が変更された場合は再処理を行う
const normalizedId = id.replace(/\\/g, '/');
const hasContentChanged = !transformedCodeCache[normalizedId] || transformedCodeCache[normalizedId] !== code;
const transformed = await processVueFile(code, id, options, transformedCodeCache);
transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新
if (isDevServer) {
await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す
if (isDevServer && hasContentChanged) {
await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行
}
return transformed;
},

View File

@@ -154,55 +154,55 @@ export async function common(createVue: () => App<Element>) {
//#endregion
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
watch(store.reactiveState.darkMode, (darkMode) => {
watch(store.r.darkMode, (darkMode) => {
applyTheme(darkMode
? (prefer.s.darkTheme ?? defaultDarkTheme)
: (prefer.s.lightTheme ?? defaultLightTheme),
);
}, { immediate: miLocalStorage.getItem('theme') == null });
document.documentElement.dataset.colorScheme = store.state.darkMode ? 'dark' : 'light';
document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
watch(darkTheme, (theme) => {
if (store.state.darkMode) {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(lightTheme, (theme) => {
if (!store.state.darkMode) {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', isDeviceDarkmode());
store.commit('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', mql.matches);
store.commit('darkMode', mql.matches);
}
});
//#endregion
if (prefer.s.darkTheme && store.state.darkMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.state.darkMode) {
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
watch(store.reactiveState.overridedDeviceKind, (kind) => {
watch(prefer.r.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
}, { immediate: true });

View File

@@ -27,7 +27,7 @@ import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom
import { prefer } from '@/preferences.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { launchPlugin } from '@/plugin.js';
import { launchPlugins } from '@/plugin.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => {
@@ -105,9 +105,7 @@ export async function mainBoot() {
removeCustomEmojis(emojiData.emojis);
});
for (const plugin of prefer.s.plugins.filter(p => p.active)) {
launchPlugin(plugin);
}
launchPlugins();
try {
if (prefer.s.enableSeasonalScreenEffect) {
@@ -140,97 +138,98 @@ export async function mainBoot() {
store.loaded.then(async () => {
// prefereces migration
// TODO: そのうち消す
if (store.state.menu.length > 0) {
if (store.s.menu.length > 0) {
const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
if (themes.length > 0) {
prefer.set('themes', themes);
prefer.commit('themes', themes);
}
const plugins = ColdDeviceStorage.get('plugins');
prefer.set('plugins', plugins.map(p => ({
prefer.commit('plugins', plugins.map(p => ({
...p,
installId: (p as any).id,
id: undefined,
})));
prefer.set('lightTheme', ColdDeviceStorage.get('lightTheme'));
prefer.set('darkTheme', ColdDeviceStorage.get('darkTheme'));
prefer.set('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
prefer.set('widgets', store.state.widgets);
prefer.set('keepCw', store.state.keepCw);
prefer.set('collapseRenotes', store.state.collapseRenotes);
prefer.set('rememberNoteVisibility', store.state.rememberNoteVisibility);
prefer.set('uploadFolder', store.state.uploadFolder);
prefer.set('keepOriginalUploading', store.state.keepOriginalUploading);
prefer.set('menu', store.state.menu);
prefer.set('statusbars', store.state.statusbars);
prefer.set('pinnedUserLists', store.state.pinnedUserLists);
prefer.set('serverDisconnectedBehavior', store.state.serverDisconnectedBehavior);
prefer.set('nsfw', store.state.nsfw);
prefer.set('highlightSensitiveMedia', store.state.highlightSensitiveMedia);
prefer.set('animation', store.state.animation);
prefer.set('animatedMfm', store.state.animatedMfm);
prefer.set('advancedMfm', store.state.advancedMfm);
prefer.set('showReactionsCount', store.state.showReactionsCount);
prefer.set('enableQuickAddMfmFunction', store.state.enableQuickAddMfmFunction);
prefer.set('loadRawImages', store.state.loadRawImages);
prefer.set('imageNewTab', store.state.imageNewTab);
prefer.set('disableShowingAnimatedImages', store.state.disableShowingAnimatedImages);
prefer.set('emojiStyle', store.state.emojiStyle);
prefer.set('menuStyle', store.state.menuStyle);
prefer.set('useBlurEffectForModal', store.state.useBlurEffectForModal);
prefer.set('useBlurEffect', store.state.useBlurEffect);
prefer.set('showFixedPostForm', store.state.showFixedPostForm);
prefer.set('showFixedPostFormInChannel', store.state.showFixedPostFormInChannel);
prefer.set('enableInfiniteScroll', store.state.enableInfiniteScroll);
prefer.set('useReactionPickerForContextMenu', store.state.useReactionPickerForContextMenu);
prefer.set('showGapBetweenNotesInTimeline', store.state.showGapBetweenNotesInTimeline);
prefer.set('instanceTicker', store.state.instanceTicker);
prefer.set('emojiPickerScale', store.state.emojiPickerScale);
prefer.set('emojiPickerWidth', store.state.emojiPickerWidth);
prefer.set('emojiPickerHeight', store.state.emojiPickerHeight);
prefer.set('emojiPickerStyle', store.state.emojiPickerStyle);
prefer.set('reportError', store.state.reportError);
prefer.set('squareAvatars', store.state.squareAvatars);
prefer.set('showAvatarDecorations', store.state.showAvatarDecorations);
prefer.set('numberOfPageCache', store.state.numberOfPageCache);
prefer.set('showNoteActionsOnlyHover', store.state.showNoteActionsOnlyHover);
prefer.set('showClipButtonInNoteFooter', store.state.showClipButtonInNoteFooter);
prefer.set('reactionsDisplaySize', store.state.reactionsDisplaySize);
prefer.set('limitWidthOfReaction', store.state.limitWidthOfReaction);
prefer.set('forceShowAds', store.state.forceShowAds);
prefer.set('aiChanMode', store.state.aiChanMode);
prefer.set('devMode', store.state.devMode);
prefer.set('mediaListWithOneImageAppearance', store.state.mediaListWithOneImageAppearance);
prefer.set('notificationPosition', store.state.notificationPosition);
prefer.set('notificationStackAxis', store.state.notificationStackAxis);
prefer.set('enableCondensedLine', store.state.enableCondensedLine);
prefer.set('keepScreenOn', store.state.keepScreenOn);
prefer.set('disableStreamingTimeline', store.state.disableStreamingTimeline);
prefer.set('useGroupedNotifications', store.state.useGroupedNotifications);
prefer.set('dataSaver', store.state.dataSaver);
prefer.set('enableSeasonalScreenEffect', store.state.enableSeasonalScreenEffect);
prefer.set('enableHorizontalSwipe', store.state.enableHorizontalSwipe);
prefer.set('useNativeUiForVideoAudioPlayer', store.state.useNativeUIForVideoAudioPlayer);
prefer.set('keepOriginalFilename', store.state.keepOriginalFilename);
prefer.set('alwaysConfirmFollow', store.state.alwaysConfirmFollow);
prefer.set('confirmWhenRevealingSensitiveMedia', store.state.confirmWhenRevealingSensitiveMedia);
prefer.set('contextMenu', store.state.contextMenu);
prefer.set('skipNoteRender', store.state.skipNoteRender);
prefer.set('showSoftWordMutedWord', store.state.showSoftWordMutedWord);
prefer.set('confirmOnReact', store.state.confirmOnReact);
prefer.set('sound.masterVolume', store.state.sound_masterVolume);
prefer.set('sound.notUseSound', store.state.sound_notUseSound);
prefer.set('sound.useSoundOnlyWhenActive', store.state.sound_useSoundOnlyWhenActive);
prefer.set('sound.on.note', store.state.sound_note as any);
prefer.set('sound.on.noteMy', store.state.sound_noteMy as any);
prefer.set('sound.on.notification', store.state.sound_notification as any);
prefer.set('sound.on.reaction', store.state.sound_reaction as any);
store.set('deck.profile', deckStore.state.profile);
store.set('deck.columns', deckStore.state.columns);
store.set('deck.layout', deckStore.state.layout);
store.set('menu', []);
prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
prefer.commit('widgets', store.s.widgets);
prefer.commit('keepCw', store.s.keepCw);
prefer.commit('collapseRenotes', store.s.collapseRenotes);
prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
prefer.commit('uploadFolder', store.s.uploadFolder);
prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
prefer.commit('menu', store.s.menu);
prefer.commit('statusbars', store.s.statusbars);
prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
prefer.commit('nsfw', store.s.nsfw);
prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
prefer.commit('animation', store.s.animation);
prefer.commit('animatedMfm', store.s.animatedMfm);
prefer.commit('advancedMfm', store.s.advancedMfm);
prefer.commit('showReactionsCount', store.s.showReactionsCount);
prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
prefer.commit('loadRawImages', store.s.loadRawImages);
prefer.commit('imageNewTab', store.s.imageNewTab);
prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
prefer.commit('emojiStyle', store.s.emojiStyle);
prefer.commit('menuStyle', store.s.menuStyle);
prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
prefer.commit('useBlurEffect', store.s.useBlurEffect);
prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
prefer.commit('instanceTicker', store.s.instanceTicker);
prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
prefer.commit('reportError', store.s.reportError);
prefer.commit('squareAvatars', store.s.squareAvatars);
prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
prefer.commit('forceShowAds', store.s.forceShowAds);
prefer.commit('aiChanMode', store.s.aiChanMode);
prefer.commit('devMode', store.s.devMode);
prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
prefer.commit('notificationPosition', store.s.notificationPosition);
prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
prefer.commit('keepScreenOn', store.s.keepScreenOn);
prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
prefer.commit('dataSaver', store.s.dataSaver);
prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
prefer.commit('contextMenu', store.s.contextMenu);
prefer.commit('skipNoteRender', store.s.skipNoteRender);
prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
prefer.commit('confirmOnReact', store.s.confirmOnReact);
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
prefer.commit('sound.on.note', store.s.sound_note as any);
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
prefer.commit('sound.on.notification', store.s.sound_notification as any);
prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
store.commit('deck.profile', deckStore.s.profile);
store.commit('deck.columns', deckStore.s.columns);
store.commit('deck.layout', deckStore.s.layout);
store.commit('menu', []);
}
if (store.state.accountSetupWizard !== -1) {
if (store.s.accountSetupWizard !== -1) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
closed: () => dispose(),
});
@@ -503,7 +502,7 @@ export async function mainBoot() {
post();
},
'd': () => {
store.set('darkMode', !store.state.darkMode);
store.commit('darkMode', !store.s.darkMode);
},
's': () => {
mainRouter.push('/search');

View File

@@ -73,7 +73,7 @@ const emojiDb = computed(() => {
url: char2path(x.char),
}));
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
for (const [emoji, keywords] of Object.entries(index)) {
for (const k of keywords) {
unicodeEmojiDB.push({
@@ -155,10 +155,10 @@ function complete(type: string, value: any) {
emit('done', { type, value });
emit('closed');
if (type === 'emoji') {
let recents = store.state.recentlyUsedEmojis;
let recents = store.s.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== value);
recents.unshift(value);
store.set('recentlyUsedEmojis', recents.splice(0, 32));
store.commit('recentlyUsedEmojis', recents.splice(0, 32));
}
}
@@ -238,7 +238,7 @@ function exec() {
} else if (props.type === 'emoji') {
if (!props.q || props.q === '') {
// 最近使った絵文字をサジェスト
emojis.value = store.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
return;
}

View File

@@ -154,7 +154,7 @@ async function requestRender() {
captchaWidgetId.value = captcha.value.render(elem, {
sitekey: props.sitekey,
theme: store.state.darkMode ? 'dark' : 'light',
theme: store.s.darkMode ? 'dark' : 'light',
callback: callback,
'expired-callback': () => callback(undefined),
'error-callback': () => callback(undefined),

View File

@@ -161,7 +161,7 @@ const render = () => {
chartInstance.destroy();
}
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));

View File

@@ -22,7 +22,7 @@ const props = defineProps<{
}>();
const highlighter = await getHighlighter();
const darkMode = store.reactiveState.darkMode;
const darkMode = store.r.darkMode;
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
const [lightThemeName, darkThemeName] = await Promise.all([
@@ -74,10 +74,8 @@ watch(() => props.lang, (to) => {
<style module lang="scss">
.codeBlockRoot :global(.shiki) {
padding: 1em;
margin: .5em 0;
margin: 0;
overflow: auto;
border-radius: 8px;
border: 1px solid var(--MI_THEME-divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);

View File

@@ -245,7 +245,7 @@ function deleteFolder() {
folderId: props.folder.id,
}).then(() => {
if (prefer.s.uploadFolder === props.folder.id) {
prefer.set('uploadFolder', null);
prefer.commit('uploadFolder', null);
}
}).catch(err => {
switch (err.id) {
@@ -266,7 +266,7 @@ function deleteFolder() {
}
function setAsUploadFolder() {
prefer.set('uploadFolder', props.folder.id);
prefer.commit('uploadFolder', props.folder.id);
}
function onContextmenu(ev: MouseEvent) {

View File

@@ -166,7 +166,7 @@ const {
emojiPickerHeight,
} = prefer.r;
const recentlyUsedEmojis = store.reactiveState.recentlyUsedEmojis;
const recentlyUsedEmojis = store.r.recentlyUsedEmojis;
const recentlyUsedEmojisDef = computed(() => {
return recentlyUsedEmojis.value.map(getDef);
@@ -319,7 +319,7 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji);
@@ -336,7 +336,7 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji);
@@ -353,7 +353,7 @@ watch(q, () => {
}
if (matches.size >= max) return matches;
for (const index of Object.values(store.state.additionalUnicodeEmojiIndexes)) {
for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji);
@@ -429,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
// 最近使った絵文字更新
if (!pinned.value?.includes(key)) {
let recents = store.state.recentlyUsedEmojis;
let recents = store.s.recentlyUsedEmojis;
recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key);
store.set('recentlyUsedEmojis', recents.splice(0, 32));
store.commit('recentlyUsedEmojis', recents.splice(0, 32));
}
}

View File

@@ -121,11 +121,11 @@ async function onClick() {
} else {
await misskeyApi('following/create', {
userId: props.user.id,
withReplies: store.state.defaultWithReplies,
withReplies: store.s.defaultWithReplies,
});
emit('update:user', {
...props.user,
withReplies: store.state.defaultWithReplies,
withReplies: store.s.defaultWithReplies,
});
hasPendingFollowRequestFromYou.value = true;

View File

@@ -106,7 +106,7 @@ async function renderChart() {
await nextTick();
const color = store.state.darkMode ? '#b4e900' : '#86b300';
const color = store.s.darkMode ? '#b4e900' : '#86b300';
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;

View File

@@ -206,7 +206,6 @@ import number from '@/filters/number.js';
import * as os from '@/os.js';
import * as sound from '@/utility/sound.js';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/account.js';
@@ -223,6 +222,7 @@ import { isEnabledUrlPreview } from '@/instance.js';
import { focusPrev, focusNext } from '@/utility/focus.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -248,6 +248,7 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
const note = ref(deepClone(props.note));
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note.value);

View File

@@ -236,7 +236,6 @@ import number from '@/filters/number.js';
import * as os from '@/os.js';
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js';
import { noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/account.js';
@@ -255,6 +254,7 @@ import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -268,6 +268,7 @@ const inChannel = inject('inChannel', null);
const note = ref(deepClone(props.note));
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note.value);

View File

@@ -104,18 +104,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import type { ShallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js';
import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor from '@/components/MkPollEditor.vue';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import { erase, unique } from '@/utility/array.js';
import { extractMentions } from '@/utility/extract-mentions.js';
import { formatTimeString } from '@/utility/format-time-string.js';
@@ -123,7 +123,7 @@ import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFiles } from '@/utility/select-file.js';
import { store, notePostInterruptors, postFormActions } from '@/store.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
@@ -136,6 +136,7 @@ import { claimAchievement } from '@/utility/achievements.js';
import { emojiPicker } from '@/utility/emoji-picker.js';
import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
const $i = signinRequired();
@@ -175,18 +176,18 @@ const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []);
const poll = ref<PollEditorModelValue | null>(null);
const useCw = ref<boolean>(!!props.initialCw);
const showPreview = ref(store.state.showPreview);
watch(showPreview, () => store.set('showPreview', showPreview.value));
const showPreview = ref(store.s.showPreview);
watch(showPreview, () => store.commit('showPreview', showPreview.value));
const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
watch(showAddMfmFunction, () => prefer.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
const cw = ref<string | null>(props.initialCw ?? null);
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly));
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility));
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly));
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility));
const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
}
const reactionAcceptance = ref(store.state.reactionAcceptance);
const reactionAcceptance = ref(store.s.reactionAcceptance);
const draghover = ref(false);
const quoteId = ref<string | null>(null);
const hasNotSpecifiedMentions = ref(false);
@@ -196,6 +197,7 @@ const showingOptions = ref(false);
const textAreaReadOnly = ref(false);
const justEndedComposition = ref(false);
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const postFormActions = getPluginHandlers('post_form_action');
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@@ -268,8 +270,8 @@ const canPost = computed((): boolean => {
(!poll.value || poll.value.choices.length >= 2);
});
const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
const withHashtags = store.model('postFormWithHashtags');
const hashtags = store.model('postFormHashtags');
watch(text, () => {
checkMissingMention();
@@ -478,7 +480,7 @@ function setVisibility() {
changeVisibility: v => {
visibility.value = v;
if (prefer.s.rememberNoteVisibility) {
store.set('visibility', visibility.value);
store.commit('visibility', visibility.value);
}
},
closed: () => dispose(),
@@ -526,7 +528,7 @@ async function toggleLocalOnly() {
localOnly.value = !localOnly.value;
if (prefer.s.rememberNoteVisibility) {
store.set('localOnly', localOnly.value);
store.commit('localOnly', localOnly.value);
}
}
@@ -822,6 +824,7 @@ async function post(ev?: MouseEvent) {
}
// plugin
const notePostInterruptors = getPluginHandlers('note_post_interruptor');
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
try {

View File

@@ -75,7 +75,7 @@ async function renderChart() {
await nextTick();
const color = store.state.darkMode ? '#b4e900' : '#86b300';
const color = store.s.darkMode ? '#b4e900' : '#86b300';
const getYYYYMMDD = (date: Date) => {
const y = date.getFullYear().toString().padStart(2, '0');

View File

@@ -42,7 +42,7 @@ const getDate = (ymd: string) => {
onMounted(async () => {
let raw = await misskeyApi('retention', { });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
const color = accent.toHex();

View File

@@ -79,8 +79,8 @@ const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admi
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const name = ref(props.initialName);
const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
if (props.initialPermissions) {
for (const kind of props.initialPermissions) {

View File

@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
scrolling="no"
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${store.s.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
></iframe>
</div>
<div :class="$style.action">

View File

@@ -128,10 +128,10 @@ async function ok() {
dialogEl.value?.close();
// 最近使ったユーザー更新
let recents = store.state.recentlyUsedUsers;
let recents = store.s.recentlyUsedUsers;
recents = recents.filter(x => x !== selected.value?.id);
recents.unshift(selected.value.id);
store.set('recentlyUsedUsers', recents.splice(0, 16));
store.commit('recentlyUsedUsers', recents.splice(0, 16));
}
function cancel() {
@@ -141,7 +141,7 @@ function cancel() {
onMounted(() => {
misskeyApi('users/show', {
userIds: store.state.recentlyUsedUsers,
userIds: store.s.recentlyUsedUsers,
}).then(foundUsers => {
let _users = foundUsers;
_users = _users.filter((u) => {

View File

@@ -149,10 +149,10 @@ const emit = defineEmits<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const page = ref(store.state.accountSetupWizard);
const page = ref(store.s.accountSetupWizard);
watch(page, () => {
store.set('accountSetupWizard', page.value);
store.commit('accountSetupWizard', page.value);
});
async function close(skip: boolean) {
@@ -165,11 +165,11 @@ async function close(skip: boolean) {
}
dialog.value?.close();
store.set('accountSetupWizard', -1);
store.commit('accountSetupWizard', -1);
}
function setupComplete() {
store.set('accountSetupWizard', -1);
store.commit('accountSetupWizard', -1);
dialog.value?.close();
}
@@ -194,7 +194,7 @@ async function later(later: boolean) {
}
dialog.value?.close();
store.set('accountSetupWizard', 0);
store.commit('accountSetupWizard', 0);
}
</script>

View File

@@ -59,7 +59,7 @@ async function renderChart() {
await nextTick();
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const computedStyle = getComputedStyle(document.documentElement);
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();

View File

@@ -67,7 +67,7 @@ const choseAd = (): Ad | null => {
return props.specify;
}
const allAds = instance.ads.map(ad => store.state.mutedAds.includes(ad.id) ? {
const allAds = instance.ads.map(ad => store.s.mutedAds.includes(ad.id) ? {
...ad,
ratio: 0,
} : ad);
@@ -112,8 +112,8 @@ const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds &&
function reduceFrequency(): void {
if (chosen.value == null) return;
if (store.state.mutedAds.includes(chosen.value.id)) return;
store.push('mutedAds', chosen.value.id);
if (store.s.mutedAds.includes(chosen.value.id)) return;
store.commit('mutedAds', [...store.s.mutedAds, chosen.value.id]);
os.success();
chosen.value = choseAd();
showMenu.value = false;

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.spacer, store.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
<div :class="[$style.spacer, store.r.darkMode.value ? $style.dark : $style.light]"></div>
</template>
<script lang="ts" setup>

View File

@@ -43,14 +43,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import { scrollToTop } from '@@/js/scroll.js';
import XTabs from './MkPageHeader.tabs.vue';
import type { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@@/js/scroll.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import type { PageMetadata } from '@/utility/page-metadata.js';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/utility/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import type { PageMetadata } from '@/utility/page-metadata.js';
const props = withDefaults(defineProps<{
overridePageMetadata?: PageMetadata;
@@ -114,7 +114,7 @@ let ro: ResizeObserver | null;
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
globalEvents.on('themeChanging', calcBg);
if (el.value && el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
@@ -128,7 +128,7 @@ onMounted(() => {
});
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
globalEvents.off('themeChanging', calcBg);
if (ro) ro.disconnect();
});
</script>

View File

@@ -59,34 +59,34 @@ export const loadDeck = async () => {
try {
deck = await misskeyApi('i/registry/get', {
scope: ['client', 'deck', 'profiles'],
key: store.state['deck.profile'],
key: store.s['deck.profile'],
});
} catch (err) {
if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
// 後方互換性のため
if (store.state['deck.profile'] === 'default') {
if (store.s['deck.profile'] === 'default') {
saveDeck();
return;
}
store.set('deck.columns', []);
store.set('deck.layout', []);
store.commit('deck.columns', []);
store.commit('deck.layout', []);
return;
}
throw err;
}
store.set('deck.columns', deck.columns);
store.set('deck.layout', deck.layout);
store.commit('deck.columns', deck.columns);
store.commit('deck.layout', deck.layout);
};
export async function forceSaveDeck() {
await misskeyApi('i/registry/set', {
scope: ['client', 'deck', 'profiles'],
key: store.state['deck.profile'],
key: store.s['deck.profile'],
value: {
columns: store.reactiveState['deck.columns'].value,
layout: store.reactiveState['deck.layout'].value,
columns: store.r['deck.columns'].value,
layout: store.r['deck.layout'].value,
},
});
}
@@ -111,40 +111,40 @@ export async function deleteProfile(key: string): Promise<void> {
export function addColumn(column: Column) {
if (column.name === undefined) column.name = null;
store.push('deck.columns', column);
store.push('deck.layout', [column.id]);
store.commit('deck.columns', [...store.s['deck.columns'], column]);
store.commit('deck.layout', [...store.s['deck.layout'], [column.id]]);
saveDeck();
}
export function removeColumn(id: Column['id']) {
store.set('deck.columns', store.state['deck.columns'].filter(c => c.id !== id));
store.set('deck.layout', store.state['deck.layout']
store.commit('deck.columns', store.s['deck.columns'].filter(c => c.id !== id));
store.commit('deck.layout', store.s['deck.layout']
.map(ids => ids.filter(_id => _id !== id))
.filter(ids => ids.length > 0));
saveDeck();
}
export function swapColumn(a: Column['id'], b: Column['id']) {
const aX = store.state['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
const aY = store.state['deck.layout'][aX].findIndex(id => id === a);
const bX = store.state['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
const bY = store.state['deck.layout'][bX].findIndex(id => id === b);
const layout = deepClone(store.state['deck.layout']);
const aX = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
const aY = store.s['deck.layout'][aX].findIndex(id => id === a);
const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
const bY = store.s['deck.layout'][bX].findIndex(id => id === b);
const layout = deepClone(store.s['deck.layout']);
layout[aX][aY] = b;
layout[bX][bY] = a;
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
saveDeck();
}
export function swapLeftColumn(id: Column['id']) {
const layout = deepClone(store.state['deck.layout']);
store.state['deck.layout'].some((ids, i) => {
const layout = deepClone(store.s['deck.layout']);
store.s['deck.layout'].some((ids, i) => {
if (ids.includes(id)) {
const left = store.state['deck.layout'][i - 1];
const left = store.s['deck.layout'][i - 1];
if (left) {
layout[i - 1] = store.state['deck.layout'][i];
layout[i - 1] = store.s['deck.layout'][i];
layout[i] = left;
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
}
return true;
}
@@ -154,14 +154,14 @@ export function swapLeftColumn(id: Column['id']) {
}
export function swapRightColumn(id: Column['id']) {
const layout = deepClone(store.state['deck.layout']);
store.state['deck.layout'].some((ids, i) => {
const layout = deepClone(store.s['deck.layout']);
store.s['deck.layout'].some((ids, i) => {
if (ids.includes(id)) {
const right = store.state['deck.layout'][i + 1];
const right = store.s['deck.layout'][i + 1];
if (right) {
layout[i + 1] = store.state['deck.layout'][i];
layout[i + 1] = store.s['deck.layout'][i];
layout[i] = right;
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
}
return true;
}
@@ -171,9 +171,9 @@ export function swapRightColumn(id: Column['id']) {
}
export function swapUpColumn(id: Column['id']) {
const layout = deepClone(store.state['deck.layout']);
const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
const ids = deepClone(store.state['deck.layout'][idsIndex]);
const layout = deepClone(store.s['deck.layout']);
const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
const ids = deepClone(store.s['deck.layout'][idsIndex]);
ids.some((x, i) => {
if (x === id) {
const up = ids[i - 1];
@@ -182,7 +182,7 @@ export function swapUpColumn(id: Column['id']) {
ids[i] = up;
layout[idsIndex] = ids;
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
}
return true;
}
@@ -192,9 +192,9 @@ export function swapUpColumn(id: Column['id']) {
}
export function swapDownColumn(id: Column['id']) {
const layout = deepClone(store.state['deck.layout']);
const idsIndex = store.state['deck.layout'].findIndex(ids => ids.includes(id));
const ids = deepClone(store.state['deck.layout'][idsIndex]);
const layout = deepClone(store.s['deck.layout']);
const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
const ids = deepClone(store.s['deck.layout'][idsIndex]);
ids.some((x, i) => {
if (x === id) {
const down = ids[i + 1];
@@ -203,7 +203,7 @@ export function swapDownColumn(id: Column['id']) {
ids[i] = down;
layout[idsIndex] = ids;
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
}
return true;
}
@@ -213,74 +213,74 @@ export function swapDownColumn(id: Column['id']) {
}
export function stackLeftColumn(id: Column['id']) {
let layout = deepClone(store.state['deck.layout']);
const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
let layout = deepClone(store.s['deck.layout']);
const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout[i - 1].push(id);
layout = layout.filter(ids => ids.length > 0);
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
saveDeck();
}
export function popRightColumn(id: Column['id']) {
let layout = deepClone(store.state['deck.layout']);
const i = store.state['deck.layout'].findIndex(ids => ids.includes(id));
let layout = deepClone(store.s['deck.layout']);
const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
const affected = layout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout.splice(i + 1, 0, [id]);
layout = layout.filter(ids => ids.length > 0);
store.set('deck.layout', layout);
store.commit('deck.layout', layout);
const columns = deepClone(store.state['deck.columns']);
const columns = deepClone(store.s['deck.columns']);
for (const column of columns) {
if (affected.includes(column.id)) {
column.active = true;
}
}
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = deepClone(store.state['deck.columns']);
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.state['deck.columns'][columnIndex]);
const columns = deepClone(store.s['deck.columns']);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]);
if (column == null) return;
if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget);
columns[columnIndex] = column;
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = deepClone(store.state['deck.columns']);
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.state['deck.columns'][columnIndex]);
const columns = deepClone(store.s['deck.columns']);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]);
if (column == null) return;
if (column.widgets == null) column.widgets = [];
column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column;
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
const columns = deepClone(store.state['deck.columns']);
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.state['deck.columns'][columnIndex]);
const columns = deepClone(store.s['deck.columns']);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]);
if (column == null) return;
column.widgets = widgets;
columns[columnIndex] = column;
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
const columns = deepClone(store.state['deck.columns']);
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.state['deck.columns'][columnIndex]);
const columns = deepClone(store.s['deck.columns']);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
const column = deepClone(store.s['deck.columns'][columnIndex]);
if (column == null) return;
if (column.widgets == null) column.widgets = [];
column.widgets = column.widgets.map(w => w.id === widgetId ? {
@@ -288,19 +288,19 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
data: widgetData,
} : w);
columns[columnIndex] = column;
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}
export function updateColumn(id: Column['id'], column: Partial<Column>) {
const columns = deepClone(store.state['deck.columns']);
const columnIndex = store.state['deck.columns'].findIndex(c => c.id === id);
const currentColumn = deepClone(store.state['deck.columns'][columnIndex]);
const columns = deepClone(store.s['deck.columns']);
const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
const currentColumn = deepClone(store.s['deck.columns'][columnIndex]);
if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v;
}
columns[columnIndex] = currentColumn;
store.set('deck.columns', columns);
store.commit('deck.columns', columns);
saveDeck();
}

View File

@@ -5,17 +5,32 @@
import type { Directive } from 'vue';
import { getBgColor } from '@/utility/get-bg-color.js';
import { globalEvents } from '@/events.js';
const handlerMap = new WeakMap<any, any>();
export default {
mounted(src, binding, vn) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
function calc() {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor;
const myBg = window.getComputedStyle(src).backgroundColor;
if (parentBg === myBg) {
src.style.borderColor = 'var(--MI_THEME-divider)';
} else {
src.style.borderColor = myBg;
if (parentBg === myBg) {
src.style.borderColor = 'var(--MI_THEME-divider)';
} else {
src.style.borderColor = myBg;
}
}
handlerMap.set(src, calc);
calc();
globalEvents.on('themeChanged', calc);
},
unmounted(src, binding, vn) {
globalEvents.off('themeChanged', handlerMap.get(src));
},
} as Directive;

View File

@@ -7,6 +7,7 @@ import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
export const globalEvents = new EventEmitter<{
themeChanging: () => void;
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
requestClearPageCache: () => void;

View File

@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
const containerEl = shallowRef<HTMLElement>();
function iconLoaded() {
const emojis = store.state.reactions;
const emojis = store.s.reactions;
const containerWidth = containerEl.value.offsetWidth;
for (let i = 0; i < 32; i++) {
easterEggEmojis.value.push({

View File

@@ -35,8 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os.js';
import { scrollToTop } from '@@/js/scroll.js';
import { popupMenu } from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/utility/page-metadata.js';
@@ -127,7 +127,7 @@ const calcBg = () => {
onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
globalEvents.on('themeChanging', calcBg);
watch(() => [props.tab, props.tabs], () => {
nextTick(() => {
@@ -147,7 +147,7 @@ onMounted(() => {
});
onUnmounted(() => {
globalEvents.off('themeChanged', calcBg);
globalEvents.off('themeChanging', calcBg);
});
</script>

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div>
<MkInfo v-if="!store.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
<MkInfo v-if="!store.r.abusesTutorial.value" closable @close="closeTutorial()">
{{ i18n.ts._abuseUserReport.resolveTutorial }}
</MkInfo>
@@ -93,7 +93,7 @@ function resolved(reportId) {
}
function closeTutorial() {
store.set('abusesTutorial', false);
store.commit('abusesTutorial', false);
}
const headerActions = computed(() => []);

View File

@@ -54,7 +54,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorRead = '#3498db';
const colorWrite = '#2ecc71';

View File

@@ -68,7 +68,7 @@ onMounted(async () => {
const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const succColor = '#87e000';
const failColor = '#ff4400';

View File

@@ -67,7 +67,7 @@ const color =
'?' as never;
onMounted(() => {
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {
type: 'line',

View File

@@ -67,7 +67,7 @@ const color =
'?' as never;
onMounted(() => {
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {
type: 'line',

View File

@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
<img v-if="store.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
<canvas ref="canvasEl" :class="$style.canvas"/>
<Transition
@@ -858,7 +858,7 @@ function updateSettings<
>(key: K, value: V) {
const changes: { [P in K]?: V } = {};
changes[key] = value;
prefer.set('game.dropAndFusion', {
prefer.commit('game.dropAndFusion', {
...prefer.s['game.dropAndFusion'],
...changes,
});

View File

@@ -114,7 +114,6 @@ import MkPagination from '@/components/MkPagination.vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/utility/page-metadata.js';
import { pageViewInterruptors } from '@/store.js';
import { deepClone } from '@/utility/clone.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/utility/navigator.js';
@@ -123,6 +122,7 @@ import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
const router = useRouter();
@@ -151,6 +151,7 @@ function fetchPage() {
page.value = _page;
// plugin
const pageViewInterruptors = getPluginHandlers('page_view_interruptor');
if (pageViewInterruptors.length > 0) {
let result = deepClone(_page);
for (const interruptor of pageViewInterruptors) {

View File

@@ -144,7 +144,7 @@ if (prefer.s.uploadFolder) {
function chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
prefer.set('uploadFolder', folder[0] ? folder[0].id : null);
prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
os.success();
if (prefer.s.uploadFolder) {
uploadFolder.value = await misskeyApi('drive/folders/show', {

View File

@@ -154,8 +154,8 @@ import MkFolder from '@/components/MkFolder.vue';
import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.state.reactions));
const pinnedEmojis: Ref<string[]> = ref(deepClone(store.state.pinnedEmojis));
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.s.reactions));
const pinnedEmojis: Ref<string[]> = ref(deepClone(store.s.pinnedEmojis));
const emojiPickerScale = prefer.model('emojiPickerScale');
const emojiPickerWidth = prefer.model('emojiPickerWidth');
@@ -240,13 +240,13 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
}
watch(pinnedEmojisForReaction, () => {
store.set('reactions', pinnedEmojisForReaction.value);
store.commit('reactions', pinnedEmojisForReaction.value);
}, {
deep: true,
});
watch(pinnedEmojis, () => {
store.set('pinnedEmojis', pinnedEmojis.value);
store.commit('pinnedEmojis', pinnedEmojis.value);
}, {
deep: true,
});

View File

@@ -159,7 +159,7 @@ import { store } from '@/store.js';
const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false);
const withReplies = ref(store.state.defaultWithReplies);
const withReplies = ref(store.s.defaultWithReplies);
const onExportSuccess = () => {
os.alert({

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="baaadecd">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="!store.reactiveState.enablePreferencesAutoCloudBackup.value && store.reactiveState.showPreferencesAutoCloudBackupSuggestion.value" class="info">
<MkInfo v-if="!store.r.enablePreferencesAutoCloudBackup.value && store.r.showPreferencesAutoCloudBackupSuggestion.value" class="info">
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
</MkInfo>
@@ -72,7 +72,7 @@ const ro = new ResizeObserver((entries, observer) => {
});
function skipAutoBackup() {
store.set('showPreferencesAutoCloudBackupSuggestion', false);
store.commit('showPreferencesAutoCloudBackupSuggestion', false);
}
const menuDef = computed<SuperMenuDef[]>(() => [{

View File

@@ -67,7 +67,7 @@ const items = ref(prefer.s.menu.map(x => ({
type: x,
})));
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
const menuDisplay = store.model('menuDisplay');
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
@@ -91,7 +91,7 @@ function removeItem(index: number) {
}
async function save() {
prefer.set('menu', items.value.map(x => x.type));
prefer.commit('menu', items.value.map(x => x.type));
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
}

View File

@@ -131,7 +131,7 @@ const reportError = prefer.model('reportError');
const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');
const devMode = prefer.model('devMode');
const defaultWithReplies = computed(store.makeGetterSetter('defaultWithReplies'));
const defaultWithReplies = store.model('defaultWithReplies');
watch(skipNoteRender, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkCodeEditor>
<div>
<MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
<MkButton :disabled="code == null || code.trim() === ''" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div>
</div>
</template>
@@ -23,11 +23,12 @@ import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/utility/page-metadata.js';
import { installPlugin } from '@/plugin.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();
const code = ref<string | null>(null);
async function install() {
@@ -36,10 +37,9 @@ async function install() {
try {
await installPlugin(code.value);
os.success();
code.value = null;
nextTick(() => {
unisonReload();
});
router.push('/settings/plugin');
} catch (err) {
os.alert({
type: 'error',

View File

@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-for="plugin in plugins" :key="plugin.installId">
<template #icon><i class="ti ti-plug"></i></template>
<template #suffix>
<i v-if="plugin.active" class="ti ti-player-play" style="color: var(--MI_THEME-accent);"></i>
<i v-if="plugin.active" class="ti ti-player-play" style="color: var(--MI_THEME-success);"></i>
<i v-else class="ti ti-player-pause" style="opacity: 0.7;"></i>
</template>
<template #label>
@@ -28,14 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #footer>
<div class="_buttons">
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
<MkButton :disabled="!plugin.active" @click="reload(plugin)"><i class="ti ti-refresh"></i> {{ i18n.ts.reload }}</MkButton>
<MkButton danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
<MkButton v-if="plugin.config" style="margin-left: auto;" @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
</div>
</template>
<div class="_gaps_m">
<div class="_gaps_s">
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
</div>
@@ -59,31 +59,27 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</div>
<MkFolder>
<template #icon><i class="ti ti-terminal-2"></i></template>
<template #label>{{ i18n.ts._plugin.viewLog }}</template>
<div class="_gaps_s">
<MkFolder>
<template #icon><i class="ti ti-terminal-2"></i></template>
<template #label>{{ i18n.ts.logs }}</template>
<div class="_gaps_s">
<div class="_buttons">
<MkButton inline @click="copy(pluginLogs.get(plugin.installId)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
<div>
<div v-for="log in pluginLogs.get(plugin.installId)" :class="[$style.log, { [$style.isSystemLog]: log.isSystem }]">
<div class="_monospace">{{ timeToHhMmSs(log.at) }} {{ log.message }}</div>
</div>
</div>
</MkFolder>
<MkCode :code="pluginLogs.get(plugin.installId)?.join('\n') ?? ''"/>
</div>
</MkFolder>
<MkFolder :withSpacer="false">
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._plugin.viewSource }}</template>
<div class="_gaps_s">
<div class="_buttons">
<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
<div class="_gaps_s">
<MkCode :code="plugin.src ?? ''" lang="ais"/>
</div>
<MkCode :code="plugin.src ?? ''" lang="ais"/>
</div>
</MkFolder>
</MkFolder>
</div>
</div>
</MkFolder>
</div>
@@ -94,6 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, ref, computed } from 'vue';
import type { Plugin } from '@/plugin.js';
import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@@ -101,40 +98,40 @@ import MkButton from '@/components/MkButton.vue';
import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/utility/page-metadata.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin } from '@/plugin.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js';
import * as os from '@/os.js';
const plugins = prefer.r.plugins;
async function uninstall(plugin) {
await uninstallPlugin(plugin);
nextTick(() => {
unisonReload();
async function uninstall(plugin: Plugin) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: plugin.name }),
});
}
if (canceled) return;
await uninstallPlugin(plugin);
function copy(text) {
copyToClipboard(text ?? '');
os.success();
}
async function config(plugin) {
await configPlugin(plugin);
nextTick(() => {
location.reload();
});
function reload(plugin: Plugin) {
reloadPlugin(plugin);
}
function changeActive(plugin, active) {
async function config(plugin: Plugin) {
await configPlugin(plugin);
}
function changeActive(plugin: Plugin, active: boolean) {
changePluginActive(plugin, active);
nextTick(() => {
location.reload();
});
}
function timeToHhMmSs(unixtime: number) {
return new Date(unixtime).toTimeString().split(' ')[0];
}
const headerActions = computed(() => []);
@@ -146,3 +143,12 @@ definePageMetadata(() => ({
icon: 'ti ti-plug',
}));
</script>
<style module>
.log {
}
.isSystemLog {
opacity: 0.5;
}
</style>

View File

@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
<MkPreferenceContainer k="rememberNoteVisibility">
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">
<MkSwitch v-model="rememberNoteVisibility">
<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
@@ -337,8 +337,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
<div class="_buttons">
<template v-for="lang in emojiIndexLangs" :key="lang">
<MkButton v-if="store.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
<MkButton v-if="store.r.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.r.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
</template>
</div>
</MkFolder>
@@ -385,8 +385,7 @@ import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
const overridedDeviceKind = computed(store.makeGetterSetter('overridedDeviceKind'));
const overridedDeviceKind = prefer.model('overridedDeviceKind');
const keepCw = prefer.model('keepCw');
const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior');
const hemisphere = prefer.model('hemisphere');
@@ -450,7 +449,7 @@ function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
async function main() {
const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
@@ -462,7 +461,7 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
}
currentIndexes[lang] = await download();
await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
await store.commit('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
@@ -470,9 +469,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
function removeEmojiIndex(lang: string) {
async function main() {
const currentIndexes = store.state.additionalUnicodeEmojiIndexes;
const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
delete currentIndexes[lang];
await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
await store.commit('additionalUnicodeEmojiIndexes', currentIndexes);
}
os.promiseDialog(main());
@@ -489,11 +488,11 @@ async function setPinnedList() {
if (canceled) return;
if (list == null) return;
prefer.set('pinnedUserLists', [list]);
prefer.commit('pinnedUserLists', [list]);
}
function removePinnedList() {
prefer.set('pinnedUserLists', []);
prefer.commit('pinnedUserLists', []);
}
function enableAllDataSaver() {
@@ -513,7 +512,7 @@ function disableAllDataSaver() {
}
watch(dataSaver, (to) => {
prefer.set('dataSaver', to);
prefer.commit('dataSaver', to);
}, {
deep: true,
});

View File

@@ -177,7 +177,7 @@ const $i = signinRequired();
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
const reactionAcceptance = store.model('reactionAcceptance');
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
return lang != null && lang in langmap;

View File

@@ -101,14 +101,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
volume: sound.volume,
};
prefer.set(`sound.on.${type}`, v);
prefer.commit(`sound.on.${type}`, v);
sounds.value[type] = v;
}
function reset() {
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
const v = PREF_DEF[`sound.on.${sound}`].default;
prefer.set(`sound.on.${sound}`, v);
prefer.commit(`sound.on.${sound}`, v);
sounds.value[sound] = v;
}
}

View File

@@ -137,10 +137,10 @@ async function save() {
const i = prefer.s.statusbars.findIndex(x => x.id === props._id);
const statusbars = deepClone(prefer.s.statusbars);
statusbars[i] = deepClone(statusbar);
prefer.set('statusbars', statusbars);
prefer.commit('statusbars', statusbars);
}
function del() {
prefer.set('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
prefer.commit('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
}
</script>

View File

@@ -37,7 +37,7 @@ onMounted(() => {
});
async function add() {
prefer.set('statusbars', [...statusbars.value, {
prefer.commit('statusbars', [...statusbars.value, {
id: uuid(),
type: null,
black: false,

View File

@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkCodeEditor>
<div class="_buttons">
<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
</div>
</div>
</template>
@@ -24,7 +24,9 @@ import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/utility/page-metadata.js';
import { useRouter } from '@/router/supplier.js';
const router = useRouter();
const installThemeCode = ref<string | null>(null);
async function install(code: string): Promise<void> {
@@ -35,6 +37,8 @@ async function install(code: string): Promise<void> {
type: 'success',
text: i18n.tsx._theme.installed({ name: theme.name }),
});
installThemeCode.value = null;
router.push('/settings/theme');
} catch (err) {
switch (err.message.toLowerCase()) {
case 'this theme is already installed':

View File

@@ -179,7 +179,7 @@ const darkThemeId = computed({
set(id) {
const t = themes.value.find(x => x.id === id);
if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
prefer.set('darkTheme', t);
prefer.commit('darkTheme', t);
}
},
});
@@ -191,19 +191,19 @@ const lightThemeId = computed({
set(id) {
const t = themes.value.find(x => x.id === id);
if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
prefer.set('lightTheme', t);
prefer.commit('lightTheme', t);
}
},
});
const darkMode = computed(store.makeGetterSetter('darkMode'));
const darkMode = store.model('darkMode');
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
const themesCount = installedThemes.value.length;
watch(syncDeviceDarkMode, () => {
if (syncDeviceDarkMode.value) {
store.set('darkMode', isDeviceDarkmode());
store.commit('darkMode', isDeviceDarkmode());
}
});

View File

@@ -44,11 +44,11 @@ const pagination = {
const notes = ref<InstanceType<typeof MkNotes>>();
async function post() {
store.set('postFormHashtags', props.tag);
store.set('postFormWithHashtags', true);
store.commit('postFormHashtags', props.tag);
store.commit('postFormWithHashtags', true);
await os.post();
store.set('postFormHashtags', '');
store.set('postFormWithHashtags', false);
store.commit('postFormHashtags', '');
store.commit('postFormWithHashtags', false);
notes.value?.pagingComponent?.reload();
}

View File

@@ -200,10 +200,10 @@ async function saveAs() {
if (description.value) theme.value.desc = description.value;
await addTheme(theme.value);
applyTheme(theme.value);
if (store.state.darkMode) {
prefer.set('darkTheme', theme.value);
if (store.s.darkMode) {
prefer.commit('darkTheme', theme.value);
} else {
prefer.set('lightTheme', theme.value);
prefer.commit('lightTheme', theme.value);
}
changed.value = false;
os.alert({

View File

@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl">
<MkInfo v-if="isBasicTimeline(src) && !store.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
@@ -67,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`;
const queue = ref(0);
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
const src = computed<TimelinePageSrc>({
get: () => ($i ? store.reactiveState.tl.value.src : srcWhenNotSignin.value),
get: () => ($i ? store.r.tl.value.src : srcWhenNotSignin.value),
set: (x) => saveSrc(x),
});
const withRenotes = computed<boolean>({
get: () => store.reactiveState.tl.value.filter.withRenotes,
get: () => store.r.tl.value.filter.withRenotes,
set: (x) => saveTlFilter('withRenotes', x),
});
// computed内での無限ループを防ぐためのフラグ
const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
store.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
store.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
store.r.tl.value.filter.withReplies ? 'withReplies' :
store.r.tl.value.filter.onlyFiles ? 'onlyFiles' :
false,
);
@@ -88,7 +88,7 @@ const withReplies = computed<boolean>({
if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
return false;
} else {
return store.reactiveState.tl.value.filter.withReplies;
return store.r.tl.value.filter.withReplies;
}
},
set: (x) => saveTlFilter('withReplies', x),
@@ -98,7 +98,7 @@ const onlyFiles = computed<boolean>({
if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
return false;
} else {
return store.reactiveState.tl.value.filter.onlyFiles;
return store.r.tl.value.filter.onlyFiles;
}
},
set: (x) => saveTlFilter('onlyFiles', x),
@@ -115,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
});
const withSensitive = computed<boolean>({
get: () => store.reactiveState.tl.value.filter.withSensitive,
get: () => store.r.tl.value.filter.withSensitive,
set: (x) => saveTlFilter('withSensitive', x),
});
@@ -196,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
}
function saveSrc(newSrc: TimelinePageSrc): void {
const out = deepMerge({ src: newSrc }, store.state.tl);
const out = deepMerge({ src: newSrc }, store.s.tl);
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null;
}
store.set('tl', out);
store.commit('tl', out);
if (['local', 'global'].includes(newSrc)) {
srcWhenNotSignin.value = newSrc as 'local' | 'global';
}
}
function saveTlFilter(key: keyof typeof store.state.tl.filter, newValue: boolean) {
function saveTlFilter(key: keyof typeof store.s.tl.filter, newValue: boolean) {
if (key !== 'withReplies' || $i) {
const out = deepMerge({ filter: { [key]: newValue } }, store.state.tl);
store.set('tl', out);
const out = deepMerge({ filter: { [key]: newValue } }, store.s.tl);
store.commit('tl', out);
}
}
@@ -231,9 +231,9 @@ function focus(): void {
function closeTutorial(): void {
if (!isBasicTimeline(src.value)) return;
const before = store.state.timelineTutorials;
const before = store.s.timelineTutorials;
before[src.value] = true;
store.set('timelineTutorials', before);
store.commit('timelineTutorials', before);
}
function switchTlIfNeeded() {

View File

@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorFollowLocal = '#008FFB';
const colorFollowRemote = '#008FFB88';

View File

@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorNormal = '#008FFB';
const colorReply = '#FEB019';

View File

@@ -64,7 +64,7 @@ async function renderChart() {
const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorUser = '#3498db';
const colorVisitor = '#2ecc71';

View File

@@ -3,251 +3,70 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// PIZZAX --- A lightweight store
import { computed, onUnmounted, ref, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import type { Ref, WritableComputedRef } from 'vue';
import { onUnmounted, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel';
import type { Ref } from 'vue';
import { $i } from '@/account.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { get, set } from '@/utility/idb-proxy.js';
import { store } from '@/store.js';
import { useStream } from '@/stream.js';
import { deepClone } from '@/utility/clone.js';
import { deepMerge } from '@/utility/merge.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
default: any;
}>;
//type DottedToNested<T extends Record<string, any>> = {
// [K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
//};
type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; };
type ReactiveState<T extends StateDef> = { [K in keyof T]: Ref<T[K]['default']>; };
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type PizzaxChannelMessage<T extends StateDef> = {
where: 'device' | 'deviceAccount';
key: keyof T;
value: T[keyof T]['default'];
userId?: string;
type PizzaxEvent<Data extends Record<string, any>> = {
updated: <K extends keyof Data>(ctx: {
key: K;
value: Data[K];
}) => void;
};
export class Storage<T extends StateDef> {
public readonly ready: Promise<void>;
public readonly loaded: Promise<void>;
export class Pizzax<Data extends Record<string, any>> extends EventEmitter<PizzaxEvent<Data>> {
/**
* static / state の略 (static が予約語のため)
*/
public s = {} as {
[K in keyof Data]: Data[K];
};
public readonly key: string;
public readonly deviceStateKeyName: `pizzax::${this['key']}`;
public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | '';
public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | '';
/**
* reactive の略
*/
public r = {} as {
[K in keyof Data]: Ref<Data[K]>;
};
public readonly def: T;
constructor(data: { [K in keyof Data]: Data[K] }) {
super();
// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
public readonly state: State<T>;
public readonly reactiveState: ReactiveState<T>;
private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
// 簡易的にキューイングして占有ロックとする
private currentIdbJob: Promise<any> = Promise.resolve();
private addIdbSetJob<T>(job: () => Promise<T>) {
const promise = this.currentIdbJob.then(job, err => {
console.error('Pizzax failed to save data to idb!', err);
return job();
});
this.currentIdbJob = promise;
return promise;
}
constructor(key: string, def: T) {
this.key = key;
this.deviceStateKeyName = `pizzax::${key}`;
this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : '';
this.def = def;
this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
this.state = {} as State<T>;
this.reactiveState = {} as ReactiveState<T>;
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
this.state[k] = v.default;
this.reactiveState[k] = ref(v.default);
}
this.ready = this.init();
this.loaded = this.ready.then(() => this.load());
}
private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
const merged = deepMerge(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
return merged as X;
}
return value;
}
private async init(): Promise<void> {
await this.migrate();
const deviceState: State<T> = await get(this.deviceStateKeyName) || {};
const deviceAccountState = $i ? await get(this.deviceAccountStateKeyName) || {} : {};
const registryCache = $i ? await get(this.registryCacheKeyName) || {} : {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else {
this.reactiveState[k].value = this.state[k] = v.default;
}
}
this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => {
// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
// まずないと思うけど一応弾いておく
if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
this.reactiveState[key].value = this.state[key] = value;
});
if ($i) {
const connection = useStream().useChannel('main');
// streamingのuser storage updateイベントを監視して更新
connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
this.reactiveState[key].value = this.state[key] = value;
this.addIdbSetJob(async () => {
const cache = await get(this.registryCacheKeyName);
if (cache[key] !== value) {
cache[key] = value;
await set(this.registryCacheKeyName, cache);
}
});
});
for (const key in data) {
this.s[key] = data[key];
this.r[key] = ref(this.s[key]);
}
}
private load(): Promise<void> {
return new Promise((resolve, reject) => {
if ($i) {
// api関数と循環参照なので一応setTimeoutしておく
window.setTimeout(async () => {
await store.ready;
misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
.then(kvs => {
const cache: Partial<T> = {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'account') {
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
cache[k] = (kvs as Partial<T>)[k];
} else {
this.reactiveState[k].value = this.state[k] = v.default;
}
}
}
return set(this.registryCacheKeyName, cache);
})
.then(() => resolve());
}, 1);
} else {
resolve();
}
});
public commit<K extends keyof Data>(key: K, value: Data[K]) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
this.emit('updated', { key, value: v });
}
public set<K extends keyof T>(key: K, value: T[K]['default']): Promise<void> {
// IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする
// (JSON.parse(JSON.stringify(value))の代わり)
const rawValue = deepClone(value);
this.reactiveState[key].value = this.state[key] = rawValue;
return this.addIdbSetJob(async () => {
switch (this.def[key].where) {
case 'device': {
this.pizzaxChannel.postMessage({
where: 'device',
key,
value: rawValue,
});
const deviceState = await get(this.deviceStateKeyName) || {};
deviceState[key] = rawValue;
await set(this.deviceStateKeyName, deviceState);
break;
}
case 'deviceAccount': {
if ($i == null) break;
this.pizzaxChannel.postMessage({
where: 'deviceAccount',
key,
value: rawValue,
userId: $i.id,
});
const deviceAccountState = await get(this.deviceAccountStateKeyName) || {};
deviceAccountState[key] = rawValue;
await set(this.deviceAccountStateKeyName, deviceAccountState);
break;
}
case 'account': {
if ($i == null) break;
const cache = await get(this.registryCacheKeyName) || {};
cache[key] = rawValue;
await set(this.registryCacheKeyName, cache);
await misskeyApi('i/registry/set', {
scope: ['client', this.key],
key: key.toString(),
value: rawValue,
});
break;
}
}
});
}
public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
const currentState = this.state[key];
this.set(key, [...currentState, value]);
}
public reset(key: keyof T) {
this.set(key, this.def[key].default);
return this.def[key].default;
public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
}
/**
* 特定のキーの、簡易的なgetter/setterを作ります
* 特定のキーの、簡易的なcomputed refを作ります
* 主にvue上で設定コントロールのmodelとして使う用
*/
public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
public model<K extends keyof Data, V extends Data[K] = Data[K]>(
key: K,
getter?: (v: T[K]['default']) => R,
setter?: (v: R) => T[K]['default'],
): {
get: () => R;
set: (value: R) => void;
} {
const valueRef = ref(this.state[key]);
getter?: (v: Data[K]) => V,
setter?: (v: V) => Data[K],
): WritableComputedRef<V> {
const valueRef = ref(this.s[key]);
const stop = watch(this.reactiveState[key], val => {
const stop = watch(this.r[key], val => {
valueRef.value = val;
});
@@ -257,7 +76,7 @@ export class Storage<T extends StateDef> {
});
// TODO: VueのcustomRef使うと良い感じになるかも
return {
return computed({
get: () => {
if (getter) {
return getter(valueRef.value);
@@ -267,30 +86,9 @@ export class Storage<T extends StateDef> {
},
set: (value) => {
const val = setter ? setter(value) : value;
this.set(key, val);
this.commit(key, val);
valueRef.value = val;
},
};
}
// localStorage => indexedDBのマイグレーション
private async migrate() {
const deviceState = localStorage.getItem(this.deviceStateKeyName);
if (deviceState) {
await set(this.deviceStateKeyName, JSON.parse(deviceState));
localStorage.removeItem(this.deviceStateKeyName);
}
const deviceAccountState = $i && localStorage.getItem(this.deviceAccountStateKeyName);
if ($i && deviceAccountState) {
await set(this.deviceAccountStateKeyName, JSON.parse(deviceAccountState));
localStorage.removeItem(this.deviceAccountStateKeyName);
}
const registryCache = $i && localStorage.getItem(this.registryCacheKeyName);
if ($i && registryCache) {
await set(this.registryCacheKeyName, JSON.parse(registryCache));
localStorage.removeItem(this.registryCacheKeyName);
}
});
}
}

View File

@@ -7,8 +7,9 @@ import { ref, defineAsyncComponent } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions';
import { v4 as uuid } from 'uuid';
import * as Misskey from 'misskey-js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors, store } from '@/store.js';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
@@ -92,7 +93,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
export async function authorizePlugin(plugin: Plugin) {
if (plugin.permissions == null || plugin.permissions.length === 0) return;
if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) return;
if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) return;
const token = await new Promise<string>((res, rej) => {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
@@ -114,8 +115,8 @@ export async function authorizePlugin(plugin: Plugin) {
});
});
store.set('pluginTokens', {
...store.state.pluginTokens,
store.commit('pluginTokens', {
...store.s.pluginTokens,
[plugin.installId]: token,
});
}
@@ -130,6 +131,10 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
realMeta = meta;
}
if (prefer.s.plugins.some(x => x.name === realMeta.name)) {
throw new Error('Plugin already installed');
}
const installId = uuid();
const plugin = {
@@ -140,23 +145,159 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
src: code,
};
prefer.set('plugins', prefer.s.plugins.concat(plugin));
prefer.commit('plugins', prefer.s.plugins.concat(plugin));
await authorizePlugin(plugin);
await launchPlugin(installId);
}
export async function uninstallPlugin(plugin: Plugin) {
prefer.set('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
if (Object.hasOwn(store.state.pluginTokens, plugin.installId)) {
abortPlugin(plugin);
prefer.commit('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) {
await os.apiWithDialog('i/revoke-token', {
token: store.state.pluginTokens[plugin.installId],
token: store.s.pluginTokens[plugin.installId],
});
const pluginTokens = { ...store.state.pluginTokens };
const pluginTokens = { ...store.s.pluginTokens };
delete pluginTokens[plugin.installId];
store.set('pluginTokens', pluginTokens);
store.commit('pluginTokens', pluginTokens);
}
}
const pluginContexts = new Map<Plugin['installId'], Interpreter>();
export const pluginLogs = ref(new Map<Plugin['installId'], {
at: number;
message: string;
isSystem?: boolean;
isError?: boolean;
}[]>());
type HandlerDef = {
post_form_action: {
title: string,
handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void;
};
user_action: {
title: string,
handler: (user: Misskey.entities.UserDetailed) => void;
};
note_action: {
title: string,
handler: (note: Misskey.entities.Note) => void;
};
note_view_interruptor: {
handler: (note: Misskey.entities.Note) => unknown;
};
note_post_interruptor: {
handler: (note: FIXME) => unknown;
};
page_view_interruptor: {
handler: (page: Misskey.entities.Page) => unknown;
};
};
type PluginHandler<K extends keyof HandlerDef> = {
pluginInstallId: string;
type: K;
ctx: HandlerDef[K];
};
let pluginHandlers: PluginHandler<keyof HandlerDef>[] = [];
function addPluginHandler<K extends keyof HandlerDef>(installId: Plugin['installId'], type: K, ctx: PluginHandler<K>['ctx']) {
pluginLogs.value.get(installId)!.push({
at: Date.now(),
isSystem: true,
message: `Handler registered: ${type}`,
});
pluginHandlers.push({ pluginInstallId: installId, type, ctx });
}
export function launchPlugins() {
for (const plugin of prefer.s.plugins) {
if (plugin.active) {
launchPlugin(plugin.installId);
}
}
}
async function launchPlugin(id: Plugin['installId']): Promise<void> {
const plugin = prefer.s.plugins.find(x => x.installId === id);
if (!plugin) return;
// 後方互換性のため
if (plugin.src == null) return;
pluginLogs.value.set(plugin.installId, []);
function systemLog(message: string, isError = false): void {
pluginLogs.value.get(plugin.installId)?.push({
at: Date.now(),
isSystem: true,
message,
isError,
});
}
systemLog('Starting plugin...');
await authorizePlugin(plugin);
const aiscript = new Interpreter(createPluginEnv({
plugin: plugin,
storageKey: 'plugins:' + plugin.installId,
}), {
in: aiScriptReadline,
out: (value): void => {
pluginLogs.value.get(plugin.installId)!.push({
at: Date.now(),
message: utils.reprValue(value),
});
},
log: (): void => {
},
err: (err): void => {
pluginLogs.value.get(plugin.installId)!.push({
at: Date.now(),
message: `${err}`,
isError: true,
});
throw err; // install時のtry-catchに反応させる
},
});
pluginContexts.set(plugin.installId, aiscript);
aiscript.exec(parser.parse(plugin.src)).then(
() => {
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
systemLog('Plugin started');
},
(err) => {
console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
systemLog(`${err}`, true);
throw err;
},
);
}
export function abortPlugin(plugin: Plugin): void {
const pluginContext = pluginContexts.get(plugin.installId);
if (!pluginContext) return;
pluginContext.abort();
pluginContexts.delete(plugin.installId);
pluginLogs.value.delete(plugin.installId);
pluginHandlers = pluginHandlers.filter(x => x.pluginInstallId !== plugin.installId);
}
export function reloadPlugin(plugin: Plugin): void {
abortPlugin(plugin);
launchPlugin(plugin.installId);
}
export async function configPlugin(plugin: Plugin) {
if (plugin.config == null) {
throw new Error('This plugin does not have a config');
@@ -170,51 +311,19 @@ export async function configPlugin(plugin: Plugin) {
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
reloadPlugin(plugin);
}
export function changePluginActive(plugin: Plugin, active: boolean) {
prefer.set('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
}
prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
const pluginContexts = new Map<string, Interpreter>();
export const pluginLogs = ref(new Map<string, string[]>());
export async function launchPlugin(plugin: Plugin): Promise<void> {
// 後方互換性のため
if (plugin.src == null) return;
await authorizePlugin(plugin);
const aiscript = new Interpreter(createPluginEnv({
plugin: plugin,
storageKey: 'plugins:' + plugin.installId,
}), {
in: aiScriptReadline,
out: (value): void => {
console.log(value);
pluginLogs.value.get(plugin.installId).push(utils.reprValue(value));
},
log: (): void => {
},
err: (err): void => {
pluginLogs.value.get(plugin.installId).push(`${err}`);
throw err; // install時のtry-catchに反応させる
},
});
pluginContexts.set(plugin.installId, aiscript);
pluginLogs.value.set(plugin.installId, []);
aiscript.exec(parser.parse(plugin.src)).then(
() => {
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
},
(err) => {
console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
throw err;
},
);
if (active) {
launchPlugin(plugin.installId);
} else {
abortPlugin(plugin);
}
}
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
@@ -225,111 +334,99 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s
config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
}
return {
...createAiScriptEnv({ ...opts, token: store.state.pluginTokens[id] }),
function withContext<T>(fn: (ctx: Interpreter) => T): T {
const ctx = pluginContexts.get(id);
if (!ctx) throw new Error('Plugin context not found');
return fn(ctx);
}
'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
const env: Record<string, values.Value> = {
...createAiScriptEnv({ ...opts, token: store.s.pluginTokens[id] }),
'Plugin:register:post_form_action': values.FN_NATIVE(([title, handler]) => {
utils.assertString(title);
registerPostFormAction({ pluginId: id, title: title.value, handler });
utils.assertFunction(handler);
addPluginHandler(id, 'post_form_action', {
title: title.value,
handler: withContext(ctx => (form, update) => {
ctx.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
if (!key || !value) {
return;
}
update(utils.valToJs(key), utils.valToJs(value));
})]);
}),
});
}),
'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
'Plugin:register:user_action': values.FN_NATIVE(([title, handler]) => {
utils.assertString(title);
registerUserAction({ pluginId: id, title: title.value, handler });
utils.assertFunction(handler);
addPluginHandler(id, 'user_action', {
title: title.value,
handler: withContext(ctx => (user) => {
ctx.execFn(handler, [utils.jsToVal(user)]);
}),
});
}),
'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
'Plugin:register:note_action': values.FN_NATIVE(([title, handler]) => {
utils.assertString(title);
registerNoteAction({ pluginId: id, title: title.value, handler });
utils.assertFunction(handler);
addPluginHandler(id, 'note_action', {
title: title.value,
handler: withContext(ctx => (note) => {
ctx.execFn(handler, [utils.jsToVal(note)]);
}),
});
}),
'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
registerNoteViewInterruptor({ pluginId: id, handler });
'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'note_view_interruptor', {
handler: withContext(ctx => async (note) => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
}),
});
}),
'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
registerNotePostInterruptor({ pluginId: id, handler });
'Plugin:register:note_post_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'note_post_interruptor', {
handler: withContext(ctx => async (note) => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
}),
});
}),
'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => {
registerPageViewInterruptor({ pluginId: id, handler });
'Plugin:register:page_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'page_view_interruptor', {
handler: withContext(ctx => async (page) => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(page)]));
}),
});
}),
'Plugin:open_url': values.FN_NATIVE(([url]) => {
utils.assertString(url);
window.open(url.value, '_blank', 'noopener');
}),
'Plugin:config': values.OBJ(config),
};
// 後方互換性のため
env['Plugin:register_post_form_action'] = env['Plugin:register:post_form_action'];
env['Plugin:register_user_action'] = env['Plugin:register:user_action'];
env['Plugin:register_note_action'] = env['Plugin:register:note_action'];
env['Plugin:register_note_view_interruptor'] = env['Plugin:register:note_view_interruptor'];
env['Plugin:register_note_post_interruptor'] = env['Plugin:register:note_post_interruptor'];
env['Plugin:register_page_view_interruptor'] = env['Plugin:register:page_view_interruptor'];
return env;
}
function registerPostFormAction({ pluginId, title, handler }): void {
postFormActions.push({
title, handler: (form, update) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
pluginContext.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
if (!key || !value) {
return;
}
update(utils.valToJs(key), utils.valToJs(value));
})]);
},
});
}
function registerUserAction({ pluginId, title, handler }): void {
userActions.push({
title, handler: (user) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
pluginContext.execFn(handler, [utils.jsToVal(user)]);
},
});
}
function registerNoteAction({ pluginId, title, handler }): void {
noteActions.push({
title, handler: (note) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
pluginContext.execFn(handler, [utils.jsToVal(note)]);
},
});
}
function registerNoteViewInterruptor({ pluginId, handler }): void {
noteViewInterruptors.push({
handler: async (note) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
},
});
}
function registerNotePostInterruptor({ pluginId, handler }): void {
notePostInterruptors.push({
handler: async (note) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
},
});
}
function registerPageViewInterruptor({ pluginId, handler }): void {
pageViewInterruptors.push({
handler: async (page) => {
const pluginContext = pluginContexts.get(pluginId);
if (!pluginContext) {
return;
}
return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(page)]));
},
});
export function getPluginHandlers<K extends keyof HandlerDef>(type: K): HandlerDef[K][] {
return pluginHandlers.filter((x): x is PluginHandler<K> => x.type === type).map(x => x.ctx);
}

View File

@@ -65,7 +65,7 @@ let latestBackupAt = 0;
window.setInterval(() => {
if ($i == null) return;
if (!store.state.enablePreferencesAutoCloudBackup) return;
if (!store.s.enablePreferencesAutoCloudBackup) return;
if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
if (profileManager.profile.modifiedAt <= latestBackupAt) return;

View File

@@ -8,6 +8,7 @@ import { hemisphere } from '@@/js/intl-const.js';
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 { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
/** サウンド設定 */
@@ -45,6 +46,9 @@ export const PREF_DEF = {
}[],
},
overridedDeviceKind: {
default: null as DeviceKind | null,
},
themes: {
default: [] as Theme[],
},

View File

@@ -8,11 +8,11 @@ import { v4 as uuid } from 'uuid';
import { host, version } from '@@/js/config.js';
import { EventEmitter } from 'eventemitter3';
import { PREF_DEF } from './def.js';
import { Store } from './store.js';
import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import { Pizzax } from '@/pizzax.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@@ -48,7 +48,7 @@ export class ProfileManager extends EventEmitter<{
}) => void;
}> {
public profile: PreferencesProfile;
public store: Store<{
public store: Pizzax<{
[K in keyof PREF]: ValueOf<K>;
}>;
@@ -58,7 +58,7 @@ export class ProfileManager extends EventEmitter<{
const states = this.genStates();
this.store = new Store(states);
this.store = new Pizzax(states);
this.store.addListener('updated', ({ key, value }) => {
console.log('prefer:set', key, value);
@@ -222,7 +222,7 @@ export class ProfileManager extends EventEmitter<{
text: i18n.ts.resetToDefaultValue,
danger: true,
action: () => {
this.store.set(key, PREF_DEF[key].default);
this.store.commit(key, PREF_DEF[key].default);
},
}, {
type: 'divider',

View File

@@ -1,92 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, onUnmounted, ref, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import type { Ref, WritableComputedRef } from 'vue';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
//type DottedToNested<T extends Record<string, any>> = {
// [K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
//};
type StoreEvent<Data extends Record<string, any>> = {
updated: <K extends keyof Data>(ctx: {
key: K;
value: Data[K];
}) => void;
};
export class Store<Data extends Record<string, any>> extends EventEmitter<StoreEvent<Data>> {
/**
* static の略 (static が予約語のため)
*/
public s = {} as {
[K in keyof Data]: Data[K];
};
/**
* reactive の略
*/
public r = {} as {
[K in keyof Data]: Ref<Data[K]>;
};
constructor(data: { [K in keyof Data]: Data[K] }) {
super();
for (const key in data) {
this.s[key] = data[key];
this.r[key] = ref(this.s[key]);
}
}
public set<K extends keyof Data>(key: K, value: Data[K]) {
this.r[key].value = this.s[key] = value;
this.emit('updated', { key, value });
}
public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
this.r[key].value = this.s[key] = value;
}
/**
* 特定のキーの、簡易的なcomputed refを作ります
* 主にvue上で設定コントロールのmodelとして使う用
*/
public model<K extends keyof Data, V extends Data[K] = Data[K]>(
key: K,
getter?: (v: Data[K]) => V,
setter?: (v: V) => Data[K],
): WritableComputedRef<V> {
const valueRef = ref(this.s[key]);
const stop = watch(this.r[key], val => {
valueRef.value = val;
});
// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
onUnmounted(() => {
stop();
});
// TODO: VueのcustomRef使うと良い感じになるかも
return computed({
get: () => {
if (getter) {
return getter(valueRef.value);
} else {
return valueRef.value;
}
},
set: (value) => {
const val = setter ? setter(value) : value;
this.set(key, val);
valueRef.value = val;
},
});
}
}

View File

@@ -21,7 +21,7 @@ function canAutoBackup() {
}
export function getPreferencesProfileMenu(): MenuItem[] {
const autoBackupEnabled = ref(store.state.enablePreferencesAutoCloudBackup);
const autoBackupEnabled = ref(store.s.enablePreferencesAutoCloudBackup);
watch(autoBackupEnabled, () => {
if (autoBackupEnabled.value) {
@@ -34,9 +34,9 @@ export function getPreferencesProfileMenu(): MenuItem[] {
return;
}
store.set('enablePreferencesAutoCloudBackup', true);
store.commit('enablePreferencesAutoCloudBackup', true);
} else {
store.set('enablePreferencesAutoCloudBackup', false);
store.commit('enablePreferencesAutoCloudBackup', false);
}
});
@@ -183,7 +183,7 @@ export async function restoreFromCloudBackup() {
miLocalStorage.setItem('preferences', JSON.stringify(profile));
miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
store.set('enablePreferencesAutoCloudBackup', true);
store.commit('enablePreferencesAutoCloudBackup', true);
shouldSuggestRestoreBackup.value = false;
unisonReload();
}
@@ -197,7 +197,7 @@ export async function enableAutoBackup() {
return;
}
store.set('enablePreferencesAutoCloudBackup', true);
store.commit('enablePreferencesAutoCloudBackup', true);
}
export const shouldSuggestRestoreBackup = ref(false);

View File

@@ -8,51 +8,219 @@ import * as Misskey from 'misskey-js';
import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5';
import { hemisphere } from '@@/js/intl-const.js';
import { BroadcastChannel } from 'broadcast-channel';
import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js';
import type { Column } from '@/deck.js';
import { miLocalStorage } from '@/local-storage.js';
import { Storage } from '@/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
import { Pizzax } from '@/pizzax.js';
import { $i } from '@/account.js';
import * as idb from '@/utility/idb-proxy.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
import { deepMerge } from '@/utility/merge.js';
import { deepClone } from '@/utility/clone.js';
interface PostFormAction {
title: string,
handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void;
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
default: any;
}>;
type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; };
type PizzaxChannelMessage<T extends StateDef> = {
where: 'device' | 'deviceAccount';
key: keyof T;
value: T[keyof T]['default'];
userId?: string;
};
// TODO: export消す
export class Store<T extends StateDef> extends Pizzax<State<T>> {
public readonly def: T;
public readonly ready: Promise<void>;
public readonly loaded: Promise<void>;
public readonly key: string;
public readonly deviceStateKeyName: `pizzax::${this['key']}`;
public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | '';
public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | '';
private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
// 簡易的にキューイングして占有ロックとする
private currentIdbJob: Promise<any> = Promise.resolve();
private addIdbSetJob<T>(job: () => Promise<T>) {
const promise = this.currentIdbJob.then(job, err => {
console.error('Pizzax failed to save data to idb!', err);
return job();
});
this.currentIdbJob = promise;
return promise;
}
constructor(def: T, key = 'base') {
const data = {} as State<T>;
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
data[k] = v.default;
}
super(data);
this.key = key;
this.deviceStateKeyName = `pizzax::${key}`;
this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : '';
this.def = def;
this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
this.ready = this.init();
this.loaded = this.ready.then(() => this.load());
this.addListener('updated', ({ key, value }) => {
// IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする
// (JSON.parse(JSON.stringify(value))の代わり)
const rawValue = deepClone(value);
this.r[key].value = this.s[key] = rawValue;
return this.addIdbSetJob(async () => {
switch (this.def[key].where) {
case 'device': {
this.pizzaxChannel.postMessage({
where: 'device',
key,
value: rawValue,
});
const deviceState = await idb.get(this.deviceStateKeyName) || {};
deviceState[key] = rawValue;
await idb.set(this.deviceStateKeyName, deviceState);
break;
}
case 'deviceAccount': {
if ($i == null) break;
this.pizzaxChannel.postMessage({
where: 'deviceAccount',
key,
value: rawValue,
userId: $i.id,
});
const deviceAccountState = await idb.get(this.deviceAccountStateKeyName) || {};
deviceAccountState[key] = rawValue;
await idb.set(this.deviceAccountStateKeyName, deviceAccountState);
break;
}
case 'account': {
if ($i == null) break;
const cache = await idb.get(this.registryCacheKeyName) || {};
cache[key] = rawValue;
await idb.set(this.registryCacheKeyName, cache);
await misskeyApi('i/registry/set', {
scope: ['client', this.key],
key: key.toString(),
value: rawValue,
});
break;
}
}
});
});
}
private async init(): Promise<void> {
const deviceState: State<T> = await idb.get(this.deviceStateKeyName) || {};
const deviceAccountState = $i ? await idb.get(this.deviceAccountStateKeyName) || {} : {};
const registryCache = $i ? await idb.get(this.registryCacheKeyName) || {} : {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
this.rewrite(k, this.mergeState<T[keyof T]['default']>(deviceState[k], v.default));
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
this.rewrite(k, this.mergeState<T[keyof T]['default']>(registryCache[k], v.default));
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
this.rewrite(k, this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default));
} else {
this.rewrite(k, v.default);
}
}
this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => {
// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
// まずないと思うけど一応弾いておく
if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
this.r[key].value = this.s[key] = value;
});
if ($i) {
const connection = useStream().useChannel('main');
// streamingのuser storage updateイベントを監視して更新
connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.s[key] === value) return;
this.rewrite(key, value);
this.addIdbSetJob(async () => {
const cache = await idb.get(this.registryCacheKeyName);
if (cache[key] !== value) {
cache[key] = value;
await idb.set(this.registryCacheKeyName, cache);
}
});
});
}
}
private load(): Promise<void> {
return new Promise((resolve, reject) => {
if ($i) {
// api関数と循環参照なので一応setTimeoutしておく
window.setTimeout(async () => {
await store.ready;
misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
.then(kvs => {
const cache: Partial<T> = {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'account') {
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
this.rewrite(k, (kvs as Partial<T>)[k]);
cache[k] = (kvs as Partial<T>)[k];
} else {
this.r[k].value = this.s[k] = v.default;
}
}
}
return idb.set(this.registryCacheKeyName, cache);
})
.then(() => resolve());
}, 1);
} else {
resolve();
}
});
}
private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
const merged = deepMerge(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
return merged as X;
}
return value;
}
}
interface UserAction {
title: string,
handler: (user: Misskey.entities.UserDetailed) => void;
}
interface NoteAction {
title: string,
handler: (note: Misskey.entities.Note) => void;
}
interface NoteViewInterruptor {
handler: (note: Misskey.entities.Note) => unknown;
}
interface NotePostInterruptor {
handler: (note: FIXME) => unknown;
}
interface PageViewInterruptor {
handler: (page: Misskey.entities.Page) => unknown;
}
export const postFormActions: PostFormAction[] = [];
export const userActions: UserAction[] = [];
export const noteActions: NoteAction[] = [];
export const noteViewInterruptors: NoteViewInterruptor[] = [];
export const notePostInterruptors: NotePostInterruptor[] = [];
export const pageViewInterruptors: PageViewInterruptor[] = [];
/**
* 「状態」を管理するストア(not「設定」)
*/
export const store = markRaw(new Storage('base', {
const STORE_DEF = {
accountSetupWizard: {
where: 'account',
default: 0,
@@ -115,10 +283,6 @@ export const store = markRaw(new Storage('base', {
},
},
},
overridedDeviceKind: {
where: 'device',
default: null as DeviceKind | null,
},
darkMode: {
where: 'device',
default: false,
@@ -187,6 +351,10 @@ export const store = markRaw(new Storage('base', {
data: Record<string, any>;
}[],
},
overridedDeviceKind: {
where: 'device',
default: null as DeviceKind | null,
},
defaultSideView: {
where: 'device',
default: false,
@@ -499,9 +667,12 @@ export const store = markRaw(new Storage('base', {
},
},
//#endregion
}));
} satisfies StateDef;
// TODO: 他のタブと永続化されたstateを同期
/**
* 「状態」を管理するストア(not「設定」)
*/
export const store = markRaw(new Store(STORE_DEF));
const PREFIX = 'miux:' as const;
@@ -581,7 +752,7 @@ export class ColdDeviceStorage {
* 特定のキーの、簡易的なgetter/setterを作ります
* 主にvue場で設定コントロールのmodelとして使う用
*/
public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
public static model<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
// TODO: VueのcustomRef使うと良い感じになるかも
const valueRef = ColdDeviceStorage.ref(key);
return {

View File

@@ -23,11 +23,11 @@ export async function addTheme(theme: Theme): Promise<void> {
if (themes.some(t => t.id === theme.id)) {
throw new Error('already exists');
}
prefer.set('themes', [...themes, theme]);
prefer.commit('themes', [...themes, theme]);
}
export async function removeTheme(theme: Theme): Promise<void> {
if ($i == null) return;
const themes = getThemes().filter(t => t.id !== theme.id);
prefer.set('themes', themes);
prefer.commit('themes', themes);
}

View File

@@ -72,6 +72,9 @@ export function applyTheme(theme: Theme, persist = true) {
timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_');
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged');
}, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
@@ -108,7 +111,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged');
globalEvents.emit('themeChanging');
}
function compile(theme: Theme): Record<string, string> {

View File

@@ -105,7 +105,7 @@ const router = useRouter();
const forceIconOnly = ref(window.innerWidth <= 1279);
const iconOnly = computed(() => {
return forceIconOnly.value || (store.reactiveState.menuDisplay.value === 'sideIcon');
return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
});
const menu = computed(() => prefer.s.menu);
@@ -123,12 +123,12 @@ function calcViewState() {
window.addEventListener('resize', calcViewState);
watch(store.reactiveState.menuDisplay, () => {
watch(store.r.menuDisplay, () => {
calcViewState();
});
function toggleIconOnly() {
store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
store.commit('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
}
function openAccountMenu(ev: MouseEvent) {

View File

@@ -62,7 +62,7 @@ const WINDOW_THRESHOLD = 1400;
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
const menu = ref(prefer.s.menu);
// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
// const menuDisplay = store.model('menuDisplay');
const otherNavItemIndicated = computed<boolean>(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;

View File

@@ -67,7 +67,7 @@ import { prefer } from '@/preferences.js';
const WINDOW_THRESHOLD = 1400;
const menu = ref(prefer.s.menu);
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
const menuDisplay = store.model('menuDisplay');
const otherNavItemIndicated = computed<boolean>(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
@@ -100,7 +100,7 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
watch(store.reactiveState.menuDisplay, () => {
watch(store.r.menuDisplay, () => {
calcViewState();
});

View File

@@ -75,7 +75,7 @@ const widgetsShowing = ref(false);
const fullView = ref(false);
const globalHeaderHeight = ref(0);
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
const showMenuOnTop = computed(() => store.state.menuDisplay === 'top');
const showMenuOnTop = computed(() => store.s.menuDisplay === 'top');
const live2d = shallowRef<HTMLIFrameElement>();
const widgetsLeft = ref<HTMLElement>();
const widgetsRight = ref<HTMLElement>();
@@ -97,7 +97,7 @@ provide('shouldHeaderThin', showMenuOnTop.value);
provide('forceSpacerMin', true);
function attachSticky(el: HTMLElement) {
const sticky = new StickySidebar(el, 0, store.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
@@ -144,7 +144,7 @@ if (window.innerWidth < 1024) {
document.documentElement.style.overflowY = 'scroll';
if (prefer.s.widgets.length === 0) {
prefer.set('widgets', [{
prefer.commit('widgets', [{
name: 'calendar',
id: 'a', place: null, data: {},
}, {

View File

@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.sideMenu">
<div :class="$style.sideMenuTop">
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.state['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div :class="$style.sideMenuMiddle">
@@ -137,7 +137,7 @@ const columnComponents = {
mainRouter.navHook = (path, flag): boolean => {
if (flag === 'forcePage') return false;
const noMainColumn = !store.state['deck.columns'].some(x => x.type === 'main');
const noMainColumn = !store.s['deck.columns'].some(x => x.type === 'main');
if (prefer.s['deck.navWindow'] || noMainColumn) {
os.pageWindow(path);
return true;
@@ -160,8 +160,8 @@ watch(route, () => {
});
*/
const columns = store.reactiveState['deck.columns'];
const layout = store.reactiveState['deck.layout'];
const columns = store.r['deck.columns'];
const layout = store.r['deck.layout'];
const menuIndicated = computed(() => {
if ($i == null) return false;
for (const def in navbarItemDef) {
@@ -214,15 +214,15 @@ loadDeck();
function changeProfile(ev: MouseEvent) {
let items: MenuItem[] = [{
text: store.state['deck.profile'],
text: store.s['deck.profile'],
active: true,
action: () => {},
}];
getProfiles().then(profiles => {
items.push(...(profiles.filter(k => k !== store.state['deck.profile']).map(k => ({
items.push(...(profiles.filter(k => k !== store.s['deck.profile']).map(k => ({
text: k,
action: () => {
store.set('deck.profile', k);
store.commit('deck.profile', k);
unisonReload();
},
}))), { type: 'divider' as const }, {
@@ -237,7 +237,7 @@ function changeProfile(ev: MouseEvent) {
if (canceled || name == null) return;
os.promiseDialog((async () => {
await store.set('deck.profile', name);
await store.commit('deck.profile', name);
await forceSaveDeck();
})(), () => {
unisonReload();
@@ -252,19 +252,19 @@ function changeProfile(ev: MouseEvent) {
async function deleteProfile() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.deleteAreYouSure({ x: store.state['deck.profile'] }),
text: i18n.tsx.deleteAreYouSure({ x: store.s['deck.profile'] }),
});
if (canceled) return;
os.promiseDialog((async () => {
if (store.state['deck.profile'] === 'default') {
await store.set('deck.columns', []);
await store.set('deck.layout', []);
if (store.s['deck.profile'] === 'default') {
await store.commit('deck.columns', []);
await store.commit('deck.layout', []);
await forceSaveDeck();
} else {
await deleteProfile_(store.state['deck.profile']);
await deleteProfile_(store.s['deck.profile']);
}
await store.set('deck.profile', 'default');
await store.commit('deck.profile', 'default');
})(), () => {
unisonReload();
});

View File

@@ -5,10 +5,10 @@
import { markRaw } from 'vue';
import type { Column } from '@/deck.js';
import { Storage } from '@/pizzax.js';
import { Store } from '@/store.js';
// TODO: 消す(移行済みのため)
export const deckStore = markRaw(new Storage('deck', {
export const deckStore = markRaw(new Store({
profile: {
where: 'deviceAccount',
default: 'default',
@@ -21,4 +21,4 @@ export const deckStore = markRaw(new Storage('deck', {
where: 'deviceAccount',
default: [] as Column['id'][][],
},
}));
}, 'deck'));

View File

@@ -178,7 +178,7 @@ if (window.innerWidth > 1024) {
}
if (prefer.s.widgets.length === 0) {
prefer.set('widgets', [{
prefer.commit('widgets', [{
name: 'calendar',
id: 'a', place: 'right', data: {},
}, {

View File

@@ -37,18 +37,18 @@ const widgets = computed(() => {
});
function addWidget(widget) {
prefer.set('widgets', [{
prefer.commit('widgets', [{
...widget,
place: props.place,
}, ...prefer.s.widgets]);
}
function removeWidget(widget) {
prefer.set('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
prefer.commit('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
}
function updateWidget({ id, data }) {
prefer.set('widgets', prefer.s.widgets.map(w => w.id === id ? {
prefer.commit('widgets', prefer.s.widgets.map(w => w.id === id ? {
...w,
data,
place: props.place,
@@ -57,17 +57,17 @@ function updateWidget({ id, data }) {
function updateWidgets(thisWidgets) {
if (props.place === null) {
prefer.set('widgets', thisWidgets);
prefer.commit('widgets', thisWidgets);
return;
}
if (props.place === 'left') {
prefer.set('widgets', [
prefer.commit('widgets', [
...thisWidgets.map(w => ({ ...w, place: 'left' })),
...prefer.s.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
]);
return;
}
prefer.set('widgets', [
prefer.commit('widgets', [
...prefer.s.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
...thisWidgets.map(w => ({ ...w, place: 'right' })),
]);

View File

@@ -25,7 +25,7 @@ class EmojiPicker {
}
public async init() {
const emojisRef = store.reactiveState.pinnedEmojis;
const emojisRef = store.r.pinnedEmojis;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: emojisRef,

View File

@@ -4,10 +4,10 @@
*/
import { defineAsyncComponent } from 'vue';
import type { Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import { claimAchievement } from './achievements.js';
import type { Ref, ShallowRef } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
@@ -15,7 +15,7 @@ import { instance } from '@/instance.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { store, noteActions } from '@/store.js';
import { store } from '@/store.js';
import { miLocalStorage } from '@/local-storage.js';
import { getUserMenu } from '@/utility/get-user-menu.js';
import { clipsCache, favoritedChannelsCache } from '@/cache.js';
@@ -24,6 +24,7 @@ import { isSupportShare } from '@/utility/navigator.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@@ -496,6 +497,7 @@ export function getNoteMenu(props: {
}
}
const noteActions = getPluginHandlers('note_action');
if (noteActions.length > 0) {
menuItems.push({ type: 'divider' });
@@ -606,8 +608,8 @@ export function getRenoteMenu(props: {
});
}
const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility;
const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly;
const configuredVisibility = prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility;
const localOnly = prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly;
let visibility = appearNote.visibility;
visibility = smallerVisibility(visibility, configuredVisibility);

View File

@@ -13,13 +13,13 @@ import { i18n } from '@/i18n.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
const meId = $i ? $i.id : null;
@@ -419,6 +419,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
});
}
const userActions = getPluginHandlers('user_action');
if (userActions.length > 0) {
menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
icon: 'ti ti-plug',

View File

@@ -52,7 +52,7 @@ export function initChart() {
// フォントカラー
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.animation = false;
}

View File

@@ -21,7 +21,7 @@ class ReactionPicker {
}
public async init() {
const reactionsRef = store.reactiveState.reactions;
const reactionsRef = store.r.reactions;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: reactionsRef,

View File

@@ -48,12 +48,12 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit,
);
const text = ref<string | null>(store.state.memo);
const text = ref<string | null>(store.s.memo);
const changed = ref(false);
let timeoutId;
const saveMemo = () => {
store.set('memo', text.value);
store.commit('memo', text.value);
changed.value = false;
};
@@ -63,7 +63,7 @@ const onChange = () => {
timeoutId = window.setTimeout(saveMemo, 1000);
};
watch(() => store.reactiveState.memo, newText => {
watch(() => store.r.memo, newText => {
text.value = newText.value;
});

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.3.2-alpha.1",
"version": "2025.3.2-alpha.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",