merge upstream
This commit is contained in:
commit
d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions
64
packages/frontend/src/ui/_common_/PreferenceRestore.vue
Normal file
64
packages/frontend/src/ui/_common_/PreferenceRestore.vue
Normal file
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 [{
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')), {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' })),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
.mk-app {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
> .side {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 500px;
|
||||
height: 100vh;
|
||||
background: var(--MI_THEME-accent);
|
||||
z-index: 1;
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
> .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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
> .dashboard {
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.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);
|
||||
}
|
||||
|
||||
> .header {
|
||||
background: var(--MI_THEME-panel);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.dashboard {
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
margin: 8px auto;
|
||||
width: calc(100% - 32px);
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
> .action {
|
||||
padding: 16px;
|
||||
|
||||
> button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
border-radius: var(--MI-radius-ellipse);
|
||||
|
||||
&._button {
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue