summaryrefslogtreecommitdiff
path: root/packages/client/src/pages/settings
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-11-17 23:35:55 +0900
committertamaina <tamaina@hotmail.co.jp>2022-11-17 23:35:55 +0900
commit764da890b6ad3d53808ec592099a93d9d39d7b08 (patch)
treeb3e9b08bfafa2bbbb5f657af3adb60bcc9510b67 /packages/client/src/pages/settings
parentfix (diff)
parentMerge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff)
downloadmisskey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.gz
misskey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.bz2
misskey-764da890b6ad3d53808ec592099a93d9d39d7b08.zip
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/client/src/pages/settings')
-rw-r--r--packages/client/src/pages/settings/2fa.vue6
-rw-r--r--packages/client/src/pages/settings/account-info.vue2
-rw-r--r--packages/client/src/pages/settings/accounts.vue7
-rw-r--r--packages/client/src/pages/settings/api.vue5
-rw-r--r--packages/client/src/pages/settings/apps.vue3
-rw-r--r--packages/client/src/pages/settings/custom-css.vue3
-rw-r--r--packages/client/src/pages/settings/deck.vue45
-rw-r--r--packages/client/src/pages/settings/delete-account.vue5
-rw-r--r--packages/client/src/pages/settings/drive.vue27
-rw-r--r--packages/client/src/pages/settings/email.vue25
-rw-r--r--packages/client/src/pages/settings/general.vue23
-rw-r--r--packages/client/src/pages/settings/import-export.vue77
-rw-r--r--packages/client/src/pages/settings/index.vue174
-rw-r--r--packages/client/src/pages/settings/instance-mute.vue4
-rw-r--r--packages/client/src/pages/settings/integration.vue3
-rw-r--r--packages/client/src/pages/settings/mute-block.vue15
-rw-r--r--packages/client/src/pages/settings/navbar.vue (renamed from packages/client/src/pages/settings/menu.vue)13
-rw-r--r--packages/client/src/pages/settings/notifications.vue5
-rw-r--r--packages/client/src/pages/settings/other.vue3
-rw-r--r--packages/client/src/pages/settings/plugin.install.vue7
-rw-r--r--packages/client/src/pages/settings/plugin.vue5
-rw-r--r--packages/client/src/pages/settings/preferences-backups.vue444
-rw-r--r--packages/client/src/pages/settings/privacy.vue58
-rw-r--r--packages/client/src/pages/settings/profile.vue18
-rw-r--r--packages/client/src/pages/settings/reaction.vue42
-rw-r--r--packages/client/src/pages/settings/security.vue7
-rw-r--r--packages/client/src/pages/settings/sounds.vue5
-rw-r--r--packages/client/src/pages/settings/statusbar.statusbar.vue140
-rw-r--r--packages/client/src/pages/settings/statusbar.vue54
-rw-r--r--packages/client/src/pages/settings/theme.install.vue3
-rw-r--r--packages/client/src/pages/settings/theme.manage.vue3
-rw-r--r--packages/client/src/pages/settings/theme.vue112
-rw-r--r--packages/client/src/pages/settings/webhook.edit.vue10
-rw-r--r--packages/client/src/pages/settings/webhook.new.vue3
-rw-r--r--packages/client/src/pages/settings/webhook.vue5
-rw-r--r--packages/client/src/pages/settings/word-mute.vue9
36 files changed, 977 insertions, 393 deletions
diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue
index fb3a7a17f3..89d8178dc6 100644
--- a/packages/client/src/pages/settings/2fa.vue
+++ b/packages/client/src/pages/settings/2fa.vue
@@ -55,7 +55,7 @@
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
<li>
{{ i18n.ts._2fa.step3 }}<br>
- <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
+ <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
<MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton>
</li>
</ol>
@@ -68,8 +68,8 @@
import { ref } from 'vue';
import { hostname } from '@/config';
import { byteify, hexify, stringify } from '@/scripts/2fa';
-import MkButton from '@/components/ui/button.vue';
-import MkInfo from '@/components/ui/info.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue
index 65b6233693..93e65d55b1 100644
--- a/packages/client/src/pages/settings/account-info.vue
+++ b/packages/client/src/pages/settings/account-info.vue
@@ -129,7 +129,7 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import FormSection from '@/components/form/section.vue';
-import MkKeyValue from '@/components/key-value.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 47b816243f..e16931a9ca 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -23,7 +23,7 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account';
import { i18n } from '@/i18n';
@@ -75,7 +75,7 @@ function removeAccount(account) {
}
function addExistingAccount() {
- os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
+ os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
os.success();
@@ -84,7 +84,7 @@ function addExistingAccount() {
}
function createAccount() {
- os.popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
+ os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
switchAccountWithToken(res.i);
@@ -109,7 +109,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.accounts,
icon: 'fas fa-users',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue
index d94862712e..7165089e39 100644
--- a/packages/client/src/pages/settings/api.vue
+++ b/packages/client/src/pages/settings/api.vue
@@ -9,7 +9,7 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import FormLink from '@/components/form/link.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
@@ -17,7 +17,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const isDesktop = ref(window.innerWidth >= 1100);
function generateToken() {
- os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, {
+ os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
@@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: 'API',
icon: 'fas fa-key',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
index 673e91fe6b..8b345c8e9f 100644
--- a/packages/client/src/pages/settings/apps.vue
+++ b/packages/client/src/pages/settings/apps.vue
@@ -39,7 +39,7 @@
<script lang="ts" setup>
import { ref } from 'vue';
-import FormPagination from '@/components/ui/pagination.vue';
+import FormPagination from '@/components/MkPagination.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
@@ -67,7 +67,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.installedApps,
icon: 'fas fa-plug',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue
index 3e032be257..2992906e6d 100644
--- a/packages/client/src/pages/settings/custom-css.vue
+++ b/packages/client/src/pages/settings/custom-css.vue
@@ -11,7 +11,7 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
-import FormInfo from '@/components/ui/info.vue';
+import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
@@ -42,6 +42,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.customCss,
icon: 'fas fa-code',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index edada683ae..1285a6641c 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -1,9 +1,6 @@
<template>
<div class="_formRoot">
- <FormGroup>
- <template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
- <FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch>
- </FormGroup>
+ <FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
@@ -12,20 +9,6 @@
<option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option>
</FormRadios>
-
- <FormRadios v-model="columnHeaderHeight" class="_formBlock">
- <template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
- <option :value="42">{{ i18n.ts.narrow }}</option>
- <option :value="45">{{ i18n.ts.medium }}</option>
- <option :value="48">{{ i18n.ts.wide }}</option>
- </FormRadios>
-
- <FormInput v-model="columnMargin" type="number" class="_formBlock">
- <template #label>{{ i18n.ts._deck.columnMargin }}</template>
- <template #suffix>px</template>
- </FormInput>
-
- <FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div>
</template>
@@ -35,7 +18,6 @@ import FormSwitch from '@/components/form/switch.vue';
import FormLink from '@/components/form/link.vue';
import FormRadios from '@/components/form/radios.vue';
import FormInput from '@/components/form/input.vue';
-import FormGroup from '@/components/form/group.vue';
import { deckStore } from '@/ui/deck/deck-store';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
@@ -45,30 +27,6 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
-const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
-const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
-const profile = computed(deckStore.makeGetterSetter('profile'));
-
-watch(navWindow, async () => {
- const { canceled } = await os.confirm({
- type: 'info',
- text: i18n.ts.reloadToApplySetting,
- });
- if (canceled) return;
-
- unisonReload();
-});
-
-async function setProfile() {
- const { canceled, result: name } = await os.inputText({
- title: i18n.ts._deck.profile,
- allowEmpty: false,
- });
- if (canceled) return;
-
- profile.value = name;
- unisonReload();
-}
const headerActions = $computed(() => []);
@@ -77,6 +35,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.deck,
icon: 'fas fa-columns',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
index a587c32998..851a857fed 100644
--- a/packages/client/src/pages/settings/delete-account.vue
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -8,8 +8,8 @@
</template>
<script lang="ts" setup>
-import FormInfo from '@/components/ui/info.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormInfo from '@/components/MkInfo.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { signout } from '@/account';
import { i18n } from '@/i18n';
@@ -48,6 +48,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts._accountDelete.accountDelete,
icon: 'fas fa-exclamation-triangle',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue
index 73c0384f1f..a10e2d9f7d 100644
--- a/packages/client/src/pages/settings/drive.vue
+++ b/packages/client/src/pages/settings/drive.vue
@@ -28,7 +28,17 @@
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
</FormLink>
- <FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch>
+ <FormSwitch v-model="keepOriginalUploading" class="_formBlock">
+ <template #label>{{ i18n.ts.keepOriginalUploading }}</template>
+ <template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
+ </FormSwitch>
+ <FormSwitch v-model="alwaysMarkNsfw" class="_formBlock" @update:modelValue="saveProfile()">
+ <template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
+ </FormSwitch>
+ <FormSwitch v-model="autoSensitive" class="_formBlock" @update:modelValue="saveProfile()">
+ <template #label>{{ i18n.ts.enableAutoSensitive }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #caption>{{ i18n.ts.enableAutoSensitiveDescription }}</template>
+ </FormSwitch>
</FormSection>
</div>
</template>
@@ -39,19 +49,22 @@ import tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue';
-import MkKeyValue from '@/components/key-value.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import bytes from '@/filters/bytes';
import { defaultStore } from '@/store';
-import MkChart from '@/components/chart.vue';
+import MkChart from '@/components/MkChart.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
+import { $i } from '@/account';
const fetching = ref(true);
const usage = ref<any>(null);
const capacity = ref<any>(null);
const uploadFolder = ref<any>(null);
+let alwaysMarkNsfw = $ref($i.alwaysMarkNsfw);
+let autoSensitive = $ref($i.autoSensitive);
const meterStyle = computed(() => {
return {
@@ -94,6 +107,13 @@ function chooseUploadFolder() {
});
}
+function saveProfile() {
+ os.api('i/update', {
+ alwaysMarkNsfw: !!alwaysMarkNsfw,
+ autoSensitive: !!autoSensitive,
+ });
+}
+
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
@@ -101,7 +121,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.drive,
icon: 'fas fa-cloud',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index 8b67ff34dd..1dae233a07 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -1,39 +1,39 @@
<template>
<div class="_formRoot">
<FormSection>
- <template #label>{{ $ts.emailAddress }}</template>
+ <template #label>{{ i18n.ts.emailAddress }}</template>
<FormInput v-model="emailAddress" type="email" manual-save>
<template #prefix><i class="fas fa-envelope"></i></template>
- <template v-if="$i.email && !$i.emailVerified" #caption>{{ $ts.verificationEmailSent }}</template>
- <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="fas fa-check" style="color: var(--success);"></i> {{ $ts.emailVerified }}</template>
+ <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
+ <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="fas fa-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template>
</FormInput>
</FormSection>
<FormSection>
- <FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
- {{ $ts.receiveAnnouncementFromInstance }}
+ <FormSwitch :model-value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
+ {{ i18n.ts.receiveAnnouncementFromInstance }}
</FormSwitch>
</FormSection>
<FormSection>
- <template #label>{{ $ts.emailNotification }}</template>
+ <template #label>{{ i18n.ts.emailNotification }}</template>
<FormSwitch v-model="emailNotification_mention" class="_formBlock">
- {{ $ts._notification._types.mention }}
+ {{ i18n.ts._notification._types.mention }}
</FormSwitch>
<FormSwitch v-model="emailNotification_reply" class="_formBlock">
- {{ $ts._notification._types.reply }}
+ {{ i18n.ts._notification._types.reply }}
</FormSwitch>
<FormSwitch v-model="emailNotification_quote" class="_formBlock">
- {{ $ts._notification._types.quote }}
+ {{ i18n.ts._notification._types.quote }}
</FormSwitch>
<FormSwitch v-model="emailNotification_follow" class="_formBlock">
- {{ $ts._notification._types.follow }}
+ {{ i18n.ts._notification._types.follow }}
</FormSwitch>
<FormSwitch v-model="emailNotification_receiveFollowRequest" class="_formBlock">
- {{ $ts._notification._types.receiveFollowRequest }}
+ {{ i18n.ts._notification._types.receiveFollowRequest }}
</FormSwitch>
<FormSwitch v-model="emailNotification_groupInvited" class="_formBlock">
- {{ $ts._notification._types.groupInvited }}
+ {{ i18n.ts._notification._types.groupInvited }}
</FormSwitch>
</FormSection>
</div>
@@ -107,6 +107,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.email,
icon: 'fas fa-envelope',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index ac2e3a4968..9072bcefc9 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -56,10 +56,10 @@
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template>
- <option value="small"><span style="font-size: 14px;">Aa</span></option>
- <option :value="null"><span style="font-size: 16px;">Aa</span></option>
- <option value="large"><span style="font-size: 18px;">Aa</span></option>
- <option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
+ <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>
</FormRadios>
</FormSection>
@@ -81,10 +81,10 @@
<option value="force">{{ i18n.ts._nsfw.force }}</option>
</FormSelect>
- <FormGroup>
- <template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
- <FormSwitch v-model="defaultSideView">{{ i18n.ts.openInSideView }}</FormSwitch>
- </FormGroup>
+ <FormRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing class="_formBlock">
+ <template #label>{{ i18n.ts.numberOfPageCache }}</template>
+ <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
+ </FormRange>
<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
@@ -97,10 +97,10 @@ import { computed, ref, watch } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
-import FormGroup from '@/components/form/group.vue';
+import FormRange from '@/components/form/range.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
-import MkLink from '@/components/link.vue';
+import MkLink from '@/components/MkLink.vue';
import { langs } from '@/config';
import { defaultStore } from '@/store';
import * as os from '@/os';
@@ -137,7 +137,7 @@ const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const disablePagesScript = computed(defaultStore.makeGetterSetter('disablePagesScript'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
-const defaultSideView = computed(defaultStore.makeGetterSetter('defaultSideView'));
+const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
@@ -186,6 +186,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.general,
icon: 'fas fa-cogs',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index 438ecbd330..d3d155894e 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -1,47 +1,79 @@
<template>
<div class="_formRoot">
<FormSection>
- <template #label>{{ $ts._exportOrImport.allNotes }}</template>
- <MkButton :class="$style.button" inline @click="exportNotes()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <template #label>{{ i18n.ts._exportOrImport.allNotes }}</template>
+ <FormFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="fas fa-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="fas fa-download"></i> {{ i18n.ts.export }}</MkButton>
+ </FormFolder>
</FormSection>
<FormSection>
- <template #label>{{ $ts._exportOrImport.followingList }}</template>
- <FormGroup>
+ <template #label>{{ i18n.ts._exportOrImport.followingList }}</template>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="fas fa-download"></i></template>
<FormSwitch v-model="excludeMutingUsers" class="_formBlock">
- {{ $ts._exportOrImport.excludeMutingUsers }}
+ {{ i18n.ts._exportOrImport.excludeMutingUsers }}
</FormSwitch>
<FormSwitch v-model="excludeInactiveUsers" class="_formBlock">
- {{ $ts._exportOrImport.excludeInactiveUsers }}
+ {{ i18n.ts._exportOrImport.excludeInactiveUsers }}
</FormSwitch>
- <MkButton :class="$style.button" inline @click="exportFollowing()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
- </FormGroup>
- <FormGroup>
- <MkButton :class="$style.button" inline @click="importFollowing($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
- </FormGroup>
+ <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="fas fa-download"></i> {{ i18n.ts.export }}</MkButton>
+ </FormFolder>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="fas fa-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="fas fa-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </FormFolder>
</FormSection>
<FormSection>
- <template #label>{{ $ts._exportOrImport.userLists }}</template>
- <MkButton :class="$style.button" inline @click="exportUserLists()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
- <MkButton :class="$style.button" inline @click="importUserLists($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+ <template #label>{{ i18n.ts._exportOrImport.userLists }}</template>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="fas fa-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="fas fa-download"></i> {{ i18n.ts.export }}</MkButton>
+ </FormFolder>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="fas fa-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="fas fa-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </FormFolder>
</FormSection>
<FormSection>
- <template #label>{{ $ts._exportOrImport.muteList }}</template>
- <MkButton :class="$style.button" inline @click="exportMuting()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
- <MkButton :class="$style.button" inline @click="importMuting($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+ <template #label>{{ i18n.ts._exportOrImport.muteList }}</template>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="fas fa-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="fas fa-download"></i> {{ i18n.ts.export }}</MkButton>
+ </FormFolder>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="fas fa-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="fas fa-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </FormFolder>
</FormSection>
<FormSection>
- <template #label>{{ $ts._exportOrImport.blockingList }}</template>
- <MkButton :class="$style.button" inline @click="exportBlocking()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
- <MkButton :class="$style.button" inline @click="importBlocking($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+ <template #label>{{ i18n.ts._exportOrImport.blockingList }}</template>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="fas fa-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="fas fa-download"></i> {{ i18n.ts.export }}</MkButton>
+ </FormFolder>
+ <FormFolder class="_formBlock">
+ <template #label>{{ i18n.ts.import }}</template>
+ <template #icon><i class="fas fa-upload"></i></template>
+ <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="fas fa-upload"></i> {{ i18n.ts.import }}</MkButton>
+ </FormFolder>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
-import MkButton from '@/components/ui/button.vue';
+import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
-import FormGroup from '@/components/form/group.vue';
+import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
@@ -123,7 +155,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.importAndExport,
icon: 'fas fa-boxes',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 12fbbdaa18..73407ff5fb 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -4,15 +4,15 @@
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div class="body">
- <div v-if="!narrow || initialPage == null" class="nav">
+ <div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="baaadecd">
- <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
- <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
+ <MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu>
</div>
</div>
- <div v-if="!(narrow && initialPage == null)" class="main">
+ <div v-if="!(narrow && currentPage?.route.name == null)" class="main">
<div class="bkzroven">
- <component :is="component" :key="initialPage" v-bind="pageProps"/>
+ <RouterView/>
</div>
</div>
</div>
@@ -22,26 +22,21 @@
</template>
<script setup lang="ts">
-import { computed, defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, inject, nextTick, onActivated, onMounted, onUnmounted, provide, ref, watch } from 'vue';
import { i18n } from '@/i18n';
-import MkInfo from '@/components/ui/info.vue';
-import MkSuperMenu from '@/components/ui/super-menu.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkSuperMenu from '@/components/MkSuperMenu.vue';
import { scroll } from '@/scripts/scroll';
import { signout , $i } from '@/account';
import { unisonReload } from '@/scripts/unison-reload';
import { instance } from '@/instance';
import { useRouter } from '@/router';
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
-
-const props = withDefaults(defineProps<{
- initialPage?: string;
-}>(), {
-});
+import * as os from '@/os';
const indexInfo = {
title: i18n.ts.settings,
icon: 'fas fa-cog',
- bg: 'var(--bg)',
hideHeader: true,
};
const INFO = ref(indexInfo);
@@ -50,12 +45,14 @@ const childInfo = ref(null);
const router = useRouter();
-const narrow = ref(false);
+let narrow = $ref(false);
const NARROW_THRESHOLD = 600;
+let currentPage = $computed(() => router.currentRef.value.child);
+
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
- narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
+ narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = computed(() => [{
@@ -64,42 +61,42 @@ const menuDef = computed(() => [{
icon: 'fas fa-user',
text: i18n.ts.profile,
to: '/settings/profile',
- active: props.initialPage === 'profile',
+ active: currentPage?.route.name === 'profile',
}, {
icon: 'fas fa-lock-open',
text: i18n.ts.privacy,
to: '/settings/privacy',
- active: props.initialPage === 'privacy',
+ active: currentPage?.route.name === 'privacy',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.reaction,
to: '/settings/reaction',
- active: props.initialPage === 'reaction',
+ active: currentPage?.route.name === 'reaction',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.drive,
to: '/settings/drive',
- active: props.initialPage === 'drive',
+ active: currentPage?.route.name === 'drive',
}, {
icon: 'fas fa-bell',
text: i18n.ts.notifications,
to: '/settings/notifications',
- active: props.initialPage === 'notifications',
+ active: currentPage?.route.name === 'notifications',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.email,
to: '/settings/email',
- active: props.initialPage === 'email',
+ active: currentPage?.route.name === 'email',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/settings/integration',
- active: props.initialPage === 'integration',
+ active: currentPage?.route.name === 'integration',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/settings/security',
- active: props.initialPage === 'security',
+ active: currentPage?.route.name === 'security',
}],
}, {
title: i18n.ts.clientSettings,
@@ -107,32 +104,32 @@ const menuDef = computed(() => [{
icon: 'fas fa-cogs',
text: i18n.ts.general,
to: '/settings/general',
- active: props.initialPage === 'general',
+ active: currentPage?.route.name === 'general',
}, {
icon: 'fas fa-palette',
text: i18n.ts.theme,
to: '/settings/theme',
- active: props.initialPage === 'theme',
+ active: currentPage?.route.name === 'theme',
}, {
- icon: 'fas fa-list-ul',
- text: i18n.ts.menu,
- to: '/settings/menu',
- active: props.initialPage === 'menu',
+ icon: 'fas fa-bars',
+ text: i18n.ts.navbar,
+ to: '/settings/navbar',
+ active: currentPage?.route.name === 'navbar',
+ }, {
+ icon: 'fas fa-bars-progress',
+ text: i18n.ts.statusbar,
+ to: '/settings/statusbar',
+ active: currentPage?.route.name === 'statusbar',
}, {
icon: 'fas fa-music',
text: i18n.ts.sounds,
to: '/settings/sounds',
- active: props.initialPage === 'sounds',
+ active: currentPage?.route.name === 'sounds',
}, {
icon: 'fas fa-plug',
text: i18n.ts.plugins,
to: '/settings/plugin',
- active: props.initialPage === 'plugin',
- }, {
- icon: 'fas fa-floppy-disk',
- text: i18n.ts.preferencesRegistryShort,
- to: '/settings/preferences-registry',
- active: props.initialPage === 'preferences-registry',
+ active: currentPage?.route.name === 'plugin',
}],
}, {
title: i18n.ts.otherSettings,
@@ -140,40 +137,45 @@ const menuDef = computed(() => [{
icon: 'fas fa-boxes',
text: i18n.ts.importAndExport,
to: '/settings/import-export',
- active: props.initialPage === 'import-export',
+ active: currentPage?.route.name === 'import-export',
}, {
icon: 'fas fa-volume-mute',
text: i18n.ts.instanceMute,
to: '/settings/instance-mute',
- active: props.initialPage === 'instance-mute',
+ active: currentPage?.route.name === 'instance-mute',
}, {
icon: 'fas fa-ban',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
- active: props.initialPage === 'mute-block',
+ active: currentPage?.route.name === 'mute-block',
}, {
icon: 'fas fa-comment-slash',
text: i18n.ts.wordMute,
to: '/settings/word-mute',
- active: props.initialPage === 'word-mute',
+ active: currentPage?.route.name === 'word-mute',
}, {
icon: 'fas fa-key',
text: 'API',
to: '/settings/api',
- active: props.initialPage === 'api',
+ active: currentPage?.route.name === 'api',
}, {
icon: 'fas fa-bolt',
text: 'Webhook',
to: '/settings/webhook',
- active: props.initialPage === 'webhook',
+ active: currentPage?.route.name === 'webhook',
}, {
icon: 'fas fa-ellipsis-h',
text: i18n.ts.other,
to: '/settings/other',
- active: props.initialPage === 'other',
+ active: currentPage?.route.name === 'other',
}],
}, {
items: [{
+ icon: 'fas fa-floppy-disk',
+ text: i18n.ts.preferencesBackups,
+ to: '/settings/preferences-backups',
+ active: currentPage?.route.name === 'preferences-backups',
+ }, {
type: 'button',
icon: 'fas fa-trash',
text: i18n.ts.clearCache,
@@ -186,84 +188,36 @@ const menuDef = computed(() => [{
type: 'button',
icon: 'fas fa-sign-in-alt fa-flip-horizontal',
text: i18n.ts.logout,
- action: () => {
+ action: async () => {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.logoutConfirm,
+ });
+ if (canceled) return;
signout();
},
danger: true,
}],
}]);
-const pageProps = ref({});
-const component = computed(() => {
- if (props.initialPage == null) return null;
- switch (props.initialPage) {
- case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
- case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
- case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
- case 'reaction': return defineAsyncComponent(() => import('./reaction.vue'));
- case 'drive': return defineAsyncComponent(() => import('./drive.vue'));
- case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
- case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
- case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
- case 'instance-mute': return defineAsyncComponent(() => import('./instance-mute.vue'));
- case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
- case 'security': return defineAsyncComponent(() => import('./security.vue'));
- case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
- case 'api': return defineAsyncComponent(() => import('./api.vue'));
- case 'webhook': return defineAsyncComponent(() => import('./webhook.vue'));
- case 'webhook/new': return defineAsyncComponent(() => import('./webhook.new.vue'));
- case 'webhook/edit': return defineAsyncComponent(() => import('./webhook.edit.vue'));
- case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
- case 'other': return defineAsyncComponent(() => import('./other.vue'));
- case 'general': return defineAsyncComponent(() => import('./general.vue'));
- case 'email': return defineAsyncComponent(() => import('./email.vue'));
- case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
- case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
- case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
- case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
- case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
- case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
- case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
- case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
- case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
- case 'preferences-registry': return defineAsyncComponent(() => import('./preferences-registry.vue'));
- case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
- case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
- case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
- }
- return null;
+watch($$(narrow), () => {
});
-watch(component, () => {
- pageProps.value = {};
+onMounted(() => {
+ ro.observe(el.value);
- nextTick(() => {
- scroll(el.value, { top: 0 });
- });
-}, { immediate: true });
+ narrow = el.value.offsetWidth < NARROW_THRESHOLD;
-watch(() => props.initialPage, () => {
- if (props.initialPage == null && !narrow.value) {
- router.push('/settings/profile');
- } else {
- if (props.initialPage == null) {
- INFO.value = indexInfo;
- }
+ if (!narrow && currentPage?.route.name == null) {
+ router.replace('/settings/profile');
}
});
-watch(narrow, () => {
- if (props.initialPage == null && !narrow.value) {
- router.push('/settings/profile');
- }
-});
-
-onMounted(() => {
- ro.observe(el.value);
+onActivated(() => {
+ narrow = el.value.offsetWidth < NARROW_THRESHOLD;
- narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
- if (props.initialPage == null && !narrow.value) {
- router.push('/settings/profile');
+ if (!narrow && currentPage?.route.name == null) {
+ router.replace('/settings/profile');
}
});
@@ -286,6 +240,8 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(INFO);
+// w 890
+// h 700
</script>
<style lang="scss" scoped>
@@ -323,13 +279,11 @@ definePageMetadata(INFO);
width: 34%;
padding-right: 32px;
box-sizing: border-box;
- overflow: auto;
}
> .main {
flex: 1;
min-width: 0;
- overflow: auto;
}
}
}
diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue
index d0ca85adca..5a0d48b82e 100644
--- a/packages/client/src/pages/settings/instance-mute.vue
+++ b/packages/client/src/pages/settings/instance-mute.vue
@@ -12,8 +12,8 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
-import MkInfo from '@/components/ui/info.vue';
-import MkButton from '@/components/ui/button.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { i18n } from '@/i18n';
diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue
index 7de151040e..c8219519f8 100644
--- a/packages/client/src/pages/settings/integration.vue
+++ b/packages/client/src/pages/settings/integration.vue
@@ -27,7 +27,7 @@
import { computed, onMounted, ref, watch } from 'vue';
import { apiUrl } from '@/config';
import FormSection from '@/components/form/section.vue';
-import MkButton from '@/components/ui/button.vue';
+import MkButton from '@/components/MkButton.vue';
import { $i } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
@@ -95,6 +95,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index d8cb286626..3832933cf9 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -1,12 +1,12 @@
<template>
<div class="_formRoot">
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
- <option value="mute">{{ $ts.mutedUsers }}</option>
- <option value="block">{{ $ts.blockedUsers }}</option>
+ <option value="mute">{{ i18n.ts.mutedUsers }}</option>
+ <option value="block">{{ i18n.ts.blockedUsers }}</option>
</MkTab>
<div v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
- <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
+ <template #empty><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template>
<template #default="{items}">
<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
<MkAcct :user="mute.mutee"/>
@@ -16,7 +16,7 @@
</div>
<div v-if="tab === 'block'">
<MkPagination :pagination="blockingPagination" class="blocking">
- <template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
+ <template #empty><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template>
<template #default="{items}">
<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
<MkAcct :user="block.blockee"/>
@@ -29,9 +29,9 @@
<script lang="ts" setup>
import { } from 'vue';
-import MkPagination from '@/components/ui/pagination.vue';
-import MkTab from '@/components/tab.vue';
-import FormInfo from '@/components/ui/info.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import MkTab from '@/components/MkTab.vue';
+import FormInfo from '@/components/MkInfo.vue';
import FormLink from '@/components/form/link.vue';
import { userPage } from '@/filters/user';
import * as os from '@/os';
@@ -57,6 +57,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.muteAndBlock,
icon: 'fas fa-ban',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/navbar.vue
index 1b4d8799c8..6c501e9f2f 100644
--- a/packages/client/src/pages/settings/menu.vue
+++ b/packages/client/src/pages/settings/navbar.vue
@@ -1,7 +1,7 @@
<template>
<div class="_formRoot">
<FormTextarea v-model="items" tall manual-save class="_formBlock">
- <template #label>{{ i18n.ts.menu }}</template>
+ <template #label>{{ i18n.ts.navbar }}</template>
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
</FormTextarea>
@@ -21,9 +21,9 @@
import { computed, ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
-import { menuDef } from '@/menu';
+import { navbarItemDef } from '@/navbar';
import { defaultStore } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
@@ -45,11 +45,11 @@ async function reloadAsk() {
}
async function addItem() {
- const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k));
+ const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: [...menu.map(k => ({
- value: k, text: i18n.ts[menuDef[k].title],
+ value: k, text: i18n.ts[navbarItemDef[k].title],
})), {
value: '-', text: i18n.ts.divider,
}],
@@ -81,8 +81,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
- title: i18n.ts.menu,
+ title: i18n.ts.navbar,
icon: 'fas fa-list-ul',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 494a3eebe0..5703e0c6b6 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -12,7 +12,7 @@
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { notificationTypes } from 'misskey-js';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
@@ -34,7 +34,7 @@ async function readAllNotifications() {
function configure() {
const includingTypes = notificationTypes.filter(x => !$i!.mutingNotificationTypes.includes(x));
- os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
+ os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
includingTypes,
showGlobalToggle: false,
}, {
@@ -56,6 +56,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.notifications,
icon: 'fas fa-bell',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue
index 283d87a066..51dab04cfa 100644
--- a/packages/client/src/pages/settings/other.vue
+++ b/packages/client/src/pages/settings/other.vue
@@ -10,6 +10,8 @@
<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink>
+ <FormLink to="/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ i18n.ts.registry }}</FormLink>
+
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
</div>
</template>
@@ -41,6 +43,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.other,
icon: 'fas fa-ellipsis-h',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue
index 7ff55e9d83..e259bbeb3a 100644
--- a/packages/client/src/pages/settings/plugin.install.vue
+++ b/packages/client/src/pages/settings/plugin.install.vue
@@ -18,8 +18,8 @@ import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import FormTextarea from '@/components/form/textarea.vue';
-import FormButton from '@/components/ui/button.vue';
-import FormInfo from '@/components/ui/info.vue';
+import FormButton from '@/components/MkButton.vue';
+import FormInfo from '@/components/MkInfo.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
@@ -79,7 +79,7 @@ async function install() {
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
- os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {
+ os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription,
initialName: name,
@@ -120,6 +120,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts._plugin.install,
icon: 'fas fa-download',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue
index 75cf42bb89..8ce6fe4445 100644
--- a/packages/client/src/pages/settings/plugin.vue
+++ b/packages/client/src/pages/settings/plugin.vue
@@ -36,8 +36,8 @@ import { nextTick, ref } from 'vue';
import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkKeyValue from '@/components/key-value.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
@@ -90,7 +90,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.plugins,
icon: 'fas fa-plug',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/preferences-backups.vue b/packages/client/src/pages/settings/preferences-backups.vue
new file mode 100644
index 0000000000..fac67185bc
--- /dev/null
+++ b/packages/client/src/pages/settings/preferences-backups.vue
@@ -0,0 +1,444 @@
+<template>
+<div class="_formRoot">
+ <div :class="$style.buttons">
+ <MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton>
+ <MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton>
+ </div>
+
+ <FormSection>
+ <template #label>{{ ts._preferencesBackups.list }}</template>
+ <template v-if="profiles && Object.keys(profiles).length > 0">
+ <div
+ v-for="(profile, id) in profiles"
+ :key="id"
+ class="_formBlock _panel"
+ :class="$style.profile"
+ @click="$event => menu($event, id)"
+ @contextmenu.prevent.stop="$event => menu($event, id)"
+ >
+ <div :class="$style.profileName">{{ profile.name }}</div>
+ <div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
+ <div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
+ </div>
+ </template>
+ <div v-else-if="profiles">
+ <MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo>
+ </div>
+ <MkLoading v-else/>
+ </FormSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, onUnmounted, useCssModule } from 'vue';
+import { v4 as uuid } from 'uuid';
+import FormSection from '@/components/form/section.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import * as os from '@/os';
+import { ColdDeviceStorage, defaultStore } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import { stream } from '@/stream';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { version, host } from '@/config';
+import { definePageMetadata } from '@/scripts/page-metadata';
+const { t, ts } = i18n;
+
+useCssModule();
+
+const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
+ 'menu',
+ 'visibility',
+ 'localOnly',
+ 'statusbars',
+ 'widgets',
+ 'tl',
+ 'overridedDeviceKind',
+ 'serverDisconnectedBehavior',
+ 'nsfw',
+ 'animation',
+ 'animatedMfm',
+ 'loadRawImages',
+ 'imageNewTab',
+ 'disableShowingAnimatedImages',
+ 'disablePagesScript',
+ 'useOsNativeEmojis',
+ 'disableDrawer',
+ 'useBlurEffectForModal',
+ 'useBlurEffect',
+ 'showFixedPostForm',
+ 'enableInfiniteScroll',
+ 'useReactionPickerForContextMenu',
+ 'showGapBetweenNotesInTimeline',
+ 'instanceTicker',
+ 'reactionPickerSize',
+ 'reactionPickerWidth',
+ 'reactionPickerHeight',
+ 'reactionPickerUseDrawerForMobile',
+ 'defaultSideView',
+ 'menuDisplay',
+ 'reportError',
+ 'squareAvatars',
+ 'numberOfPageCache',
+ 'aiChanMode',
+];
+const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
+ 'lightTheme',
+ 'darkTheme',
+ 'syncDeviceDarkMode',
+ 'plugins',
+ 'mediaVolume',
+ 'sound_masterVolume',
+ 'sound_note',
+ 'sound_noteMy',
+ 'sound_notification',
+ 'sound_chat',
+ 'sound_chatBg',
+ 'sound_antenna',
+ 'sound_channel',
+];
+
+const scope = ['clientPreferencesProfiles'];
+
+const profileProps = ['name', 'createdAt', 'updatedAt', 'misskeyVersion', 'settings'];
+
+type Profile = {
+ name: string;
+ createdAt: string;
+ updatedAt: string | null;
+ misskeyVersion: string;
+ host: string;
+ settings: {
+ hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
+ cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
+ fontSize: string | null;
+ useSystemFont: 't' | null;
+ wallpaper: string | null;
+ };
+};
+
+const connection = $i && stream.useChannel('main');
+
+let profiles = $ref<Record<string, Profile> | null>(null);
+
+os.api('i/registry/get-all', { scope })
+ .then(res => {
+ profiles = res || {};
+ });
+
+function isObject(value: unknown): value is Record<string, unknown> {
+ return value != null && typeof value === 'object' && !Array.isArray(value);
+}
+
+function validate(profile: unknown): void {
+ if (!isObject(profile)) throw new Error('not an object');
+
+ // Check if unnecessary properties exist
+ if (Object.keys(profile).some(key => !profileProps.includes(key))) throw new Error('Unnecessary properties exist');
+
+ if (!profile.name) throw new Error('Missing required prop: name');
+ if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion');
+
+ // Check if createdAt and updatedAt is Date
+ // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
+ if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt).getTime())) throw new Error('createdAt is falsy or not Date');
+ if (profile.updatedAt) {
+ if (Number.isNaN(new Date(profile.updatedAt).getTime())) {
+ throw new Error('updatedAt is not Date');
+ }
+ } else if (profile.updatedAt !== null) {
+ throw new Error('updatedAt is not null');
+ }
+
+ if (!profile.settings) throw new Error('Missing required prop: settings');
+ if (!isObject(profile.settings)) throw new Error('Invalid prop: settings');
+}
+
+function getSettings(): Profile['settings'] {
+ const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>;
+ for (const key of defaultStoreSaveKeys) {
+ hot[key] = defaultStore.state[key];
+ }
+
+ const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
+ for (const key of coldDeviceStorageSaveKeys) {
+ cold[key] = ColdDeviceStorage.get(key);
+ }
+
+ return {
+ hot,
+ cold,
+ fontSize: localStorage.getItem('fontSize'),
+ useSystemFont: localStorage.getItem('useSystemFont') as 't' | null,
+ wallpaper: localStorage.getItem('wallpaper'),
+ };
+}
+
+async function saveNew(): Promise<void> {
+ if (!profiles) return;
+
+ const { canceled, result: name } = await os.inputText({
+ title: ts._preferencesBackups.inputName,
+ });
+ if (canceled) return;
+
+ if (Object.values(profiles).some(x => x.name === name)) {
+ return os.alert({
+ title: ts._preferencesBackups.cannotSave,
+ text: t('_preferencesBackups.nameAlreadyExists', { name }),
+ });
+ }
+
+ const id = uuid();
+ const profile: Profile = {
+ name,
+ createdAt: (new Date()).toISOString(),
+ updatedAt: null,
+ misskeyVersion: version,
+ host,
+ settings: getSettings(),
+ };
+ await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
+}
+
+function loadFile(): void {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.multiple = false;
+ input.onchange = async () => {
+ if (!profiles) return;
+ if (!input.files || input.files.length === 0) return;
+
+ const file = input.files[0];
+
+ if (file.type !== 'application/json') {
+ return os.alert({
+ type: 'error',
+ title: ts._preferencesBackups.cannotLoad,
+ text: ts._preferencesBackups.invalidFile,
+ });
+ }
+
+ let profile: Profile;
+ try {
+ profile = JSON.parse(await file.text()) as unknown as Profile;
+ validate(profile);
+ } catch (err) {
+ return os.alert({
+ type: 'error',
+ title: ts._preferencesBackups.cannotLoad,
+ text: err?.message,
+ });
+ }
+
+ const id = uuid();
+ await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
+
+ // 一応廃棄
+ (window as any).__misskey_input_ref__ = null;
+ };
+
+ // https://qiita.com/fukasawah/items/b9dc732d95d99551013d
+ // iOS Safari で正常に動かす為のおまじない
+ (window as any).__misskey_input_ref__ = input;
+
+ input.click();
+}
+
+async function applyProfile(id: string): Promise<void> {
+ if (!profiles) return;
+
+ const profile = profiles[id];
+
+ const { canceled: cancel1 } = await os.confirm({
+ type: 'warning',
+ title: ts._preferencesBackups.apply,
+ text: t('_preferencesBackups.applyConfirm', { name: profile.name }),
+ });
+ if (cancel1) return;
+
+ // TODO: バージョン or ホストが違ったらさらに警告を表示
+
+ const settings = profile.settings;
+
+ // defaultStore
+ for (const key of defaultStoreSaveKeys) {
+ if (settings.hot[key] !== undefined) {
+ defaultStore.set(key, settings.hot[key]);
+ }
+ }
+
+ // coldDeviceStorage
+ for (const key of coldDeviceStorageSaveKeys) {
+ if (settings.cold[key] !== undefined) {
+ ColdDeviceStorage.set(key, settings.cold[key]);
+ }
+ }
+
+ // fontSize
+ if (settings.fontSize) {
+ localStorage.setItem('fontSize', settings.fontSize);
+ } else {
+ localStorage.removeItem('fontSize');
+ }
+
+ // useSystemFont
+ if (settings.useSystemFont) {
+ localStorage.setItem('useSystemFont', settings.useSystemFont);
+ } else {
+ localStorage.removeItem('useSystemFont');
+ }
+
+ // wallpaper
+ if (settings.wallpaper != null) {
+ localStorage.setItem('wallpaper', settings.wallpaper);
+ } else {
+ localStorage.removeItem('wallpaper');
+ }
+
+ const { canceled: cancel2 } = await os.confirm({
+ type: 'info',
+ text: ts.reloadToApplySetting,
+ });
+ if (cancel2) return;
+
+ unisonReload();
+}
+
+async function deleteProfile(id: string): Promise<void> {
+ if (!profiles) return;
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ title: ts.delete,
+ text: t('deleteAreYouSure', { x: profiles[id].name }),
+ });
+ if (canceled) return;
+
+ await os.apiWithDialog('i/registry/remove', { scope, key: id });
+ delete profiles[id];
+}
+
+async function save(id: string): Promise<void> {
+ if (!profiles) return;
+
+ const { name, createdAt } = profiles[id];
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ title: ts._preferencesBackups.save,
+ text: t('_preferencesBackups.saveConfirm', { name }),
+ });
+ if (canceled) return;
+
+ const profile: Profile = {
+ name,
+ createdAt,
+ updatedAt: (new Date()).toISOString(),
+ misskeyVersion: version,
+ host,
+ settings: getSettings(),
+ };
+ await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
+}
+
+async function rename(id: string): Promise<void> {
+ if (!profiles) return;
+
+ const { canceled: cancel1, result: name } = await os.inputText({
+ title: ts._preferencesBackups.inputName,
+ });
+ if (cancel1 || profiles[id].name === name) return;
+
+ if (Object.values(profiles).some(x => x.name === name)) {
+ return os.alert({
+ title: ts._preferencesBackups.cannotSave,
+ text: t('_preferencesBackups.nameAlreadyExists', { name }),
+ });
+ }
+
+ const registry = Object.assign({}, { ...profiles[id] });
+
+ const { canceled: cancel2 } = await os.confirm({
+ type: 'info',
+ title: ts._preferencesBackups.rename,
+ text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }),
+ });
+ if (cancel2) return;
+
+ registry.name = name;
+ await os.apiWithDialog('i/registry/set', { scope, key: id, value: registry });
+}
+
+function menu(ev: MouseEvent, profileId: string) {
+ if (!profiles) return;
+
+ return os.popupMenu([{
+ text: ts._preferencesBackups.apply,
+ icon: 'fas fa-circle-down',
+ action: () => applyProfile(profileId),
+ }, {
+ type: 'a',
+ text: ts.download,
+ icon: 'fas fa-download',
+ href: URL.createObjectURL(new Blob([JSON.stringify(profiles[profileId], null, 2)], { type: 'application/json' })),
+ download: `${profiles[profileId].name}.json`,
+ }, null, {
+ text: ts.rename,
+ icon: 'fas fa-i-cursor',
+ action: () => rename(profileId),
+ }, {
+ text: ts._preferencesBackups.save,
+ icon: 'fas fa-floppy-disk',
+ action: () => save(profileId),
+ }, null, {
+ text: ts._preferencesBackups.delete,
+ icon: 'fas fa-trash-can',
+ action: () => deleteProfile(profileId),
+ danger: true,
+ }], ev.currentTarget ?? ev.target);
+}
+
+onMounted(() => {
+ // streamingのuser storage updateイベントを監視して更新
+ connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => {
+ if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return;
+ if (!profiles) return;
+
+ profiles[key] = value;
+ });
+});
+
+onUnmounted(() => {
+ connection?.off('registryUpdated');
+});
+
+definePageMetadata(computed(() => ({
+ title: ts.preferencesBackups,
+ icon: 'fas fa-floppy-disk',
+ bg: 'var(--bg)',
+})));
+</script>
+
+<style lang="scss" module>
+.buttons {
+ display: flex;
+ gap: var(--margin);
+ flex-wrap: wrap;
+}
+
+.profile {
+ padding: 20px;
+ cursor: pointer;
+
+ &Name {
+ font-weight: 700;
+ }
+
+ &Time {
+ font-size: .85em;
+ opacity: .7;
+ }
+}
+</style>
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
index a209c3f469..45a0358a92 100644
--- a/packages/client/src/pages/settings/privacy.vue
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -1,49 +1,54 @@
<template>
<div class="_formRoot">
- <FormSwitch v-model="isLocked" class="_formBlock" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}<template #caption>{{ $ts.lockedAccountInfo }}</template></FormSwitch>
- <FormSwitch v-if="isLocked" v-model="autoAcceptFollowed" class="_formBlock" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
+ <FormSwitch v-model="isLocked" class="_formBlock" @update:modelValue="save()">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></FormSwitch>
+ <FormSwitch v-if="isLocked" v-model="autoAcceptFollowed" class="_formBlock" @update:modelValue="save()">{{ i18n.ts.autoAcceptFollowed }}</FormSwitch>
<FormSwitch v-model="publicReactions" class="_formBlock" @update:modelValue="save()">
- {{ $ts.makeReactionsPublic }}
- <template #caption>{{ $ts.makeReactionsPublicDescription }}</template>
+ {{ i18n.ts.makeReactionsPublic }}
+ <template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
</FormSwitch>
<FormSelect v-model="ffVisibility" class="_formBlock" @update:modelValue="save()">
- <template #label>{{ $ts.ffVisibility }}</template>
- <option value="public">{{ $ts._ffVisibility.public }}</option>
- <option value="followers">{{ $ts._ffVisibility.followers }}</option>
- <option value="private">{{ $ts._ffVisibility.private }}</option>
- <template #caption>{{ $ts.ffVisibilityDescription }}</template>
+ <template #label>{{ i18n.ts.ffVisibility }}</template>
+ <option value="public">{{ i18n.ts._ffVisibility.public }}</option>
+ <option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
+ <option value="private">{{ i18n.ts._ffVisibility.private }}</option>
+ <template #caption>{{ i18n.ts.ffVisibilityDescription }}</template>
</FormSelect>
<FormSwitch v-model="hideOnlineStatus" class="_formBlock" @update:modelValue="save()">
- {{ $ts.hideOnlineStatus }}
- <template #caption>{{ $ts.hideOnlineStatusDescription }}</template>
+ {{ i18n.ts.hideOnlineStatus }}
+ <template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template>
</FormSwitch>
<FormSwitch v-model="noCrawle" class="_formBlock" @update:modelValue="save()">
- {{ $ts.noCrawle }}
- <template #caption>{{ $ts.noCrawleDescription }}</template>
+ {{ i18n.ts.noCrawle }}
+ <template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</FormSwitch>
<FormSwitch v-model="isExplorable" class="_formBlock" @update:modelValue="save()">
- {{ $ts.makeExplorable }}
- <template #caption>{{ $ts.makeExplorableDescription }}</template>
+ {{ i18n.ts.makeExplorable }}
+ <template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
</FormSwitch>
<FormSection>
- <FormSwitch v-model="rememberNoteVisibility" class="_formBlock" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
- <FormGroup v-if="!rememberNoteVisibility" class="_formBlock">
- <template #label>{{ $ts.defaultNoteVisibility }}</template>
+ <FormSwitch v-model="rememberNoteVisibility" class="_formBlock" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</FormSwitch>
+ <FormFolder v-if="!rememberNoteVisibility" class="_formBlock">
+ <template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
+ <template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
+ <template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
+ <template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
+ <template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+
<FormSelect v-model="defaultNoteVisibility" class="_formBlock">
- <option value="public">{{ $ts._visibility.public }}</option>
- <option value="home">{{ $ts._visibility.home }}</option>
- <option value="followers">{{ $ts._visibility.followers }}</option>
- <option value="specified">{{ $ts._visibility.specified }}</option>
+ <option value="public">{{ i18n.ts._visibility.public }}</option>
+ <option value="home">{{ i18n.ts._visibility.home }}</option>
+ <option value="followers">{{ i18n.ts._visibility.followers }}</option>
+ <option value="specified">{{ i18n.ts._visibility.specified }}</option>
</FormSelect>
- <FormSwitch v-model="defaultNoteLocalOnly" class="_formBlock">{{ $ts._visibility.localOnly }}</FormSwitch>
- </FormGroup>
+ <FormSwitch v-model="defaultNoteLocalOnly" class="_formBlock">{{ i18n.ts._visibility.localOnly }}</FormSwitch>
+ </FormFolder>
</FormSection>
- <FormSwitch v-model="keepCw" class="_formBlock" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch>
+ <FormSwitch v-model="keepCw" class="_formBlock" @update:modelValue="save()">{{ i18n.ts.keepCw }}</FormSwitch>
</div>
</template>
@@ -52,7 +57,7 @@ import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormSection from '@/components/form/section.vue';
-import FormGroup from '@/components/form/group.vue';
+import FormFolder from '@/components/form/folder.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
@@ -91,6 +96,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.privacy,
icon: 'fas fa-lock-open',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index b662de9e3d..aaf60c8d55 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -1,11 +1,11 @@
<template>
<div class="_formRoot">
<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
- <div class="avatar _acrylic">
+ <div class="avatar">
<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
- <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+ <MkButton primary rounded class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
</div>
- <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
+ <MkButton primary rounded class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
</div>
<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
@@ -39,10 +39,10 @@
<div class="_formRoot">
<FormSplit v-for="(record, i) in fields" :min-width="250" class="_formBlock">
- <FormInput v-model="record.name">
+ <FormInput v-model="record.name" small>
<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
</FormInput>
- <FormInput v-model="record.value">
+ <FormInput v-model="record.value" small>
<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
</FormInput>
</FormSplit>
@@ -56,14 +56,12 @@
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
-
- <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch>
</div>
</template>
<script lang="ts" setup>
import { reactive, watch } from 'vue';
-import MkButton from '@/components/ui/button.vue';
+import MkButton from '@/components/MkButton.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSwitch from '@/components/form/switch.vue';
@@ -88,7 +86,6 @@ const profile = reactive({
isBot: $i.isBot,
isCat: $i.isCat,
showTimelineReplies: $i.showTimelineReplies,
- alwaysMarkNsfw: $i.alwaysMarkNsfw,
});
watch(() => profile, () => {
@@ -126,7 +123,6 @@ function save() {
isBot: !!profile.isBot,
isCat: !!profile.isCat,
showTimelineReplies: !!profile.showTimelineReplies,
- alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
});
}
@@ -183,7 +179,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.profile,
icon: 'fas fa-user',
- bg: 'var(--bg)',
});
</script>
@@ -192,6 +187,7 @@ definePageMetadata({
position: relative;
background-size: cover;
background-position: center;
+ border: solid 1px var(--divider);
border-radius: 10px;
overflow: clip;
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index d0fdf835cf..f8d57cbcd5 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -1,7 +1,7 @@
<template>
<div class="_formRoot">
<FromSlot class="_formBlock">
- <template #label>{{ $ts.reactionSettingDescription }}</template>
+ <template #label>{{ i18n.ts.reactionSettingDescription }}</template>
<div v-panel style="border-radius: 6px;">
<XDraggable v-model="reactions" class="zoaiodol" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true">
<template #item="{element}">
@@ -14,17 +14,17 @@
</template>
</XDraggable>
</div>
- <template #caption>{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></template>
+ <template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template>
</FromSlot>
<FormRadios v-model="reactionPickerSize" class="_formBlock">
- <template #label>{{ $ts.size }}</template>
- <option :value="1">{{ $ts.small }}</option>
- <option :value="2">{{ $ts.medium }}</option>
- <option :value="3">{{ $ts.large }}</option>
+ <template #label>{{ i18n.ts.size }}</template>
+ <option :value="1">{{ i18n.ts.small }}</option>
+ <option :value="2">{{ i18n.ts.medium }}</option>
+ <option :value="3">{{ i18n.ts.large }}</option>
</FormRadios>
<FormRadios v-model="reactionPickerWidth" class="_formBlock">
- <template #label>{{ $ts.numberOfColumn }}</template>
+ <template #label>{{ i18n.ts.numberOfColumn }}</template>
<option :value="1">5</option>
<option :value="2">6</option>
<option :value="3">7</option>
@@ -32,22 +32,22 @@
<option :value="5">9</option>
</FormRadios>
<FormRadios v-model="reactionPickerHeight" class="_formBlock">
- <template #label>{{ $ts.height }}</template>
- <option :value="1">{{ $ts.small }}</option>
- <option :value="2">{{ $ts.medium }}</option>
- <option :value="3">{{ $ts.large }}</option>
- <option :value="4">{{ $ts.large }}+</option>
+ <template #label>{{ i18n.ts.height }}</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>
</FormRadios>
<FormSwitch v-model="reactionPickerUseDrawerForMobile" class="_formBlock">
- {{ $ts.useDrawerReactionPickerForMobile }}
- <template #caption>{{ $ts.needReloadToApply }}</template>
+ {{ i18n.ts.useDrawerReactionPickerForMobile }}
+ <template #caption>{{ i18n.ts.needReloadToApply }}</template>
</FormSwitch>
<FormSection>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
- <FormButton inline @click="preview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
- <FormButton inline danger @click="setDefault"><i class="fas fa-undo"></i> {{ $ts.default }}</FormButton>
+ <FormButton inline @click="preview"><i class="fas fa-eye"></i> {{ i18n.ts.preview }}</FormButton>
+ <FormButton inline danger @click="setDefault"><i class="fas fa-undo"></i> {{ i18n.ts.default }}</FormButton>
</div>
</FormSection>
</div>
@@ -59,15 +59,16 @@ import XDraggable from 'vuedraggable';
import FormInput from '@/components/form/input.vue';
import FormRadios from '@/components/form/radios.vue';
import FromSlot from '@/components/form/slot.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
+import { deepClone } from '@/scripts/clone';
-let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
+let reactions = $ref(deepClone(defaultStore.state.reactions));
const reactionPickerSize = $computed(defaultStore.makeGetterSetter('reactionPickerSize'));
const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
@@ -88,7 +89,7 @@ function remove(reaction, ev: MouseEvent) {
}
function preview(ev: MouseEvent) {
- os.popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
+ os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
asReactionPicker: true,
src: ev.currentTarget ?? ev.target,
}, {}, 'closed');
@@ -101,7 +102,7 @@ async function setDefault() {
});
if (canceled) return;
- reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
+ reactions = deepClone(defaultStore.def.reactions.default);
}
function chooseEmoji(ev: MouseEvent) {
@@ -131,7 +132,6 @@ definePageMetadata({
icon: 'fas fa-eye',
handler: preview,
},
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 57880ef3dd..d109a4ba7c 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -12,7 +12,7 @@
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
- <MkPagination :pagination="pagination">
+ <MkPagination :pagination="pagination" disable-auto-load>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
@@ -41,8 +41,8 @@
import X2fa from './2fa.vue';
import FormSection from '@/components/form/section.vue';
import FormSlot from '@/components/form/slot.vue';
-import FormButton from '@/components/ui/button.vue';
-import MkPagination from '@/components/ui/pagination.vue';
+import FormButton from '@/components/MkButton.vue';
+import MkPagination from '@/components/MkPagination.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
@@ -104,7 +104,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.security,
icon: 'fas fa-lock',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index bb23257d7a..2729609522 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -20,7 +20,7 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import FormRange from '@/components/form/range.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
@@ -90,7 +90,7 @@ async function edit(type) {
},
volume: {
type: 'range',
- mim: 0,
+ min: 0,
max: 1,
step: 0.05,
textConverter: (v) => `${Math.floor(v * 100)}%`,
@@ -131,6 +131,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.sounds,
icon: 'fas fa-music',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/statusbar.statusbar.vue b/packages/client/src/pages/settings/statusbar.statusbar.vue
new file mode 100644
index 0000000000..608222386e
--- /dev/null
+++ b/packages/client/src/pages/settings/statusbar.statusbar.vue
@@ -0,0 +1,140 @@
+<template>
+<div class="_formRoot">
+ <FormSelect v-model="statusbar.type" placeholder="Please select" class="_formBlock">
+ <template #label>{{ i18n.ts.type }}</template>
+ <option value="rss">RSS</option>
+ <option value="federation">Federation</option>
+ <option value="userList">User list timeline</option>
+ </FormSelect>
+
+ <MkInput v-model="statusbar.name" manual-save class="_formBlock">
+ <template #label>{{ i18n.ts.label }}</template>
+ </MkInput>
+
+ <MkSwitch v-model="statusbar.black" class="_formBlock">
+ <template #label>Black</template>
+ </MkSwitch>
+
+ <FormRadios v-model="statusbar.size" class="_formBlock">
+ <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>
+ </FormRadios>
+
+ <template v-if="statusbar.type === 'rss'">
+ <MkInput v-model="statusbar.props.url" manual-save class="_formBlock" type="url">
+ <template #label>URL</template>
+ </MkInput>
+ <MkSwitch v-model="statusbar.props.shuffle" class="_formBlock">
+ <template #label>{{ i18n.ts.shuffle }}</template>
+ </MkSwitch>
+ <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+ <template #label>{{ i18n.ts.refreshInterval }}</template>
+ </MkInput>
+ <FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+ <template #label>{{ i18n.ts.speed }}</template>
+ <template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
+ </FormRange>
+ <MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+ <template #label>{{ i18n.ts.reverse }}</template>
+ </MkSwitch>
+ </template>
+ <template v-else-if="statusbar.type === 'federation'">
+ <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+ <template #label>{{ i18n.ts.refreshInterval }}</template>
+ </MkInput>
+ <FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+ <template #label>{{ i18n.ts.speed }}</template>
+ <template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
+ </FormRange>
+ <MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+ <template #label>{{ i18n.ts.reverse }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="statusbar.props.colored" class="_formBlock">
+ <template #label>{{ i18n.ts.colored }}</template>
+ </MkSwitch>
+ </template>
+ <template v-else-if="statusbar.type === 'userList' && userLists != null">
+ <FormSelect v-model="statusbar.props.userListId" class="_formBlock">
+ <template #label>{{ i18n.ts.userList }}</template>
+ <option v-for="list in userLists" :value="list.id">{{ list.name }}</option>
+ </FormSelect>
+ <MkInput v-model="statusbar.props.refreshIntervalSec" manual-save class="_formBlock" type="number">
+ <template #label>{{ i18n.ts.refreshInterval }}</template>
+ </MkInput>
+ <FormRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1" class="_formBlock">
+ <template #label>{{ i18n.ts.speed }}</template>
+ <template #caption>{{ i18n.ts.fast }} &lt;-&gt; {{ i18n.ts.slow }}</template>
+ </FormRange>
+ <MkSwitch v-model="statusbar.props.marqueeReverse" class="_formBlock">
+ <template #label>{{ i18n.ts.reverse }}</template>
+ </MkSwitch>
+ </template>
+
+ <div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <FormButton danger @click="del">{{ i18n.ts.remove }}</FormButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive, ref, watch } from 'vue';
+import FormSelect from '@/components/form/select.vue';
+import MkInput from '@/components/form/input.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormButton from '@/components/MkButton.vue';
+import FormRange from '@/components/form/range.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+import { deepClone } from '@/scripts/clone';
+
+const props = defineProps<{
+ _id: string;
+ userLists: any[] | null;
+}>();
+
+const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
+
+watch(() => statusbar.type, () => {
+ if (statusbar.type === 'rss') {
+ statusbar.name = 'NEWS';
+ statusbar.props.url = 'http://feeds.afpbb.com/rss/afpbb/afpbbnews';
+ statusbar.props.shuffle = true;
+ statusbar.props.refreshIntervalSec = 120;
+ statusbar.props.display = 'marquee';
+ statusbar.props.marqueeDuration = 100;
+ statusbar.props.marqueeReverse = false;
+ } else if (statusbar.type === 'federation') {
+ statusbar.name = 'FEDERATION';
+ statusbar.props.refreshIntervalSec = 120;
+ statusbar.props.display = 'marquee';
+ statusbar.props.marqueeDuration = 100;
+ statusbar.props.marqueeReverse = false;
+ statusbar.props.colored = false;
+ } else if (statusbar.type === 'userList') {
+ statusbar.name = 'LIST TL';
+ statusbar.props.refreshIntervalSec = 120;
+ statusbar.props.display = 'marquee';
+ statusbar.props.marqueeDuration = 100;
+ statusbar.props.marqueeReverse = false;
+ }
+});
+
+watch(statusbar, save);
+
+async function save() {
+ const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
+ const statusbars = deepClone(defaultStore.state.statusbars);
+ statusbars[i] = deepClone(statusbar);
+ defaultStore.set('statusbars', statusbars);
+}
+
+function del() {
+ defaultStore.set('statusbars', defaultStore.state.statusbars.filter(x => x.id !== props._id));
+}
+</script>
diff --git a/packages/client/src/pages/settings/statusbar.vue b/packages/client/src/pages/settings/statusbar.vue
new file mode 100644
index 0000000000..9dbf182142
--- /dev/null
+++ b/packages/client/src/pages/settings/statusbar.vue
@@ -0,0 +1,54 @@
+<template>
+<div class="_formRoot">
+ <FormFolder v-for="x in statusbars" :key="x.id" class="_formBlock">
+ <template #label>{{ x.type ?? i18n.ts.notSet }}</template>
+ <template #suffix>{{ x.name }}</template>
+ <XStatusbar :_id="x.id" :user-lists="userLists"/>
+ </FormFolder>
+ <FormButton primary @click="add">{{ i18n.ts.add }}</FormButton>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XStatusbar from './statusbar.statusbar.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormFolder from '@/components/form/folder.vue';
+import FormButton from '@/components/MkButton.vue';
+import * as os from '@/os';
+import { defaultStore } from '@/store';
+import { unisonReload } from '@/scripts/unison-reload';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+
+const statusbars = defaultStore.reactiveState.statusbars;
+
+let userLists = $ref();
+
+onMounted(() => {
+ os.api('users/lists/list').then(res => {
+ userLists = res;
+ });
+});
+
+async function add() {
+ defaultStore.push('statusbars', {
+ id: uuid(),
+ type: null,
+ black: false,
+ size: 'medium',
+ props: {},
+ });
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata({
+ title: i18n.ts.statusbar,
+ icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
+});
+</script>
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
index 6a863ed9e6..34f8384d87 100644
--- a/packages/client/src/pages/settings/theme.install.vue
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -15,7 +15,7 @@
import { } from 'vue';
import JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import { applyTheme, validateTheme } from '@/scripts/theme';
import * as os from '@/os';
import { addTheme, getThemes } from '@/theme-store';
@@ -76,6 +76,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts._theme.install,
icon: 'fas fa-download',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
index 68cbf3c353..792bb15e5d 100644
--- a/packages/client/src/pages/settings/theme.manage.vue
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -31,7 +31,7 @@ import JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import { Theme, getBuiltinThemesRef } from '@/scripts/theme';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
@@ -74,6 +74,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts._theme.manage,
icon: 'fas fa-folder-open',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index db4262ba7e..6571a881a9 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -1,12 +1,12 @@
<template>
-<div class="_formRoot">
+<div class="_formRoot rsljpzjq">
<div v-adaptive-border class="rfqxtzch _panel _formBlock">
<div class="toggle">
<div class="toggleWrapper">
<input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
<label for="dn" class="toggle">
- <span class="before">{{ $ts.light }}</span>
- <span class="after">{{ $ts.dark }}</span>
+ <span class="before">{{ i18n.ts.light }}</span>
+ <span class="after">{{ i18n.ts.dark }}</span>
<span class="toggle__handler">
<span class="crater crater--1"></span>
<span class="crater crater--2"></span>
@@ -22,66 +22,46 @@
</div>
</div>
<div class="sync">
- <FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
+ <FormSwitch v-model="syncDeviceDarkMode">{{ i18n.ts.syncDeviceDarkMode }}</FormSwitch>
</div>
</div>
- <template v-if="darkMode">
- <FormSelect v-model="darkThemeId" class="_formBlock">
- <template #label>{{ $ts.themeForDarkMode }}</template>
- <template #prefix><i class="fas fa-moon"></i></template>
- <optgroup :label="$ts.darkThemes">
- <option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$ts.lightThemes">
- <option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- </FormSelect>
- <FormSelect v-model="lightThemeId" class="_formBlock">
- <template #label>{{ $ts.themeForLightMode }}</template>
- <template #prefix><i class="fas fa-sun"></i></template>
- <optgroup :label="$ts.lightThemes">
- <option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- <optgroup :label="$ts.darkThemes">
- <option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
- </optgroup>
- </FormSelect>
- </template>
- <template v-else>
- <FormSelect v-model="lightThemeId" class="_formBlock">
- <template #label>{{ $ts.themeForLightMode }}</template>
+ <div class="selects _formBlock">
+ <FormSelect v-model="lightThemeId" large class="select">
+ <template #label>{{ i18n.ts.themeForLightMode }}</template>
<template #prefix><i class="fas fa-sun"></i></template>
- <optgroup :label="$ts.lightThemes">
- <option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
+ <option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id">{{ instanceLightTheme.name }}</option>
+ <optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes">
+ <option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
- <optgroup :label="$ts.darkThemes">
- <option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
+ <optgroup :label="i18n.ts._theme.builtinThemes">
+ <option v-for="x in builtinLightThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
- <FormSelect v-model="darkThemeId" class="_formBlock">
- <template #label>{{ $ts.themeForDarkMode }}</template>
+ <FormSelect v-model="darkThemeId" large class="select">
+ <template #label>{{ i18n.ts.themeForDarkMode }}</template>
<template #prefix><i class="fas fa-moon"></i></template>
- <optgroup :label="$ts.darkThemes">
- <option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
+ <option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id">{{ instanceDarkTheme.name }}</option>
+ <optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes">
+ <option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
- <optgroup :label="$ts.lightThemes">
- <option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
+ <optgroup :label="i18n.ts._theme.builtinThemes">
+ <option v-for="x in builtinDarkThemes" :key="'builtin:' + x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
- </template>
+ </div>
<FormSection>
<div class="_formLinksGrid">
- <FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
- <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ $ts._theme.explore }}</FormLink>
- <FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._theme.install }}</FormLink>
- <FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ $ts._theme.make }}</FormLink>
+ <FormLink to="/settings/theme/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
+ <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="fas fa-globe"></i></template>{{ i18n.ts._theme.explore }}</FormLink>
+ <FormLink to="/settings/theme/install"><template #icon><i class="fas fa-download"></i></template>{{ i18n.ts._theme.install }}</FormLink>
+ <FormLink to="/theme-editor"><template #icon><i class="fas fa-paint-roller"></i></template>{{ i18n.ts._theme.make }}</FormLink>
</div>
</FormSection>
- <FormButton v-if="wallpaper == null" class="_formBlock" @click="setWallpaper">{{ $ts.setWallpaper }}</FormButton>
- <FormButton v-else class="_formBlock" @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
+ <FormButton v-if="wallpaper == null" class="_formBlock" @click="setWallpaper">{{ i18n.ts.setWallpaper }}</FormButton>
+ <FormButton v-else class="_formBlock" @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</FormButton>
</div>
</template>
@@ -92,7 +72,7 @@ import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import { getBuiltinThemesRef } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
@@ -105,21 +85,25 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
-const instanceThemes = [];
-if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
-if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
+const instanceDarkTheme = computed(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
+const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
+const builtinDarkThemes = computed(() => builtinThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
+const instanceLightTheme = computed(() => instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null);
+const installedLightThemes = computed(() => installedThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
+const builtinLightThemes = computed(() => builtinThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
+const themes = computed(() => uniqueBy([ instanceDarkTheme.value, instanceLightTheme.value, ...builtinThemes.value, ...installedThemes.value ].filter(x => x != null), theme => theme.id));
-const themes = computed(() => uniqueBy([ ...instanceThemes, ...builtinThemes.value, ...installedThemes.value ], theme => theme.id));
-const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
-const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const darkTheme = ColdDeviceStorage.ref('darkTheme');
const darkThemeId = computed({
get() {
return darkTheme.value.id;
},
set(id) {
- ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
+ const t = themes.value.find(x => x.id === id);
+ if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
+ ColdDeviceStorage.set('darkTheme', t);
+ }
},
});
const lightTheme = ColdDeviceStorage.ref('lightTheme');
@@ -128,7 +112,10 @@ const lightThemeId = computed({
return lightTheme.value.id;
},
set(id) {
- ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
+ const t = themes.value.find(x => x.id === id);
+ if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
+ ColdDeviceStorage.set('lightTheme', t);
+ }
},
});
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
@@ -174,7 +161,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.theme,
icon: 'fas fa-palette',
- bg: 'var(--bg)',
});
</script>
@@ -200,6 +186,7 @@ definePageMetadata({
text-align: left;
overflow: clip;
padding: 0 100px;
+ vertical-align: bottom;
input {
position: absolute;
@@ -406,4 +393,17 @@ definePageMetadata({
border-top: solid 0.5px var(--divider);
}
}
+
+.rsljpzjq {
+ > .selects {
+ display: flex;
+ gap: 1.5em var(--margin);
+ flex-wrap: wrap;
+
+ > .select {
+ flex: 1;
+ min-width: 280px;
+ }
+ }
+}
</style>
diff --git a/packages/client/src/pages/settings/webhook.edit.vue b/packages/client/src/pages/settings/webhook.edit.vue
index d3cf5d7b79..5d41f3d087 100644
--- a/packages/client/src/pages/settings/webhook.edit.vue
+++ b/packages/client/src/pages/settings/webhook.edit.vue
@@ -38,13 +38,17 @@ import { } from 'vue';
import FormInput from '@/components/form/input.vue';
import FormSection from '@/components/form/section.vue';
import FormSwitch from '@/components/form/switch.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
+const props = defineProps<{
+ webhookId: string;
+}>();
+
const webhook = await os.api('i/webhooks/show', {
- webhookId: new URLSearchParams(window.location.search).get('id'),
+ webhookId: props.webhookId,
});
let name = $ref(webhook.name);
@@ -74,6 +78,7 @@ async function save(): Promise<void> {
name,
url,
secret,
+ webhookId: props.webhookId,
on: events,
active,
});
@@ -86,6 +91,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: 'Edit webhook',
icon: 'fas fa-bolt',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/webhook.new.vue b/packages/client/src/pages/settings/webhook.new.vue
index 508c0d78be..fcf1329ff6 100644
--- a/packages/client/src/pages/settings/webhook.new.vue
+++ b/packages/client/src/pages/settings/webhook.new.vue
@@ -36,7 +36,7 @@ import { } from 'vue';
import FormInput from '@/components/form/input.vue';
import FormSection from '@/components/form/section.vue';
import FormSwitch from '@/components/form/switch.vue';
-import FormButton from '@/components/ui/button.vue';
+import FormButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
@@ -78,6 +78,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: 'Create new webhook',
icon: 'fas fa-bolt',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/webhook.vue b/packages/client/src/pages/settings/webhook.vue
index 50739e2fd1..1a7e73940c 100644
--- a/packages/client/src/pages/settings/webhook.vue
+++ b/packages/client/src/pages/settings/webhook.vue
@@ -9,7 +9,7 @@
<FormSection>
<MkPagination :pagination="pagination">
<template #default="{items}">
- <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit?id=${webhook.id}`" class="_formBlock">
+ <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_formBlock">
<template #icon>
<i v-if="webhook.active === false" class="fas fa-circle-pause"></i>
<i v-else-if="webhook.latestStatus === null" class="far fa-circle"></i>
@@ -29,7 +29,7 @@
<script lang="ts" setup>
import { } from 'vue';
-import MkPagination from '@/components/ui/pagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import { userPage } from '@/filters/user';
@@ -49,6 +49,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: 'Webhook',
icon: 'fas fa-bolt',
- bg: 'var(--bg)',
});
</script>
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
index c6af0e7661..e297379568 100644
--- a/packages/client/src/pages/settings/word-mute.vue
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -31,10 +31,10 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
-import MkKeyValue from '@/components/key-value.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkInfo from '@/components/ui/info.vue';
-import MkTab from '@/components/tab.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkTab from '@/components/MkTab.vue';
import * as os from '@/os';
import number from '@/filters/number';
import { defaultStore } from '@/store';
@@ -124,6 +124,5 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.wordMute,
icon: 'fas fa-comment-slash',
- bg: 'var(--bg)',
});
</script>