diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:14:53 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:14:53 -0400 |
| commit | d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054 (patch) | |
| tree | 0c8d3e0385ce7021c7187ef8b608f1abd87496e5 /packages/frontend/src/ui | |
| parent | merge: enhance: Update de-DE.yml (!949) (diff) | |
| parent | enhance(frontend): 設定の移行を手動でトリガーできるように (diff) | |
| download | sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.gz sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.bz2 sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.zip | |
merge upstream
Diffstat (limited to 'packages/frontend/src/ui')
35 files changed, 673 insertions, 1041 deletions
diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue new file mode 100644 index 0000000000..5fd9f5e44b --- /dev/null +++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue @@ -0,0 +1,64 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <span :class="$style.icon"> + <i class="ti ti-info-circle"></i> + </span> + <span :class="$style.title">{{ i18n.ts._preferencesBackup.backupFound }}</span> + <span :class="$style.body"><button class="_textButton" @click="restore">{{ i18n.ts.restore }}</button> | <button class="_textButton" @click="skip">{{ i18n.ts.skip }}</button></span> +</div> +</template> + +<script lang="ts" setup> +import { $i } from '@/i.js'; +import { i18n } from '@/i18n.js'; +import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js'; + +function restore() { + restoreFromCloudBackup(); +} + +function skip() { + hideRestoreBackupSuggestion(); +} +</script> + +<style lang="scss" module> +.root { + --height: 24px; + font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + background: var(--MI_THEME-panel); +} + +.icon { + margin-left: 10px; +} + +.title { + padding: 0 10px; + font-weight: bold; + + &:empty { + display: none; + } +} + +.body { + min-width: 0; + flex: 1; + overflow: clip; + white-space: nowrap; + text-overflow: ellipsis; +} +</style> diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index d153dc8726..f9af8e1ee7 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; </script> <style lang="scss" module> diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 6d36df9874..fcb9f2acfc 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -9,7 +9,7 @@ import * as os from '@/os.js'; import { instance } from '@/instance.js'; import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; function toolsMenuItems(): MenuItem[] { return [{ diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 227d9bb7e6..a7203b1eb9 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -17,18 +17,18 @@ SPDX-License-Identifier: AGPL-3.0-only <TransitionGroup tag="div" :class="[$style.notifications, { - [$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop', - [$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom', - [$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop', - [$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom', - [$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical', - [$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal', + [$style.notificationsPosition_leftTop]: prefer.s.notificationPosition === 'leftTop', + [$style.notificationsPosition_leftBottom]: prefer.s.notificationPosition === 'leftBottom', + [$style.notificationsPosition_rightTop]: prefer.s.notificationPosition === 'rightTop', + [$style.notificationsPosition_rightBottom]: prefer.s.notificationPosition === 'rightBottom', + [$style.notificationsStackAxis_vertical]: prefer.s.notificationStackAxis === 'vertical', + [$style.notificationsStackAxis_horizontal]: prefer.s.notificationStackAxis === 'horizontal', }]" - :moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''" - :enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" + :moveClass="prefer.s.animation ? $style.transition_notification_move : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_notification_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_notification_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_notification_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_notification_leaveTo : ''" > <div v-for="notification in notifications" :key="notification.id" :class="$style.notification" :style="{ @@ -56,13 +56,13 @@ import * as Misskey from 'misskey-js'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; import { popups } from '@/os.js'; -import { pendingApiRequestsCount } from '@/scripts/misskey-api.js'; -import { uploads } from '@/scripts/upload.js'; -import * as sound from '@/scripts/sound.js'; -import { $i } from '@/account.js'; +import { pendingApiRequestsCount } from '@/utility/misskey-api.js'; +import { uploads } from '@/utility/upload.js'; +import * as sound from '@/utility/sound.js'; +import { $i } from '@/i.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { globalEvents } from '@/events.js'; const SkOneko = defineAsyncComponent(() => import('@/components/SkOneko.vue')); @@ -75,7 +75,7 @@ const dev = _DEV_; const notifications = ref<Misskey.entities.Notification[]>([]); function onNotification(notification: Misskey.entities.Notification, isClient = false) { - if (document.visibilityState === 'visible') { + if (window.document.visibilityState === 'visible') { if (!isClient && notification.type !== 'test') { // サーバーサイドのテスト通知の際は自動で既読をつけない(テストできないので) useStream().send('readNotification'); diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index aa72de6089..18d9f2f5f8 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -53,12 +53,13 @@ import { computed, defineAsyncComponent, toRef } from 'vue'; import { openInstanceMenu } from './common.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; -const menu = toRef(defaultStore.state, 'menu'); +const menu = toRef(prefer.s, 'menu'); const otherMenuItemIndicated = computed(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 062a8faf3f..d590455ae5 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.top"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="instance.sidebarLogoUrl && !iconOnly ? $style.wideInstanceIcon : $style.instanceIcon"/> + <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="instance.sidebarLogoUrl && !iconOnly ? $style.wideInstanceIcon : $style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/> </button> </div> <div :class="$style.middle"> <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> + <i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" :class="$style.divider"></div> @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}" > - <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> + <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]" :style="{ viewTransitionName: 'navbar-item-' + item }"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span> <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink"> <span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span> <i v-else class="_indicatorCircle"></i> @@ -37,14 +37,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div :class="$style.divider"></div> <MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="viewTransitionName: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> </MkA> <button class="_button" :class="$style.item" @click="more"> - <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="viewTransitionName: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span> </button> <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="viewTransitionName: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> </MkA> </div> <div :class="$style.bottom"> @@ -52,25 +52,39 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> </button> <button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> - <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> + <MkAvatar :user="$i" :class="$style.avatar" style="viewTransitionName: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> </button> </div> </div> - <button v-if="!forceIconOnly" class="_button" :class="$style.toggleButton" @click="toggleIconOnly"> - <!-- - <svg viewBox="0 0 16 48" :class="$style.toggleButtonShape"> - <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)"> - <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/> - </g> - </svg> - --> - <svg viewBox="0 0 16 64" :class="$style.toggleButtonShape"> - <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> - <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> - </g> - </svg> - <i :class="'ti ' + `ti-chevron-${ iconOnly ? 'right' : 'left' }`" style="font-size: 12px; margin-left: -8px;"></i> - </button> + + <!-- + <svg viewBox="0 0 16 48" :class="$style.subButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,13.3333)"> + <path d="M23.935,-24C37.223,-24 47.995,-7.842 47.995,12.09C47.995,34.077 47.995,62.07 47.995,84.034C47.995,93.573 45.469,102.721 40.972,109.466C36.475,116.211 30.377,120 24.018,120L23.997,120C10.743,120 -0.003,136.118 -0.003,156C-0.003,156 -0.003,156 -0.003,156L-0.003,-60L-0.003,-59.901C-0.003,-50.379 2.519,-41.248 7.007,-34.515C11.496,-27.782 17.584,-24 23.931,-24C23.932,-24 23.934,-24 23.935,-24Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + --> + + <div v-if="!forceIconOnly" :class="$style.subButtons"> + <div :class="[$style.subButton, $style.menuEditButton]"> + <svg viewBox="0 0 16 64" :class="$style.subButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> + <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + <button class="_button" :class="$style.subButtonClickable" @click="menuEdit"><i :class="$style.subButtonIcon" class="ti ti-settings-2"></i></button> + </div> + <div :class="$style.subButtonGapFill"></div> + <div :class="$style.subButtonGapFillDivider"></div> + <div :class="[$style.subButton, $style.toggleButton]"> + <svg viewBox="0 0 16 64" :class="$style.subButtonShape"> + <g transform="matrix(0.333333,0,0,0.222222,0.000895785,21.3333)"> + <path d="M47.488,7.995C47.79,10.11 47.943,12.266 47.943,14.429C47.997,26.989 47.997,84 47.997,84C47.997,84 44.018,118.246 23.997,133.5C-0.374,152.07 -0.003,192 -0.003,192L-0.003,-96C-0.003,-96 0.151,-56.216 23.997,-37.5C40.861,-24.265 46.043,-1.243 47.488,7.995Z" style="fill:var(--MI_THEME-navBg);"/> + </g> + </svg> + <button class="_button" :class="$style.subButtonClickable" @click="toggleIconOnly"><i v-if="iconOnly" class="ti ti-chevron-right" :class="$style.subButtonIcon"></i><i v-else class="ti ti-chevron-left" :class="$style.subButtonIcon"></i></button> + </div> + </div> </div> </template> @@ -79,18 +93,23 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue'; import { openInstanceMenu } from './common.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; +import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; +import { useRouter } from '@/router.js'; +import { prefer } from '@/preferences.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; + +const router = useRouter(); const forceIconOnly = ref(window.innerWidth <= 1279); const iconOnly = computed(() => { - return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon'); + return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon'); }); -const menu = computed(() => defaultStore.state.menu); +const menu = computed(() => prefer.s.menu); const otherMenuItemIndicated = computed(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; @@ -105,12 +124,18 @@ function calcViewState() { window.addEventListener('resize', calcViewState); -watch(defaultStore.reactiveState.menuDisplay, () => { +watch(store.r.menuDisplay, () => { calcViewState(); }); function toggleIconOnly() { - defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); + if (window.document.startViewTransition && prefer.s.animation) { + window.document.startViewTransition(() => { + store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); + }); + } else { + store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon'); + } } function openAccountMenu(ev: MouseEvent) { @@ -128,6 +153,10 @@ function more(ev: MouseEvent) { closed: () => dispose(), }); } + +function menuEdit() { + router.push('/settings/navbar'); +} </script> <style lang="scss" module> @@ -136,6 +165,8 @@ function more(ev: MouseEvent) { --nav-icon-only-width: 80px; --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5); + --subButtonWidth: 20px; + flex: 0 0 var(--nav-width); width: var(--nav-width); box-sizing: border-box; @@ -171,23 +202,80 @@ function more(ev: MouseEvent) { direction: ltr; } -.toggleButton { +.subButtons { position: fixed; - bottom: 20px; left: var(--nav-width); + bottom: 80px; z-index: 1001; - width: 16px; - height: 64px; box-sizing: border-box; } -.toggleButtonShape { +.subButton { + display: block; + position: relative; + z-index: 1002; + width: var(--subButtonWidth); + height: 50px; + box-sizing: border-box; + align-content: center; +} + +.subButtonShape { position: absolute; z-index: -1; top: 0; + bottom: 0; left: 0; - width: 16px; + margin: auto; + width: var(--subButtonWidth); + height: calc(var(--subButtonWidth) * 4); +} + +.subButtonClickable { + position: absolute; + display: block; + max-width: unset; + width: 24px; + height: 42px; + top: 0; + bottom: 0; + left: -4px; + margin: auto; + font-size: 10px; + + &:hover { + color: var(--MI_THEME-fgHighlighted); + + .subButtonIcon { + opacity: 1; + } + } +} + +.subButtonIcon { + margin-left: -4px; + opacity: 0.7; +} + +.subButtonGapFill { + position: relative; + z-index: 1001; + width: var(--subButtonWidth); height: 64px; + margin-top: -32px; + margin-bottom: -32px; + pointer-events: none; + background: var(--MI_THEME-navBg); +} + +.subButtonGapFillDivider { + position: relative; + z-index: 1010; + margin-left: -2px; + width: 14px; + height: 1px; + background: var(--MI_THEME-divider); + pointer-events: none; } .root:not(.iconOnly) { @@ -426,7 +514,7 @@ function more(ev: MouseEvent) { font-size: 0.9em; } - .toggleButton { + .subButtons { left: var(--nav-width); } } @@ -630,7 +718,7 @@ function more(ev: MouseEvent) { } } - .toggleButton { + .subButtons { left: var(--nav-icon-only-width); } } diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index e234bb3a33..16e72fa227 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { useInterval } from '@@/js/use-interval.js'; -import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; +import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; const props = defineProps<{ display?: 'marquee' | 'oneByOne'; diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index da8fa8bb21..4da89a181e 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -31,7 +31,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { useInterval } from '@@/js/use-interval.js'; -import { shuffle } from '@/scripts/shuffle.js'; +import { shuffle } from '@/utility/shuffle.js'; const props = defineProps<{ url?: string; diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 078b595dca..c5bee51162 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { useInterval } from '@@/js/use-interval.js'; -import { getNoteSummary } from '@/scripts/get-note-summary.js'; +import { getNoteSummary } from '@/utility/get-note-summary.js'; import { notePage } from '@/filters/note.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index ed881bef22..a8d87599e6 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> <div - v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, + v-for="x in prefer.r.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black, [$style.verySmall]: x.size === 'verySmall', [$style.small]: x.size === 'small', [$style.large]: x.size === 'large', @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent } from 'vue'; import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue')); const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue')); const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')); diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index cc62a28b14..5f7600881f 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> +<div v-if="hasDisconnected && prefer.s.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div> <div :class="$style.command" class="_buttons"> <MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton> @@ -19,7 +19,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const zIndex = os.claimZIndex('high'); @@ -34,7 +34,7 @@ function resetDisconnected() { } function reload() { - location.reload(); + window.location.reload(); } useStream().on('_disconnected_', onDisconnected); diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index ff851ad99f..1459881ba1 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -4,11 +4,12 @@ */ import { post } from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { $i, login } from '@/account.js'; -import { getAccountFromId } from '@/scripts/get-account-from-id.js'; -import { deepClone } from '@/scripts/clone.js'; -import { mainRouter } from '@/router/main.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { $i } from '@/i.js'; +import { getAccountFromId } from '@/utility/get-account-from-id.js'; +import { deepClone } from '@/utility/clone.js'; +import { mainRouter } from '@/router.js'; +import { login } from '@/accounts.js'; export function swInject() { navigator.serviceWorker.addEventListener('message', async ev => { diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue index 12de579d90..1135d236fc 100644 --- a/packages/frontend/src/ui/_common_/upload.vue +++ b/packages/frontend/src/ui/_common_/upload.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; import * as os from '@/os.js'; -import { uploads } from '@/scripts/upload.js'; +import { uploads } from '@/utility/upload.js'; import { i18n } from '@/i18n.js'; const zIndex = os.claimZIndex('high'); diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index f4633314ae..7d4235bd4e 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="body"> <div class="left"> <button v-click-anime class="item _button instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> + <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" draggable="false"/> </button> <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> <i class="ti ti-home ti-fw"></i> @@ -51,17 +51,18 @@ import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; import { openInstanceMenu } from './_common_/common.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; -import { defaultStore } from '@/store.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; const WINDOW_THRESHOLD = 1400; const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); -const menu = ref(defaultStore.state.menu); -// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); +const menu = ref(prefer.s.menu); +// const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index f17027bcde..6e81f72549 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="divider"></div> <div class="about"> <button v-click-anime class="item _button" @click="openInstanceMenu"> - <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" :class="{ wideIcon: instance.sidebarLogoUrl && !iconOnly }" class="_ghost" /> + <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" :class="{ wideIcon: instance.sidebarLogoUrl && !iconOnly }" class="_ghost" draggable="false" /> </button> </div> <!--<MisskeyLogo class="misskey"/>--> @@ -49,23 +49,25 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue'; +import { defineAsyncComponent, computed, watch, ref, useTemplateRef } from 'vue'; import { openInstanceMenu } from './_common_/common.js'; // import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; import MkButton from '@/components/MkButton.vue'; -// import { StickySidebar } from '@/scripts/sticky-sidebar.js'; +// import { StickySidebar } from '@/utility/sticky-sidebar.js'; // import { mainRouter } from '@/router.js'; //import MisskeyLogo from '@assets/client/sharkey.svg'; -import { defaultStore } from '@/store.js'; +import { store } from '@/store.js'; import { instance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { prefer } from '@/preferences.js'; +import { openAccountMenu as openAccountMenu_ } from '@/accounts.js'; +import { $i } from '@/i.js'; const WINDOW_THRESHOLD = 1400; -const menu = ref(defaultStore.state.menu); +const menu = ref(prefer.s.menu); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; @@ -73,7 +75,7 @@ const otherNavItemIndicated = computed<boolean>(() => { } return false; }); -const el = shallowRef<HTMLElement>(); +const el = useTemplateRef('el'); // let accounts = $ref([]); // let connection = $ref(null); const iconOnly = ref(false); @@ -100,7 +102,7 @@ function openAccountMenu(ev: MouseEvent) { }, ev); } -watch(defaultStore.reactiveState.menuDisplay, () => { +watch(store.r.menuDisplay, () => { calcViewState(); }); diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index f8cd5fa8be..c252b03c82 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <Transition :name="defaultStore.state.animation ? 'tray-back' : ''"> + <Transition :name="prefer.s.animation ? 'tray-back' : ''"> <div v-if="widgetsShowing" class="tray-back _modalBg" @@ -35,29 +35,32 @@ SPDX-License-Identifier: AGPL-3.0-only ></div> </Transition> - <Transition :name="defaultStore.state.animation ? 'tray' : ''"> + <Transition :name="prefer.s.animation ? 'tray' : ''"> <XWidgets v-if="widgetsShowing" class="tray"/> </Transition> - <iframe v-if="defaultStore.state.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> + <iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> <XCommon/> </div> </template> <script lang="ts" setup> -import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue'; +import { defineAsyncComponent, onMounted, provide, ref, computed, useTemplateRef } from 'vue'; +import { instanceName } from '@@/js/config.js'; +import { isLink } from '@@/js/is-link.js'; import XSidebar from './classic.sidebar.vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@@/js/config.js'; -import { StickySidebar } from '@/scripts/sticky-sidebar.js'; +import type { PageMetadata } from '@/page.js'; +import { StickySidebar } from '@/utility/sticky-sidebar.js'; import * as os from '@/os.js'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { defaultStore } from '@/store.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; +import { store } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; -import { mainRouter } from '@/router/main.js'; -import { isLink } from '@@/js/is-link.js'; +import { mainRouter } from '@/router.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); @@ -73,20 +76,20 @@ const widgetsShowing = ref(false); const fullView = ref(false); const globalHeaderHeight = ref(0); const wallpaper = miLocalStorage.getItem('wallpaper') != null; -const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top'); -const live2d = shallowRef<HTMLIFrameElement>(); +const showMenuOnTop = computed(() => store.s.menuDisplay === 'top'); +const live2d = useTemplateRef('live2d'); const widgetsLeft = ref<HTMLElement>(); const widgetsRight = ref<HTMLElement>(); -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; if (pageMetadata.value) { if (isRoot.value && pageMetadata.value.title === instanceName) { - document.title = pageMetadata.value.title; + window.document.title = pageMetadata.value.title; } else { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + window.document.title = `${pageMetadata.value.title} | ${instanceName}`; } } }); @@ -95,7 +98,7 @@ provide('shouldHeaderThin', showMenuOnTop.value); provide('forceSpacerMin', true); function attachSticky(el: HTMLElement) { - const sticky = new StickySidebar(el, 0, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す + const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す window.addEventListener('scroll', () => { sticky.calc(window.scrollY); }, { passive: true }); @@ -109,7 +112,7 @@ function onContextmenu(ev: MouseEvent) { if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; - const path = mainRouter.getCurrentPath(); + const path = mainRouter.getCurrentFullPath(); os.contextMenu([{ type: 'label', text: path, @@ -136,32 +139,17 @@ if (window.innerWidth < 1024) { const currentUI = miLocalStorage.getItem('ui'); miLocalStorage.setItem('ui_temp', currentUI ?? 'default'); miLocalStorage.setItem('ui', 'default'); - location.reload(); + window.location.reload(); } -document.documentElement.style.overflowY = 'scroll'; - -defaultStore.loaded.then(() => { - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: null, data: {}, - }, { - name: 'notifications', - id: 'b', place: null, data: {}, - }, { - name: 'trends', - id: 'c', place: null, data: {}, - }]); - } -}); +window.document.documentElement.style.overflowY = 'scroll'; onMounted(() => { window.addEventListener('resize', () => { isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD); }, { passive: true }); - if (defaultStore.state.aiChanMode) { + if (prefer.s.aiChanMode) { const iframeRect = live2d.value.getBoundingClientRect(); window.addEventListener('mousemove', ev => { live2d.value.contentWindow.postMessage({ diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 36caca5fc0..18094b4444 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.main"> <XAnnouncements v-if="$i"/> <XStatusBars/> - <div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> + <div ref="columnsEl" :class="[$style.sections, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> <section v-for="ids in layout" @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.sideMenu"> <div :class="$style.sideMenuTop"> - <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button> + <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button> <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button> </div> <div :class="$style.sideMenuMiddle"> @@ -67,10 +67,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -82,10 +82,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menu"> <XDrawerMenu/> @@ -97,22 +97,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; +import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; -import { deckStore, columnTypes, addColumn as addColumnToStore, forceSaveDeck, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; -import type { ColumnType } from './deck/deck-store.js'; -import type { MenuItem } from '@/types/menu.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; -import { $i } from '@/account.js'; +import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; -import { deviceKind } from '@/scripts/device-kind.js'; -import { defaultStore } from '@/store.js'; +import { deviceKind } from '@/utility/device-kind.js'; +import { prefer } from '@/preferences.js'; import XMainColumn from '@/ui/deck/main-column.vue'; import XTlColumn from '@/ui/deck/tl-column.vue'; import XAntennaColumn from '@/ui/deck/antenna-column.vue'; @@ -124,7 +120,8 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import XFollowingColumn from '@/ui/deck/following-column.vue'; -import { mainRouter } from '@/router/main.js'; +import { mainRouter } from '@/router.js'; +import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -144,8 +141,8 @@ const columnComponents = { mainRouter.navHook = (path, flag): boolean => { if (flag === 'forcePage') return false; - const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main'); - if (deckStore.state.navWindow || noMainColumn) { + const noMainColumn = !columns.value.some(x => x.type === 'main'); + if (prefer.s['deck.navWindow'] || noMainColumn) { os.pageWindow(path); return true; } @@ -167,8 +164,6 @@ watch(route, () => { }); */ -const columns = deckStore.reactiveState.columns; -const layout = deckStore.reactiveState.layout; const menuIndicated = computed(() => { if ($i == null) return false; for (const def in navbarItemDef) { @@ -181,7 +176,7 @@ function showSettings() { os.pageWindow('/settings/deck'); } -const columnsEl = shallowRef<HTMLElement>(); +const columnsEl = useTemplateRef('columnsEl'); const addColumn = async (ev) => { const { canceled, result: column } = await os.select({ @@ -195,7 +190,7 @@ const addColumn = async (ev) => { addColumnToStore({ type: column, id: uuid(), - name: i18n.ts._deck._columns[column], + name: null, width: 330, soundSetting: { type: null, volume: 1 }, }); @@ -214,68 +209,23 @@ function onWheel(ev: WheelEvent) { } } -document.documentElement.style.overflowY = 'hidden'; -document.documentElement.style.scrollBehavior = 'auto'; - -loadDeck(); - -function changeProfile(ev: MouseEvent) { - let items: MenuItem[] = [{ - text: deckStore.state.profile, - active: true, - action: () => {}, - }]; - getProfiles().then(profiles => { - items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({ - text: k, - action: () => { - deckStore.set('profile', k); - unisonReload(); - }, - }))), { type: 'divider' as const }, { - text: i18n.ts._deck.newProfile, - icon: 'ti ti-plus', - action: async () => { - const { canceled, result: name } = await os.inputText({ - title: i18n.ts._deck.profile, - minLength: 1, - }); - - if (canceled || name == null) return; - - os.promiseDialog((async () => { - await deckStore.set('profile', name); - await forceSaveDeck(); - })(), () => { - unisonReload(); - }); - }, - }); - }).then(() => { - os.popupMenu(items, ev.currentTarget ?? ev.target); - }); -} +window.document.documentElement.style.overflowY = 'hidden'; +window.document.documentElement.style.scrollBehavior = 'auto'; async function deleteProfile() { + if (prefer.s['deck.profile'] == null) return; + const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }), + text: i18n.tsx.deleteAreYouSure({ x: prefer.s['deck.profile'] }), }); if (canceled) return; - os.promiseDialog((async () => { - if (deckStore.state.profile === 'default') { - await deckStore.set('columns', []); - await deckStore.set('layout', []); - await forceSaveDeck(); - } else { - await deleteProfile_(deckStore.state.profile); - } - await deckStore.set('profile', 'default'); - })(), () => { - unisonReload(); - }); + await deleteProfile_(prefer.s['deck.profile']); + + os.success(); } + </script> <style> diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index a41639e71c..194b56c842 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name || antennaName || i18n.ts._deck._columns.antenna }}</span> </template> <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/> @@ -14,27 +14,29 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue'; -import type { entities as MisskeyEntities } from 'misskey-js'; +import { onMounted, ref, useTemplateRef, watch, defineAsyncComponent } from 'vue'; import XColumn from './column.vue'; -import { updateColumn, Column } from './deck-store.js'; +import type { entities as MisskeyEntities } from 'misskey-js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; import { antennasCache } from '@/cache.js'; -import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; -import * as sound from '@/scripts/sound.js'; +import * as sound from '@/utility/sound.js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = useTemplateRef('timeline'); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const antennaName = ref<string | null>(null); onMounted(() => { if (props.column.antennaId == null) { @@ -42,6 +44,13 @@ onMounted(() => { } }); +watch([() => props.column.name, () => props.column.antennaId], () => { + if (!props.column.name && props.column.antennaId) { + misskeyApi('antennas/show', { antennaId: props.column.antennaId }) + .then(value => antennaName.value = value.name); + } +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 661d45b110..39376108d4 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name || channel?.name || i18n.ts._deck._columns.channel }}</span> </template> <template v-if="column.channelId"> @@ -19,27 +19,28 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, shallowRef, watch, useTemplateRef } from 'vue'; import * as Misskey from 'misskey-js'; import XColumn from './column.vue'; -import { updateColumn, Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { favoritedChannelsCache } from '@/cache.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; -import * as sound from '@/scripts/sound.js'; +import * as sound from '@/utility/sound.js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = useTemplateRef('timeline'); const channel = shallowRef<Misskey.entities.Channel>(); const withRenotes = ref(props.column.withRenotes ?? true); const onlyFiles = ref(props.column.onlyFiles ?? false); @@ -58,9 +59,18 @@ watch(onlyFiles, v => { const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); -if (props.column.channelId == null) { - setChannel(); -} +onMounted(() => { + if (props.column.channelId == null) { + setChannel(); + } +}); + +watch([() => props.column.name, () => props.column.channelId], () => { + if (!props.column.name && props.column.channelId) { + misskeyApi('channels/show', { channelId: props.column.channelId }) + .then(value => channel.value = value); + } +}); watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 2eb232096e..febf8ca6f8 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -42,11 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue'; -import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store.js'; +import { onBeforeUnmount, onMounted, provide, watch, useTemplateRef, ref, computed } from 'vue'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; provide('shouldHeaderThin', true); provide('shouldOmitHeaderTitle', true); @@ -67,7 +68,7 @@ const emit = defineEmits<{ (ev: 'headerWheel', ctx: WheelEvent): void; }>(); -const body = shallowRef<HTMLDivElement | null>(); +const body = useTemplateRef('body'); const dragging = ref(false); watch(dragging, v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd')); @@ -99,7 +100,7 @@ function onOtherDragEnd() { function toggleActive() { if (!props.isStacked) return; updateColumn(props.column.id, { - active: !props.column.active, + active: props.column.active == null ? false : !props.column.active, }); } @@ -128,7 +129,8 @@ function getMenu() { icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { - const { canceled, result } = await os.form(props.column.name, { + const name = props.column.name ?? i18n.ts._deck._columns[props.column.type]; + const { canceled, result } = await os.form(name, { name: { type: 'string', label: i18n.ts.name, @@ -143,7 +145,7 @@ function getMenu() { flexible: { type: 'boolean', label: i18n.ts._deck.flexible, - default: props.column.flexible, + default: props.column.flexible ?? null, }, }); if (canceled) return; @@ -356,7 +358,6 @@ function onDrop(ev) { > .body { background: var(--MI_THEME-bg) !important; - overflow-y: scroll !important; scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; &::-webkit-scrollbar-track { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 8e5b1dd1ac..c58b5d7aad 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -3,59 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { throttle } from 'throttle-debounce'; -import { computed, markRaw, Ref } from 'vue'; -import { notificationTypes } from 'misskey-js'; -import type { BasicTimelineType } from '@/timelines.js'; -import { Storage } from '@/pizzax.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { deepClone } from '@/scripts/clone.js'; -import { SoundStore } from '@/store.js'; +import { markRaw } from 'vue'; +import type { Column } from '@/deck.js'; +import { Pizzax } from '@/lib/pizzax.js'; -type ColumnWidget = { - name: string; - id: string; - data: Record<string, any>; -}; - -export const columnTypes = [ - 'main', - 'widgets', - 'notifications', - 'tl', - 'antenna', - 'list', - 'channel', - 'mentions', - 'direct', - 'roleTimeline', - 'following', -] as const; - -export type ColumnType = typeof columnTypes[number]; - -export type Column = { - id: string; - type: ColumnType; - name: string | null; - width: number; - widgets?: ColumnWidget[]; - active?: boolean; - flexible?: boolean; - antennaId?: string; - listId?: string; - channelId?: string; - roleId?: string; - excludeTypes?: typeof notificationTypes[number][]; - tl?: BasicTimelineType; - withRenotes?: boolean; - withReplies?: boolean; - withSensitive?: boolean; - onlyFiles?: boolean; - soundSetting: SoundStore; -}; - -export const deckStore = markRaw(new Storage('deck', { +// TODO: 消す(移行済みのため) +export const deckStore = markRaw(new Pizzax('deck', { profile: { where: 'deviceAccount', default: 'default', @@ -68,278 +21,4 @@ export const deckStore = markRaw(new Storage('deck', { where: 'deviceAccount', default: [] as Column['id'][][], }, - columnAlign: { - where: 'deviceAccount', - default: 'left' as 'left' | 'right' | 'center', - }, - alwaysShowMainColumn: { - where: 'deviceAccount', - default: true, - }, - navWindow: { - where: 'deviceAccount', - default: true, - }, - useSimpleUiForNonRootPages: { - where: 'deviceAccount', - default: true, - }, })); - -export const loadDeck = async () => { - let deck; - - try { - deck = await misskeyApi('i/registry/get', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - }); - } catch (err) { - if (err.code === 'NO_SUCH_KEY') { - // 後方互換性のため - if (deckStore.state.profile === 'default') { - saveDeck(); - return; - } - - deckStore.set('columns', []); - deckStore.set('layout', []); - return; - } - throw err; - } - - deckStore.set('columns', deck.columns); - deckStore.set('layout', deck.layout); -}; - -export async function forceSaveDeck() { - await misskeyApi('i/registry/set', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - value: { - columns: deckStore.reactiveState.columns.value, - layout: deckStore.reactiveState.layout.value, - }, - }); -} - -// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, () => { - forceSaveDeck(); -}); - -export async function getProfiles(): Promise<string[]> { - return await misskeyApi('i/registry/keys', { - scope: ['client', 'deck', 'profiles'], - }); -} - -export async function deleteProfile(key: string): Promise<void> { - return await misskeyApi('i/registry/remove', { - scope: ['client', 'deck', 'profiles'], - key: key, - }); -} - -export function addColumn(column: Column) { - if (column.name === undefined) column.name = null; - deckStore.push('columns', column); - deckStore.push('layout', [column.id]); - saveDeck(); -} - -export function removeColumn(id: Column['id']) { - deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id)); - deckStore.set('layout', deckStore.state.layout - .map(ids => ids.filter(_id => _id !== id)) - .filter(ids => ids.length > 0)); - saveDeck(); -} - -export function swapColumn(a: Column['id'], b: Column['id']) { - const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1); - const aY = deckStore.state.layout[aX].findIndex(id => id === a); - const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); - const bY = deckStore.state.layout[bX].findIndex(id => id === b); - const layout = deepClone(deckStore.state.layout); - layout[aX][aY] = b; - layout[bX][bY] = a; - deckStore.set('layout', layout); - saveDeck(); -} - -export function swapLeftColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const left = deckStore.state.layout[i - 1]; - if (left) { - layout[i - 1] = deckStore.state.layout[i]; - layout[i] = left; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapRightColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const right = deckStore.state.layout[i + 1]; - if (right) { - layout[i + 1] = deckStore.state.layout[i]; - layout[i] = right; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapUpColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const up = ids[i - 1]; - if (up) { - ids[i - 1] = id; - ids[i] = up; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapDownColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const down = ids[i + 1]; - if (down) { - ids[i + 1] = id; - ids[i] = down; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function stackLeftColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout[i - 1].push(id); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - saveDeck(); -} - -export function popRightColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const affected = layout[i]; - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout.splice(i + 1, 0, [id]); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - - const columns = deepClone(deckStore.state.columns); - for (const column of columns) { - if (affected.includes(column.id)) { - column.active = true; - } - } - deckStore.set('columns', columns); - - saveDeck(); -} - -export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - if (column.widgets == null) column.widgets = []; - column.widgets.unshift(widget); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null || column.widgets == null) return; - column.widgets = column.widgets.filter(w => w.id !== widget.id); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = widgets; - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null || column.widgets == null) return; - column.widgets = column.widgets.map(w => w.id === widgetId ? { - ...w, - data: widgetData, - } : w); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export async function updateColumn<TColumn>(id: Column['id'], column: Partial<TColumn>) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const currentColumn = deepClone(deckStore.state.columns[columnIndex]); - if (currentColumn == null) return; - for (const [k, v] of Object.entries(column)) { - currentColumn[k] = v; - } - columns[columnIndex] = currentColumn; - await Promise.all([ - deckStore.set('columns', columns), - saveDeck(), - ]); -} - -export function getColumn<TColumn extends Column>(id: Column['id']): TColumn { - return deckStore.state.columns.find(c => c.id === id) as TColumn; -} - -export function getReactiveColumn<TColumn extends Column>(id: Column['id']): Ref<TColumn> { - return computed(() => { - return deckStore.reactiveState.columns.value.find(c => c.id === id) as TColumn; - }); -} diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index d12a18f760..772188d773 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -14,8 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import MkNotes from '@/components/MkNotes.vue'; +import { i18n } from '@/i18n.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 8762fb0cce..77c1ea80a7 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ (column.name || listName) ?? i18n.ts._deck._columns.list }}</span> </template> <MkTimeline v-if="column.listId" ref="timeline" :key="column.listId + column.withRenotes + column.onlyFiles" src="list" :list="column.listId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @note="onNote"/> @@ -14,33 +14,44 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { watch, shallowRef, ref } from 'vue'; -import type { entities as MisskeyEntities } from 'misskey-js'; +import { watch, useTemplateRef, ref, onMounted } from 'vue'; import XColumn from './column.vue'; -import { updateColumn, Column } from './deck-store.js'; +import type { entities as MisskeyEntities } from 'misskey-js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import { SoundStore } from '@/store.js'; import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; -import * as sound from '@/scripts/sound.js'; +import * as sound from '@/utility/sound.js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = useTemplateRef('timeline'); const withRenotes = ref(props.column.withRenotes ?? true); const onlyFiles = ref(props.column.onlyFiles ?? false); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const listName = ref<string | null>(null); -if (props.column.listId == null) { - setList(); -} +onMounted(() => { + if (props.column.listId == null) { + setList(); + } +}); + +watch([() => props.column.name, () => props.column.listId], () => { + if (!props.column.name && props.column.listId) { + misskeyApi('users/lists/show', { listId: props.column.listId }) + .then(value => listName.value = value.name); + } +}); watch(withRenotes, v => { updateColumn(props.column.id, { diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index f8c712c371..78454d2e49 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> +<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked"> <template #header> <template v-if="pageMetadata"> <i :class="pageMetadata.icon"></i> @@ -12,33 +12,34 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </template> - <div ref="contents"> - <RouterView @contextmenu.stop="onContextmenu"/> + <div style="height: 100%;"> + <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" @contextmenu.stop="onContextmenu"/> + <RouterView v-else @contextmenu.stop="onContextmenu"/> </div> </XColumn> </template> <script lang="ts" setup> import { provide, shallowRef, ref } from 'vue'; +import { isLink } from '@@/js/is-link.js'; import XColumn from './column.vue'; -import { deckStore, Column } from '@/ui/deck/deck-store.js'; +import type { Column } from '@/deck.js'; +import type { PageMetadata } from '@/page.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@@/js/scroll.js'; -import { isLink } from '@@/js/is-link.js'; -import { mainRouter } from '@/router/main.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; +import { mainRouter } from '@/router.js'; +import { prefer } from '@/preferences.js'; +import { DI } from '@/di.js'; defineProps<{ column: Column; isStacked: boolean; }>(); -const contents = shallowRef<HTMLElement>(); const pageMetadata = ref<null | PageMetadata>(null); -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; @@ -68,6 +69,4 @@ function onContextmenu(ev: MouseEvent) { }, }], ev); } - -useScrollPositionManager(() => getScrollContainer(contents.value), mainRouter); </script> diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 7b25a55ec3..ffd0307940 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -14,8 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import { Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import MkNotes from '@/components/MkNotes.vue'; +import { i18n } from '../../i18n.js'; defineProps<{ column: Column; diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index 19ccfc1f7c..8378dddfef 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -5,16 +5,17 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }"> - <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.notifications }}</template> <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> </XColumn> </template> <script lang="ts" setup> -import { defineAsyncComponent, shallowRef } from 'vue'; +import { defineAsyncComponent, useTemplateRef } from 'vue'; import XColumn from './column.vue'; -import { updateColumn, Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import { updateColumn } from '@/deck.js'; import XNotifications from '@/components/MkNotifications.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -24,7 +25,7 @@ const props = defineProps<{ isStacked: boolean; }>(); -const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>(); +const notificationsComponent = useTemplateRef('notificationsComponent'); function func() { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index beb4237978..468b3e49e0 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name || roleName || i18n.ts._deck._columns.roleTimeline }}</span> </template> <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/> @@ -14,25 +14,27 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, useTemplateRef, watch } from 'vue'; import XColumn from './column.vue'; -import { updateColumn, Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; +import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; -import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; -import * as sound from '@/scripts/sound.js'; +import * as sound from '@/utility/sound.js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = useTemplateRef('timeline'); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); +const roleName = ref<string | null>(null); onMounted(() => { if (props.column.roleId == null) { @@ -40,6 +42,13 @@ onMounted(() => { } }); +watch([() => props.column.name, () => props.column.roleId], () => { + if (!props.column.name && props.column.roleId) { + misskeyApi('roles/show', { roleId: props.column.roleId }) + .then(value => roleName.value = value.name); + } +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 8f5553ccae..c3e68dadf0 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> <i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/> - <span style="margin-left: 8px;">{{ column.name }}</span> + <span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span> </template> <div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled"> @@ -32,25 +32,25 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, ref, shallowRef, computed } from 'vue'; +import { onMounted, watch, ref, useTemplateRef, computed } from 'vue'; import XColumn from './column.vue'; -import { removeColumn, updateColumn, Column } from './deck-store.js'; +import type { Column } from '@/deck.js'; import type { MenuItem } from '@/types/menu.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { removeColumn, updateColumn } from '@/deck.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; -import { instance } from '@/instance.js'; -import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; -import * as sound from '@/scripts/sound.js'; +import * as sound from '@/utility/sound.js'; const props = defineProps<{ column: Column; isStacked: boolean; }>(); -const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const timeline = useTemplateRef('timeline'); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const withRenotes = ref(props.column.withRenotes ?? true); diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts index 275ea56ba0..728c0d0d29 100644 --- a/packages/frontend/src/ui/deck/tl-note-notification.ts +++ b/packages/frontend/src/ui/deck/tl-note-notification.ts @@ -4,9 +4,10 @@ */ import * as Misskey from 'misskey-js'; -import { Ref } from 'vue'; -import { SoundStore } from '@/store.js'; -import { getSoundDuration, playMisskeySfxFile, soundsTypes, SoundType } from '@/scripts/sound.js'; +import type { Ref } from 'vue'; +import type { SoundType } from '@/utility/sound.js'; +import type { SoundStore } from '@/preferences/def.js'; +import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/utility/sound.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index a0e62c8264..4e84ef0ba0 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked"> - <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns[props.column.type] }}</template> <div :class="$style.root"> <div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div> @@ -17,7 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import XColumn from './column.vue'; -import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js'; +import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from '@/deck.js'; +import type { Column } from '@/deck.js'; import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index 9e41c48c5b..ec20ac1114 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -5,9 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <div style="container-type: inline-size;"> - <RouterView/> - </div> + <RouterView/> <XCommon/> </div> @@ -15,35 +13,35 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, provide, ref } from 'vue'; -import XCommon from './_common_/common.vue'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { instanceName } from '@@/js/config.js'; -import { mainRouter } from '@/router/main.js'; +import XCommon from './_common_/common.vue'; +import type { PageMetadata } from '@/page.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; +import { mainRouter } from '@/router.js'; +import { DI } from '@/di.js'; const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); const pageMetadata = ref<null | PageMetadata>(null); -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; if (pageMetadata.value) { if (isRoot.value && pageMetadata.value.title === instanceName) { - document.title = pageMetadata.value.title; + window.document.title = pageMetadata.value.title; } else { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + window.document.title = `${pageMetadata.value.title} | ${instanceName}`; } } }); provideReactiveMetadata(pageMetadata); - -document.documentElement.style.overflowY = 'scroll'; </script> <style lang="scss" module> .root { - min-height: 100dvh; - box-sizing: border-box; + position: relative; + height: 100dvh; } </style> diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index e8c71f61cf..6724c6f6c9 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -7,16 +7,29 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <XSidebar v-if="!isMobile" :class="$style.sidebar"/> - <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> - <template #header> - <div> - <XAnnouncements v-if="$i"/> - <XStatusBars :class="$style.statusbars"/> - </div> - </template> - <RouterView/> - <div :class="$style.spacer"></div> - </MkStickyContainer> + <div :class="$style.contents" @contextmenu.stop="onContextmenu"> + <div> + <XPreferenceRestore v-if="shouldSuggestRestoreBackup"/> + <XAnnouncements v-if="$i"/> + <XStatusBars :class="$style.statusbars"/> + </div> + <div :class="$style.content"> + <StackingRouterView v-if="prefer.s['experimental.stackingRouterView']"/> + <RouterView v-else/> + </div> + <div v-if="isMobile" ref="navFooter" :class="$style.nav"> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> + <i :class="$style.navButtonIcon" class="ti ti-bell"></i> + <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> + <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> + </span> + </button> + <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button> + <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> + </div> + </div> <div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets"> <XWidgets/> @@ -24,24 +37,11 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> - <div v-if="isMobile" ref="navFooter" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button> - <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> - <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> - <i :class="$style.navButtonIcon" class="ti ti-bell"></i> - <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink"> - <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> - </span> - </button> - <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button> - <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> - </div> - <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''" > <div v-if="drawerMenuShowing" @@ -53,10 +53,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''" > <div v-if="drawerMenuShowing" :class="$style.menuDrawer"> <XDrawerMenu/> @@ -64,10 +64,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''" > <div v-if="widgetsShowing" @@ -79,10 +79,10 @@ SPDX-License-Identifier: AGPL-3.0-only </Transition> <Transition - :enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''" - :leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''" - :enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''" - :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" + :enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''" + :leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''" + :enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''" + :leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''" > <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> @@ -95,28 +95,30 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; +import { defineAsyncComponent, provide, onMounted, computed, ref, watch, useTemplateRef } from 'vue'; import { instanceName } from '@@/js/config.js'; -import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { isLink } from '@@/js/is-link.js'; import XCommon from './_common_/common.vue'; -import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; +import type { Ref } from 'vue'; +import type { PageMetadata } from '@/page.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; import { navbarItemDef } from '@/navbar.js'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { $i } from '@/i.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; +import { deviceKind } from '@/utility/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { useScrollPositionManager } from '@/nirax.js'; -import { mainRouter } from '@/router/main.js'; +import { mainRouter } from '@/router.js'; +import { prefer } from '@/preferences.js'; +import { shouldSuggestRestoreBackup } from '@/preferences/utility.js'; +import { DI } from '@/di.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); +const XPreferenceRestore = defineAsyncComponent(() => import('@/ui/_common_/PreferenceRestore.vue')); const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); @@ -132,18 +134,17 @@ window.addEventListener('resize', () => { const pageMetadata = ref<null | PageMetadata>(null); const widgetsShowing = ref(false); -const navFooter = shallowRef<HTMLElement>(); -const contents = shallowRef<InstanceType<typeof MkStickyContainer>>(); +const navFooter = useTemplateRef('navFooter'); -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; if (pageMetadata.value) { if (isRoot.value && pageMetadata.value.title === instanceName) { - document.title = pageMetadata.value.title; + window.document.title = pageMetadata.value.title; } else { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + window.document.title = `${pageMetadata.value.title} | ${instanceName}`; } } }); @@ -168,25 +169,10 @@ if (window.innerWidth > 1024) { if (tempUI) { miLocalStorage.setItem('ui', tempUI); miLocalStorage.removeItem('ui_temp'); - location.reload(); + window.location.reload(); } } -defaultStore.loaded.then(() => { - if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: 'right', data: {}, - }, { - name: 'notifications', - id: 'b', place: 'right', data: {}, - }, { - name: 'trends', - id: 'c', place: 'right', data: {}, - }]); - } -}); - onMounted(() => { if (!isDesktop.value) { window.addEventListener('resize', () => { @@ -199,7 +185,7 @@ const onContextmenu = (ev) => { if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; - const path = mainRouter.getCurrentPath(); + const path = mainRouter.getCurrentFullPath(); os.contextMenu([{ type: 'label', text: path, @@ -212,31 +198,19 @@ const onContextmenu = (ev) => { }], ev); }; -function top() { - contents.value.rootEl.scrollTo({ - top: 0, - behavior: 'smooth', - }); -} - const navFooterHeight = ref(0); -provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight); watch(navFooter, () => { if (navFooter.value) { navFooterHeight.value = navFooter.value.offsetHeight; - document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`); - document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)'); + window.document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)'); } else { navFooterHeight.value = 0; - document.body.style.setProperty('--MI-stickyBottom', '0px'); - document.body.style.setProperty('--MI-minBottomSpacing', '0px'); + window.document.body.style.setProperty('--MI-minBottomSpacing', '0px'); } }, { immediate: true, }); - -useScrollPositionManager(() => contents.value.rootEl, mainRouter); </script> <style> @@ -322,87 +296,27 @@ $widgets-hide-threshold: 1090px; } .contents { + display: flex; + flex-direction: column; flex: 1; height: 100%; min-width: 0; - overflow: auto; - overflow-y: scroll; - overscroll-behavior: unset; background: var(--MI_THEME-bg); } -.widgets { - width: 350px; - height: 100%; - box-sizing: border-box; - overflow: auto; - padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); - border-left: solid 0.5px var(--MI_THEME-divider); - background: var(--MI_THEME-bg); - - @media (max-width: $widgets-hide-threshold) { - display: none; - } -} - -.widgetButton { - display: block; - position: fixed; - z-index: 1000; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - border-radius: var(--MI-radius-full); - box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); - font-size: 22px; - background: var(--MI_THEME-panel); -} - -.widgetsDrawerBg { - z-index: 1001; -} - -.widgetsDrawer { - position: fixed; - top: 0; - right: 0; - z-index: 1001; - width: 310px; - height: 100dvh; - padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important; - box-sizing: border-box; - overflow: auto; - overscroll-behavior: contain; - background: var(--MI_THEME-bg); -} - -.widgetsCloseButton { - padding: 8px; - display: block; - margin: 0 auto; -} - -@media (min-width: 370px) { - .widgetsCloseButton { - display: none; - } +.content { + flex: 1; + min-height: 0; } .nav { - position: fixed; - z-index: 1000; - bottom: 0; - left: 0; padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; grid-gap: 8px; width: 100%; box-sizing: border-box; - -webkit-backdrop-filter: var(--MI-blur, blur(24px)); - backdrop-filter: var(--MI-blur, blur(24px)); - background-color: var(--MI_THEME-header); + background: var(--MI_THEME-bg); border-top: solid 0.5px var(--MI_THEME-divider); } @@ -486,7 +400,61 @@ $widgets-hide-threshold: 1090px; left: 0; } -.spacer { - height: calc(var(--MI-minBottomSpacing)); +.widgets { + width: 350px; + height: 100%; + box-sizing: border-box; + overflow: auto; + padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); + border-left: solid 0.5px var(--MI_THEME-divider); + background: var(--MI_THEME-bg); + + @media (max-width: $widgets-hide-threshold) { + display: none; + } +} + +.widgetButton { + display: block; + position: fixed; + z-index: 1000; + bottom: 32px; + right: 32px; + width: 64px; + height: 64px; + border-radius: 100%; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); + font-size: 22px; + background: var(--MI_THEME-panel); +} + +.widgetsDrawerBg { + z-index: 1001; +} + +.widgetsDrawer { + position: fixed; + top: 0; + right: 0; + z-index: 1001; + width: 310px; + height: 100dvh; + padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important; + box-sizing: border-box; + overflow: auto; + overscroll-behavior: contain; + background: var(--MI_THEME-bg); +} + +.widgetsCloseButton { + padding: 8px; + display: block; + margin: 0 auto; +} + +@media (min-width: 370px) { + .widgetsCloseButton { + display: none; + } } </style> diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index fc0a4475d2..1a6d62e19b 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -19,7 +19,7 @@ const editMode = ref(false); <script lang="ts" setup> import XWidgets from '@/components/MkWidgets.vue'; import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; +import { prefer } from '@/preferences.js'; const props = withDefaults(defineProps<{ // null = 全てのウィジェットを表示 @@ -31,24 +31,24 @@ const props = withDefaults(defineProps<{ }); const widgets = computed(() => { - if (props.place === null) return defaultStore.reactiveState.widgets.value; - if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left'); - return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left'); + if (props.place === null) return prefer.r.widgets.value; + if (props.place === 'left') return prefer.r.widgets.value.filter(w => w.place === 'left'); + return prefer.r.widgets.value.filter(w => w.place !== 'left'); }); function addWidget(widget) { - defaultStore.set('widgets', [{ + prefer.commit('widgets', [{ ...widget, place: props.place, - }, ...defaultStore.state.widgets]); + }, ...prefer.s.widgets]); } function removeWidget(widget) { - defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id)); + prefer.commit('widgets', prefer.s.widgets.filter(w => w.id !== widget.id)); } function updateWidget({ id, data }) { - defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? { + prefer.commit('widgets', prefer.s.widgets.map(w => w.id === id ? { ...w, data, place: props.place, @@ -57,18 +57,18 @@ function updateWidget({ id, data }) { function updateWidgets(thisWidgets) { if (props.place === null) { - defaultStore.set('widgets', thisWidgets); + prefer.commit('widgets', thisWidgets); return; } if (props.place === 'left') { - defaultStore.set('widgets', [ + prefer.commit('widgets', [ ...thisWidgets.map(w => ({ ...w, place: 'left' })), - ...defaultStore.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)), + ...prefer.s.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)), ]); return; } - defaultStore.set('widgets', [ - ...defaultStore.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)), + prefer.commit('widgets', [ + ...prefer.s.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)), ...thisWidgets.map(w => ({ ...w, place: 'right' })), ]); } diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index f048ac2124..8fef1fd997 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -4,81 +4,40 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="mk-app"> - <div v-if="!narrow && !isRoot" class="side"> - <div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div> - <div class="dashboard"> +<div :class="$style.root"> + <a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> + + <div v-if="!narrow && !isRoot" :class="$style.side"> + <div :class="$style.banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div> + <div :class="$style.dashboard"> <MkVisitorDashboard/> </div> </div> - <div class="main"> - <div v-if="!isRoot" class="header"> - <div v-if="narrow === false" class="wide"> - <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA> - <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i> {{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i> {{ i18n.ts.explore }}</MkA> - <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i> {{ i18n.ts.channel }}</MkA> - </div> - <div v-else-if="narrow === true" class="narrow"> - <button class="menu _button" @click="showMenu = true"> - <i class="ti ti-menu-2 icon"></i> - </button> - </div> - </div> - <div class="contents"> - <main v-if="!isRoot" style="container-type: inline-size;"> - <RouterView/> - </main> - <main v-else> - <RouterView/> - </main> + <div :class="$style.main"> + <button v-if="!isRoot" :class="$style.homeButton" class="_button" @click="goHome"> + <i class="ti ti-home"></i> + </button> + <div :class="$style.content"> + <RouterView/> </div> </div> - - <Transition :name="'tray-back'"> - <div - v-if="showMenu" - class="menu-back _modalBg" - @click="showMenu = false" - @touchstart.passive="showMenu = false" - ></div> - </Transition> - - <Transition :name="'tray'"> - <div v-if="showMenu" class="menu"> - <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> - <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i>{{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> - <MkA to="/announcements" class="link" activeClass="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> - <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> - <div class="divider"></div> - <MkA to="/pages" class="link" activeClass="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> - <MkA to="/play" class="link" activeClass="active"><i class="ti ti-player-play icon"></i>Play</MkA> - <MkA to="/gallery" class="link" activeClass="active"><i class="ph-images-square ph-bold ph-lgs icon"></i>{{ i18n.ts.gallery }}</MkA> - <div class="action"> - <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> - <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> - </div> - </div> - </Transition> </div> <XCommon/> </template> <script lang="ts" setup> import { onMounted, provide, ref, computed } from 'vue'; -import XCommon from './_common_/common.vue'; import { instanceName } from '@@/js/config.js'; +import XCommon from './_common_/common.vue'; +import type { PageMetadata } from '@/page.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; import { i18n } from '@/i18n.js'; import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; -import { mainRouter } from '@/router/main.js'; +import { mainRouter } from '@/router.js'; +import { DI } from '@/di.js'; const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); @@ -86,57 +45,25 @@ const DESKTOP_THRESHOLD = 1100; const pageMetadata = ref<null | PageMetadata>(null); -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; if (pageMetadata.value) { if (isRoot.value && pageMetadata.value.title === instanceName) { - document.title = pageMetadata.value.title; + window.document.title = pageMetadata.value.title; } else { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + window.document.title = `${pageMetadata.value.title} | ${instanceName}`; } } }); provideReactiveMetadata(pageMetadata); -const announcements = { - endpoint: 'announcements', - limit: 10, -}; - -const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); - -const showMenu = ref(false); const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); const narrow = ref(window.innerWidth < 1280); -const keymap = computed(() => { - return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': () => { - mainRouter.push('/search'); - }, - }; -}); - -function signin() { - const { dispose } = os.popup(XSigninDialog, { - autoSet: true, - }, { - closed: () => dispose(), - }); -} - -function signup() { - const { dispose } = os.popup(XSignupDialog, { - autoSet: true, - }, { - closed: () => dispose(), - }); +function goHome() { + mainRouter.push('/'); } onMounted(() => { @@ -146,152 +73,73 @@ onMounted(() => { }, { passive: true }); } }); - -defineExpose({ - showMenu: showMenu, -}); </script> <style> .github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} </style> -<style lang="scss" scoped> -.tray-enter-active, -.tray-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-enter-from, -.tray-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.tray-back-enter-active, -.tray-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-back-enter-from, -.tray-back-leave-active { - opacity: 0; +<style lang="scss" module> +.root { + display: flex; + height: 100dvh; + overflow: clip; } -.mk-app { +.main { display: flex; - min-height: 100vh; - - > .side { - position: sticky; - top: 0; - left: 0; - width: 500px; - height: 100vh; - background: var(--MI_THEME-accent); - z-index: 1; - - > .banner { - position: absolute; - top: 0; - left: 0; - width: 100%; - aspect-ratio: 1.5; - background-position: center; - background-size: cover; - -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); - mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); - } - - > .dashboard { - position: relative; - padding: 32px; - box-sizing: border-box; - max-height: 100%; - overflow: auto; - } - } - - > .main { - flex: 1; - min-width: 0; - - > .header { - background: var(--MI_THEME-panel); - position: relative; - z-index: 1; - - > .wide { - line-height: 50px; - padding: 0 16px; - - > .link { - padding: 0 16px; - } - } - - > .narrow { - > .menu { - padding: 16px; - } - } - } - } - - > .menu-back { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - } - - > .menu { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 240px; - height: 100vh; - background: var(--MI_THEME-panel); - - > .link { - display: block; - padding: 16px; - - > .icon { - margin-right: 1em; - } - } + flex-direction: column; + flex: 1; + min-width: 0; +} - > .divider { - margin: 8px auto; - width: calc(100% - 32px); - border-top: solid 0.5px var(--MI_THEME-divider); - } +.homeButton { + position: fixed; + z-index: 1000; + bottom: 16px; + right: 16px; + width: 60px; + height: 60px; + background: var(--MI_THEME-panel); + border-radius: 999px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} - > .action { - padding: 16px; +.side { + position: sticky; + top: 0; + left: 0; + width: 500px; + height: 100vh; + background: var(--MI_THEME-accent); + z-index: 1; + overflow-y: scroll; + background: var(--MI_THEME-accent); +} - > button { - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border-radius: var(--MI-radius-ellipse); +.banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + aspect-ratio: 1.5; + background-position: center; + background-size: cover; + -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); + mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); +} - &._button { - background: var(--MI_THEME-panel); - } +.dashboard { + position: relative; + padding: 32px; + box-sizing: border-box; + max-height: 100%; + overflow: auto; +} - &:first-child { - margin-bottom: 16px; - } - } - } - } +.content { + display: flex; + flex-direction: column; + height: 100dvh; } </style> diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 757aa6669d..66b4496827 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -4,46 +4,50 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="showBottom ? $style.rootWithBottom : $style.root"> - <div style="container-type: inline-size;"> - <RouterView/> +<div :class="$style.root"> + <div :class="$style.contents"> + <div style="flex: 1; min-height: 0;"> + <RouterView/> + </div> + + <!-- + デッキUIが設定されている場合はデッキUIに戻れるようにする (ただし?zenが明示された場合は表示しない) + See https://github.com/misskey-dev/misskey/issues/10905 + --> + <div v-if="showBottom" :class="$style.bottom"> + <button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button> + </div> </div> <XCommon/> </div> - -<!-- - デッキUIが設定されている場合はデッキUIに戻れるようにする (ただし?zenが明示された場合は表示しない) - See https://github.com/misskey-dev/misskey/issues/10905 ---> -<div v-if="showBottom" :class="$style.bottom"> - <button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button> -</div> </template> <script lang="ts" setup> import { computed, provide, ref } from 'vue'; -import XCommon from './_common_/common.vue'; -import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { instanceName, ui } from '@@/js/config.js'; +import XCommon from './_common_/common.vue'; +import type { PageMetadata } from '@/page.js'; +import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router/main.js'; +import { mainRouter } from '@/router.js'; +import { DI } from '@/di.js'; const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); const pageMetadata = ref<null | PageMetadata>(null); -const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck'; +const showBottom = !(new URLSearchParams(window.location.search)).has('zen') && ui === 'deck'; -provide('router', mainRouter); +provide(DI.router, mainRouter); provideMetadataReceiver((metadataGetter) => { const info = metadataGetter(); pageMetadata.value = info; if (pageMetadata.value) { if (isRoot.value && pageMetadata.value.title === instanceName) { - document.title = pageMetadata.value.title; + window.document.title = pageMetadata.value.title; } else { - document.title = `${pageMetadata.value.title} | ${instanceName}`; + window.document.title = `${pageMetadata.value.title} | ${instanceName}`; } } }); @@ -52,19 +56,16 @@ provideReactiveMetadata(pageMetadata); function goToMisskey() { window.location.href = '/'; } - -document.documentElement.style.overflowY = 'scroll'; </script> <style lang="scss" module> .root { - min-height: 100dvh; - box-sizing: border-box; } -.rootWithBottom { - min-height: calc(100dvh - (60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px))); - box-sizing: border-box; +.contents { + display: flex; + flex-direction: column; + height: 100dvh; } .bottom { @@ -74,7 +75,6 @@ document.documentElement.style.overflowY = 'scroll'; } .button { - position: fixed !important; padding: 0; aspect-ratio: 1; width: 100%; |