Compare commits
9 Commits
bfcb8c21e3
...
00b58ed587
Author | SHA1 | Date |
---|---|---|
|
00b58ed587 | |
|
4254f52ced | |
|
f14787bd10 | |
|
14cbc78031 | |
|
62e333191d | |
|
0c2d799acd | |
|
aa4c7a1313 | |
|
743995e469 | |
|
0254570fbf |
|
@ -39,6 +39,7 @@
|
|||
- Feat: 絵文字をミュート可能にする機能
|
||||
- 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました
|
||||
- Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的)
|
||||
- Enhance: 設定の同期をオンにするときに競合したときに値をマージできるように
|
||||
- Enhance: メモリ使用量を軽減しました
|
||||
- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加
|
||||
- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように
|
||||
|
@ -53,6 +54,7 @@
|
|||
- フロントエンドの読み込みサイズを軽量化しました
|
||||
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
|
||||
- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正
|
||||
- Fix: アカウント依存かつ初期状態である設定値をサーバー同期しようとした際に正しくコンフリクト検出されない問題を修正
|
||||
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
|
||||
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
|
||||
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
|
||||
|
@ -70,7 +72,7 @@
|
|||
- Fix: ミュート対象ユーザーが引用されているノートがRNされたときにミュートを貫通してしまう問題を修正 #16009
|
||||
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
|
||||
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正
|
||||
|
||||
- Fix: コントロールパネルのジョブキューページからPausedなジョブ一覧を閲覧できない問題を修正
|
||||
|
||||
## 2025.5.0
|
||||
|
||||
|
|
|
@ -1330,6 +1330,7 @@ restore: "Restaurar "
|
|||
syncBetweenDevices: "Sincronització entre dispositius"
|
||||
preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu"
|
||||
preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?"
|
||||
preferenceSyncConflictChoiceMerge: "Integració "
|
||||
preferenceSyncConflictChoiceServer: "Valors de configuració del servidor"
|
||||
preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu "
|
||||
preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització "
|
||||
|
|
|
@ -1330,6 +1330,7 @@ restore: "Restore"
|
|||
syncBetweenDevices: "Sync between devices"
|
||||
preferenceSyncConflictTitle: "The configured value exists on the server."
|
||||
preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?"
|
||||
preferenceSyncConflictChoiceMerge: "Merge"
|
||||
preferenceSyncConflictChoiceServer: "Configured value on server"
|
||||
preferenceSyncConflictChoiceDevice: "Configured value on device"
|
||||
preferenceSyncConflictChoiceCancel: "Cancel enabling sync"
|
||||
|
|
|
@ -5335,15 +5335,19 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"preferenceSyncConflictTitle": string;
|
||||
/**
|
||||
* 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?
|
||||
* 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか?
|
||||
*/
|
||||
"preferenceSyncConflictText": string;
|
||||
/**
|
||||
* サーバーの設定値
|
||||
* 統合する
|
||||
*/
|
||||
"preferenceSyncConflictChoiceMerge": string;
|
||||
/**
|
||||
* サーバーの設定値で上書き
|
||||
*/
|
||||
"preferenceSyncConflictChoiceServer": string;
|
||||
/**
|
||||
* デバイスの設定値
|
||||
* デバイスの設定値で上書き
|
||||
*/
|
||||
"preferenceSyncConflictChoiceDevice": string;
|
||||
/**
|
||||
|
|
|
@ -1329,9 +1329,10 @@ skip: "スキップ"
|
|||
restore: "復元"
|
||||
syncBetweenDevices: "デバイス間で同期"
|
||||
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
|
||||
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
|
||||
preferenceSyncConflictChoiceServer: "サーバーの設定値"
|
||||
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
|
||||
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どうしますか?"
|
||||
preferenceSyncConflictChoiceMerge: "統合する"
|
||||
preferenceSyncConflictChoiceServer: "サーバーの設定値で上書き"
|
||||
preferenceSyncConflictChoiceDevice: "デバイスの設定値で上書き"
|
||||
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
|
||||
paste: "ペースト"
|
||||
emojiPalette: "絵文字パレット"
|
||||
|
|
|
@ -1330,6 +1330,7 @@ restore: "恢复"
|
|||
syncBetweenDevices: "设备间同步"
|
||||
preferenceSyncConflictTitle: "服务器上已存在设定值"
|
||||
preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?"
|
||||
preferenceSyncConflictChoiceMerge: "合并"
|
||||
preferenceSyncConflictChoiceServer: "服务器上的设定值"
|
||||
preferenceSyncConflictChoiceDevice: "设备上的设定值"
|
||||
preferenceSyncConflictChoiceCancel: "取消同步"
|
||||
|
|
|
@ -327,6 +327,7 @@ dark: "深色"
|
|||
lightThemes: "淺色佈景主題"
|
||||
darkThemes: "深色佈景主題"
|
||||
syncDeviceDarkMode: "與裝置的深色模式同步"
|
||||
switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已開啟。要關閉同步並手動切換模式嗎?\n"
|
||||
drive: "雲端硬碟"
|
||||
fileName: "檔案名稱"
|
||||
selectFile: "選擇檔案"
|
||||
|
@ -1329,6 +1330,7 @@ restore: "還原"
|
|||
syncBetweenDevices: "裝置之間的同步化"
|
||||
preferenceSyncConflictTitle: "伺服器上存在設定值"
|
||||
preferenceSyncConflictText: "已啟用同步的設定項目會將設定值儲存至伺服器,並已找到該設定項目在伺服器上儲存的設定值。請選擇要使用哪個設定值進行覆寫。"
|
||||
preferenceSyncConflictChoiceMerge: "合併至"
|
||||
preferenceSyncConflictChoiceServer: "伺服器設定值"
|
||||
preferenceSyncConflictChoiceDevice: "裝置的設定值"
|
||||
preferenceSyncConflictChoiceCancel: "取消啟用同步"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.5.1-beta.5",
|
||||
"version": "2025.5.1-beta.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -28,7 +28,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
|
||||
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } },
|
||||
search: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'state'],
|
||||
|
|
|
@ -547,14 +547,28 @@ export function success(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export function waiting(text?: string | null): () => void {
|
||||
export function waiting(options: { text?: string } = {}) {
|
||||
window.document.body.setAttribute('inert', 'true');
|
||||
|
||||
const showing = ref(true);
|
||||
const isSuccess = ref(false);
|
||||
|
||||
function done(doneOptions: { success?: boolean } = {}) {
|
||||
if (doneOptions.success) {
|
||||
isSuccess.value = true;
|
||||
window.setTimeout(() => {
|
||||
showing.value = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
showing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
|
||||
const { dispose } = popup(MkWaitingDialog, {
|
||||
success: false,
|
||||
success: isSuccess,
|
||||
showing: showing,
|
||||
text,
|
||||
text: options.text,
|
||||
}, {
|
||||
closed: () => {
|
||||
window.document.body.removeAttribute('inert');
|
||||
|
@ -562,9 +576,7 @@ export function waiting(text?: string | null): () => void {
|
|||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
showing.value = false;
|
||||
};
|
||||
return done;
|
||||
}
|
||||
|
||||
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
|
||||
|
|
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkFolder>
|
||||
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary rounded @click="onFileSelectClicked">{{ i18n.ts.uplaod }}</MkButton>
|
||||
<MkButton primary rounded @click="onFileSelectClicked">{{ i18n.ts.upload }}</MkButton>
|
||||
<MkButton primary rounded @click="onDriveSelectClicked">{{ i18n.ts.fromDrive }}</MkButton>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])">
|
||||
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
|
||||
<template #suffix>...</template>
|
||||
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')">
|
||||
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')" @update:modelValue="v => policies.uploadableFileTypes = v.split('\n')">
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>
|
||||
|
|
|
@ -69,6 +69,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { PREF_DEF } from '@/preferences/def.js';
|
||||
import { getInitialPrefValue } from '@/preferences/manager.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
@ -106,7 +107,7 @@ async function save() {
|
|||
}
|
||||
|
||||
function reset() {
|
||||
items.value = PREF_DEF.menu.default.map(x => ({
|
||||
items.value = getInitialPrefValue('menu').map(x => ({
|
||||
id: Math.random().toString(),
|
||||
type: x,
|
||||
}));
|
||||
|
|
|
@ -75,6 +75,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
|
||||
import { PREF_DEF } from '@/preferences/def.js';
|
||||
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
||||
import { getInitialPrefValue } from '@/preferences/manager.js';
|
||||
|
||||
const notUseSound = prefer.model('sound.notUseSound');
|
||||
const useSoundOnlyWhenActive = prefer.model('sound.useSoundOnlyWhenActive');
|
||||
|
@ -113,7 +114,7 @@ async function updated(type: keyof typeof sounds.value, sound) {
|
|||
|
||||
function reset() {
|
||||
for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
|
||||
const v = PREF_DEF[`sound.on.${sound}`].default;
|
||||
const v = getInitialPrefValue(`sound.on.${sound}`);
|
||||
prefer.commit(`sound.on.${sound}`, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { i18n } from '@/i18n.js';
|
|||
|
||||
// TODO: そのうち消す
|
||||
export function migrateOldSettings() {
|
||||
os.waiting(i18n.ts.settingsMigrating);
|
||||
os.waiting({ text: i18n.ts.settingsMigrating });
|
||||
|
||||
store.loaded.then(async () => {
|
||||
misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: any) => {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { hemisphere } from '@@/js/intl-const.js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import type { SoundType } from '@/utility/sound.js';
|
||||
import type { Plugin } from '@/plugin.js';
|
||||
|
@ -12,6 +13,7 @@ import type { DeviceKind } from '@/utility/device-kind.js';
|
|||
import type { DeckProfile } from '@/deck.js';
|
||||
import type { PreferencesDefinition } from './manager.js';
|
||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
|
||||
/** サウンド設定 */
|
||||
export type SoundStore = {
|
||||
|
@ -49,15 +51,15 @@ export const PREF_DEF = {
|
|||
},
|
||||
widgets: {
|
||||
accountDependent: true,
|
||||
default: [{
|
||||
default: () => [{
|
||||
name: 'calendar',
|
||||
id: 'a', place: 'right', data: {},
|
||||
id: uuid(), place: 'right', data: {},
|
||||
}, {
|
||||
name: 'notifications',
|
||||
id: 'b', place: 'right', data: {},
|
||||
id: uuid(), place: 'right', data: {},
|
||||
}, {
|
||||
name: 'trends',
|
||||
id: 'c', place: 'right', data: {},
|
||||
id: uuid(), place: 'right', data: {},
|
||||
}] as {
|
||||
name: string;
|
||||
id: string;
|
||||
|
@ -76,8 +78,8 @@ export const PREF_DEF = {
|
|||
|
||||
emojiPalettes: {
|
||||
serverDependent: true,
|
||||
default: [{
|
||||
id: 'a',
|
||||
default: () => [{
|
||||
id: uuid(),
|
||||
name: '',
|
||||
emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||
}] as {
|
||||
|
@ -85,6 +87,22 @@ export const PREF_DEF = {
|
|||
name: string;
|
||||
emojis: string[];
|
||||
}[],
|
||||
mergeStrategy: (a, b) => {
|
||||
const mergedItems = [] as (typeof a)[];
|
||||
for (const x of a.concat(b)) {
|
||||
const sameIdItem = mergedItems.find(y => y.id === x.id);
|
||||
if (sameIdItem != null) {
|
||||
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
|
||||
continue;
|
||||
} else { // IDは同じなのに内容が違う場合はマージ不可とする
|
||||
throw new Error();
|
||||
}
|
||||
} else {
|
||||
mergedItems.push(x);
|
||||
}
|
||||
}
|
||||
return mergedItems;
|
||||
},
|
||||
},
|
||||
emojiPaletteForReaction: {
|
||||
serverDependent: true,
|
||||
|
@ -100,6 +118,22 @@ export const PREF_DEF = {
|
|||
},
|
||||
themes: {
|
||||
default: [] as Theme[],
|
||||
mergeStrategy: (a, b) => {
|
||||
const mergedItems = [] as (typeof a)[];
|
||||
for (const x of a.concat(b)) {
|
||||
const sameIdItem = mergedItems.find(y => y.id === x.id);
|
||||
if (sameIdItem != null) {
|
||||
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
|
||||
continue;
|
||||
} else { // IDは同じなのに内容が違う場合はマージ不可とする
|
||||
throw new Error();
|
||||
}
|
||||
} else {
|
||||
mergedItems.push(x);
|
||||
}
|
||||
}
|
||||
return mergedItems;
|
||||
},
|
||||
},
|
||||
lightTheme: {
|
||||
default: null as Theme | null,
|
||||
|
@ -345,9 +379,19 @@ export const PREF_DEF = {
|
|||
},
|
||||
plugins: {
|
||||
default: [] as Plugin[],
|
||||
mergeStrategy: (a, b) => {
|
||||
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
|
||||
if (sameIdExists) throw new Error();
|
||||
const sameNameExists = a.some(x => b.some(y => x.name === y.name));
|
||||
if (sameNameExists) throw new Error();
|
||||
return a.concat(b);
|
||||
},
|
||||
},
|
||||
mutingEmojis: {
|
||||
default: [] as string[],
|
||||
mergeStrategy: (a, b) => {
|
||||
return [...new Set(a.concat(b))];
|
||||
},
|
||||
},
|
||||
|
||||
'sound.masterVolume': {
|
||||
|
|
|
@ -22,7 +22,10 @@ import { deepEqual } from '@/utility/deep-equal.js';
|
|||
//};
|
||||
|
||||
type PREF = typeof PREF_DEF;
|
||||
type ValueOf<K extends keyof PREF> = PREF[K]['default'];
|
||||
type DefaultValues = {
|
||||
[K in keyof PREF]: PREF[K]['default'] extends (...args: any) => infer R ? R : PREF[K]['default'];
|
||||
};
|
||||
type ValueOf<K extends keyof PREF> = DefaultValues[K];
|
||||
|
||||
type Scope = Partial<{
|
||||
server: string | null; // host
|
||||
|
@ -84,12 +87,25 @@ export type StorageProvider = {
|
|||
cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; }) => Promise<void>;
|
||||
};
|
||||
|
||||
export type PreferencesDefinition = Record<string, {
|
||||
default: any;
|
||||
type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => infer R ? R : Default> = {
|
||||
default: Default;
|
||||
accountDependent?: boolean;
|
||||
serverDependent?: boolean;
|
||||
}>;
|
||||
mergeStrategy?: (a: T, b: T) => T;
|
||||
};
|
||||
|
||||
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
|
||||
|
||||
export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
|
||||
if (typeof PREF_DEF[k].default === 'function') { // factory
|
||||
return PREF_DEF[k].default();
|
||||
} else {
|
||||
return PREF_DEF[k].default;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すれば$iのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない
|
||||
// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる
|
||||
export class PreferencesManager {
|
||||
private storageProvider: StorageProvider;
|
||||
public profile: PreferencesProfile;
|
||||
|
@ -125,11 +141,11 @@ export class PreferencesManager {
|
|||
// TODO: 定期的にクラウドの値をフェッチ
|
||||
}
|
||||
|
||||
private isAccountDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
private static isAccountDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true;
|
||||
}
|
||||
|
||||
private isServerDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
private static isServerDependentKey<K extends keyof PREF>(key: K): boolean {
|
||||
return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true;
|
||||
}
|
||||
|
||||
|
@ -152,7 +168,7 @@ export class PreferencesManager {
|
|||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) {
|
||||
if (parseScope(record[0]).account == null && PreferencesManager.isAccountDependentKey(key)) {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
server: host,
|
||||
account: $i!.id,
|
||||
|
@ -161,7 +177,7 @@ export class PreferencesManager {
|
|||
return;
|
||||
}
|
||||
|
||||
if (parseScope(record[0]).server == null && this.isServerDependentKey(key)) {
|
||||
if (parseScope(record[0]).server == null && PreferencesManager.isServerDependentKey(key)) {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
server: host,
|
||||
}), v, {}]);
|
||||
|
@ -262,7 +278,19 @@ export class PreferencesManager {
|
|||
public static newProfile(): PreferencesProfile {
|
||||
const data = {} as PreferencesProfile['preferences'];
|
||||
for (const key in PREF_DEF) {
|
||||
data[key] = [[makeScope({}), PREF_DEF[key].default, {}]];
|
||||
const v = getInitialPrefValue(key as keyof typeof PREF_DEF);
|
||||
if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) {
|
||||
data[key] = $i ? [[makeScope({}), v, {}], [makeScope({
|
||||
server: host,
|
||||
account: $i.id,
|
||||
}), v, {}]] : [[makeScope({}), v, {}]];
|
||||
} else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) {
|
||||
data[key] = [[makeScope({
|
||||
server: host,
|
||||
}), v, {}]];
|
||||
} else {
|
||||
data[key] = [[makeScope({}), v, {}]];
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuid(),
|
||||
|
@ -279,18 +307,36 @@ export class PreferencesManager {
|
|||
for (const key in PREF_DEF) {
|
||||
const records = profileLike.preferences[key];
|
||||
if (records == null || records.length === 0) {
|
||||
data[key] = [[makeScope({}), PREF_DEF[key].default, {}]];
|
||||
const v = getInitialPrefValue(key as keyof typeof PREF_DEF);
|
||||
if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) {
|
||||
data[key] = $i ? [[makeScope({}), v, {}], [makeScope({
|
||||
server: host,
|
||||
account: $i.id,
|
||||
}), v, {}]] : [[makeScope({}), v, {}]];
|
||||
} else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) {
|
||||
data[key] = [[makeScope({
|
||||
server: host,
|
||||
}), v, {}]];
|
||||
} else {
|
||||
data[key] = [[makeScope({}), v, {}]];
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
data[key] = records;
|
||||
|
||||
// alpha段階ではmetaが無かったのでマイグレート
|
||||
// TODO: そのうち消す
|
||||
for (const record of data[key] as any[][]) {
|
||||
if (record.length === 2) {
|
||||
record.push({});
|
||||
}
|
||||
if ($i && PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id)) {
|
||||
data[key] = records.concat([[makeScope({
|
||||
server: host,
|
||||
account: $i.id,
|
||||
}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]);
|
||||
continue;
|
||||
}
|
||||
if ($i && PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host)) {
|
||||
data[key] = records.concat([[makeScope({
|
||||
server: host,
|
||||
}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]);
|
||||
continue;
|
||||
}
|
||||
|
||||
data[key] = records;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,7 +374,7 @@ export class PreferencesManager {
|
|||
|
||||
public setAccountOverride<K extends keyof PREF>(key: K) {
|
||||
if ($i == null) return;
|
||||
if (this.isAccountDependentKey(key)) throw new Error('already account-dependent');
|
||||
if (PreferencesManager.isAccountDependentKey(key)) throw new Error('already account-dependent');
|
||||
if (this.isAccountOverrided(key)) return;
|
||||
|
||||
const records = this.profile.preferences[key];
|
||||
|
@ -342,7 +388,7 @@ export class PreferencesManager {
|
|||
|
||||
public clearAccountOverride<K extends keyof PREF>(key: K) {
|
||||
if ($i == null) return;
|
||||
if (this.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property');
|
||||
if (PreferencesManager.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property');
|
||||
|
||||
const records = this.profile.preferences[key];
|
||||
|
||||
|
@ -363,14 +409,22 @@ export class PreferencesManager {
|
|||
public async enableSync<K extends keyof PREF>(key: K): Promise<{ enabled: boolean; } | null> {
|
||||
if (this.isSyncEnabled(key)) return Promise.resolve(null);
|
||||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
const existing = await this.storageProvider.cloudGet({ key, scope: record[0] });
|
||||
if (existing != null && !deepEqual(existing.value, record[1])) {
|
||||
const { canceled, result } = await os.select({
|
||||
// undefined ... cancel
|
||||
async function resolveConflict(local: ValueOf<K>, remote: ValueOf<K>): Promise<ValueOf<K> | undefined> {
|
||||
const merge = (PREF_DEF as PreferencesDefinition)[key].mergeStrategy;
|
||||
let mergedValue: ValueOf<K> | undefined = undefined; // null と区別したいため
|
||||
try {
|
||||
if (merge != null) mergedValue = merge(local, remote);
|
||||
} catch (err) {
|
||||
// nop
|
||||
}
|
||||
const { canceled, result: choice } = await os.select({
|
||||
title: i18n.ts.preferenceSyncConflictTitle,
|
||||
text: i18n.ts.preferenceSyncConflictText,
|
||||
items: [{
|
||||
items: [...(mergedValue !== undefined ? [{
|
||||
text: i18n.ts.preferenceSyncConflictChoiceMerge,
|
||||
value: 'merge',
|
||||
}] : []), {
|
||||
text: i18n.ts.preferenceSyncConflictChoiceServer,
|
||||
value: 'remote',
|
||||
}, {
|
||||
|
@ -380,23 +434,53 @@ export class PreferencesManager {
|
|||
text: i18n.ts.preferenceSyncConflictChoiceCancel,
|
||||
value: null,
|
||||
}],
|
||||
default: 'remote',
|
||||
default: mergedValue !== undefined ? 'merge' : 'remote',
|
||||
});
|
||||
if (canceled || result == null) return { enabled: false };
|
||||
if (canceled || choice == null) return undefined;
|
||||
|
||||
if (result === 'remote') {
|
||||
this.commit(key, existing.value);
|
||||
} else if (result === 'local') {
|
||||
// nop
|
||||
if (choice === 'remote') {
|
||||
return remote;
|
||||
} else if (choice === 'local') {
|
||||
return local;
|
||||
} else if (choice === 'merge') {
|
||||
return mergedValue!;
|
||||
}
|
||||
}
|
||||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
let newValue = record[1];
|
||||
|
||||
const existing = await this.storageProvider.cloudGet({ key, scope: record[0] });
|
||||
if (existing != null && !deepEqual(record[1], existing.value)) {
|
||||
const resolvedValue = await resolveConflict(record[1], existing.value);
|
||||
if (resolvedValue === undefined) return { enabled: false }; // canceled
|
||||
newValue = resolvedValue;
|
||||
}
|
||||
|
||||
this.commit(key, newValue);
|
||||
|
||||
const done = os.waiting();
|
||||
|
||||
try {
|
||||
await this.storageProvider.cloudSet({ key, scope: record[0], value: newValue });
|
||||
} catch (err) {
|
||||
done();
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.somethingHappened,
|
||||
text: err,
|
||||
});
|
||||
|
||||
return { enabled: false };
|
||||
}
|
||||
|
||||
done({ success: true });
|
||||
|
||||
record[2].sync = true;
|
||||
this.save();
|
||||
|
||||
// awaitの必要性は無い
|
||||
this.storageProvider.cloudSet({ key, scope: record[0], value: this.s[key] });
|
||||
|
||||
return { enabled: true };
|
||||
}
|
||||
|
||||
|
@ -457,7 +541,7 @@ export class PreferencesManager {
|
|||
text: i18n.ts.resetToDefaultValue,
|
||||
danger: true,
|
||||
action: () => {
|
||||
this.commit(key, PREF_DEF[key].default);
|
||||
this.commit(key, getInitialPrefValue(key));
|
||||
},
|
||||
}, {
|
||||
type: 'divider',
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import type { SoundStore } from '@/preferences/def.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { PREF_DEF } from '@/preferences/def.js';
|
||||
import { getInitialPrefValue } from '@/preferences/manager.js';
|
||||
|
||||
let ctx: AudioContext;
|
||||
const cache = new Map<string, AudioBuffer>();
|
||||
|
@ -133,7 +134,8 @@ export function playMisskeySfx(operationType: OperationType) {
|
|||
playMisskeySfxFile(sound).then((succeed) => {
|
||||
if (!succeed && sound.type === '_driveFile_') {
|
||||
// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
|
||||
const soundName = PREF_DEF[`sound.on.${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
|
||||
const default_ = getInitialPrefValue(`sound.on.${operationType}`);
|
||||
const soundName = default_.type as Exclude<SoundType, '_driveFile_'>;
|
||||
if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
|
||||
playMisskeySfxFileInternal({
|
||||
type: soundName,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.5.1-beta.5",
|
||||
"version": "2025.5.1-beta.6",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -9074,7 +9074,7 @@ export type operations = {
|
|||
'application/json': {
|
||||
/** @enum {string} */
|
||||
queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
|
||||
state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed')[];
|
||||
state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed' | 'paused')[];
|
||||
search?: string;
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue