summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/settings
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2026-03-05 10:56:50 +0000
committerGitHub <noreply@github.com>2026-03-05 10:56:50 +0000
commitfe3dd8edb5f30104cd0a7ed755eb254feda2922d (patch)
treeaf6cf5fa4ca75302ac2de5db742cead00bc13d21 /packages/frontend/src/pages/settings
parentMerge pull request #16998 from misskey-dev/develop (diff)
parentRelease: 2026.3.0 (diff)
downloadmisskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.gz
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.tar.bz2
misskey-fe3dd8edb5f30104cd0a7ed755eb254feda2922d.zip
Merge pull request #17217 from misskey-dev/develop
Release: 2026.3.0
Diffstat (limited to 'packages/frontend/src/pages/settings')
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue7
-rw-r--r--packages/frontend/src/pages/settings/account-data.vue12
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue4
-rw-r--r--packages/frontend/src/pages/settings/apps.vue4
-rw-r--r--packages/frontend/src/pages/settings/deck.vue34
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue11
-rw-r--r--packages/frontend/src/pages/settings/drive.ImageFrameItem.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive.WatermarkItem.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive.vue5
-rw-r--r--packages/frontend/src/pages/settings/email.vue4
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.palette.vue33
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.vue57
-rw-r--r--packages/frontend/src/pages/settings/index.vue8
-rw-r--r--packages/frontend/src/pages/settings/mute-block.emoji-mute.vue6
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue7
-rw-r--r--packages/frontend/src/pages/settings/mute-block.word-mute.vue8
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue39
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue22
-rw-r--r--packages/frontend/src/pages/settings/other.vue8
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue4
-rw-r--r--packages/frontend/src/pages/settings/preferences.vue117
-rw-r--r--packages/frontend/src/pages/settings/profile.vue43
-rw-r--r--packages/frontend/src/pages/settings/profiles.vue9
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue4
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue11
-rw-r--r--packages/frontend/src/pages/settings/statusbar.statusbar.vue16
-rw-r--r--packages/frontend/src/pages/settings/theme.vue2
27 files changed, 282 insertions, 197 deletions
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 2cc13744b1..bf71845a38 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
- <div v-text="i18n.ts._2fa.alreadyRegistered"/>
+ <div>{{ i18n.ts._2fa.alreadyRegistered }}</div>
<template v-if="$i.securityKeysList!.length > 0">
<MkButton @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
@@ -85,6 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, computed } from 'vue';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -156,7 +157,7 @@ function renewTOTP(): void {
});
}
-async function unregisterKey(key) {
+async function unregisterKey(key: NonNullable<Misskey.entities.MeDetailedOnly['securityKeysList']>[number]) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._2fa.removeKey,
@@ -175,7 +176,7 @@ async function unregisterKey(key) {
os.success();
}
-async function renameKey(key) {
+async function renameKey(key: NonNullable<Misskey.entities.MeDetailedOnly['securityKeysList']>[number]) {
const name = await os.inputText({
title: i18n.ts.rename,
default: key.name,
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index c75667b06b..b07515a49a 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -189,7 +189,7 @@ const onImportSuccess = () => {
});
};
-const onError = (ev) => {
+const onError = (ev: Error) => {
os.alert({
type: 'error',
text: ev.message,
@@ -232,7 +232,7 @@ const exportAntennas = () => {
misskeyApi('i/export-antennas', {}).then(onExportSuccess).catch(onError);
};
-const importFollowing = async (ev) => {
+const importFollowing = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -243,7 +243,7 @@ const importFollowing = async (ev) => {
}).then(onImportSuccess).catch(onError);
};
-const importUserLists = async (ev) => {
+const importUserLists = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -251,7 +251,7 @@ const importUserLists = async (ev) => {
misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importMuting = async (ev) => {
+const importMuting = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -259,7 +259,7 @@ const importMuting = async (ev) => {
misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importBlocking = async (ev) => {
+const importBlocking = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
@@ -267,7 +267,7 @@ const importBlocking = async (ev) => {
misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
-const importAntennas = async (ev) => {
+const importAntennas = async (ev: PointerEvent) => {
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 764ec72652..55a81bbf38 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -38,7 +38,7 @@ function refreshAllAccounts() {
// TODO
}
-function showMenu(host: string, id: string, ev: MouseEvent) {
+function showMenu(host: string, id: string, ev: PointerEvent) {
let menu: MenuItem[];
menu = [{
@@ -54,7 +54,7 @@ function showMenu(host: string, id: string, ev: MouseEvent) {
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
-function addAccount(ev: MouseEvent) {
+function addAccount(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.existingAccount,
action: () => { addExistingAccount(); },
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 10901f737b..e9857b1e0b 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.permission }}</template>
<template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
<ul>
- <li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
+ <li v-for="p in token.permission" :key="p">{{ (i18n.ts._permissions as any)[p] ?? p }}</li>
</ul>
</MkFolder>
</div>
@@ -68,7 +68,7 @@ const paginator = markRaw(new Paginator('i/apps', {
},
}));
-function revoke(token) {
+function revoke(token: Misskey.entities.IAppsResponse[number]) {
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
paginator.reload();
});
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 7a19b0495b..40fee6caaf 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -40,31 +40,43 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['column', 'align']">
<MkPreferenceContainer k="deck.columnAlign">
- <MkRadios v-model="columnAlign">
+ <MkRadios
+ v-model="columnAlign"
+ :options="[
+ { value: 'left', label: i18n.ts.left },
+ { value: 'center', label: i18n.ts.center },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
- <option value="left">{{ i18n.ts.left }}</option>
- <option value="center">{{ i18n.ts.center }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['menu', 'position']">
<MkPreferenceContainer k="deck.menuPosition">
- <MkRadios v-model="menuPosition">
+ <MkRadios
+ v-model="menuPosition"
+ :options="[
+ { value: 'right', label: i18n.ts.right },
+ { value: 'bottom', label: i18n.ts.bottom },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
- <option value="right">{{ i18n.ts.right }}</option>
- <option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['navbar', 'position']">
<MkPreferenceContainer k="deck.navbarPosition">
- <MkRadios v-model="navbarPosition">
+ <MkRadios
+ v-model="navbarPosition"
+ :options="[
+ { value: 'left', label: i18n.ts.left },
+ { value: 'top', label: i18n.ts.top },
+ { value: 'bottom', label: i18n.ts.bottom },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
- <option value="left">{{ i18n.ts.left }}</option>
- <option value="top">{{ i18n.ts.top }}</option>
- <option value="bottom">{{ i18n.ts.bottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -113,7 +125,7 @@ watch(wallpaper, () => {
suggestReload();
});
-function setWallpaper(ev: MouseEvent) {
+function setWallpaper(ev: PointerEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 57192c0fb7..7189e19780 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -60,6 +60,7 @@ import bytes from '@/filters/bytes.js';
import { definePage } from '@/page.js';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
+import { useGlobalEvent } from '@/events.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
@@ -115,14 +116,20 @@ function genUsageBar(fsize: number): StyleValue {
};
}
-function onClick(ev: MouseEvent, file) {
+function onClick(ev: PointerEvent, file: Misskey.entities.DriveFile) {
os.popupMenu(getDriveFileMenu(file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
-function onContextMenu(ev: MouseEvent, file): void {
+function onContextMenu(ev: PointerEvent, file: Misskey.entities.DriveFile): void {
os.contextMenu(getDriveFileMenu(file), ev);
}
+useGlobalEvent('driveFilesDeleted', (files) => {
+ for (const f of files) {
+ paginator.removeItem(f.id);
+ }
+});
+
definePage(() => ({
title: i18n.ts.drivecleaner,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
index 62922fc964..f92e87375f 100644
--- a/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
+++ b/packages/frontend/src/pages/settings/drive.ImageFrameItem.vue
@@ -52,7 +52,7 @@ async function edit() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
index 0c03a4493a..9e80d719de 100644
--- a/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
+++ b/packages/frontend/src/pages/settings/drive.WatermarkItem.vue
@@ -52,7 +52,7 @@ async function edit() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 8d443921a9..b170d17a5a 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -296,8 +296,9 @@ if (prefer.s.uploadFolder) {
}
function chooseUploadFolder() {
- selectDriveFolder(null).then(async folder => {
- prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
+ selectDriveFolder(null).then(async ({ canceled, folders }) => {
+ if (canceled) return;
+ prefer.commit('uploadFolder', folders[0] ? folders[0].id : null);
os.success();
if (prefer.s.uploadFolder) {
uploadFolder.value = await misskeyApi('drive/folders/show', {
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 469a3c2f1c..85fea7ae66 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -76,11 +76,11 @@ const $i = ensureSignin();
const emailAddress = ref($i.email ?? '');
-const onChangeReceiveAnnouncementEmail = (v) => {
+function onChangeReceiveAnnouncementEmail(v: boolean) {
misskeyApi('i/update', {
receiveAnnouncementEmail: v,
});
-};
+}
async function saveEmailAddress() {
const auth = await os.authenticateDialog();
diff --git a/packages/frontend/src/pages/settings/emoji-palette.palette.vue b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
index b624d424f3..d8a5f16b7d 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
@@ -18,19 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div v-panel style="border-radius: 6px;">
- <Sortable
- v-model="emojis"
+ <MkDraggable
+ :modelValue="emojis.map(emoji => ({ id: emoji, emoji }))"
+ direction="horizontal"
:class="$style.emojis"
- :itemKey="item => item"
- :animation="150"
- :delay="100"
- :delayOnTouchOnly="true"
- :group="{ name: 'SortableEmojiPalettes' }"
+ group="emojiPalettes"
+ @update:modelValue="v => emojis = v.map(x => x.emoji)"
>
- <template #item="{element}">
- <button class="_button" :class="$style.emojisItem" @click="remove(element, $event)">
- <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
- <MkEmoji v-else :emoji="element" :normal="true"/>
+ <template #default="{ item }">
+ <button class="_button" :class="$style.emojisItem" @click="remove(item.emoji, $event)">
+ <!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
+ <MkCustomEmoji v-if="item.emoji[0] === ':'" style="pointer-events: none;" :name="item.emoji" :normal="true" :fallbackToImage="true"/>
+ <MkEmoji v-else style="pointer-events: none;" :emoji="item.emoji" :normal="true"/>
</button>
</template>
<template #footer>
@@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-plus"></i>
</button>
</template>
- </Sortable>
+ </MkDraggable>
</div>
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
</div>
@@ -47,7 +46,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
-import Sortable from 'vuedraggable';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -55,6 +53,7 @@ import { deepClone } from '@/utility/clone.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
const props = defineProps<{
@@ -77,7 +76,7 @@ watch(emojis, () => {
emit('updateEmojis', emojis.value);
}, { deep: true });
-function remove(reaction: string, ev: MouseEvent) {
+function remove(reaction: string, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
@@ -86,7 +85,7 @@ function remove(reaction: string, ev: MouseEvent) {
}], getHTMLElement(ev));
}
-function pick(ev: MouseEvent) {
+function pick(ev: PointerEvent) {
os.pickEmoji(getHTMLElement(ev), {
showPinned: false,
}).then(it => {
@@ -97,7 +96,7 @@ function pick(ev: MouseEvent) {
});
}
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
@@ -125,7 +124,7 @@ function paste() {
});
}
-function del(ev: MouseEvent) {
+function del(ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.delete,
action: () => {
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index 7f31699ed1..cb665554cd 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -63,38 +63,33 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
<MkPreferenceContainer k="emojiPickerScale">
- <MkRadios v-model="emojiPickerScale">
+ <MkRadios
+ v-model="emojiPickerScale"
+ :options="emojiPickerScaleDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- <option :value="4">{{ i18n.ts.large }}+</option>
- <option :value="5">{{ i18n.ts.large }}++</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
<MkPreferenceContainer k="emojiPickerWidth">
- <MkRadios v-model="emojiPickerWidth">
+ <MkRadios
+ v-model="emojiPickerWidth"
+ :options="emojiPickerWidthDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
- <option :value="1">5</option>
- <option :value="2">6</option>
- <option :value="3">7</option>
- <option :value="4">8</option>
- <option :value="5">9</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
<MkPreferenceContainer k="emojiPickerHeight">
- <MkRadios v-model="emojiPickerHeight">
+ <MkRadios
+ v-model="emojiPickerHeight"
+ :options="emojiPickerHeightDef"
+ >
<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
- <option :value="1">{{ i18n.ts.small }}</option>
- <option :value="2">{{ i18n.ts.medium }}</option>
- <option :value="3">{{ i18n.ts.large }}</option>
- <option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -126,6 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, watch } from 'vue';
import XPalette from './emoji-palette.palette.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
+import type { MkRadiosOption } from '@/components/MkRadios.vue';
import { genId } from '@/utility/id.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkRadios from '@/components/MkRadios.vue';
@@ -158,8 +154,31 @@ const emojiPaletteForMainDef = computed<MkSelectItem[]>(() => [
})),
]);
const emojiPickerScale = prefer.model('emojiPickerScale');
+const emojiPickerScaleDef = [
+ { label: i18n.ts.small, value: 1 },
+ { label: i18n.ts.medium, value: 2 },
+ { label: i18n.ts.large, value: 3 },
+ { label: i18n.ts.large + '+', value: 4 },
+ { label: i18n.ts.large + '++', value: 5 },
+] as MkRadiosOption<number>[];
+
const emojiPickerWidth = prefer.model('emojiPickerWidth');
+const emojiPickerWidthDef = [
+ { label: '5', value: 1 },
+ { label: '6', value: 2 },
+ { label: '7', value: 3 },
+ { label: '8', value: 4 },
+ { label: '9', value: 5 },
+] as MkRadiosOption<number>[];
+
const emojiPickerHeight = prefer.model('emojiPickerHeight');
+const emojiPickerHeightDef = [
+ { label: i18n.ts.small, value: 1 },
+ { label: i18n.ts.medium, value: 2 },
+ { label: i18n.ts.large, value: 3 },
+ { label: i18n.ts.large + '+', value: 4 },
+] as MkRadiosOption<number>[];
+
const emojiPickerStyle = prefer.model('emojiPickerStyle');
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
@@ -226,12 +245,12 @@ function delPalette(id: string) {
}
}
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
-function previewPicker(ev: MouseEvent) {
+function previewPicker(ev: PointerEvent) {
emojiPicker.show(getHTMLElement(ev));
}
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 39c32d347f..abfac37275 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="_gaps_s">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="!storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
+ <MkInfo v-if="storagePersistenceSupported && !storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
<div>{{ i18n.ts._settings.settingsPersistence_description1 }}</div>
<div>{{ i18n.ts._settings.settingsPersistence_description2 }}</div>
<div><button class="_textButton" @click="enableStoragePersistence">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipStoragePersistence">{{ i18n.ts.skip }}</button></div>
@@ -51,10 +51,12 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
-import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const indexInfo = {
title: i18n.ts.settings,
icon: 'ti ti-settings',
@@ -166,7 +168,7 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
type: 'button',
icon: 'ti ti-settings-2',
text: i18n.ts.preferencesProfile,
- action: async (ev: MouseEvent) => {
+ action: async (ev) => {
os.popupMenu(getPreferencesProfileMenu(), ev.currentTarget ?? ev.target);
},
}, {
diff --git a/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue b/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
index ea131381a1..37cd9fa67d 100644
--- a/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.emoji-mute.vue
@@ -55,12 +55,12 @@ import {
const emojis = prefer.model('mutingEmojis');
-function getHTMLElement(ev: MouseEvent): HTMLElement {
+function getHTMLElement(ev: PointerEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
-function add(ev: MouseEvent) {
+function add(ev: PointerEvent) {
os.pickEmoji(getHTMLElement(ev), { showPinned: false }).then((emoji) => {
if (emoji) {
muteEmoji(emoji);
@@ -68,7 +68,7 @@ function add(ev: MouseEvent) {
});
}
-function onEmojiClick(ev: MouseEvent, emoji: string) {
+function onEmojiClick(ev: PointerEvent, emoji: string) {
const menuItems : MenuItem[] = [{
type: 'label',
text: emoji,
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 6fd9f07a47..433969f474 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -173,6 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch, markRaw } from 'vue';
+import * as Misskey from 'misskey-js';
import XEmojiMute from './mute-block.emoji-mute.vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
@@ -218,7 +219,7 @@ watch([
suggestReload();
});
-async function unrenoteMute(user, ev) {
+async function unrenoteMute(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.renoteUnmute,
icon: 'ti ti-x',
@@ -229,7 +230,7 @@ async function unrenoteMute(user, ev) {
}], ev.currentTarget ?? ev.target);
}
-async function unmute(user, ev) {
+async function unmute(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unmute,
icon: 'ti ti-x',
@@ -240,7 +241,7 @@ async function unmute(user, ev) {
}], ev.currentTarget ?? ev.target);
}
-async function unblock(user, ev) {
+async function unblock(user: Misskey.entities.UserDetailed, ev: PointerEvent) {
os.popupMenu([{
text: i18n.ts.unblock,
icon: 'ti ti-x',
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index f5837abe98..49d8ecd92d 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -30,7 +30,7 @@ const emit = defineEmits<{
(ev: 'save', value: (string[] | string)[]): void;
}>();
-const render = (mutedWords) => mutedWords.map(x => {
+const render = (mutedWords: (string | string[])[]) => mutedWords.map(x => {
if (Array.isArray(x)) {
return x.join(' ');
} else {
@@ -46,13 +46,13 @@ watch(mutedWords, () => {
});
async function save() {
- const parseMutes = (mutes) => {
+ const parseMutes = (mutes: string) => {
// split into lines, remove empty lines and unnecessary whitespace
- let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
+ let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '') as (string | string[])[];
// check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
+ const line = lines[i] as string;
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) {
// check that the RegExp is valid
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index d25708dcb4..997a9f00c2 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -9,25 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSlot>
<template #label>{{ i18n.ts.navbar }}</template>
<MkContainer :showHeader="false">
- <Sortable
+ <MkDraggable
v-model="items"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.itemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
+ direction="vertical"
>
- <template #item="{element,index}">
+ <template #default="{ item }">
<div
- v-if="element.type === '-' || navbarItemDef[element.type]"
+ v-if="item.type === '-' || navbarItemDef[item.type]"
:class="$style.item"
>
<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
- <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
- <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
+ <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item.type]?.title ?? i18n.ts.divider }}</span>
+ <button class="_button" :class="$style.itemRemove" @click="removeItem(item.id)"><i class="ti ti-x"></i></button>
</div>
</template>
- </Sortable>
+ </MkDraggable>
</MkContainer>
</FormSlot>
<div class="_buttons">
@@ -36,10 +32,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
- <MkRadios v-model="menuDisplay">
+ <MkRadios
+ v-model="menuDisplay"
+ :options="[
+ { value: 'sideFull', label: i18n.ts._menuDisplay.sideFull },
+ { value: 'sideIcon', label: i18n.ts._menuDisplay.sideIcon },
+ ]"
+ >
<template #label>{{ i18n.ts.display }}</template>
- <option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
- <option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
</MkRadios>
<SearchMarker :keywords="['navbar', 'sidebar', 'toggle', 'button', 'sub']">
@@ -54,13 +54,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import MkContainer from '@/components/MkContainer.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { store } from '@/store.js';
@@ -70,15 +71,13 @@ import { prefer } from '@/preferences.js';
import { getInitialPrefValue } from '@/preferences/manager.js';
import { genId } from '@/utility/id.js';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-
const items = ref(prefer.s.menu.map(x => ({
id: genId(),
type: x,
})));
const itemTypeValues = computed(() => items.value.map(x => x.type));
-const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
+const menuDisplay = store.model('menuDisplay');
const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() {
@@ -98,8 +97,8 @@ async function addItem() {
}];
}
-function removeItem(index: number) {
- items.value.splice(index, 1);
+function removeItem(itemId: string) {
+ items.value = items.value.filter(i => i.id !== itemId);
}
function save() {
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 2802d3263e..3787e07626 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection first>
<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
<div class="_gaps_s">
- <MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type">
+ <MkFolder v-for="type in configurableNotificationTypes" :key="type">
<template #label>{{ i18n.ts._notification._types[type] }}</template>
<template #suffix>
{{
- $i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none :
- $i.notificationRecieveConfig[type]?.type === 'following' ? i18n.ts.following :
- $i.notificationRecieveConfig[type]?.type === 'follower' ? i18n.ts.followers :
- $i.notificationRecieveConfig[type]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
- $i.notificationRecieveConfig[type]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower :
- $i.notificationRecieveConfig[type]?.type === 'list' ? i18n.ts.userList :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'never' ? i18n.ts.none :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'following' ? i18n.ts.following :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'follower' ? i18n.ts.followers :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'mutualFollow' ? i18n.ts.mutualFollow :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'followingOrFollower' ? i18n.ts.followingOrFollower :
+ $i.notificationRecieveConfig[type as (typeof configurableNotificationTypes)[number]]?.type === 'list' ? i18n.ts.userList :
i18n.ts.all
}}
</template>
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XNotificationConfig
:userLists="userLists"
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
- :configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
+ :configurableTypes="(onlyOnOrOffNotificationTypes as string[]).includes(type) ? ['all', 'never'] : undefined"
@update="(res) => updateReceiveConfig(type, res)"
/>
</MkFolder>
@@ -83,9 +83,11 @@ import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
const $i = ensureSignin();
-const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
+const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
-const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'] satisfies (typeof notificationTypes[number])[] as string[];
+const configurableNotificationTypes = notificationTypes.filter(type => !nonConfigurableNotificationTypes.includes(type as any)) as Exclude<typeof notificationTypes[number], typeof nonConfigurableNotificationTypes[number]>[];
+
+const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'] as const satisfies (typeof notificationTypes[number])[];
const allowButton = useTemplateRef('allowButton');
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index d4097bde94..4facc696a4 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div v-for="policy in Object.keys($i.policies)" :key="policy">
- {{ policy }} ... {{ $i.policies[policy] }}
+ {{ policy }} ... {{ $i.policies[policy as keyof typeof $i.policies] }}
</div>
</div>
</MkFolder>
@@ -142,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
</template>
- <MkButton v-if="!storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
+ <MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
@@ -165,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
-import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
+import { enableStoragePersistence, getStoragePersistenceStatusRef, storagePersistenceSupported } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -180,6 +180,8 @@ import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin();
+const storagePersisted = await getStoragePersistenceStatusRef();
+
const reportError = prefer.model('reportError');
const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 7c6ce90e7e..89f457cf69 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.permission }}</template>
<template #value>
<ul style="margin-top: 0; margin-bottom: 0;">
- <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+ <li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
</ul>
</template>
@@ -96,6 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, ref, computed } from 'vue';
+import { isSafeMode } from '@@/js/config.js';
import type { Plugin } from '@/plugin.js';
import FormLink from '@/components/form/link.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -110,7 +111,6 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js';
-import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
const plugins = prefer.r.plugins;
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 972b50f8cd..1a613466db 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -31,12 +31,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
- <MkRadios v-model="overridedDeviceKind">
+ <MkRadios
+ v-model="overridedDeviceKind"
+ :options="[
+ { value: null, label: i18n.ts.auto },
+ { value: 'smartphone', label: i18n.ts.smartphone, icon: 'ti ti-device-mobile' },
+ { value: 'tablet', label: i18n.ts.tablet, icon: 'ti ti-device-tablet' },
+ { value: 'desktop', label: i18n.ts.desktop, icon: 'ti ti-device-desktop' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
- <option :value="null">{{ i18n.ts.auto }}</option>
- <option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
- <option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
- <option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
</MkRadios>
</SearchMarker>
@@ -121,11 +125,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
<MkPreferenceContainer k="emojiStyle">
<div>
- <MkRadios v-model="emojiStyle">
+ <MkRadios
+ v-model="emojiStyle"
+ :options="[
+ { value: 'native', label: i18n.ts.native },
+ { value: 'fluentEmoji', label: 'Fluent Emoji' },
+ { value: 'twemoji', label: 'Twemoji' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
- <option value="native">{{ i18n.ts.native }}</option>
- <option value="fluentEmoji">Fluent Emoji</option>
- <option value="twemoji">Twemoji</option>
</MkRadios>
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</div>
@@ -240,11 +248,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
<MkPreferenceContainer k="reactionsDisplaySize">
- <MkRadios v-model="reactionsDisplaySize">
+ <MkRadios
+ v-model="reactionsDisplaySize"
+ :options="[
+ { value: 'small', label: i18n.ts.small },
+ { value: 'medium', label: i18n.ts.medium },
+ { value: 'large', label: i18n.ts.large },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
- <option value="small">{{ i18n.ts.small }}</option>
- <option value="medium">{{ i18n.ts.medium }}</option>
- <option value="large">{{ i18n.ts.large }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -259,16 +271,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
<MkPreferenceContainer k="mediaListWithOneImageAppearance">
- <MkRadios v-model="mediaListWithOneImageAppearance">
+ <MkRadios
+ v-model="mediaListWithOneImageAppearance"
+ :options="[
+ { value: 'expand', label: i18n.ts.default },
+ { value: '16_9', label: i18n.tsx.limitTo({ x: '16:9' }) },
+ { value: '1_1', label: i18n.tsx.limitTo({ x: '1:1' }) },
+ { value: '2_3', label: i18n.tsx.limitTo({ x: '2:3' }) },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
- <option value="expand">{{ i18n.ts.default }}</option>
- <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
- <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
- <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
+ <SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'grid', 'wide', 'area']">
+ <MkPreferenceContainer k="showMediaListByGridInWideArea">
+ <MkSwitch v-model="showMediaListByGridInWideArea">
+ <template #label><SearchLabel>{{ i18n.ts.showMediaListByGridInWideArea }}</SearchLabel></template>
+ </MkSwitch>
+ </MkPreferenceContainer>
+ </SearchMarker>
+
<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
<MkPreferenceContainer k="instanceTicker">
<MkSelect
@@ -386,22 +410,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['position']">
<MkPreferenceContainer k="notificationPosition">
- <MkRadios v-model="notificationPosition">
+ <MkRadios
+ v-model="notificationPosition"
+ :options="[
+ { value: 'leftTop', label: i18n.ts.leftTop, icon: 'ti ti-align-box-left-top' },
+ { value: 'rightTop', label: i18n.ts.rightTop, icon: 'ti ti-align-box-right-top' },
+ { value: 'leftBottom', label: i18n.ts.leftBottom, icon: 'ti ti-align-box-left-bottom' },
+ { value: 'rightBottom', label: i18n.ts.rightBottom, icon: 'ti ti-align-box-right-bottom' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
- <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
- <option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
- <option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
- <option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['stack', 'axis', 'direction']">
<MkPreferenceContainer k="notificationStackAxis">
- <MkRadios v-model="notificationStackAxis">
+ <MkRadios
+ v-model="notificationStackAxis"
+ :options="[
+ { value: 'vertical', label: i18n.ts.vertical, icon: 'ti ti-carousel-vertical' },
+ { value: 'horizontal', label: i18n.ts.horizontal, icon: 'ti ti-carousel-horizontal' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
- <option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
- <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@@ -570,12 +602,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['font', 'size']">
- <MkRadios v-model="fontSize">
+ <MkRadios
+ v-model="fontSize"
+ :options="[
+ { value: null, label: 'Aa', labelStyle: 'font-size: 14px;' },
+ { value: '1', label: 'Aa', labelStyle: 'font-size: 15px;' },
+ { value: '2', label: 'Aa', labelStyle: 'font-size: 16px;' },
+ { value: '3', label: 'Aa', labelStyle: 'font-size: 17px;' },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
- <option :value="null"><span style="font-size: 14px;">Aa</span></option>
- <option value="1"><span style="font-size: 15px;">Aa</span></option>
- <option value="2"><span style="font-size: 16px;">Aa</span></option>
- <option value="3"><span style="font-size: 17px;">Aa</span></option>
</MkRadios>
</SearchMarker>
@@ -784,10 +820,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker>
<MkPreferenceContainer k="hemisphere">
- <MkRadios v-model="hemisphere">
+ <MkRadios
+ v-model="hemisphere"
+ :options="[
+ { value: 'N', label: i18n.ts._hemisphere.N },
+ { value: 'S', label: i18n.ts._hemisphere.S },
+ ]"
+ >
<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></template>
- <option value="N">{{ i18n.ts._hemisphere.N }}</option>
- <option value="S">{{ i18n.ts._hemisphere.S }}</option>
<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
</MkRadios>
</MkPreferenceContainer>
@@ -855,7 +895,7 @@ const $i = ensureSignin();
const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
-const realtimeMode = computed(store.makeGetterSetter('realtimeMode'));
+const realtimeMode = store.model('realtimeMode');
const overridedDeviceKind = prefer.model('overridedDeviceKind');
const pollingInterval = prefer.model('pollingInterval');
@@ -890,6 +930,7 @@ const notificationStackAxis = prefer.model('notificationStackAxis');
const instanceTicker = prefer.model('instanceTicker');
const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia');
const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance');
+const showMediaListByGridInWideArea = prefer.model('showMediaListByGridInWideArea');
const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
const squareAvatars = prefer.model('squareAvatars');
@@ -916,7 +957,7 @@ const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
-const fontSize = ref(miLocalStorage.getItem('fontSize'));
+const fontSize = ref(miLocalStorage.getItem('fontSize') as '1' | '2' | '3' | null);
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
watch(lang, () => {
@@ -1042,7 +1083,7 @@ function removePinnedList() {
function enableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
- Object.keys(g).forEach((key) => { g[key] = true; });
+ (Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = true; });
dataSaver.value = g;
}
@@ -1050,7 +1091,7 @@ function enableAllDataSaver() {
function disableAllDataSaver() {
const g = { ...prefer.s.dataSaver };
- Object.keys(g).forEach((key) => { g[key] = false; });
+ (Object.keys(g) as (keyof typeof g)[]).forEach((key) => { g[key] = false; });
dataSaver.value = g;
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 7d3da470d6..a7aea9bde4 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -75,30 +75,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
- <Sortable
+ <MkDraggable
v-model="fields"
- class="_gaps_s"
- itemKey="id"
- :animation="150"
- :handle="'.' + $style.dragItemHandle"
- @start="e => e.item.classList.add('active')"
- @end="e => e.item.classList.remove('active')"
+ direction="vertical"
+ withGaps
+ manualDragStart
>
- <template #item="{element, index}">
+ <template #default="{ item, dragStart }">
<div v-panel :class="$style.fieldDragItem">
- <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
- <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
+ <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></button>
+ <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(item.id)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
- <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
+ <MkInput v-model="item.name" small :placeholder="i18n.ts._profile.metadataLabel">
</MkInput>
- <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
+ <MkInput v-model="item.value" small :placeholder="i18n.ts._profile.metadataContent">
</MkInput>
</FormSplit>
</div>
</div>
</template>
- </Sortable>
+ </MkDraggable>
</div>
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -165,7 +162,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
+import { computed, reactive, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -174,6 +172,7 @@ import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSlot from '@/components/form/slot.vue';
import FormLink from '@/components/form/link.vue';
+import MkDraggable from '@/components/MkDraggable.vue';
import { chooseDriveFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
@@ -188,9 +187,7 @@ import { genId } from '@/utility/id.js';
const $i = ensureSignin();
-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;
@@ -228,8 +225,8 @@ while (fields.value.length < 4) {
addField();
}
-function deleteField(index: number) {
- fields.value.splice(index, 1);
+function deleteField(itemId: string) {
+ fields.value = fields.value.filter(f => f.id !== itemId);
}
function saveFields() {
@@ -270,8 +267,8 @@ function save() {
}
}
-function changeAvatar(ev) {
- async function done(driveFile) {
+function changeAvatar(ev: PointerEvent) {
+ async function done(driveFile: Misskey.entities.DriveFile) {
const i = await os.apiWithDialog('i/update', {
avatarId: driveFile.id,
});
@@ -319,8 +316,8 @@ function changeAvatar(ev) {
}], ev.currentTarget ?? ev.target);
}
-function changeBanner(ev) {
- async function done(driveFile) {
+function changeBanner(ev: PointerEvent) {
+ async function done(driveFile: Misskey.entities.DriveFile) {
const i = await os.apiWithDialog('i/update', {
bannerId: driveFile.id,
});
diff --git a/packages/frontend/src/pages/settings/profiles.vue b/packages/frontend/src/pages/settings/profiles.vue
index 4804c11f7a..b3d02ba3fe 100644
--- a/packages/frontend/src/pages/settings/profiles.vue
+++ b/packages/frontend/src/pages/settings/profiles.vue
@@ -15,21 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, computed } from 'vue';
-import type { MenuItem } from '@/types/menu.js';
+import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
-import { prefer } from '@/preferences.js';
import { deleteCloudBackup, listCloudBackups } from '@/preferences/utility.js';
const backups = await listCloudBackups();
-function del(backup) {
+function del(backup: { name: string }): void {
deleteCloudBackup(backup.name);
}
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 31fe9a64db..050586c2e1 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import type { SoundType } from '@/utility/sound.js';
+import type { SoundStore } from '@/preferences/def.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
@@ -41,7 +42,6 @@ import { useMkSelect } from '@/composables/use-mkselect.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/drive.js';
-import type { SoundStore } from '@/preferences/def.js';
const props = defineProps<{
def: SoundStore;
@@ -100,7 +100,7 @@ const friendlyFileName = computed<string>(() => {
return i18n.ts._soundSettings.driveFileWarn;
});
-function selectSound(ev) {
+function selectSound(ev: PointerEvent) {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 1b851825d6..0d0623f11f 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -100,11 +100,14 @@ function getSoundTypeName(f: SoundType): string {
}
}
-async function updated(type: keyof typeof sounds.value, sound) {
- const v: SoundStore = {
+async function updated(type: keyof typeof sounds.value, sound: { type: SoundType; fileId?: string; fileUrl?: string; volume: number; }) {
+ const v: SoundStore = sound.type === '_driveFile_' ? {
+ type: sound.type,
+ fileId: sound.fileId!,
+ fileUrl: sound.fileUrl!,
+ volume: sound.volume,
+ } : {
type: sound.type,
- fileId: sound.fileId,
- fileUrl: sound.fileUrl,
volume: sound.volume,
};
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index b69fd2596d..83c8a7b9a7 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -17,13 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>Black</template>
</MkSwitch>
- <MkRadios v-model="statusbar.size">
+ <MkRadios
+ v-model="statusbar.size"
+ :options="[
+ { value: 'verySmall', label: i18n.ts.small + '+' },
+ { value: 'small', label: i18n.ts.small },
+ { value: 'medium', label: i18n.ts.medium },
+ { value: 'large', label: i18n.ts.large },
+ { value: 'veryLarge', label: i18n.ts.large + '+' },
+ ]"
+ >
<template #label>{{ i18n.ts.size }}</template>
- <option value="verySmall">{{ i18n.ts.small }}+</option>
- <option value="small">{{ i18n.ts.small }}</option>
- <option value="medium">{{ i18n.ts.medium }}</option>
- <option value="large">{{ i18n.ts.large }}</option>
- <option value="veryLarge">{{ i18n.ts.large }}+</option>
</MkRadios>
<template v-if="statusbar.type === 'rss'">
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 0129aebe94..46b537f866 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -306,7 +306,7 @@ function changeThemesSyncEnabled(value: boolean) {
}
}
-function onThemeContextmenu(theme: Theme, ev: MouseEvent) {
+function onThemeContextmenu(theme: Theme, ev: PointerEvent) {
os.contextMenu([{
type: 'label',
text: theme.name,