merge upstream

This commit is contained in:
Hazelnoot 2025-03-25 16:14:53 -04:00
commit d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions

View 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>

View file

@ -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>

View file

@ -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 [{

View file

@ -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');

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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;

View file

@ -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<{

View file

@ -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'));

View file

@ -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);

View file

@ -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 => {

View file

@ -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');

View file

@ -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;

View file

@ -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();
});

View file

@ -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({

View file

@ -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>

View file

@ -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 });
});

View file

@ -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 });

View file

@ -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 {

View file

@ -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;
});
}

View file

@ -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;

View file

@ -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, {

View file

@ -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>

View file

@ -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;

View file

@ -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')), {

View file

@ -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 });
});

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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>

View file

@ -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>

View file

@ -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' })),
]);
}

View file

@ -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>

View file

@ -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%;