mirror of
https://github.com/sim1222/misskey.git
synced 2025-04-29 02:37:22 +09:00
toriaezu ugoku
This commit is contained in:
parent
34eb30164c
commit
6714f38171
@ -266,7 +266,7 @@ function react(viaKeyboard = false): void {
|
|||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value, reaction => {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
reaction: reaction,
|
reaction: reaction.reaction, //TODO: #43
|
||||||
});
|
});
|
||||||
}, () => {
|
}, () => {
|
||||||
focus();
|
focus();
|
||||||
|
@ -23,22 +23,20 @@
|
|||||||
|
|
||||||
<FormRadios v-model="emojiAlign" class="_formBlock">
|
<FormRadios v-model="emojiAlign" class="_formBlock">
|
||||||
<template #label>{{ $ts._simkey.emojiAlign }}</template>
|
<template #label>{{ $ts._simkey.emojiAlign }}</template>
|
||||||
<option value="left"><i class="fas fa-align-left" /></option>
|
<option value="left"><i class="ti ti-align-left" /></option>
|
||||||
<option value="center"><i class="fas fa-align-center"></i></option>
|
<option value="center"><i class="ti ti-align-center"></i></option>
|
||||||
<option value="right"><i class="fas fa-align-right" /></option>
|
<option value="right"><i class="ti ti-align-right" /></option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
|
|
||||||
<FormFolder :default-open="false" class="_formBlock">
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
<template #label>{{ $ts._simkey.emojiSizeSetting }}</template>
|
<template #label>{{ $ts._simkey.emojiSizeSetting }}</template>
|
||||||
<FormSection>
|
<FormSwitch v-model="emojiSizeFixed" class="_formBlock">
|
||||||
<FormSwitch v-model="emojiSizeFixed" class="_formBlock">
|
<template #label>{{ $ts._simkey.emojiSizeFixed }}</template>
|
||||||
<template #label>{{ $ts._simkey.emojiSizeFixed }}</template>
|
</FormSwitch>
|
||||||
</FormSwitch>
|
|
||||||
|
|
||||||
<FormSwitch v-model="emojiStretch" class="_formBlock">
|
<FormSwitch v-model="emojiStretch" class="_formBlock">
|
||||||
<template #label>{{ $ts._simkey.emojiStretch }}</template>
|
<template #label>{{ $ts._simkey.emojiStretch }}</template>
|
||||||
</FormSwitch>
|
</FormSwitch>
|
||||||
</FormSection>
|
|
||||||
</FormFolder>
|
</FormFolder>
|
||||||
|
|
||||||
<FormFolder :default-open="false" class="_formBlock">
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
@ -53,26 +51,23 @@
|
|||||||
</FormRadios>
|
</FormRadios>
|
||||||
</FormFolder>
|
</FormFolder>
|
||||||
|
|
||||||
|
|
||||||
<FormFolder :default-open="false" class="_formBlock">
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
<template #label>{{ $ts._simkey.emojiColor }}</template>
|
<template #label>{{ $ts._simkey.emojiColor }}</template>
|
||||||
<FormSection>
|
<div class="cwepdizn-colors">
|
||||||
<div class="cwepdizn-colors">
|
<div class="row">
|
||||||
<div class="row">
|
<button v-for="color in accentColors" :key="color" class="color rounded _button"
|
||||||
<button v-for="color in accentColors" :key="color" class="color rounded _button"
|
@click="setAccentColor(color)">
|
||||||
@click="setAccentColor(color)">
|
<div class="preview" :style="{ background: color }"></div>
|
||||||
<div class="preview" :style="{ background: color }"></div>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormInput v-model="emojiColor" class="_formBlock" :style="{ color: '#' + emojiColor }">
|
<FormInput v-model="emojiColor" class="_formBlock" :style="{ color: '#' + emojiColor }">
|
||||||
<template #prefix><i class="fas fa-palette"></i></template>
|
<template #prefix><i class="ti ti-palette"></i></template>
|
||||||
<template #label @click="colorPick()">{{ $ts._simkey.emojiColor }}</template>
|
<template #label @click="colorPick()">{{ $ts._simkey.emojiColor }}</template>
|
||||||
<template #caption>#RRGGBB</template>
|
<template #caption>#RRGGBB</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormButton @click="colorPick()">{{ $ts._simkey.colorPicker }}</FormButton>
|
<FormButton @click="colorPick()">{{ $ts._simkey.colorPicker }}</FormButton>
|
||||||
</FormSection>
|
|
||||||
</FormFolder>
|
</FormFolder>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,7 +108,7 @@ const menuDef = $computed(() => [{
|
|||||||
to: '/admin/emojis',
|
to: '/admin/emojis',
|
||||||
active: currentPage?.route.name === 'emojis',
|
active: currentPage?.route.name === 'emojis',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-kiss-wink-heart',
|
icon: 'ti ti-mood-tongue',
|
||||||
text: i18n.ts._simkey.emojiGen,
|
text: i18n.ts._simkey.emojiGen,
|
||||||
to: '/admin/emojigen',
|
to: '/admin/emojigen',
|
||||||
active: currentPage?.route.name === 'emojigen',
|
active: currentPage?.route.name === 'emojigen',
|
||||||
|
@ -170,7 +170,7 @@ const menuDef = computed(() => [{
|
|||||||
to: '/settings/other',
|
to: '/settings/other',
|
||||||
active: currentPage?.route.name === 'other',
|
active: currentPage?.route.name === 'other',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-flask',
|
icon: 'ti ti-flask',
|
||||||
text: i18n.ts._simkey.experimentalFeatures,
|
text: i18n.ts._simkey.experimentalFeatures,
|
||||||
to: '/settings/experimental-features',
|
to: '/settings/experimental-features',
|
||||||
active: currentPage?.route.name === 'experimental-features',
|
active: currentPage?.route.name === 'experimental-features',
|
||||||
|
@ -235,7 +235,7 @@ export function getNoteMenu(props: {
|
|||||||
action: pakuru,
|
action: pakuru,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'fas fa-copy',
|
icon: 'ti ti-copy',
|
||||||
text: i18n.ts.copyContent,
|
text: i18n.ts.copyContent,
|
||||||
action: copyContent,
|
action: copyContent,
|
||||||
}, {
|
}, {
|
||||||
@ -260,7 +260,7 @@ export function getNoteMenu(props: {
|
|||||||
action: translate,
|
action: translate,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{
|
{
|
||||||
icon: 'fas fa-kiss-wink-heart',
|
icon: 'ti ti-mood-tongue',
|
||||||
text: i18n.ts._simkey.reactionList,
|
text: i18n.ts._simkey.reactionList,
|
||||||
action: () => reactionList(),
|
action: () => reactionList(),
|
||||||
},
|
},
|
||||||
|
85
packages/frontend/src/ui/_common_old/common.ts
Normal file
85
packages/frontend/src/ui/_common_old/common.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import * as os from '@/os';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { host } from '@/config';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
|
export function openInstanceMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: instance.name ?? host,
|
||||||
|
type: 'label',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.instanceInfo,
|
||||||
|
icon: 'ti ti-info-circle',
|
||||||
|
to: '/about',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.customEmojis,
|
||||||
|
icon: 'ti ti-icons',
|
||||||
|
to: '/about#emojis',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.federation,
|
||||||
|
icon: 'ti ti-whirl',
|
||||||
|
to: '/about#federation',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.charts,
|
||||||
|
icon: 'ti ti-chart-line',
|
||||||
|
to: '/about#charts',
|
||||||
|
}, null, {
|
||||||
|
type: 'parent',
|
||||||
|
text: i18n.ts.tools,
|
||||||
|
icon: 'ti ti-tool',
|
||||||
|
children: [{
|
||||||
|
type: 'link',
|
||||||
|
to: '/scratchpad',
|
||||||
|
text: i18n.ts.scratchpad,
|
||||||
|
icon: 'ti ti-terminal-2',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/api-console',
|
||||||
|
text: 'API Console',
|
||||||
|
icon: 'ti ti-terminal-2',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/clicker',
|
||||||
|
text: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
|
}, ($i && ($i.isAdmin || $i.role.canInvite) && instance.disableRegistration) ? {
|
||||||
|
text: i18n.ts.invite,
|
||||||
|
icon: 'ti ti-user-plus',
|
||||||
|
action: () => {
|
||||||
|
os.api('invite').then(x => {
|
||||||
|
os.alert({
|
||||||
|
type: 'info',
|
||||||
|
text: x.code,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} : undefined, ($i && ($i.isAdmin || $i.role.canManageCustomEmojis)) ? {
|
||||||
|
type: 'link',
|
||||||
|
to: '/custom-emojis-manager',
|
||||||
|
text: i18n.ts.manageCustomEmojis,
|
||||||
|
icon: 'ti ti-icons',
|
||||||
|
} : undefined],
|
||||||
|
}, null, {
|
||||||
|
text: i18n.ts.help,
|
||||||
|
icon: 'ti ti-question-circle',
|
||||||
|
action: () => {
|
||||||
|
window.open('https://misskey-hub.net/help.html', '_blank');
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
text: i18n.ts.aboutMisskey,
|
||||||
|
to: '/about-misskey',
|
||||||
|
}], ev.currentTarget ?? ev.target, {
|
||||||
|
align: 'left',
|
||||||
|
});
|
||||||
|
}
|
@ -1,131 +1,206 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="popup.component"
|
<component
|
||||||
v-for="popup in popups"
|
:is="popup.component"
|
||||||
:key="popup.id"
|
v-for="popup in popups"
|
||||||
v-bind="popup.props"
|
:key="popup.id"
|
||||||
v-on="popup.events"
|
v-bind="popup.props"
|
||||||
/>
|
v-on="popup.events"
|
||||||
|
/>
|
||||||
<XUpload v-if="uploads.length > 0"/>
|
|
||||||
|
<XUpload v-if="uploads.length > 0"/>
|
||||||
<XStreamIndicator/>
|
|
||||||
|
<TransitionGroup
|
||||||
<div v-if="pendingApiRequestsCount > 0" id="wait"></div>
|
tag="div" :class="$style.notifications"
|
||||||
|
:move-class="$store.state.animation ? $style.transition_notification_move : ''"
|
||||||
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
:enter-active-class="$store.state.animation ? $style.transition_notification_enterActive : ''"
|
||||||
</template>
|
:leave-active-class="$store.state.animation ? $style.transition_notification_leaveActive : ''"
|
||||||
|
:enter-from-class="$store.state.animation ? $style.transition_notification_enterFrom : ''"
|
||||||
<script lang="ts">
|
:leave-to-class="$store.state.animation ? $style.transition_notification_leaveTo : ''"
|
||||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
>
|
||||||
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
<XNotification v-for="notification in notifications" :key="notification.id" :notification="notification" :class="$style.notification"/>
|
||||||
import { uploads } from '@/scripts/upload';
|
</TransitionGroup>
|
||||||
import * as sound from '@/scripts/sound';
|
|
||||||
import { $i } from '@/account';
|
<XStreamIndicator/>
|
||||||
import { swInject } from './sw-inject';
|
|
||||||
import { stream } from '@/stream';
|
<div v-if="pendingApiRequestsCount > 0" id="wait"></div>
|
||||||
import * as misskey from 'misskey-js';
|
|
||||||
|
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div>
|
||||||
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
|
</template>
|
||||||
XUpload: defineAsyncComponent(() => import('./upload.vue')),
|
|
||||||
},
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, nextTick } from 'vue';
|
||||||
setup() {
|
import * as misskey from 'misskey-js';
|
||||||
let notifications = $ref<misskey.entities.Notification[]>([]);
|
import { swInject } from './sw-inject';
|
||||||
|
import XNotification from './notification.vue';
|
||||||
const onNotification = notification => {
|
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
||||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
import { uploads } from '@/scripts/upload';
|
||||||
|
import * as sound from '@/scripts/sound';
|
||||||
if (document.visibilityState === 'visible') {
|
import { $i } from '@/account';
|
||||||
stream.send('readNotification', {
|
import { stream } from '@/stream';
|
||||||
id: notification.id,
|
import { i18n } from '@/i18n';
|
||||||
});
|
|
||||||
|
const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
|
||||||
notifications.unshift(notification);
|
const XUpload = defineAsyncComponent(() => import('./upload.vue'));
|
||||||
window.setTimeout(() => {
|
|
||||||
if (notifications.length > 3) notifications.pop();
|
const dev = _DEV_;
|
||||||
}, 500);
|
|
||||||
|
let notifications = $ref<misskey.entities.Notification[]>([]);
|
||||||
window.setTimeout(() => {
|
|
||||||
notifications = notifications.filter(x => x.id !== notification.id);
|
function onNotification(notification) {
|
||||||
}, 6000);
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
}
|
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
sound.play('notification');
|
stream.send('readNotification', {
|
||||||
};
|
id: notification.id,
|
||||||
|
});
|
||||||
if ($i) {
|
|
||||||
const connection = stream.useChannel('main', null, 'UI');
|
notifications.unshift(notification);
|
||||||
connection.on('notification', onNotification);
|
window.setTimeout(() => {
|
||||||
|
if (notifications.length > 3) notifications.pop();
|
||||||
//#region Listen message from SW
|
}, 500);
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
swInject();
|
window.setTimeout(() => {
|
||||||
}
|
notifications = notifications.filter(x => x.id !== notification.id);
|
||||||
}
|
}, 6000);
|
||||||
|
}
|
||||||
return {
|
|
||||||
uploads,
|
sound.play('notification');
|
||||||
popups,
|
}
|
||||||
pendingApiRequestsCount,
|
|
||||||
dev: _DEV_,
|
if ($i) {
|
||||||
};
|
const connection = stream.useChannel('main', null, 'UI');
|
||||||
},
|
connection.on('notification', onNotification);
|
||||||
});
|
|
||||||
</script>
|
//#region Listen message from SW
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
<style lang="scss">
|
swInject();
|
||||||
@keyframes dev-ticker-blink {
|
}
|
||||||
0% { opacity: 1; }
|
}
|
||||||
50% { opacity: 0; }
|
</script>
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
<style lang="scss" module>
|
||||||
|
.transition_notification_move,
|
||||||
@keyframes progress-spinner {
|
.transition_notification_enterActive,
|
||||||
0% {
|
.transition_notification_leaveActive {
|
||||||
transform: rotate(0deg);
|
transition: opacity 0.3s, transform 0.3s !important;
|
||||||
}
|
}
|
||||||
100% {
|
.transition_notification_enterFrom,
|
||||||
transform: rotate(360deg);
|
.transition_notification_leaveTo {
|
||||||
}
|
opacity: 0;
|
||||||
}
|
transform: translateX(-250px);
|
||||||
|
}
|
||||||
#wait {
|
|
||||||
display: block;
|
.notifications {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 4000000;
|
z-index: 3900000;
|
||||||
top: 15px;
|
left: 0;
|
||||||
right: 15px;
|
width: 250px;
|
||||||
|
top: 32px;
|
||||||
&:before {
|
padding: 0 32px;
|
||||||
content: "";
|
pointer-events: none;
|
||||||
display: block;
|
container-type: inline-size;
|
||||||
width: 18px;
|
}
|
||||||
height: 18px;
|
|
||||||
box-sizing: border-box;
|
.notification {
|
||||||
border: solid 2px transparent;
|
& + .notification {
|
||||||
border-top-color: var(--accent);
|
margin-top: 8px;
|
||||||
border-left-color: var(--accent);
|
}
|
||||||
border-radius: 50%;
|
}
|
||||||
animation: progress-spinner 400ms linear infinite;
|
|
||||||
}
|
@media (max-width: 500px) {
|
||||||
}
|
.notifications {
|
||||||
|
top: initial;
|
||||||
#devTicker {
|
bottom: calc(var(--minBottomSpacing) + var(--margin));
|
||||||
position: fixed;
|
padding: 0 var(--margin);
|
||||||
top: 0;
|
display: flex;
|
||||||
left: 0;
|
flex-direction: column-reverse;
|
||||||
z-index: 2147483647;
|
}
|
||||||
color: #ff0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
.notification {
|
||||||
padding: 4px 5px;
|
& + .notification {
|
||||||
font-size: 14px;
|
margin-top: 0;
|
||||||
pointer-events: none;
|
margin-bottom: 8px;
|
||||||
user-select: none;
|
}
|
||||||
|
}
|
||||||
> span {
|
}
|
||||||
animation: dev-ticker-blink 2s infinite;
|
</style>
|
||||||
}
|
|
||||||
}
|
<style lang="scss">
|
||||||
</style>
|
@keyframes dev-ticker-blink {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#wait {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 4000000;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: progress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#botWarn {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 2147483647;
|
||||||
|
color: #ff0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 7px;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
animation: dev-ticker-blink 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#devTicker {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2147483647;
|
||||||
|
color: #ff0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
animation: dev-ticker-blink 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
258
packages/frontend/src/ui/_common_old/navbar-for-mobile.vue
Normal file
258
packages/frontend/src/ui/_common_old/navbar-for-mobile.vue
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<div class="kmwsukvl">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: navbarItemDef[item].active }]" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
|
||||||
|
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import { openInstanceMenu } from './common';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const menu = toRef(defaultStore.state, 'menu');
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function more() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.kmwsukvl {
|
||||||
|
> .body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 24px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
466
packages/frontend/src/ui/_common_old/navbar.vue
Normal file
466
packages/frontend/src/ui/_common_old/navbar.vue
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mvcprjjd" :class="{ iconOnly }">
|
||||||
|
<div class="body">
|
||||||
|
<div class="top">
|
||||||
|
<div class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"></div>
|
||||||
|
<button v-click-anime v-tooltip.noDelay.right="$instance.name ?? i18n.ts.instance" class="item _button instance" @click="openInstanceMenu">
|
||||||
|
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="middle">
|
||||||
|
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.timeline" class="item index" active-class="active" to="/" exact>
|
||||||
|
<i class="icon ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span>
|
||||||
|
</MkA>
|
||||||
|
<template v-for="item in menu">
|
||||||
|
<div v-if="item === '-'" class="divider"></div>
|
||||||
|
<component
|
||||||
|
:is="navbarItemDef[item].to ? 'MkA' : 'button'"
|
||||||
|
v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)"
|
||||||
|
v-click-anime
|
||||||
|
v-tooltip.noDelay.right="navbarItemDef[item].title"
|
||||||
|
class="item _button"
|
||||||
|
:class="[item, { active: navbarItemDef[item].active }]"
|
||||||
|
active-class="active"
|
||||||
|
:to="navbarItemDef[item].to"
|
||||||
|
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
|
||||||
|
>
|
||||||
|
<i class="icon ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
|
||||||
|
<span v-if="navbarItemDef[item].indicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip.noDelay.right="i18n.ts.controlPanel" class="item" active-class="active" to="/admin">
|
||||||
|
<i class="icon ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||||
|
</MkA>
|
||||||
|
<button v-click-anime class="item _button" @click="more">
|
||||||
|
<i class="icon ti ti-grid-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
|
||||||
|
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon _indicatorCircle"></i></span>
|
||||||
|
</button>
|
||||||
|
<MkA v-click-anime v-tooltip.noDelay.right="i18n.ts.settings" class="item" active-class="active" to="/settings">
|
||||||
|
<i class="icon ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div class="bottom">
|
||||||
|
<button v-tooltip.noDelay.right="i18n.ts.note" class="item _button post" data-cy-open-post-form @click="os.post">
|
||||||
|
<i class="icon ti ti-pencil ti-fw"></i><span class="text">{{ i18n.ts.note }}</span>
|
||||||
|
</button>
|
||||||
|
<button v-click-anime v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="item _button account" @click="openAccountMenu">
|
||||||
|
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import { openInstanceMenu } from './common';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const iconOnly = ref(false);
|
||||||
|
|
||||||
|
const menu = computed(() => defaultStore.state.menu);
|
||||||
|
const otherMenuItemIndicated = computed(() => {
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (menu.value.includes(def)) continue;
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcViewState = () => {
|
||||||
|
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
|
||||||
|
};
|
||||||
|
|
||||||
|
calcViewState();
|
||||||
|
|
||||||
|
window.addEventListener('resize', calcViewState);
|
||||||
|
|
||||||
|
watch(defaultStore.reactiveState.menuDisplay, () => {
|
||||||
|
calcViewState();
|
||||||
|
});
|
||||||
|
|
||||||
|
function openAccountMenu(ev: MouseEvent) {
|
||||||
|
openAccountMenu_({
|
||||||
|
withExtraOperation: true,
|
||||||
|
}, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function more(ev: MouseEvent) {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
|
||||||
|
src: ev.currentTarget ?? ev.target,
|
||||||
|
}, {
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mvcprjjd {
|
||||||
|
$nav-width: 250px;
|
||||||
|
$nav-icon-only-width: 80px;
|
||||||
|
|
||||||
|
flex: 0 0 $nav-width;
|
||||||
|
width: $nav-width;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
height: 100dvh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: clip;
|
||||||
|
background: var(--navBg);
|
||||||
|
contain: strict;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.iconOnly) {
|
||||||
|
> .body {
|
||||||
|
width: $nav-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .banner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 15%, rgba(0,0,0,0.75) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 38px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 30px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 16px 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding-left: 30px;
|
||||||
|
line-height: 2.85rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--navFg);
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--navHoverFg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--navActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: calc(100% - 34px);
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.iconOnly {
|
||||||
|
flex: 0 0 $nav-icon-only-width;
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
width: $nav-icon-only-width;
|
||||||
|
|
||||||
|
> .top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
background: var(--X14);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
|
||||||
|
> .post {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 52px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
&:before {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
position: relative;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .account {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
display: inline-block;
|
||||||
|
width: 38px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .middle {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
margin: 8px auto;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding: 18px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 24px;
|
||||||
|
color: var(--navIndicator);
|
||||||
|
font-size: 8px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accentedBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon, > .text {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
24
packages/frontend/src/ui/_common_old/notification.vue
Normal file
24
packages/frontend/src/ui/_common_old/notification.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="$style.root">
|
||||||
|
<XNotification :notification="notification" class="notification _acrylic"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
notification: misskey.entities.Notification;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: clip;
|
||||||
|
contain: content;
|
||||||
|
}
|
||||||
|
</style>
|
108
packages/frontend/src/ui/_common_old/statusbar-federation.vue
Normal file
108
packages/frontend/src/ui/_common_old/statusbar-federation.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<span v-if="!fetching" class="nmidsaqw">
|
||||||
|
<template v-if="display === 'marquee'">
|
||||||
|
<Transition name="change" mode="default">
|
||||||
|
<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
|
||||||
|
<span v-for="instance in instances" :key="instance.id" class="item" :class="{ colored }" :style="{ background: colored ? instance.themeColor : null }">
|
||||||
|
<img class="icon" :src="getInstanceIcon(instance)" alt=""/>
|
||||||
|
<MkA :to="`/instance-info/${instance.host}`" class="host _monospace">
|
||||||
|
{{ instance.host }}
|
||||||
|
</MkA>
|
||||||
|
<span class="divider"></span>
|
||||||
|
</span>
|
||||||
|
</MarqueeText>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="display === 'oneByOne'">
|
||||||
|
<!-- TODO -->
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import MarqueeText from '@/components/MkMarquee.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
|
import { notePage } from '@/filters/note';
|
||||||
|
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
display?: 'marquee' | 'oneByOne';
|
||||||
|
colored?: boolean;
|
||||||
|
marqueeDuration?: number;
|
||||||
|
marqueeReverse?: boolean;
|
||||||
|
oneByOneInterval?: number;
|
||||||
|
refreshIntervalSec?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const instances = ref<misskey.entities.Instance[]>([]);
|
||||||
|
const fetching = ref(true);
|
||||||
|
let key = $ref(0);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
os.api('federation/instances', {
|
||||||
|
sort: '+latestRequestReceivedAt',
|
||||||
|
limit: 30,
|
||||||
|
}).then(res => {
|
||||||
|
instances.value = res;
|
||||||
|
fetching.value = false;
|
||||||
|
key++;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function getInstanceIcon(instance): string {
|
||||||
|
return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-enter-active, .change-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: all 1s ease;
|
||||||
|
}
|
||||||
|
.change-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
.change-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nmidsaqw {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
::v-deep(.item) {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 5em;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--height);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .host {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.colored {
|
||||||
|
padding-right: 1em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
93
packages/frontend/src/ui/_common_old/statusbar-rss.vue
Normal file
93
packages/frontend/src/ui/_common_old/statusbar-rss.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<span v-if="!fetching" class="xbhtxfms">
|
||||||
|
<template v-if="display === 'marquee'">
|
||||||
|
<Transition name="change" mode="default">
|
||||||
|
<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
|
||||||
|
<span v-for="item in items" class="item">
|
||||||
|
<a class="link" :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span class="divider"></span>
|
||||||
|
</span>
|
||||||
|
</MarqueeText>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="display === 'oneByOne'">
|
||||||
|
<!-- TODO -->
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import MarqueeText from '@/components/MkMarquee.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import { shuffle } from '@/scripts/shuffle';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
url?: string;
|
||||||
|
shuffle?: boolean;
|
||||||
|
display?: 'marquee' | 'oneByOne';
|
||||||
|
marqueeDuration?: number;
|
||||||
|
marqueeReverse?: boolean;
|
||||||
|
oneByOneInterval?: number;
|
||||||
|
refreshIntervalSec?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const items = ref([]);
|
||||||
|
const fetching = ref(true);
|
||||||
|
let key = $ref(0);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
|
||||||
|
res.json().then(feed => {
|
||||||
|
if (props.shuffle) {
|
||||||
|
shuffle(feed.items);
|
||||||
|
}
|
||||||
|
items.value = feed.items;
|
||||||
|
fetching.value = false;
|
||||||
|
key++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-enter-active, .change-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: all 1s ease;
|
||||||
|
}
|
||||||
|
.change-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
.change-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xbhtxfms {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
::v-deep(.item) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5px;
|
||||||
|
height: var(--height);
|
||||||
|
margin: 0 3em;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
113
packages/frontend/src/ui/_common_old/statusbar-user-list.vue
Normal file
113
packages/frontend/src/ui/_common_old/statusbar-user-list.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<span v-if="!fetching" class="osdsvwzy">
|
||||||
|
<template v-if="display === 'marquee'">
|
||||||
|
<Transition name="change" mode="default">
|
||||||
|
<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
|
||||||
|
<span v-for="note in notes" :key="note.id" class="item">
|
||||||
|
<img class="avatar" :src="note.user.avatarUrl" decoding="async"/>
|
||||||
|
<MkA class="text" :to="notePage(note)">
|
||||||
|
<Mfm class="text" :text="getNoteSummary(note)" :plain="true" :nowrap="true"/>
|
||||||
|
</MkA>
|
||||||
|
<span class="divider"></span>
|
||||||
|
</span>
|
||||||
|
</MarqueeText>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="display === 'oneByOne'">
|
||||||
|
<!-- TODO -->
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import MarqueeText from '@/components/MkMarquee.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
|
import { notePage } from '@/filters/note';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userListId?: string;
|
||||||
|
display?: 'marquee' | 'oneByOne';
|
||||||
|
marqueeDuration?: number;
|
||||||
|
marqueeReverse?: boolean;
|
||||||
|
oneByOneInterval?: number;
|
||||||
|
refreshIntervalSec?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const notes = ref<misskey.entities.Note[]>([]);
|
||||||
|
const fetching = ref(true);
|
||||||
|
let key = $ref(0);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (props.userListId == null) return;
|
||||||
|
os.api('notes/user-list-timeline', {
|
||||||
|
listId: props.userListId,
|
||||||
|
}).then(res => {
|
||||||
|
notes.value = res;
|
||||||
|
fetching.value = false;
|
||||||
|
key++;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.userListId, tick);
|
||||||
|
|
||||||
|
useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-enter-active, .change-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transition: all 1s ease;
|
||||||
|
}
|
||||||
|
.change-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
.change-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.osdsvwzy {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
::v-deep(.item) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--height);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
> .text {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0 3em;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
92
packages/frontend/src/ui/_common_old/statusbars.vue
Normal file
92
packages/frontend/src/ui/_common_old/statusbars.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dlrsnxqu">
|
||||||
|
<div
|
||||||
|
v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" class="item" :class="[{ black: x.black }, {
|
||||||
|
verySmall: x.size === 'verySmall',
|
||||||
|
small: x.size === 'small',
|
||||||
|
medium: x.size === 'medium',
|
||||||
|
large: x.size === 'large',
|
||||||
|
veryLarge: x.size === 'veryLarge',
|
||||||
|
}]"
|
||||||
|
>
|
||||||
|
<span class="name">{{ x.name }}</span>
|
||||||
|
<XRss v-if="x.type === 'rss'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :url="x.props.url" :shuffle="x.props.shuffle"/>
|
||||||
|
<XFederation v-else-if="x.type === 'federation'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :colored="x.props.colored"/>
|
||||||
|
<XUserList v-else-if="x.type === 'userList'" class="body" :refresh-interval-sec="x.props.refreshIntervalSec" :marquee-duration="x.props.marqueeDuration" :marquee-reverse="x.props.marqueeReverse" :display="x.props.display" :user-list-id="x.props.userListId"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
|
||||||
|
const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
|
||||||
|
const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dlrsnxqu {
|
||||||
|
font-size: 15px;
|
||||||
|
background: var(--panel);
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
--height: 24px;
|
||||||
|
--nameMargin: 10px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
|
||||||
|
&.verySmall {
|
||||||
|
--nameMargin: 7px;
|
||||||
|
--height: 16px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
--nameMargin: 8px;
|
||||||
|
--height: 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
--nameMargin: 12px;
|
||||||
|
--height: 26px;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.veryLarge {
|
||||||
|
--nameMargin: 14px;
|
||||||
|
--height: 30px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 100%;
|
||||||
|
line-height: var(--height);
|
||||||
|
height: var(--height);
|
||||||
|
overflow: clip;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
|
> .name {
|
||||||
|
padding: 0 var(--nameMargin);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.black {
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected">
|
<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected">
|
||||||
<div>{{ $ts.disconnectedFromServer }}</div>
|
<div>{{ i18n.ts.disconnectedFromServer }}</div>
|
||||||
<div class="command">
|
<div class="command">
|
||||||
<button class="_textButton" @click="reload">{{ $ts.reload }}</button>
|
<button class="_textButton" @click="reload">{{ i18n.ts.reload }}</button>
|
||||||
<button class="_textButton">{{ $ts.doNothing }}</button>
|
<button class="_textButton">{{ i18n.ts.doNothing }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -11,6 +11,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
let hasDisconnected = $ref(false);
|
let hasDisconnected = $ref(false);
|
||||||
|
|
||||||
@ -37,8 +38,8 @@ onUnmounted(() => {
|
|||||||
.nsbbhtug {
|
.nsbbhtug {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 16385;
|
z-index: 16385;
|
||||||
bottom: 8px;
|
bottom: calc(var(--minBottomSpacing) + var(--margin));
|
||||||
right: 8px;
|
right: var(--margin);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
import { post } from '@/os';
|
import { post } from '@/os';
|
||||||
import { $i, login } from '@/account';
|
import { $i, login } from '@/account';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
|
|
||||||
export function swInject() {
|
export function swInject() {
|
||||||
navigator.serviceWorker.addEventListener('message', ev => {
|
navigator.serviceWorker.addEventListener('message', ev => {
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.log('sw msg', ev.data);
|
console.log('sw msg', ev.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.data.type !== 'order') return;
|
if (ev.data.type !== 'order') return;
|
||||||
|
|
||||||
if (ev.data.loginId !== $i?.id) {
|
if (ev.data.loginId !== $i?.id) {
|
||||||
return getAccountFromId(ev.data.loginId).then(account => {
|
return getAccountFromId(ev.data.loginId).then(account => {
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
return login(account.token, ev.data.url);
|
return login(account.token, ev.data.url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ev.data.order) {
|
switch (ev.data.order) {
|
||||||
case 'post':
|
case 'post':
|
||||||
return post(ev.data.options);
|
return post(ev.data.options);
|
||||||
case 'push':
|
case 'push':
|
||||||
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||||
return window.scroll({ top: 0, behavior: 'smooth' });
|
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
return mainRouter.push(ev.data.url);
|
return mainRouter.push(ev.data.url);
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,128 +1,129 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-uploader _acrylic" :style="{ zIndex }">
|
<div class="mk-uploader _acrylic" :style="{ zIndex }">
|
||||||
<ol v-if="uploads.length > 0">
|
<ol v-if="uploads.length > 0">
|
||||||
<li v-for="ctx in uploads" :key="ctx.id">
|
<li v-for="ctx in uploads" :key="ctx.id">
|
||||||
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<p class="name"><i class="fas fa-spinner fa-pulse"></i>{{ ctx.name }}</p>
|
<p class="name"><MkLoading :em="true"/>{{ ctx.name }}</p>
|
||||||
<p class="status">
|
<p class="status">
|
||||||
<span v-if="ctx.progressValue === undefined" class="initing">{{ $ts.waiting }}<MkEllipsis/></span>
|
<span v-if="ctx.progressValue === undefined" class="initing">{{ i18n.ts.waiting }}<MkEllipsis/></span>
|
||||||
<span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
<span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
||||||
<span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
<span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
|
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { uploads } from '@/scripts/upload';
|
import { uploads } from '@/scripts/upload';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
const zIndex = os.claimZIndex('high');
|
|
||||||
</script>
|
const zIndex = os.claimZIndex('high');
|
||||||
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mk-uploader {
|
<style lang="scss" scoped>
|
||||||
position: fixed;
|
.mk-uploader {
|
||||||
right: 16px;
|
position: fixed;
|
||||||
width: 260px;
|
right: 16px;
|
||||||
top: 32px;
|
width: 260px;
|
||||||
padding: 16px 20px;
|
top: 32px;
|
||||||
pointer-events: none;
|
padding: 16px 20px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
pointer-events: none;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
}
|
border-radius: 8px;
|
||||||
.mk-uploader:empty {
|
}
|
||||||
display: none;
|
.mk-uploader:empty {
|
||||||
}
|
display: none;
|
||||||
.mk-uploader > ol {
|
}
|
||||||
display: block;
|
.mk-uploader > ol {
|
||||||
margin: 0;
|
display: block;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
list-style: none;
|
padding: 0;
|
||||||
}
|
list-style: none;
|
||||||
.mk-uploader > ol > li {
|
}
|
||||||
display: grid;
|
.mk-uploader > ol > li {
|
||||||
margin: 8px 0 0 0;
|
display: grid;
|
||||||
padding: 0;
|
margin: 8px 0 0 0;
|
||||||
height: 36px;
|
padding: 0;
|
||||||
width: 100%;
|
height: 36px;
|
||||||
border-top: solid 8px transparent;
|
width: 100%;
|
||||||
grid-template-columns: 36px calc(100% - 44px);
|
border-top: solid 8px transparent;
|
||||||
grid-template-rows: 1fr 8px;
|
grid-template-columns: 36px calc(100% - 44px);
|
||||||
column-gap: 8px;
|
grid-template-rows: 1fr 8px;
|
||||||
box-sizing: content-box;
|
column-gap: 8px;
|
||||||
}
|
box-sizing: content-box;
|
||||||
.mk-uploader > ol > li:first-child {
|
}
|
||||||
margin: 0;
|
.mk-uploader > ol > li:first-child {
|
||||||
box-shadow: none;
|
margin: 0;
|
||||||
border-top: none;
|
box-shadow: none;
|
||||||
}
|
border-top: none;
|
||||||
.mk-uploader > ol > li > .img {
|
}
|
||||||
display: block;
|
.mk-uploader > ol > li > .img {
|
||||||
background-size: cover;
|
display: block;
|
||||||
background-position: center center;
|
background-size: cover;
|
||||||
grid-column: 1/2;
|
background-position: center center;
|
||||||
grid-row: 1/3;
|
grid-column: 1/2;
|
||||||
}
|
grid-row: 1/3;
|
||||||
.mk-uploader > ol > li > .top {
|
}
|
||||||
display: flex;
|
.mk-uploader > ol > li > .top {
|
||||||
grid-column: 2/3;
|
display: flex;
|
||||||
grid-row: 1/2;
|
grid-column: 2/3;
|
||||||
}
|
grid-row: 1/2;
|
||||||
.mk-uploader > ol > li > .top > .name {
|
}
|
||||||
display: block;
|
.mk-uploader > ol > li > .top > .name {
|
||||||
padding: 0 8px 0 0;
|
display: block;
|
||||||
margin: 0;
|
padding: 0 8px 0 0;
|
||||||
font-size: 0.8em;
|
margin: 0;
|
||||||
white-space: nowrap;
|
font-size: 0.8em;
|
||||||
text-overflow: ellipsis;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
text-overflow: ellipsis;
|
||||||
flex-shrink: 1;
|
overflow: hidden;
|
||||||
}
|
flex-shrink: 1;
|
||||||
.mk-uploader > ol > li > .top > .name > i {
|
}
|
||||||
margin-right: 4px;
|
.mk-uploader > ol > li > .top > .name > i {
|
||||||
}
|
margin-right: 4px;
|
||||||
.mk-uploader > ol > li > .top > .status {
|
}
|
||||||
display: block;
|
.mk-uploader > ol > li > .top > .status {
|
||||||
margin: 0 0 0 auto;
|
display: block;
|
||||||
padding: 0;
|
margin: 0 0 0 auto;
|
||||||
font-size: 0.8em;
|
padding: 0;
|
||||||
flex-shrink: 0;
|
font-size: 0.8em;
|
||||||
}
|
flex-shrink: 0;
|
||||||
.mk-uploader > ol > li > .top > .status > .initing {
|
}
|
||||||
}
|
.mk-uploader > ol > li > .top > .status > .initing {
|
||||||
.mk-uploader > ol > li > .top > .status > .kb {
|
}
|
||||||
}
|
.mk-uploader > ol > li > .top > .status > .kb {
|
||||||
.mk-uploader > ol > li > .top > .status > .percentage {
|
}
|
||||||
display: inline-block;
|
.mk-uploader > ol > li > .top > .status > .percentage {
|
||||||
width: 48px;
|
display: inline-block;
|
||||||
text-align: right;
|
width: 48px;
|
||||||
}
|
text-align: right;
|
||||||
.mk-uploader > ol > li > .top > .status > .percentage:after {
|
}
|
||||||
content: '%';
|
.mk-uploader > ol > li > .top > .status > .percentage:after {
|
||||||
}
|
content: '%';
|
||||||
.mk-uploader > ol > li > progress {
|
}
|
||||||
display: block;
|
.mk-uploader > ol > li > progress {
|
||||||
background: transparent;
|
display: block;
|
||||||
border: none;
|
background: transparent;
|
||||||
border-radius: 4px;
|
border: none;
|
||||||
overflow: hidden;
|
border-radius: 4px;
|
||||||
grid-column: 2/3;
|
overflow: hidden;
|
||||||
grid-row: 2/3;
|
grid-column: 2/3;
|
||||||
z-index: 2;
|
grid-row: 2/3;
|
||||||
width: 100%;
|
z-index: 2;
|
||||||
height: 8px;
|
width: 100%;
|
||||||
}
|
height: 8px;
|
||||||
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
}
|
||||||
background: var(--accent);
|
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
||||||
}
|
background: var(--accent);
|
||||||
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
}
|
||||||
//background: var(--accentAlpha01);
|
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
||||||
background: transparent;
|
//background: var(--accentAlpha01);
|
||||||
}
|
background: transparent;
|
||||||
</style>
|
}
|
||||||
|
</style>
|
||||||
|
131
packages/frontend/src/ui/_common_old2/common.vue
Normal file
131
packages/frontend/src/ui/_common_old2/common.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="popup.component"
|
||||||
|
v-for="popup in popups"
|
||||||
|
:key="popup.id"
|
||||||
|
v-bind="popup.props"
|
||||||
|
v-on="popup.events"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<XUpload v-if="uploads.length > 0"/>
|
||||||
|
|
||||||
|
<XStreamIndicator/>
|
||||||
|
|
||||||
|
<div v-if="pendingApiRequestsCount > 0" id="wait"></div>
|
||||||
|
|
||||||
|
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
|
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
||||||
|
import { uploads } from '@/scripts/upload';
|
||||||
|
import * as sound from '@/scripts/sound';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { swInject } from './sw-inject';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XStreamIndicator: defineAsyncComponent(() => import('./stream-indicator.vue')),
|
||||||
|
XUpload: defineAsyncComponent(() => import('./upload.vue')),
|
||||||
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
let notifications = $ref<misskey.entities.Notification[]>([]);
|
||||||
|
|
||||||
|
const onNotification = notification => {
|
||||||
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
|
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
stream.send('readNotification', {
|
||||||
|
id: notification.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
notifications.unshift(notification);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (notifications.length > 3) notifications.pop();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
notifications = notifications.filter(x => x.id !== notification.id);
|
||||||
|
}, 6000);
|
||||||
|
}
|
||||||
|
|
||||||
|
sound.play('notification');
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($i) {
|
||||||
|
const connection = stream.useChannel('main', null, 'UI');
|
||||||
|
connection.on('notification', onNotification);
|
||||||
|
|
||||||
|
//#region Listen message from SW
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
swInject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploads,
|
||||||
|
popups,
|
||||||
|
pendingApiRequestsCount,
|
||||||
|
dev: _DEV_,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@keyframes dev-ticker-blink {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#wait {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 4000000;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: progress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#devTicker {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2147483647;
|
||||||
|
color: #ff0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
animation: dev-ticker-blink 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
60
packages/frontend/src/ui/_common_old2/stream-indicator.vue
Normal file
60
packages/frontend/src/ui/_common_old2/stream-indicator.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected">
|
||||||
|
<div>{{ $ts.disconnectedFromServer }}</div>
|
||||||
|
<div class="command">
|
||||||
|
<button class="_textButton" @click="reload">{{ $ts.reload }}</button>
|
||||||
|
<button class="_textButton">{{ $ts.doNothing }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onUnmounted } from 'vue';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
|
||||||
|
let hasDisconnected = $ref(false);
|
||||||
|
|
||||||
|
function onDisconnected() {
|
||||||
|
hasDisconnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDisconnected() {
|
||||||
|
hasDisconnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('_disconnected_', onDisconnected);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stream.off('_disconnected_', onDisconnected);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nsbbhtug {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 16385;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 320px;
|
||||||
|
|
||||||
|
> .command {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
35
packages/frontend/src/ui/_common_old2/sw-inject.ts
Normal file
35
packages/frontend/src/ui/_common_old2/sw-inject.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { inject } from 'vue';
|
||||||
|
import { post } from '@/os';
|
||||||
|
import { $i, login } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
|
||||||
|
export function swInject() {
|
||||||
|
navigator.serviceWorker.addEventListener('message', ev => {
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log('sw msg', ev.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.data.type !== 'order') return;
|
||||||
|
|
||||||
|
if (ev.data.loginId !== $i?.id) {
|
||||||
|
return getAccountFromId(ev.data.loginId).then(account => {
|
||||||
|
if (!account) return;
|
||||||
|
return login(account.token, ev.data.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ev.data.order) {
|
||||||
|
case 'post':
|
||||||
|
return post(ev.data.options);
|
||||||
|
case 'push':
|
||||||
|
if (mainRouter.currentRoute.value.path === ev.data.url) {
|
||||||
|
return window.scroll({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
return mainRouter.push(ev.data.url);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
128
packages/frontend/src/ui/_common_old2/upload.vue
Normal file
128
packages/frontend/src/ui/_common_old2/upload.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mk-uploader _acrylic" :style="{ zIndex }">
|
||||||
|
<ol v-if="uploads.length > 0">
|
||||||
|
<li v-for="ctx in uploads" :key="ctx.id">
|
||||||
|
<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
|
||||||
|
<div class="top">
|
||||||
|
<p class="name"><i class="fas fa-spinner fa-pulse"></i>{{ ctx.name }}</p>
|
||||||
|
<p class="status">
|
||||||
|
<span v-if="ctx.progressValue === undefined" class="initing">{{ $ts.waiting }}<MkEllipsis/></span>
|
||||||
|
<span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
||||||
|
<span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === undefined, waiting: ctx.progressValue !== undefined && ctx.progressValue === ctx.progressMax }"></progress>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { uploads } from '@/scripts/upload';
|
||||||
|
|
||||||
|
const zIndex = os.claimZIndex('high');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mk-uploader {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
width: 260px;
|
||||||
|
top: 32px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.mk-uploader:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li {
|
||||||
|
display: grid;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
border-top: solid 8px transparent;
|
||||||
|
grid-template-columns: 36px calc(100% - 44px);
|
||||||
|
grid-template-rows: 1fr 8px;
|
||||||
|
column-gap: 8px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li:first-child {
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .img {
|
||||||
|
display: block;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
grid-column: 1/2;
|
||||||
|
grid-row: 1/3;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 2/3;
|
||||||
|
grid-row: 1/2;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .name {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px 0 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .name > i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .initing {
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .kb {
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .percentage {
|
||||||
|
display: inline-block;
|
||||||
|
width: 48px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > .top > .status > .percentage:after {
|
||||||
|
content: '%';
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress {
|
||||||
|
display: block;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
grid-column: 2/3;
|
||||||
|
grid-row: 2/3;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
||||||
|
//background: var(--accentAlpha01);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,49 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': '12' + 'px' }"
|
<div :class="[$style.root, { [$style.rootIsMobile]: isMobile }]" :style="{ '--deckMargin': '12' + 'px' }">
|
||||||
@contextmenu.self.prevent="onContextmenu"
|
|
||||||
>
|
|
||||||
<XSidebar v-if="!isMobile"/>
|
<XSidebar v-if="!isMobile"/>
|
||||||
|
|
||||||
<template v-for="ids in layout">
|
<div :class="$style.main">
|
||||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
<XStatusBars/>
|
||||||
<section
|
<div ref="columnsEl" :class="[$style.columns, deckStore.reactiveState.columnAlign.value]" @contextmenu.self.prevent="onContextmenu">
|
||||||
v-if="ids.length > 1"
|
<template v-for="ids in layout">
|
||||||
class="folder column"
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
<section
|
||||||
>
|
v-if="ids.length > 1"
|
||||||
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
:class="$style.folder"
|
||||||
</section>
|
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
||||||
<DeckColumnCore
|
>
|
||||||
v-else
|
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
||||||
:ref="ids[0]"
|
</section>
|
||||||
:key="ids[0]"
|
<DeckColumnCore
|
||||||
class="column"
|
v-else
|
||||||
:column="columns.find(c => c.id === ids[0])"
|
:ref="ids[0]"
|
||||||
:is-stacked="false"
|
:key="ids[0]"
|
||||||
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
:class="$style.column"
|
||||||
@parent-focus="moveFocus(ids[0], $event)"
|
:column="columns.find(c => c.id === ids[0])"
|
||||||
/>
|
:is-stacked="false"
|
||||||
</template>
|
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
||||||
|
@parent-focus="moveFocus(ids[0], $event)"
|
||||||
<div v-if="isMobile" class="buttons">
|
/>
|
||||||
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
</template>
|
||||||
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
|
<div v-if="layout.length === 0" class="_panel" :class="$style.onboarding">
|
||||||
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
<div>{{ i18n.ts._deck.introduction }}</div>
|
||||||
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
<MkButton primary style="margin: 1em auto;" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton>
|
||||||
|
<div>{{ i18n.ts._deck.introduction2 }}</div>
|
||||||
|
</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.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.sideMenuMiddle">
|
||||||
|
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.sideMenuBottom">
|
||||||
|
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition :name="$store.state.animation ? 'menu-back' : ''">
|
<div v-if="isMobile" :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"><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"><i class="_indicatorCircle"></i></span></button>
|
||||||
|
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
:enter-active-class="$store.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
|
||||||
|
:leave-active-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
|
||||||
|
:enter-from-class="$store.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
|
||||||
|
:leave-to-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawerMenuShowing"
|
v-if="drawerMenuShowing"
|
||||||
class="menu-back _modalBg"
|
:class="$style.menuBg"
|
||||||
|
class="_modalBg"
|
||||||
@click="drawerMenuShowing = false"
|
@click="drawerMenuShowing = false"
|
||||||
@touchstart.passive="drawerMenuShowing = false"
|
@touchstart.passive="drawerMenuShowing = false"
|
||||||
></div>
|
></div>
|
||||||
</transition>
|
</Transition>
|
||||||
|
|
||||||
<transition :name="$store.state.animation ? 'menu' : ''">
|
<Transition
|
||||||
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
|
:enter-active-class="$store.state.animation ? $style.transition_menuDrawer_enterActive : ''"
|
||||||
</transition>
|
:leave-active-class="$store.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
|
||||||
|
:enter-from-class="$store.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
|
||||||
|
:leave-to-class="$store.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
|
||||||
|
>
|
||||||
|
<div v-if="drawerMenuShowing" :class="$style.menu">
|
||||||
|
<XDrawerMenu/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<XCommon/>
|
<XCommon/>
|
||||||
</div>
|
</div>
|
||||||
@ -52,12 +85,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import XCommon from './_common_old/common.vue';
|
import XCommon from './_common_/common.vue';
|
||||||
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deckold/deck-store';
|
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store';
|
||||||
import DeckColumnCore from '@/ui/deckold/column-core.vue';
|
import DeckColumnCore from '@/ui/deckold/column-core.vue';
|
||||||
import XSidebar from '@/ui/_common_old/sidebar.vue';
|
import XSidebar from '@/ui/_common_old/navbar.vue';
|
||||||
import XDrawerMenu from '@/ui/_common_old/sidebar-for-mobile.vue';
|
import XDrawerMenu from '@/ui/_common_old/navbar-for-mobile.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { getScrollContainer } from '@/scripts/scroll';
|
import { getScrollContainer } from '@/scripts/scroll';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { navbarItemDef } from '@/navbar';
|
import { navbarItemDef } from '@/navbar';
|
||||||
@ -65,8 +98,10 @@ import { $i } from '@/account';
|
|||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { mainRouter } from '@/router';
|
import { mainRouter } from '@/router';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||||
|
|
||||||
mainRouter.navHook = (path): boolean => {
|
mainRouter.navHook = (path, flag): boolean => {
|
||||||
|
if (flag === 'forcePage') return false;
|
||||||
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
|
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
|
||||||
if (deckStore.state.navWindow || noMainColumn) {
|
if (deckStore.state.navWindow || noMainColumn) {
|
||||||
os.pageWindow(path);
|
os.pageWindow(path);
|
||||||
@ -97,6 +132,12 @@ const menuIndicated = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showSettings() {
|
||||||
|
os.pageWindow('/settings/deck');
|
||||||
|
}
|
||||||
|
|
||||||
|
let columnsEl = $shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const addColumn = async (ev) => {
|
const addColumn = async (ev) => {
|
||||||
const columns = [
|
const columns = [
|
||||||
'main',
|
'main',
|
||||||
@ -112,8 +153,8 @@ const addColumn = async (ev) => {
|
|||||||
const { canceled, result: column } = await os.select({
|
const { canceled, result: column } = await os.select({
|
||||||
title: i18n.ts._deck.addColumn,
|
title: i18n.ts._deck.addColumn,
|
||||||
items: columns.map(column => ({
|
items: columns.map(column => ({
|
||||||
value: column, text: i18n.t('_deck._columns.' + column)
|
value: column, text: i18n.t('_deck._columns.' + column),
|
||||||
}))
|
})),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
@ -132,18 +173,13 @@ const onContextmenu = (ev) => {
|
|||||||
}], ev);
|
}], ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
provide('shouldSpacerMin', true);
|
|
||||||
if (deckStore.state.navWindow) {
|
|
||||||
provide('navHook', (url) => {
|
|
||||||
os.pageWindow(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.documentElement.style.overflowY = 'hidden';
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
document.documentElement.style.scrollBehavior = 'auto';
|
document.documentElement.style.scrollBehavior = 'auto';
|
||||||
window.addEventListener('wheel', (ev) => {
|
window.addEventListener('wheel', (ev) => {
|
||||||
if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
if (ev.target === columnsEl && ev.deltaX === 0) {
|
||||||
document.documentElement.scrollLeft += ev.deltaY;
|
columnsEl.scrollLeft += ev.deltaY;
|
||||||
|
} else if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
||||||
|
columnsEl.scrollLeft += ev.deltaY;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadDeck();
|
loadDeck();
|
||||||
@ -151,43 +187,106 @@ loadDeck();
|
|||||||
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
||||||
// TODO??
|
// TODO??
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeProfile(ev: MouseEvent) {
|
||||||
|
const items = ref([{
|
||||||
|
text: deckStore.state.profile,
|
||||||
|
active: true.valueOf,
|
||||||
|
}]);
|
||||||
|
getProfiles().then(profiles => {
|
||||||
|
items.value = [{
|
||||||
|
text: deckStore.state.profile,
|
||||||
|
active: true.valueOf,
|
||||||
|
}, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
|
||||||
|
text: k,
|
||||||
|
action: () => {
|
||||||
|
deckStore.set('profile', k);
|
||||||
|
unisonReload();
|
||||||
|
},
|
||||||
|
}))), null, {
|
||||||
|
text: i18n.ts._deck.newProfile,
|
||||||
|
icon: 'ti ti-plus',
|
||||||
|
action: async () => {
|
||||||
|
const { canceled, result: name } = await os.inputText({
|
||||||
|
title: i18n.ts._deck.profile,
|
||||||
|
allowEmpty: false,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
deckStore.set('profile', name);
|
||||||
|
unisonReload();
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProfile() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
deleteProfile_(deckStore.state.profile);
|
||||||
|
deckStore.set('profile', 'default');
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.menu-enter-active,
|
.transition_menuDrawerBg_enterActive,
|
||||||
.menu-leave-active {
|
.transition_menuDrawerBg_leaveActive {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.transition_menuDrawerBg_enterFrom,
|
||||||
|
.transition_menuDrawerBg_leaveTo {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition_menuDrawer_enterActive,
|
||||||
|
.transition_menuDrawer_leaveActive {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
.menu-enter-from,
|
.transition_menuDrawer_enterFrom,
|
||||||
.menu-leave-active {
|
.transition_menuDrawer_leaveTo {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-240px);
|
transform: translateX(-240px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-back-enter-active,
|
.root {
|
||||||
.menu-back-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
.menu-back-enter-from,
|
|
||||||
.menu-back-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mk-deck {
|
|
||||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||||
|
|
||||||
// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
|
|
||||||
--margin: var(--marginHalf);
|
--margin: var(--marginHalf);
|
||||||
|
|
||||||
|
// --deckDividerThickness: 5px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
height: 100dvh;
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: var(--deckMargin);
|
padding: var(--deckMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rootIsMobile {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
// overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
&.center {
|
&.center {
|
||||||
> .column:first-of-type {
|
> .column:first-of-type {
|
||||||
@ -198,108 +297,148 @@ function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.isMobile {
|
.column {
|
||||||
padding-bottom: 100px;
|
flex-shrink: 0;
|
||||||
}
|
margin-right: 12px;
|
||||||
|
|
||||||
> .column {
|
&.folder {
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: var(--deckMargin);
|
|
||||||
|
|
||||||
&.folder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> *:not(:last-child) {
|
|
||||||
margin-bottom: var(--deckMargin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .buttons {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> .button {
|
>*:not(:last-child) {
|
||||||
position: relative;
|
margin-bottom: 12px;
|
||||||
flex: 1;
|
|
||||||
padding: 0;
|
|
||||||
margin: auto;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--fg);
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--X2);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
color: var(--indicator);
|
|
||||||
font-size: 16px;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
> .menu-back {
|
|
||||||
z-index: 1001;
|
.folder {
|
||||||
}
|
composes: column;
|
||||||
|
display: flex;
|
||||||
> .menu {
|
flex-direction: column;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
> *:not(:last-of-type) {
|
||||||
left: 0;
|
border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
|
||||||
z-index: 1001;
|
}
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
}
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
width: 240px;
|
.onboarding {
|
||||||
box-sizing: border-box;
|
padding: 32px;
|
||||||
overflow: auto;
|
height: min-content;
|
||||||
overscroll-behavior: contain;
|
text-align: center;
|
||||||
background: var(--bg);
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sideMenu {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideMenuButton {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideMenuTop {
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideMenuMiddle {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideMenuBottom {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuBg {
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 240px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
contain: strict;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: var(--navBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
grid-gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(32px));
|
||||||
|
backdrop-filter: var(--blur, blur(32px));
|
||||||
|
background-color: var(--header);
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 60px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--panelHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--X2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.postButton {
|
||||||
|
composes: navButton;
|
||||||
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: linear-gradient(90deg, var(--X8), var(--X8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButtonIcon {
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButtonIndicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: var(--indicator);
|
||||||
|
font-size: 16px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
|
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
|
||||||
@ -11,9 +11,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
import XTimeline from '@/components/MkTimeline.vue';
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { updateColumn, Column } from './deck-store';
|
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -26,7 +26,7 @@ const emit = defineEmits<{
|
|||||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
let timeline = $shallowRef<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.column.antennaId == null) {
|
if (props.column.antennaId == null) {
|
||||||
@ -39,15 +39,22 @@ async function setAntenna() {
|
|||||||
const { canceled, result: antenna } = await os.select({
|
const { canceled, result: antenna } = await os.select({
|
||||||
title: i18n.ts.selectAntenna,
|
title: i18n.ts.selectAntenna,
|
||||||
items: antennas.map(x => ({
|
items: antennas.map(x => ({
|
||||||
value: x, text: x.name
|
value: x, text: x.name,
|
||||||
})),
|
})),
|
||||||
default: props.column.antennaId
|
default: props.column.antennaId,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
updateColumn(props.column.id, {
|
updateColumn(props.column.id, {
|
||||||
antennaId: antenna.id
|
antennaId: antenna.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menu = [{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.selectAntenna,
|
||||||
|
action: setAntenna,
|
||||||
|
}];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
function focus() {
|
function focus() {
|
||||||
timeline.focus();
|
timeline.focus();
|
||||||
@ -58,6 +65,3 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
</style>
|
|
||||||
|
@ -31,14 +31,4 @@ defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/*
|
|
||||||
export default defineComponent({
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
this.$children[0].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,62 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
<section v-hotkey="keymap" class="dnpfarvg _panel blur _narrow_"
|
<section
|
||||||
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
|
v-hotkey="keymap"
|
||||||
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
|
:class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.isStacked]: isStacked, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]"
|
||||||
|
class="blur _panel"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
@dragleave="onDragleave"
|
@dragleave="onDragleave"
|
||||||
@drop.prevent.stop="onDrop"
|
@drop.prevent.stop="onDrop"
|
||||||
>
|
>
|
||||||
<header :class="{ indicated }"
|
<header
|
||||||
|
:class="[$style.header]"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@click="goTop"
|
@click="goTop"
|
||||||
@dragstart="onDragstart"
|
@dragstart="onDragstart"
|
||||||
@dragend="onDragend"
|
@dragend="onDragend"
|
||||||
@contextmenu.prevent.stop="onContextmenu"
|
@contextmenu.prevent.stop="onContextmenu"
|
||||||
>
|
>
|
||||||
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
|
<button v-if="isStacked && !isMainColumn" :class="$style.toggleActive" class="_button" @click="toggleActive">
|
||||||
<template v-if="active"><i class="fas fa-angle-up"></i></template>
|
<template v-if="active"><i class="ti ti-chevron-up"></i></template>
|
||||||
<template v-else><i class="fas fa-angle-down"></i></template>
|
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||||
</button>
|
</button>
|
||||||
<div class="action">
|
<span :class="$style.title"><slot name="header"></slot></span>
|
||||||
<slot name="action"></slot>
|
<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
|
||||||
</div>
|
|
||||||
<span class="header"><slot name="header"></slot></span>
|
|
||||||
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-ellipsis"></i></button>
|
|
||||||
</header>
|
</header>
|
||||||
<div v-show="active" ref="body">
|
<div v-show="active" ref="body" :class="$style.body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export type DeckFunc = {
|
|
||||||
title: string;
|
|
||||||
handler: (payload: MouseEvent) => void;
|
|
||||||
icon?: string;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, provide, Ref, watch } from 'vue';
|
||||||
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
|
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column, deckStore } from './deck-store';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import { MenuItem } from '@/types/menu';
|
||||||
|
|
||||||
provide('shouldHeaderThin', true);
|
provide('shouldHeaderThin', true);
|
||||||
provide('shouldOmitHeaderTitle', true);
|
provide('shouldOmitHeaderTitle', true);
|
||||||
|
provide('forceSpacerMin', true);
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
isStacked?: boolean;
|
isStacked?: boolean;
|
||||||
func?: DeckFunc | null;
|
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
indicated?: boolean;
|
menu?: MenuItem[];
|
||||||
}>(), {
|
}>(), {
|
||||||
isStacked: false,
|
isStacked: false,
|
||||||
func: null,
|
|
||||||
naked: false,
|
naked: false,
|
||||||
indicated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -64,7 +55,7 @@ const emit = defineEmits<{
|
|||||||
(ev: 'change-active-state', v: boolean): void;
|
(ev: 'change-active-state', v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let body = $ref<HTMLDivElement>();
|
let body = $shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
let dragging = $ref(false);
|
let dragging = $ref(false);
|
||||||
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
|
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
|
||||||
@ -109,9 +100,9 @@ function toggleActive() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
const items = [{
|
let items = [{
|
||||||
icon: 'fas fa-pencil-alt',
|
icon: 'ti ti-settings',
|
||||||
text: i18n.ts.edit,
|
text: i18n.ts._deck.configureColumn,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const { canceled, result } = await os.form(props.column.name, {
|
const { canceled, result } = await os.form(props.column.name, {
|
||||||
name: {
|
name: {
|
||||||
@ -133,44 +124,49 @@ function getMenu() {
|
|||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
updateColumn(props.column.id, result);
|
updateColumn(props.column.id, result);
|
||||||
},
|
},
|
||||||
}, null, {
|
|
||||||
icon: 'fas fa-arrow-left',
|
|
||||||
text: i18n.ts._deck.swapLeft,
|
|
||||||
action: () => {
|
|
||||||
swapLeftColumn(props.column.id);
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-arrow-right',
|
type: 'parent',
|
||||||
text: i18n.ts._deck.swapRight,
|
text: i18n.ts.move + '...',
|
||||||
action: () => {
|
icon: 'ti ti-arrows-move',
|
||||||
swapRightColumn(props.column.id);
|
children: [{
|
||||||
},
|
icon: 'ti ti-arrow-left',
|
||||||
}, props.isStacked ? {
|
text: i18n.ts._deck.swapLeft,
|
||||||
icon: 'fas fa-arrow-up',
|
action: () => {
|
||||||
text: i18n.ts._deck.swapUp,
|
swapLeftColumn(props.column.id);
|
||||||
action: () => {
|
},
|
||||||
swapUpColumn(props.column.id);
|
}, {
|
||||||
},
|
icon: 'ti ti-arrow-right',
|
||||||
} : undefined, props.isStacked ? {
|
text: i18n.ts._deck.swapRight,
|
||||||
icon: 'fas fa-arrow-down',
|
action: () => {
|
||||||
text: i18n.ts._deck.swapDown,
|
swapRightColumn(props.column.id);
|
||||||
action: () => {
|
},
|
||||||
swapDownColumn(props.column.id);
|
}, props.isStacked ? {
|
||||||
},
|
icon: 'ti ti-arrow-up',
|
||||||
} : undefined, null, {
|
text: i18n.ts._deck.swapUp,
|
||||||
icon: 'fas fa-window-restore',
|
action: () => {
|
||||||
|
swapUpColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined, props.isStacked ? {
|
||||||
|
icon: 'ti ti-arrow-down',
|
||||||
|
text: i18n.ts._deck.swapDown,
|
||||||
|
action: () => {
|
||||||
|
swapDownColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined],
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-stack-2',
|
||||||
text: i18n.ts._deck.stackLeft,
|
text: i18n.ts._deck.stackLeft,
|
||||||
action: () => {
|
action: () => {
|
||||||
stackLeftColumn(props.column.id);
|
stackLeftColumn(props.column.id);
|
||||||
},
|
},
|
||||||
}, props.isStacked ? {
|
}, props.isStacked ? {
|
||||||
icon: 'fas fa-window-maximize',
|
icon: 'ti ti-window-maximize',
|
||||||
text: i18n.ts._deck.popRight,
|
text: i18n.ts._deck.popRight,
|
||||||
action: () => {
|
action: () => {
|
||||||
popRightColumn(props.column.id);
|
popRightColumn(props.column.id);
|
||||||
},
|
},
|
||||||
} : undefined, null, {
|
} : undefined, null, {
|
||||||
icon: 'fas fa-trash-alt',
|
icon: 'ti ti-trash',
|
||||||
text: i18n.ts.remove,
|
text: i18n.ts.remove,
|
||||||
danger: true,
|
danger: true,
|
||||||
action: () => {
|
action: () => {
|
||||||
@ -178,13 +174,9 @@ function getMenu() {
|
|||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
if (props.func) {
|
if (props.menu) {
|
||||||
items.unshift(null);
|
items.unshift(null);
|
||||||
items.unshift({
|
items = props.menu.concat(items);
|
||||||
icon: props.func.icon,
|
|
||||||
text: props.func.title,
|
|
||||||
action: props.func.handler,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@ -249,19 +241,16 @@ function onDrop(ev) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.dnpfarvg {
|
.root {
|
||||||
--root-margin: 10px;
|
--root-margin: 10px;
|
||||||
--deckColumnHeaderHeight: 42px;
|
--deckColumnHeaderHeight: 40px;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
contain: content;
|
contain: strict;
|
||||||
box-shadow: 0 0 8px 0 var(--shadow);
|
|
||||||
|
|
||||||
&.draghover {
|
&.draghover {
|
||||||
box-shadow: 0 0 0 2px var(--focus);
|
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
@ -276,7 +265,18 @@ function onDrop(ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
box-shadow: 0 0 0 2px var(--focus);
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--focus);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dropready {
|
&.dropready {
|
||||||
@ -288,104 +288,85 @@ function onDrop(ev) {
|
|||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
flex-basis: var(--deckColumnHeaderHeight);
|
flex-basis: var(--deckColumnHeaderHeight);
|
||||||
min-height: var(--deckColumnHeaderHeight);
|
min-height: var(--deckColumnHeaderHeight);
|
||||||
|
|
||||||
> header.indicated {
|
|
||||||
box-shadow: 4px 0px var(--accent) inset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.naked {
|
&.naked {
|
||||||
background: var(--acrylicBg) !important;
|
background: var(--acrylicBg) !important;
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||||
|
backdrop-filter: var(--blur, blur(10px));
|
||||||
|
|
||||||
> header {
|
> .header {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
color: var(--fg);
|
||||||
> button {
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.paged {
|
&.paged {
|
||||||
background: var(--bg) !important;
|
background: var(--bg) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
line-height: var(--deckColumnHeaderHeight);
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
height: var(--deckColumnHeaderHeight);
|
height: var(--deckColumnHeaderHeight);
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--panelHeaderFg);
|
color: var(--panelHeaderFg);
|
||||||
background: var(--panelHeaderBg);
|
background: var(--panelHeaderBg);
|
||||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&, * {
|
&, * {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
|
||||||
|
|
||||||
&.indicated {
|
|
||||||
box-shadow: 0 3px 0 0 var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .header {
|
|
||||||
display: inline-block;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:only-of-type {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .toggleActive,
|
|
||||||
> .action > ::v-deep(*),
|
|
||||||
> .menu {
|
|
||||||
z-index: 1;
|
|
||||||
width: var(--deckColumnHeaderHeight);
|
|
||||||
line-height: var(--deckColumnHeaderHeight);
|
|
||||||
color: var(--faceTextButton);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--faceTextButtonHover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: var(--faceTextButtonActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .toggleActive, > .action {
|
|
||||||
margin-left: -16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .action {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .action:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .menu {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: -16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> div {
|
|
||||||
height: calc(100% - var(--deckColumnHeaderHeight));
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden; // Safari does not supports clip
|
|
||||||
overflow-x: clip;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: inline-block;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleActive,
|
||||||
|
.menu {
|
||||||
|
z-index: 1;
|
||||||
|
width: var(--deckColumnHeaderHeight);
|
||||||
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
|
color: var(--faceTextButton);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--faceTextButtonHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--faceTextButtonActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleActive {
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
height: calc(100% - var(--deckColumnHeaderHeight));
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden; // Safari does not supports clip
|
||||||
|
overflow-x: clip;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
container-type: inline-size;
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,6 +4,7 @@ import { notificationTypes } from 'misskey-js';
|
|||||||
import { Storage } from '../../pizzax';
|
import { Storage } from '../../pizzax';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { api } from '@/os';
|
import { api } from '@/os';
|
||||||
|
import { deepClone } from '@/scripts/clone';
|
||||||
|
|
||||||
type ColumnWidget = {
|
type ColumnWidget = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -25,10 +26,6 @@ export type Column = {
|
|||||||
tl?: 'home' | 'local' | 'social' | 'global';
|
tl?: 'home' | 'local' | 'social' | 'global';
|
||||||
};
|
};
|
||||||
|
|
||||||
function copy<T>(x: T): T {
|
|
||||||
return JSON.parse(JSON.stringify(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deckStore = markRaw(new Storage('deck', {
|
export const deckStore = markRaw(new Storage('deck', {
|
||||||
profile: {
|
profile: {
|
||||||
where: 'deviceAccount',
|
where: 'deviceAccount',
|
||||||
@ -54,14 +51,6 @@ export const deckStore = markRaw(new Storage('deck', {
|
|||||||
where: 'deviceAccount',
|
where: 'deviceAccount',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
columnMargin: {
|
|
||||||
where: 'deviceAccount',
|
|
||||||
default: 12,
|
|
||||||
},
|
|
||||||
columnHeaderHeight: {
|
|
||||||
where: 'deviceAccount',
|
|
||||||
default: 42,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const loadDeck = async () => {
|
export const loadDeck = async () => {
|
||||||
@ -80,18 +69,8 @@ export const loadDeck = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deckStore.set('columns', [{
|
deckStore.set('columns', []);
|
||||||
id: 'a',
|
deckStore.set('layout', []);
|
||||||
type: 'main',
|
|
||||||
name: i18n.ts._deck._columns.main,
|
|
||||||
width: 350,
|
|
||||||
}, {
|
|
||||||
id: 'b',
|
|
||||||
type: 'notifications',
|
|
||||||
name: i18n.ts._deck._columns.notifications,
|
|
||||||
width: 330,
|
|
||||||
}]);
|
|
||||||
deckStore.set('layout', [['a'], ['b']]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@ -146,7 +125,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
|||||||
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||||
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
||||||
const layout = copy(deckStore.state.layout);
|
const layout = deepClone(deckStore.state.layout);
|
||||||
layout[aX][aY] = b;
|
layout[aX][aY] = b;
|
||||||
layout[bX][bY] = a;
|
layout[bX][bY] = a;
|
||||||
deckStore.set('layout', layout);
|
deckStore.set('layout', layout);
|
||||||
@ -154,7 +133,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function swapLeftColumn(id: Column['id']) {
|
export function swapLeftColumn(id: Column['id']) {
|
||||||
const layout = copy(deckStore.state.layout);
|
const layout = deepClone(deckStore.state.layout);
|
||||||
deckStore.state.layout.some((ids, i) => {
|
deckStore.state.layout.some((ids, i) => {
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
const left = deckStore.state.layout[i - 1];
|
const left = deckStore.state.layout[i - 1];
|
||||||
@ -170,7 +149,7 @@ export function swapLeftColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function swapRightColumn(id: Column['id']) {
|
export function swapRightColumn(id: Column['id']) {
|
||||||
const layout = copy(deckStore.state.layout);
|
const layout = deepClone(deckStore.state.layout);
|
||||||
deckStore.state.layout.some((ids, i) => {
|
deckStore.state.layout.some((ids, i) => {
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
const right = deckStore.state.layout[i + 1];
|
const right = deckStore.state.layout[i + 1];
|
||||||
@ -186,9 +165,9 @@ export function swapRightColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function swapUpColumn(id: Column['id']) {
|
export function swapUpColumn(id: Column['id']) {
|
||||||
const layout = copy(deckStore.state.layout);
|
const layout = deepClone(deckStore.state.layout);
|
||||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||||
ids.some((x, i) => {
|
ids.some((x, i) => {
|
||||||
if (x === id) {
|
if (x === id) {
|
||||||
const up = ids[i - 1];
|
const up = ids[i - 1];
|
||||||
@ -206,9 +185,9 @@ export function swapUpColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function swapDownColumn(id: Column['id']) {
|
export function swapDownColumn(id: Column['id']) {
|
||||||
const layout = copy(deckStore.state.layout);
|
const layout = deepClone(deckStore.state.layout);
|
||||||
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
const ids = copy(deckStore.state.layout[idsIndex]);
|
const ids = deepClone(deckStore.state.layout[idsIndex]);
|
||||||
ids.some((x, i) => {
|
ids.some((x, i) => {
|
||||||
if (x === id) {
|
if (x === id) {
|
||||||
const down = ids[i + 1];
|
const down = ids[i + 1];
|
||||||
@ -226,7 +205,7 @@ export function swapDownColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stackLeftColumn(id: Column['id']) {
|
export function stackLeftColumn(id: Column['id']) {
|
||||||
let layout = copy(deckStore.state.layout);
|
let layout = deepClone(deckStore.state.layout);
|
||||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||||
layout[i - 1].push(id);
|
layout[i - 1].push(id);
|
||||||
@ -236,7 +215,7 @@ export function stackLeftColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function popRightColumn(id: Column['id']) {
|
export function popRightColumn(id: Column['id']) {
|
||||||
let layout = copy(deckStore.state.layout);
|
let layout = deepClone(deckStore.state.layout);
|
||||||
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
const affected = layout[i];
|
const affected = layout[i];
|
||||||
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
layout = layout.map(ids => ids.filter(_id => _id !== id));
|
||||||
@ -244,7 +223,7 @@ export function popRightColumn(id: Column['id']) {
|
|||||||
layout = layout.filter(ids => ids.length > 0);
|
layout = layout.filter(ids => ids.length > 0);
|
||||||
deckStore.set('layout', layout);
|
deckStore.set('layout', layout);
|
||||||
|
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
if (affected.includes(column.id)) {
|
if (affected.includes(column.id)) {
|
||||||
column.active = true;
|
column.active = true;
|
||||||
@ -256,9 +235,9 @@ export function popRightColumn(id: Column['id']) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
const column = copy(deckStore.state.columns[columnIndex]);
|
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||||
if (column == null) return;
|
if (column == null) return;
|
||||||
if (column.widgets == null) column.widgets = [];
|
if (column.widgets == null) column.widgets = [];
|
||||||
column.widgets.unshift(widget);
|
column.widgets.unshift(widget);
|
||||||
@ -268,9 +247,9 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
const column = copy(deckStore.state.columns[columnIndex]);
|
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||||
if (column == null) return;
|
if (column == null) return;
|
||||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||||
columns[columnIndex] = column;
|
columns[columnIndex] = column;
|
||||||
@ -279,9 +258,9 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
const column = copy(deckStore.state.columns[columnIndex]);
|
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||||
if (column == null) return;
|
if (column == null) return;
|
||||||
column.widgets = widgets;
|
column.widgets = widgets;
|
||||||
columns[columnIndex] = column;
|
columns[columnIndex] = column;
|
||||||
@ -290,9 +269,9 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
const column = copy(deckStore.state.columns[columnIndex]);
|
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||||
if (column == null) return;
|
if (column == null) return;
|
||||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||||
...w,
|
...w,
|
||||||
@ -304,9 +283,9 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||||
const columns = copy(deckStore.state.columns);
|
const columns = deepClone(deckStore.state.columns);
|
||||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
const currentColumn = copy(deckStore.state.columns[columnIndex]);
|
const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
|
||||||
if (currentColumn == null) return;
|
if (currentColumn == null) return;
|
||||||
for (const [k, v] of Object.entries(column)) {
|
for (const [k, v] of Object.entries(column)) {
|
||||||
currentColumn[k] = v;
|
currentColumn[k] = v;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<XNotes :pagination="pagination"/>
|
<XNotes :pagination="pagination"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
@ -25,7 +25,7 @@ const pagination = {
|
|||||||
endpoint: 'notes/mentions' as const,
|
endpoint: 'notes/mentions' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
visibility: 'specified'
|
visibility: 'specified',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
|
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
|
||||||
@ -11,9 +11,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
import XTimeline from '@/components/MkTimeline.vue';
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { updateColumn, Column } from './deck-store';
|
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -26,7 +26,7 @@ const emit = defineEmits<{
|
|||||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
let timeline = $shallowRef<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
if (props.column.listId == null) {
|
if (props.column.listId == null) {
|
||||||
setList();
|
setList();
|
||||||
@ -37,30 +37,19 @@ async function setList() {
|
|||||||
const { canceled, result: list } = await os.select({
|
const { canceled, result: list } = await os.select({
|
||||||
title: i18n.ts.selectList,
|
title: i18n.ts.selectList,
|
||||||
items: lists.map(x => ({
|
items: lists.map(x => ({
|
||||||
value: x, text: x.name
|
value: x, text: x.name,
|
||||||
})),
|
})),
|
||||||
default: props.column.listId
|
default: props.column.listId,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
updateColumn(props.column.id, {
|
updateColumn(props.column.id, {
|
||||||
listId: list.id
|
listId: list.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
const menu = [{
|
||||||
function focus() {
|
icon: 'ti ti-pencil',
|
||||||
timeline.focus();
|
text: i18n.ts.selectList,
|
||||||
}
|
action: setList,
|
||||||
|
}];
|
||||||
export default defineComponent({
|
|
||||||
watch: {
|
|
||||||
mediaOnly() {
|
|
||||||
(this.$refs.timeline as any).reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
</style>
|
|
||||||
|
@ -58,7 +58,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||||||
type: 'label',
|
type: 'label',
|
||||||
text: path,
|
text: path,
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-window-maximize',
|
icon: 'ti ti-window-maximize',
|
||||||
text: i18n.ts.openInWindow,
|
text: i18n.ts.openInWindow,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.pageWindow(path);
|
os.pageWindow(path);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<XNotes :pagination="pagination"/>
|
<XNotes :pagination="pagination"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :column="column" :is-stacked="isStacked" :menu="menu" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<XNotifications :include-types="column.includingTypes"/>
|
<XNotifications :include-types="column.includingTypes"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
@ -9,10 +9,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
import XNotifications from '@/components/MkNotifications.vue';
|
import XNotifications from '@/components/MkNotifications.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { updateColumn } from './deck-store';
|
import { i18n } from '@/i18n';
|
||||||
import { Column } from './deck-store';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
@ -30,9 +30,15 @@ function func() {
|
|||||||
done: async (res) => {
|
done: async (res) => {
|
||||||
const { includingTypes } = res;
|
const { includingTypes } = res;
|
||||||
updateColumn(props.column.id, {
|
updateColumn(props.column.id, {
|
||||||
includingTypes: includingTypes
|
includingTypes: includingTypes,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menu = [{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.notificationSetting,
|
||||||
|
action: func,
|
||||||
|
}];
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
|
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
|
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
|
||||||
<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
|
<i v-else-if="column.tl === 'social'" class="ti ti-rocket"></i>
|
||||||
<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
|
<i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i>
|
||||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="disabled" class="iwaalbte">
|
<div v-if="disabled" :class="$style.disabled">
|
||||||
<p>
|
<p :class="$style.disabledTitle">
|
||||||
<i class="fas fa-minus-circle"></i>
|
<i class="ti ti-minus-circle"></i>
|
||||||
{{ $t('disabled-timeline.title') }}
|
{{ $t('disabled-timeline.title') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
<p :class="$style.disabledDescription">{{ $t('disabled-timeline.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
|
import { removeColumn, updateColumn, Column } from './deck-store';
|
||||||
import XTimeline from '@/components/MkTimeline.vue';
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { removeColumn, updateColumn, Column } from './deck-store';
|
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
@ -40,16 +40,12 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let disabled = $ref(false);
|
let disabled = $ref(false);
|
||||||
let indicated = $ref(false);
|
|
||||||
let columnActive = $ref(true);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.column.tl == null) {
|
if (props.column.tl == null) {
|
||||||
setType();
|
setType();
|
||||||
} else if ($i) {
|
} else if ($i) {
|
||||||
disabled = !$i.isModerator && !$i.isAdmin && (
|
disabled = false; // TODO
|
||||||
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
|
||||||
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,13 +53,13 @@ async function setType() {
|
|||||||
const { canceled, result: src } = await os.select({
|
const { canceled, result: src } = await os.select({
|
||||||
title: i18n.ts.timeline,
|
title: i18n.ts.timeline,
|
||||||
items: [{
|
items: [{
|
||||||
value: 'home' as const, text: i18n.ts._timelines.home
|
value: 'home' as const, text: i18n.ts._timelines.home,
|
||||||
}, {
|
}, {
|
||||||
value: 'local' as const, text: i18n.ts._timelines.local
|
value: 'local' as const, text: i18n.ts._timelines.local,
|
||||||
}, {
|
}, {
|
||||||
value: 'social' as const, text: i18n.ts._timelines.social
|
value: 'social' as const, text: i18n.ts._timelines.social,
|
||||||
}, {
|
}, {
|
||||||
value: 'global' as const, text: i18n.ts._timelines.global
|
value: 'global' as const, text: i18n.ts._timelines.global,
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
if (canceled) {
|
if (canceled) {
|
||||||
@ -73,57 +69,27 @@ async function setType() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateColumn(props.column.id, {
|
updateColumn(props.column.id, {
|
||||||
tl: src
|
tl: src,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueUpdated(q) {
|
const menu = [{
|
||||||
if (columnActive) {
|
icon: 'ti ti-pencil',
|
||||||
indicated = q !== 0;
|
text: i18n.ts.timeline,
|
||||||
}
|
action: setType,
|
||||||
}
|
}];
|
||||||
|
|
||||||
function onNote() {
|
|
||||||
if (!columnActive) {
|
|
||||||
indicated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeActiveState(state) {
|
|
||||||
columnActive = state;
|
|
||||||
|
|
||||||
if (columnActive) {
|
|
||||||
indicated = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
export default defineComponent({
|
|
||||||
watch: {
|
|
||||||
mediaOnly() {
|
|
||||||
(this.$refs.timeline as any).reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
(this.$refs.timeline as any).focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.iwaalbte {
|
.disabled {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
> p {
|
.disabledTitle {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&.desc {
|
.disabledDescription {
|
||||||
font-size: 14px;
|
font-size: 90%;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<div class="wtdtxvec">
|
<div :class="$style.root">
|
||||||
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
|
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
|
||||||
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
<XWidgets :edit="edit" :widgets="column.widgets ?? []" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||||
</div>
|
</div>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
@ -46,18 +46,24 @@ function updateWidgets(widgets) {
|
|||||||
function func() {
|
function func() {
|
||||||
edit = !edit;
|
edit = !edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menu = [{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.editWidgets,
|
||||||
|
action: func,
|
||||||
|
}];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.wtdtxvec {
|
.root {
|
||||||
--margin: 8px;
|
--margin: 8px;
|
||||||
--panelBorder: none;
|
--panelBorder: none;
|
||||||
|
|
||||||
padding: 0 var(--margin);
|
padding: 0 var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
> .intro {
|
.intro {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
305
packages/frontend/src/ui/deckold2.vue
Normal file
305
packages/frontend/src/ui/deckold2.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': '12' + 'px' }"
|
||||||
|
@contextmenu.self.prevent="onContextmenu"
|
||||||
|
>
|
||||||
|
<XSidebar v-if="!isMobile"/>
|
||||||
|
|
||||||
|
<template v-for="ids in layout">
|
||||||
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
|
<section
|
||||||
|
v-if="ids.length > 1"
|
||||||
|
class="folder column"
|
||||||
|
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
||||||
|
>
|
||||||
|
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
||||||
|
</section>
|
||||||
|
<DeckColumnCore
|
||||||
|
v-else
|
||||||
|
:ref="ids[0]"
|
||||||
|
:key="ids[0]"
|
||||||
|
class="column"
|
||||||
|
:column="columns.find(c => c.id === ids[0])"
|
||||||
|
:is-stacked="false"
|
||||||
|
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
||||||
|
@parent-focus="moveFocus(ids[0], $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="isMobile" class="buttons">
|
||||||
|
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||||
|
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
|
||||||
|
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
||||||
|
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition :name="$store.state.animation ? 'menu-back' : ''">
|
||||||
|
<div
|
||||||
|
v-if="drawerMenuShowing"
|
||||||
|
class="menu-back _modalBg"
|
||||||
|
@click="drawerMenuShowing = false"
|
||||||
|
@touchstart.passive="drawerMenuShowing = false"
|
||||||
|
></div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<transition :name="$store.state.animation ? 'menu' : ''">
|
||||||
|
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<XCommon/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import XCommon from './_common_/common.vue';
|
||||||
|
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deckold/deck-store';
|
||||||
|
import DeckColumnCore from '@/ui/deckold/column-core.vue';
|
||||||
|
import XSidebar from '@/ui/_common_/navbar.vue';
|
||||||
|
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { getScrollContainer } from '@/scripts/scroll';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { navbarItemDef } from '@/navbar';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
|
mainRouter.navHook = (path): boolean => {
|
||||||
|
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
|
||||||
|
if (deckStore.state.navWindow || noMainColumn) {
|
||||||
|
os.pageWindow(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = ref(window.innerWidth <= 500);
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isMobile.value = window.innerWidth <= 500;
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawerMenuShowing = ref(false);
|
||||||
|
|
||||||
|
const route = 'TODO';
|
||||||
|
watch(route, () => {
|
||||||
|
drawerMenuShowing.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = deckStore.reactiveState.columns;
|
||||||
|
const layout = deckStore.reactiveState.layout;
|
||||||
|
const menuIndicated = computed(() => {
|
||||||
|
if ($i == null) return false;
|
||||||
|
for (const def in navbarItemDef) {
|
||||||
|
if (navbarItemDef[def].indicated) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addColumn = async (ev) => {
|
||||||
|
const columns = [
|
||||||
|
'main',
|
||||||
|
'widgets',
|
||||||
|
'notifications',
|
||||||
|
'tl',
|
||||||
|
'antenna',
|
||||||
|
'list',
|
||||||
|
'mentions',
|
||||||
|
'direct',
|
||||||
|
];
|
||||||
|
|
||||||
|
const { canceled, result: column } = await os.select({
|
||||||
|
title: i18n.ts._deck.addColumn,
|
||||||
|
items: columns.map(column => ({
|
||||||
|
value: column, text: i18n.t('_deck._columns.' + column)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
addColumnToStore({
|
||||||
|
type: column,
|
||||||
|
id: uuid(),
|
||||||
|
name: i18n.t('_deck._columns.' + column),
|
||||||
|
width: 330,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextmenu = (ev) => {
|
||||||
|
os.contextMenu([{
|
||||||
|
text: i18n.ts._deck.addColumn,
|
||||||
|
action: addColumn,
|
||||||
|
}], ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
provide('shouldSpacerMin', true);
|
||||||
|
if (deckStore.state.navWindow) {
|
||||||
|
provide('navHook', (url) => {
|
||||||
|
os.pageWindow(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.documentElement.style.scrollBehavior = 'auto';
|
||||||
|
window.addEventListener('wheel', (ev) => {
|
||||||
|
if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
||||||
|
document.documentElement.scrollLeft += ev.deltaY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadDeck();
|
||||||
|
|
||||||
|
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
||||||
|
// TODO??
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-enter-active,
|
||||||
|
.menu-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);
|
||||||
|
}
|
||||||
|
.menu-enter-from,
|
||||||
|
.menu-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-back-enter-active,
|
||||||
|
.menu-back-leave-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
.menu-back-enter-from,
|
||||||
|
.menu-back-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mk-deck {
|
||||||
|
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
||||||
|
|
||||||
|
// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
|
||||||
|
--margin: var(--marginHalf);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--deckMargin);
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
> .column:first-of-type {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .column:last-of-type {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isMobile {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .column {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: var(--deckMargin);
|
||||||
|
|
||||||
|
&.folder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: var(--deckMargin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .buttons {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
height: 60px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--X2);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: var(--indicator);
|
||||||
|
font-size: 16px;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu-back {
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
||||||
|
height: calc(var(--vh, 1vh) * 100);
|
||||||
|
width: 240px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
63
packages/frontend/src/ui/deckold2/antenna-column.vue
Normal file
63
packages/frontend/src/ui/deckold2/antenna-column.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.column.antennaId == null) {
|
||||||
|
setAntenna();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setAntenna() {
|
||||||
|
const antennas = await os.api('antennas/list');
|
||||||
|
const { canceled, result: antenna } = await os.select({
|
||||||
|
title: i18n.ts.selectAntenna,
|
||||||
|
items: antennas.map(x => ({
|
||||||
|
value: x, text: x.name
|
||||||
|
})),
|
||||||
|
default: props.column.antennaId
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
antennaId: antenna.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
function focus() {
|
||||||
|
timeline.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
44
packages/frontend/src/ui/deckold2/column-core.vue
Normal file
44
packages/frontend/src/ui/deckold2/column-core.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<!-- TODO: リファクタの余地がありそう -->
|
||||||
|
<div v-if="!column">たぶん見えちゃいけないやつ</div>
|
||||||
|
<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XMainColumn from './main-column.vue';
|
||||||
|
import XTlColumn from './tl-column.vue';
|
||||||
|
import XAntennaColumn from './antenna-column.vue';
|
||||||
|
import XListColumn from './list-column.vue';
|
||||||
|
import XNotificationsColumn from './notifications-column.vue';
|
||||||
|
import XWidgetsColumn from './widgets-column.vue';
|
||||||
|
import XMentionsColumn from './mentions-column.vue';
|
||||||
|
import XDirectColumn from './direct-column.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column?: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
export default defineComponent({
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$children[0].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
391
packages/frontend/src/ui/deckold2/column.vue
Normal file
391
packages/frontend/src/ui/deckold2/column.vue
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
||||||
|
<section v-hotkey="keymap" class="dnpfarvg _panel blur _narrow_"
|
||||||
|
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
|
||||||
|
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
|
||||||
|
@dragover.prevent.stop="onDragover"
|
||||||
|
@dragleave="onDragleave"
|
||||||
|
@drop.prevent.stop="onDrop"
|
||||||
|
>
|
||||||
|
<header :class="{ indicated }"
|
||||||
|
draggable="true"
|
||||||
|
@click="goTop"
|
||||||
|
@dragstart="onDragstart"
|
||||||
|
@dragend="onDragend"
|
||||||
|
@contextmenu.prevent.stop="onContextmenu"
|
||||||
|
>
|
||||||
|
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
|
||||||
|
<template v-if="active"><i class="fas fa-angle-up"></i></template>
|
||||||
|
<template v-else><i class="fas fa-angle-down"></i></template>
|
||||||
|
</button>
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
<span class="header"><slot name="header"></slot></span>
|
||||||
|
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="fas fa-ellipsis"></i></button>
|
||||||
|
</header>
|
||||||
|
<div v-show="active" ref="body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export type DeckFunc = {
|
||||||
|
title: string;
|
||||||
|
handler: (payload: MouseEvent) => void;
|
||||||
|
icon?: string;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
|
||||||
|
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
provide('shouldHeaderThin', true);
|
||||||
|
provide('shouldOmitHeaderTitle', true);
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked?: boolean;
|
||||||
|
func?: DeckFunc | null;
|
||||||
|
naked?: boolean;
|
||||||
|
indicated?: boolean;
|
||||||
|
}>(), {
|
||||||
|
isStacked: false,
|
||||||
|
func: null,
|
||||||
|
naked: false,
|
||||||
|
indicated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
(ev: 'change-active-state', v: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let body = $ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
let dragging = $ref(false);
|
||||||
|
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
|
||||||
|
|
||||||
|
let draghover = $ref(false);
|
||||||
|
let dropready = $ref(false);
|
||||||
|
|
||||||
|
const isMainColumn = $computed(() => props.column.type === 'main');
|
||||||
|
const active = $computed(() => props.column.active !== false);
|
||||||
|
watch($$(active), v => emit('change-active-state', v));
|
||||||
|
|
||||||
|
const keymap = $computed(() => ({
|
||||||
|
'shift+up': () => emit('parent-focus', 'up'),
|
||||||
|
'shift+down': () => emit('parent-focus', 'down'),
|
||||||
|
'shift+left': () => emit('parent-focus', 'left'),
|
||||||
|
'shift+right': () => emit('parent-focus', 'right'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
|
||||||
|
os.deckGlobalEvents.on('column.dragEnd', onOtherDragEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
os.deckGlobalEvents.off('column.dragStart', onOtherDragStart);
|
||||||
|
os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOtherDragStart() {
|
||||||
|
dropready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOtherDragEnd() {
|
||||||
|
dropready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActive() {
|
||||||
|
if (!props.isStacked) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
active: !props.column.active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenu() {
|
||||||
|
const items = [{
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
action: async () => {
|
||||||
|
const { canceled, result } = await os.form(props.column.name, {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
label: i18n.ts.name,
|
||||||
|
default: props.column.name,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: 'number',
|
||||||
|
label: i18n.ts.width,
|
||||||
|
default: props.column.width,
|
||||||
|
},
|
||||||
|
flexible: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: i18n.ts.flexible,
|
||||||
|
default: props.column.flexible,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, result);
|
||||||
|
},
|
||||||
|
}, null, {
|
||||||
|
icon: 'fas fa-arrow-left',
|
||||||
|
text: i18n.ts._deck.swapLeft,
|
||||||
|
action: () => {
|
||||||
|
swapLeftColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
icon: 'fas fa-arrow-right',
|
||||||
|
text: i18n.ts._deck.swapRight,
|
||||||
|
action: () => {
|
||||||
|
swapRightColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}, props.isStacked ? {
|
||||||
|
icon: 'fas fa-arrow-up',
|
||||||
|
text: i18n.ts._deck.swapUp,
|
||||||
|
action: () => {
|
||||||
|
swapUpColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined, props.isStacked ? {
|
||||||
|
icon: 'fas fa-arrow-down',
|
||||||
|
text: i18n.ts._deck.swapDown,
|
||||||
|
action: () => {
|
||||||
|
swapDownColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined, null, {
|
||||||
|
icon: 'fas fa-window-restore',
|
||||||
|
text: i18n.ts._deck.stackLeft,
|
||||||
|
action: () => {
|
||||||
|
stackLeftColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}, props.isStacked ? {
|
||||||
|
icon: 'fas fa-window-maximize',
|
||||||
|
text: i18n.ts._deck.popRight,
|
||||||
|
action: () => {
|
||||||
|
popRightColumn(props.column.id);
|
||||||
|
},
|
||||||
|
} : undefined, null, {
|
||||||
|
icon: 'fas fa-trash-alt',
|
||||||
|
text: i18n.ts.remove,
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
removeColumn(props.column.id);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (props.func) {
|
||||||
|
items.unshift(null);
|
||||||
|
items.unshift({
|
||||||
|
icon: props.func.icon,
|
||||||
|
text: props.func.title,
|
||||||
|
action: props.func.handler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettingsMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu(getMenu(), ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
os.contextMenu(getMenu(), ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTop() {
|
||||||
|
body.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragstart(ev) {
|
||||||
|
ev.dataTransfer.effectAllowed = 'move';
|
||||||
|
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||||
|
|
||||||
|
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||||
|
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||||
|
window.setTimeout(() => {
|
||||||
|
dragging = true;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragend(ev) {
|
||||||
|
dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragover(ev) {
|
||||||
|
// 自分自身がドラッグされている場合
|
||||||
|
if (dragging) {
|
||||||
|
// 自分自身にはドロップさせない
|
||||||
|
ev.dataTransfer.dropEffect = 'none';
|
||||||
|
} else {
|
||||||
|
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
|
||||||
|
|
||||||
|
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||||
|
|
||||||
|
if (isDeckColumn) draghover = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragleave() {
|
||||||
|
draghover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(ev) {
|
||||||
|
draghover = false;
|
||||||
|
os.deckGlobalEvents.emit('column.dragEnd');
|
||||||
|
|
||||||
|
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||||
|
if (id != null && id !== '') {
|
||||||
|
swapColumn(props.column.id, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dnpfarvg {
|
||||||
|
--root-margin: 10px;
|
||||||
|
--deckColumnHeaderHeight: 42px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: content;
|
||||||
|
box-shadow: 0 0 8px 0 var(--shadow);
|
||||||
|
|
||||||
|
&.draghover {
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dropready {
|
||||||
|
* {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
flex-basis: var(--deckColumnHeaderHeight);
|
||||||
|
min-height: var(--deckColumnHeaderHeight);
|
||||||
|
|
||||||
|
> header.indicated {
|
||||||
|
box-shadow: 4px 0px var(--accent) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.naked {
|
||||||
|
background: var(--acrylicBg) !important;
|
||||||
|
|
||||||
|
> header {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.paged {
|
||||||
|
background: var(--bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
|
height: var(--deckColumnHeaderHeight);
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--panelHeaderFg);
|
||||||
|
background: var(--panelHeaderBg);
|
||||||
|
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&, * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.indicated {
|
||||||
|
box-shadow: 0 3px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .header {
|
||||||
|
display: inline-block;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span:only-of-type {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toggleActive,
|
||||||
|
> .action > ::v-deep(*),
|
||||||
|
> .menu {
|
||||||
|
z-index: 1;
|
||||||
|
width: var(--deckColumnHeaderHeight);
|
||||||
|
line-height: var(--deckColumnHeaderHeight);
|
||||||
|
color: var(--faceTextButton);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--faceTextButtonHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--faceTextButtonActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toggleActive, > .action {
|
||||||
|
margin-left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .action {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .action:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .menu {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: calc(100% - var(--deckColumnHeaderHeight));
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden; // Safari does not supports clip
|
||||||
|
overflow-x: clip;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
317
packages/frontend/src/ui/deckold2/deck-store.ts
Normal file
317
packages/frontend/src/ui/deckold2/deck-store.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { throttle } from 'throttle-debounce';
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
import { notificationTypes } from 'misskey-js';
|
||||||
|
import { Storage } from '../../pizzax';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { api } from '@/os';
|
||||||
|
|
||||||
|
type ColumnWidget = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
data: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Column = {
|
||||||
|
id: string;
|
||||||
|
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct';
|
||||||
|
name: string | null;
|
||||||
|
width: number;
|
||||||
|
widgets?: ColumnWidget[];
|
||||||
|
active?: boolean;
|
||||||
|
flexible?: boolean;
|
||||||
|
antennaId?: string;
|
||||||
|
listId?: string;
|
||||||
|
includingTypes?: typeof notificationTypes[number][];
|
||||||
|
tl?: 'home' | 'local' | 'social' | 'global';
|
||||||
|
};
|
||||||
|
|
||||||
|
function copy<T>(x: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deckStore = markRaw(new Storage('deck', {
|
||||||
|
profile: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: [] as Column[],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
columnMargin: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 12,
|
||||||
|
},
|
||||||
|
columnHeaderHeight: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: 42,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const loadDeck = async () => {
|
||||||
|
let deck;
|
||||||
|
|
||||||
|
try {
|
||||||
|
deck = await api('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', [{
|
||||||
|
id: 'a',
|
||||||
|
type: 'main',
|
||||||
|
name: i18n.ts._deck._columns.main,
|
||||||
|
width: 350,
|
||||||
|
}, {
|
||||||
|
id: 'b',
|
||||||
|
type: 'notifications',
|
||||||
|
name: i18n.ts._deck._columns.notifications,
|
||||||
|
width: 330,
|
||||||
|
}]);
|
||||||
|
deckStore.set('layout', [['a'], ['b']]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
deckStore.set('columns', deck.columns);
|
||||||
|
deckStore.set('layout', deck.layout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||||
|
export const saveDeck = throttle(1000, () => {
|
||||||
|
api('i/registry/set', {
|
||||||
|
scope: ['client', 'deck', 'profiles'],
|
||||||
|
key: deckStore.state.profile,
|
||||||
|
value: {
|
||||||
|
columns: deckStore.reactiveState.columns.value,
|
||||||
|
layout: deckStore.reactiveState.layout.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getProfiles(): Promise<string[]> {
|
||||||
|
return await api('i/registry/keys', {
|
||||||
|
scope: ['client', 'deck', 'profiles'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProfile(key: string): Promise<void> {
|
||||||
|
return await api('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 = copy(deckStore.state.layout);
|
||||||
|
layout[aX][aY] = b;
|
||||||
|
layout[bX][bY] = a;
|
||||||
|
deckStore.set('layout', layout);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapLeftColumn(id: Column['id']) {
|
||||||
|
const layout = copy(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 = copy(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 = copy(deckStore.state.layout);
|
||||||
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
const ids = copy(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 = copy(deckStore.state.layout);
|
||||||
|
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
|
||||||
|
const ids = copy(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 = copy(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 = copy(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 = copy(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 = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(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 = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == 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 = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(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 = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const column = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (column == null) return;
|
||||||
|
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||||
|
...w,
|
||||||
|
data: widgetData,
|
||||||
|
} : w);
|
||||||
|
columns[columnIndex] = column;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||||
|
const columns = copy(deckStore.state.columns);
|
||||||
|
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||||
|
const currentColumn = copy(deckStore.state.columns[columnIndex]);
|
||||||
|
if (currentColumn == null) return;
|
||||||
|
for (const [k, v] of Object.entries(column)) {
|
||||||
|
currentColumn[k] = v;
|
||||||
|
}
|
||||||
|
columns[columnIndex] = currentColumn;
|
||||||
|
deckStore.set('columns', columns);
|
||||||
|
saveDeck();
|
||||||
|
}
|
31
packages/frontend/src/ui/deckold2/direct-column.vue
Normal file
31
packages/frontend/src/ui/deckold2/direct-column.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<XNotes :pagination="pagination"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotes from '@/components/MkNotes.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'notes/mentions' as const,
|
||||||
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
visibility: 'specified'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
66
packages/frontend/src/ui/deckold2/list-column.vue
Normal file
66
packages/frontend/src/ui/deckold2/list-column.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn, Column } from './deck-store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||||
|
|
||||||
|
if (props.column.listId == null) {
|
||||||
|
setList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setList() {
|
||||||
|
const lists = await os.api('users/lists/list');
|
||||||
|
const { canceled, result: list } = await os.select({
|
||||||
|
title: i18n.ts.selectList,
|
||||||
|
items: lists.map(x => ({
|
||||||
|
value: x, text: x.name
|
||||||
|
})),
|
||||||
|
default: props.column.listId
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
listId: list.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
function focus() {
|
||||||
|
timeline.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
mediaOnly() {
|
||||||
|
(this.$refs.timeline as any).reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
68
packages/frontend/src/ui/deckold2/main-column.vue
Normal file
68
packages/frontend/src/ui/deckold2/main-column.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<template v-if="pageMetadata?.value">
|
||||||
|
<i :class="pageMetadata?.value.icon"></i>
|
||||||
|
{{ pageMetadata?.value.title }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<RouterView @contextmenu.stop="onContextmenu"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ComputedRef, provide } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import { deckStore, Column } from '@/ui/deck/deck-store';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { mainRouter } from '@/router';
|
||||||
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
|
|
||||||
|
provide('router', mainRouter);
|
||||||
|
provideMetadataReceiver((info) => {
|
||||||
|
pageMetadata = info;
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
function back() {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function onContextmenu(ev: MouseEvent) {
|
||||||
|
if (!ev.target) return;
|
||||||
|
|
||||||
|
const isLink = (el: HTMLElement) => {
|
||||||
|
if (el.tagName === 'A') return true;
|
||||||
|
if (el.parentElement) {
|
||||||
|
return isLink(el.parentElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isLink(ev.target as HTMLElement)) return;
|
||||||
|
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
|
||||||
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
const path = mainRouter.currentRoute.value.path;
|
||||||
|
os.contextMenu([{
|
||||||
|
type: 'label',
|
||||||
|
text: path,
|
||||||
|
}, {
|
||||||
|
icon: 'fas fa-window-maximize',
|
||||||
|
text: i18n.ts.openInWindow,
|
||||||
|
action: () => {
|
||||||
|
os.pageWindow(path);
|
||||||
|
},
|
||||||
|
}], ev);
|
||||||
|
}
|
||||||
|
</script>
|
28
packages/frontend/src/ui/deckold2/mentions-column.vue
Normal file
28
packages/frontend/src/ui/deckold2/mentions-column.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<XNotes :pagination="pagination"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotes from '@/components/MkNotes.vue';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'notes/mentions' as const,
|
||||||
|
limit: 10,
|
||||||
|
};
|
||||||
|
</script>
|
38
packages/frontend/src/ui/deckold2/notifications-column.vue
Normal file
38
packages/frontend/src/ui/deckold2/notifications-column.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<XNotifications :include-types="column.includingTypes"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XNotifications from '@/components/MkNotifications.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { updateColumn } from './deck-store';
|
||||||
|
import { Column } from './deck-store';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function func() {
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
|
||||||
|
includingTypes: props.column.includingTypes,
|
||||||
|
}, {
|
||||||
|
done: async (res) => {
|
||||||
|
const { includingTypes } = res;
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
includingTypes: includingTypes
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
}
|
||||||
|
</script>
|
129
packages/frontend/src/ui/deckold2/tl-column.vue
Normal file
129
packages/frontend/src/ui/deckold2/tl-column.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header>
|
||||||
|
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
|
||||||
|
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
|
||||||
|
<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
|
||||||
|
<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
|
||||||
|
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="disabled" class="iwaalbte">
|
||||||
|
<p>
|
||||||
|
<i class="fas fa-minus-circle"></i>
|
||||||
|
{{ $t('disabled-timeline.title') }}
|
||||||
|
</p>
|
||||||
|
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
|
||||||
|
</div>
|
||||||
|
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import XTimeline from '@/components/MkTimeline.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { removeColumn, updateColumn, Column } from './deck-store';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { instance } from '@/instance';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'loaded'): void;
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let disabled = $ref(false);
|
||||||
|
let indicated = $ref(false);
|
||||||
|
let columnActive = $ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.column.tl == null) {
|
||||||
|
setType();
|
||||||
|
} else if ($i) {
|
||||||
|
disabled = !$i.isModerator && !$i.isAdmin && (
|
||||||
|
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
|
||||||
|
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setType() {
|
||||||
|
const { canceled, result: src } = await os.select({
|
||||||
|
title: i18n.ts.timeline,
|
||||||
|
items: [{
|
||||||
|
value: 'home' as const, text: i18n.ts._timelines.home
|
||||||
|
}, {
|
||||||
|
value: 'local' as const, text: i18n.ts._timelines.local
|
||||||
|
}, {
|
||||||
|
value: 'social' as const, text: i18n.ts._timelines.social
|
||||||
|
}, {
|
||||||
|
value: 'global' as const, text: i18n.ts._timelines.global
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
if (canceled) {
|
||||||
|
if (props.column.tl == null) {
|
||||||
|
removeColumn(props.column.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
tl: src
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueUpdated(q) {
|
||||||
|
if (columnActive) {
|
||||||
|
indicated = q !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNote() {
|
||||||
|
if (!columnActive) {
|
||||||
|
indicated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeActiveState(state) {
|
||||||
|
columnActive = state;
|
||||||
|
|
||||||
|
if (columnActive) {
|
||||||
|
indicated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
mediaOnly() {
|
||||||
|
(this.$refs.timeline as any).reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
(this.$refs.timeline as any).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.iwaalbte {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 16px;
|
||||||
|
|
||||||
|
&.desc {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
63
packages/frontend/src/ui/deckold2/widgets-column.vue
Normal file
63
packages/frontend/src/ui/deckold2/widgets-column.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
|
||||||
|
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
|
<div class="wtdtxvec">
|
||||||
|
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
|
||||||
|
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||||
|
</div>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
|
||||||
|
import XWidgets from '@/components/MkWidgets.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let edit = $ref(false);
|
||||||
|
|
||||||
|
function addWidget(widget) {
|
||||||
|
addColumnWidget(props.column.id, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWidget(widget) {
|
||||||
|
removeColumnWidget(props.column.id, widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidget({ id, data }) {
|
||||||
|
updateColumnWidget(props.column.id, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWidgets(widgets) {
|
||||||
|
setColumnWidgets(props.column.id, widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
function func() {
|
||||||
|
edit = !edit;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wtdtxvec {
|
||||||
|
--margin: 8px;
|
||||||
|
--panelBorder: none;
|
||||||
|
|
||||||
|
padding: 0 var(--margin);
|
||||||
|
|
||||||
|
> .intro {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user