diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-11-17 23:35:55 +0900 |
|---|---|---|
| committer | tamaina <tamaina@hotmail.co.jp> | 2022-11-17 23:35:55 +0900 |
| commit | 764da890b6ad3d53808ec592099a93d9d39d7b08 (patch) | |
| tree | b3e9b08bfafa2bbbb5f657af3adb60bcc9510b67 /packages/client/src/pages/settings | |
| parent | fix (diff) | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | misskey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.gz misskey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.bz2 misskey-764da890b6ad3d53808ec592099a93d9d39d7b08.zip | |
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/client/src/pages/settings')
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 }} <-> {{ 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 }} <-> {{ 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 }} <-> {{ 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> |