summaryrefslogtreecommitdiff
path: root/packages/frontend/src/ui
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-03-25 16:14:53 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-03-25 16:14:53 -0400
commitd8908ef2d8fa84d8e0fc1d30ab90a600a3d88054 (patch)
tree0c8d3e0385ce7021c7187ef8b608f1abd87496e5 /packages/frontend/src/ui
parentmerge: enhance: Update de-DE.yml (!949) (diff)
parentenhance(frontend): 設定の移行を手動でトリガーできるように (diff)
downloadsharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.gz
sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.bz2
sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.zip
merge upstream
Diffstat (limited to 'packages/frontend/src/ui')
-rw-r--r--packages/frontend/src/ui/_common_/PreferenceRestore.vue64
-rw-r--r--packages/frontend/src/ui/_common_/announcements.vue2
-rw-r--r--packages/frontend/src/ui/_common_/common.ts2
-rw-r--r--packages/frontend/src/ui/_common_/common.vue34
-rw-r--r--packages/frontend/src/ui/_common_/navbar-for-mobile.vue7
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue162
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-federation.vue4
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-rss.vue2
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-user-list.vue4
-rw-r--r--packages/frontend/src/ui/_common_/statusbars.vue4
-rw-r--r--packages/frontend/src/ui/_common_/stream-indicator.vue6
-rw-r--r--packages/frontend/src/ui/_common_/sw-inject.ts11
-rw-r--r--packages/frontend/src/ui/_common_/upload.vue2
-rw-r--r--packages/frontend/src/ui/classic.header.vue11
-rw-r--r--packages/frontend/src/ui/classic.sidebar.vue18
-rw-r--r--packages/frontend/src/ui/classic.vue58
-rw-r--r--packages/frontend/src/ui/deck.vue108
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue27
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue32
-rw-r--r--packages/frontend/src/ui/deck/column.vue17
-rw-r--r--packages/frontend/src/ui/deck/deck-store.ts331
-rw-r--r--packages/frontend/src/ui/deck/direct-column.vue5
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue35
-rw-r--r--packages/frontend/src/ui/deck/main-column.vue25
-rw-r--r--packages/frontend/src/ui/deck/mentions-column.vue5
-rw-r--r--packages/frontend/src/ui/deck/notifications-column.vue9
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue25
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue14
-rw-r--r--packages/frontend/src/ui/deck/tl-note-notification.ts7
-rw-r--r--packages/frontend/src/ui/deck/widgets-column.vue5
-rw-r--r--packages/frontend/src/ui/minimum.vue24
-rw-r--r--packages/frontend/src/ui/universal.vue272
-rw-r--r--packages/frontend/src/ui/universal.widgets.vue26
-rw-r--r--packages/frontend/src/ui/visitor.vue304
-rw-r--r--packages/frontend/src/ui/zen.vue52
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%;