diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/ui | |
| parent | wip: retention for dashboard (diff) | |
| download | sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/ui')
34 files changed, 0 insertions, 5650 deletions
diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue deleted file mode 100644 index 7f3fc0e4af..0000000000 --- a/packages/client/src/ui/_common_/common.vue +++ /dev/null @@ -1,139 +0,0 @@ -<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> - -<div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; -import { swInject } from './sw-inject'; -import { popup, popups, pendingApiRequestsCount } from '@/os'; -import { uploads } from '@/scripts/upload'; -import * as sound from '@/scripts/sound'; -import { $i } from '@/account'; -import { stream } from '@/stream'; -import { i18n } from '@/i18n'; - -const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue')); -const XUpload = defineAsyncComponent(() => import('./upload.vue')); - -const dev = _DEV_; - -const onNotification = notification => { - if ($i.mutingNotificationTypes.includes(notification.type)) return; - - if (document.visibilityState === 'visible') { - stream.send('readNotification', { - id: notification.id, - }); - - popup(defineAsyncComponent(() => import('@/components/MkNotificationToast.vue')), { - notification, - }, {}, 'closed'); - } - - 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(); - } -} -</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; - } -} - -#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> diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue deleted file mode 100644 index 50b28de063..0000000000 --- a/packages/client/src/ui/_common_/navbar-for-mobile.vue +++ /dev/null @@ -1,314 +0,0 @@ -<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">{{ i18n.ts[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, defineComponent, ref, toRef, watch } from 'vue'; -import { host } from '@/config'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import { navbarItemDef } from '@/navbar'; -import { openAccountMenu as openAccountMenu_ } from '@/account'; -import { defaultStore } from '@/store'; -import { instance } from '@/instance'; -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 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-mood-happy', - to: '/about#emojis', - }, { - type: 'link', - text: i18n.ts.federation, - icon: 'ti ti-whirl', - to: '/about#federation', - }, null, { - type: 'parent', - text: i18n.ts.help, - icon: 'ti ti-question-circle', - children: [{ - type: 'link', - to: '/mfm-cheat-sheet', - text: i18n.ts._mfm.cheatSheet, - icon: 'ti ti-code', - }, { - 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', - }, null, { - text: i18n.ts.document, - 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', - }); -} - -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> diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue deleted file mode 100644 index b82da15f13..0000000000 --- a/packages/client/src/ui/_common_/navbar.vue +++ /dev/null @@ -1,521 +0,0 @@ -<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="i18n.ts[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">{{ i18n.ts[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 * as os from '@/os'; -import { navbarItemDef } from '@/navbar'; -import { $i, openAccountMenu as openAccountMenu_ } from '@/account'; -import { defaultStore } from '@/store'; -import { i18n } from '@/i18n'; -import { instance } from '@/instance'; -import { host } from '@/config'; - -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 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-mood-happy', - to: '/about#emojis', - }, { - type: 'link', - text: i18n.ts.federation, - icon: 'ti ti-whirl', - to: '/about#federation', - }, null, { - type: 'parent', - text: i18n.ts.help, - icon: 'ti ti-question-circle', - children: [{ - type: 'link', - to: '/mfm-cheat-sheet', - text: i18n.ts._mfm.cheatSheet, - icon: 'ti ti-code', - }, { - 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', - }, null, { - text: i18n.ts.document, - 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', - }); -} - -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> diff --git a/packages/client/src/ui/_common_/statusbar-federation.vue b/packages/client/src/ui/_common_/statusbar-federation.vue deleted file mode 100644 index 24fc4f6f6d..0000000000 --- a/packages/client/src/ui/_common_/statusbar-federation.vue +++ /dev/null @@ -1,108 +0,0 @@ -<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: '+lastCommunicatedAt', - 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> diff --git a/packages/client/src/ui/_common_/statusbar-rss.vue b/packages/client/src/ui/_common_/statusbar-rss.vue deleted file mode 100644 index e7f88e4984..0000000000 --- a/packages/client/src/ui/_common_/statusbar-rss.vue +++ /dev/null @@ -1,93 +0,0 @@ -<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> diff --git a/packages/client/src/ui/_common_/statusbar-user-list.vue b/packages/client/src/ui/_common_/statusbar-user-list.vue deleted file mode 100644 index f4d989c387..0000000000 --- a/packages/client/src/ui/_common_/statusbar-user-list.vue +++ /dev/null @@ -1,113 +0,0 @@ -<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" :custom-emojis="note.emojis"/> - </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> diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue deleted file mode 100644 index 114ca5be8c..0000000000 --- a/packages/client/src/ui/_common_/statusbars.vue +++ /dev/null @@ -1,92 +0,0 @@ -<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> diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue deleted file mode 100644 index a855de8ab9..0000000000 --- a/packages/client/src/ui/_common_/stream-indicator.vue +++ /dev/null @@ -1,61 +0,0 @@ -<template> -<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected"> - <div>{{ i18n.ts.disconnectedFromServer }}</div> - <div class="command"> - <button class="_textButton" @click="reload">{{ i18n.ts.reload }}</button> - <button class="_textButton">{{ i18n.ts.doNothing }}</button> - </div> -</div> -</template> - -<script lang="ts" setup> -import { onUnmounted } from 'vue'; -import { stream } from '@/stream'; -import { i18n } from '@/i18n'; - -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> diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts deleted file mode 100644 index 8676d2d48d..0000000000 --- a/packages/client/src/ui/_common_/sw-inject.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; - } - }); -} diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue deleted file mode 100644 index 70882bd251..0000000000 --- a/packages/client/src/ui/_common_/upload.vue +++ /dev/null @@ -1,129 +0,0 @@ -<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"><MkLoading :em="true"/>{{ ctx.name }}</p> - <p class="status"> - <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="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'; -import { i18n } from '@/i18n'; - -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> diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue deleted file mode 100644 index 46d79e6355..0000000000 --- a/packages/client/src/ui/classic.header.vue +++ /dev/null @@ -1,217 +0,0 @@ -<template> -<div class="azykntjl"> - <div class="body"> - <div class="left"> - <MkA v-click-anime v-tooltip="$ts.timeline" class="item index" active-class="active" to="/" exact> - <i class="ti ti-home ti-fw"></i> - </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="$ts[navbarItemDef[item].title]" class="item _button" :class="item" active-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="navbarItemDef[item].icon"></i> - <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span> - </component> - </template> - <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="$ts.controlPanel" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null"> - <i class="ti ti-dashboard ti-fw"></i> - </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="ti ti-dots ti-fw"></i> - <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> - </button> - </div> - <div class="right"> - <MkA v-click-anime v-tooltip="$ts.settings" class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null"> - <i class="ti ti-settings ti-fw"></i> - </MkA> - <button v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> - </button> - <div class="post" @click="post"> - <MkButton class="button" gradate full rounded> - <i class="ti ti-pencil ti-fw"></i> - </MkButton> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import { host } from '@/config'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import { navbarItemDef } from '@/navbar'; -import { openAccountMenu } from '@/account'; -import MkButton from '@/components/MkButton.vue'; - -export default defineComponent({ - components: { - MkButton, - }, - - data() { - return { - host: host, - accounts: [], - connection: null, - navbarItemDef: navbarItemDef, - settingsWindowed: false, - }; - }, - - computed: { - menu(): string[] { - return this.$store.state.menu; - }, - - otherNavItemIndicated(): boolean { - for (const def in this.navbarItemDef) { - if (this.menu.includes(def)) continue; - if (this.navbarItemDef[def].indicated) return true; - } - return false; - }, - }, - - watch: { - '$store.reactiveState.menuDisplay.value'() { - this.calcViewState(); - }, - }, - - created() { - window.addEventListener('resize', this.calcViewState); - this.calcViewState(); - }, - - methods: { - calcViewState() { - this.settingsWindowed = (window.innerWidth > 1400); - }, - - post() { - os.post(); - }, - - search() { - search(); - }, - - more(ev) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, - anchor: { x: 'center', y: 'bottom' }, - }, { - }, 'closed'); - }, - - openAccountMenu: (ev) => { - openAccountMenu({ - withExtraOperation: true, - }, ev); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.azykntjl { - $height: 60px; - $avatar-size: 32px; - $avatar-margin: 8px; - - position: sticky; - top: 0; - z-index: 1000; - width: 100%; - height: $height; - background-color: var(--bg); - - > .body { - max-width: 1380px; - margin: 0 auto; - display: flex; - - > .right, - > .left { - - > .item { - position: relative; - font-size: 0.9em; - display: inline-block; - padding: 0 12px; - line-height: $height; - - > i, - > .avatar { - margin-right: 0; - } - - > i { - left: 10px; - } - - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } - - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; - } - - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } - - &.active { - color: var(--navActive); - } - } - - > .divider { - display: inline-block; - height: 16px; - margin: 0 10px; - border-right: solid 0.5px var(--divider); - } - - > .post { - display: inline-block; - - > .button { - width: 40px; - height: 40px; - padding: 0; - min-width: 0; - } - } - - > .account { - display: inline-flex; - align-items: center; - vertical-align: top; - margin-right: 8px; - - > .acct { - margin-left: 8px; - } - } - } - - > .right { - margin-left: auto; - } - } -} -</style> diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue deleted file mode 100644 index dac09ea703..0000000000 --- a/packages/client/src/ui/classic.sidebar.vue +++ /dev/null @@ -1,268 +0,0 @@ -<template> -<div class="npcljfve" :class="{ iconOnly }"> - <button v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> - </button> - <div class="post" data-cy-open-post-form @click="post"> - <MkButton class="button" gradate full rounded> - <i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ $ts.note }}</span> - </MkButton> - </div> - <div class="divider"></div> - <MkA v-click-anime class="item index" active-class="active" to="/" exact> - <i class="ti ti-home ti-fw"></i><span class="text">{{ $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-class="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ $ts[navbarItemDef[item].title] }}</span> - <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_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" :behavior="settingsWindowed ? 'modalWindow' : null"> - <i class="ti ti-dashboard ti-fw"></i><span class="text">{{ $ts.controlPanel }}</span> - </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="ti ti-dots ti-fw"></i><span class="text">{{ $ts.more }}</span> - <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> - </button> - <MkA v-click-anime class="item" active-class="active" to="/settings" :behavior="settingsWindowed ? 'modalWindow' : null"> - <i class="ti ti-settings ti-fw"></i><span class="text">{{ $ts.settings }}</span> - </MkA> - <div class="divider"></div> - <div class="about"> - <MkA v-click-anime class="link" to="/about"> - <img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" class="_ghost"/> - </MkA> - </div> - <!--<MisskeyLogo class="misskey"/>--> -</div> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import { host } from '@/config'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import { navbarItemDef } from '@/navbar'; -import { openAccountMenu } from '@/account'; -import MkButton from '@/components/MkButton.vue'; -import { StickySidebar } from '@/scripts/sticky-sidebar'; -//import MisskeyLogo from '@assets/client/misskey.svg'; - -export default defineComponent({ - components: { - MkButton, - //MisskeyLogo, - }, - - data() { - return { - host: host, - accounts: [], - connection: null, - navbarItemDef: navbarItemDef, - iconOnly: false, - settingsWindowed: false, - }; - }, - - computed: { - menu(): string[] { - return this.$store.state.menu; - }, - - otherNavItemIndicated(): boolean { - for (const def in this.navbarItemDef) { - if (this.menu.includes(def)) continue; - if (this.navbarItemDef[def].indicated) return true; - } - return false; - }, - }, - - watch: { - '$store.reactiveState.menuDisplay.value'() { - this.calcViewState(); - }, - - iconOnly() { - this.$nextTick(() => { - this.$emit('change-view-mode'); - }); - }, - }, - - created() { - window.addEventListener('resize', this.calcViewState); - this.calcViewState(); - }, - - mounted() { - const sticky = new StickySidebar(this.$el.parentElement, 16); - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); - }, - - methods: { - calcViewState() { - this.iconOnly = (window.innerWidth <= 1400) || (this.$store.state.menuDisplay === 'sideIcon'); - this.settingsWindowed = (window.innerWidth > 1400); - }, - - post() { - os.post(); - }, - - search() { - search(); - }, - - more(ev) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { - src: ev.currentTarget ?? ev.target, - }, {}, 'closed'); - }, - - openAccountMenu: (ev) => { - openAccountMenu({ - withExtraOperation: true, - }, ev); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.npcljfve { - $ui-font-size: 1em; // TODO: どこかに集約したい - $nav-icon-only-width: 78px; // TODO: どこかに集約したい - $avatar-size: 32px; - $avatar-margin: 8px; - - padding: 0 16px; - box-sizing: border-box; - width: 260px; - - &.iconOnly { - flex: 0 0 $nav-icon-only-width; - width: $nav-icon-only-width !important; - - > .divider { - margin: 8px auto; - width: calc(100% - 32px); - } - - > .post { - > .button { - width: 46px; - height: 46px; - padding: 0; - } - } - - > .item { - padding-left: 0; - width: 100%; - text-align: center; - font-size: $ui-font-size * 1.1; - line-height: 3.7rem; - - > i, - > .avatar { - margin-right: 0; - } - - > i { - left: 10px; - } - - > .text { - display: none; - } - } - } - - > .divider { - margin: 10px 0; - border-top: solid 0.5px var(--divider); - } - - > .post { - position: sticky; - top: 0; - z-index: 1; - padding: 16px 0; - background: var(--bg); - - > .button { - min-width: 0; - } - } - - > .about { - fill: currentColor; - padding: 8px 0 16px 0; - text-align: center; - - > .link { - display: block; - width: 32px; - margin: 0 auto; - - img { - display: block; - width: 100%; - } - } - } - - > .item { - position: relative; - display: block; - font-size: $ui-font-size; - line-height: 2.6rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - text-align: left; - box-sizing: border-box; - - > i { - width: 32px; - } - - > i, - > .avatar { - margin-right: $avatar-margin; - } - - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } - - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; - } - - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } - - &.active { - color: var(--navActive); - } - } -} -</style> diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue deleted file mode 100644 index 0e726c11ed..0000000000 --- a/packages/client/src/ui/classic.vue +++ /dev/null @@ -1,320 +0,0 @@ -<template> -<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`"> - <XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/> - - <div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }"> - <div v-if="!showMenuOnTop" class="sidebar"> - <XSidebar/> - </div> - <div v-else ref="widgetsLeft" class="widgets left"> - <XWidgets :place="'left'" @mounted="attachSticky(widgetsLeft)"/> - </div> - - <main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> - <div class="content"> - <RouterView/> - </div> - </main> - - <div v-if="isDesktop" ref="widgetsRight" class="widgets right"> - <XWidgets :place="null" @mounted="attachSticky(widgetsRight)"/> - </div> - </div> - - <transition :name="$store.state.animation ? 'tray-back' : ''"> - <div - v-if="widgetsShowing" - class="tray-back _modalBg" - @click="widgetsShowing = false" - @touchstart.passive="widgetsShowing = false" - ></div> - </transition> - - <transition :name="$store.state.animation ? 'tray' : ''"> - <XWidgets v-if="widgetsShowing" class="tray"/> - </transition> - - <iframe v-if="$store.state.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe> - - <XCommon/> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, markRaw, ComputedRef, ref, onMounted, provide } from 'vue'; -import XSidebar from './classic.sidebar.vue'; -import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config'; -import { StickySidebar } from '@/scripts/sticky-sidebar'; -import * as os from '@/os'; -import { mainRouter } from '@/router'; -import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; -import { defaultStore } from '@/store'; -import { i18n } from '@/i18n'; -const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); -const XWidgets = defineAsyncComponent(() => import('./classic.widgets.vue')); - -const DESKTOP_THRESHOLD = 1100; - -let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -let widgetsShowing = $ref(false); -let fullView = $ref(false); -let globalHeaderHeight = $ref(0); -const wallpaper = localStorage.getItem('wallpaper') != null; -const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top'); -let live2d = $ref<HTMLIFrameElement>(); -let widgetsLeft = $ref(); -let widgetsRight = $ref(); - -provide('router', mainRouter); -provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; - } -}); -provide('shouldHeaderThin', showMenuOnTop); -provide('shouldSpacerMin', true); - -function attachSticky(el) { - const sticky = new StickySidebar(el, defaultStore.state.menuDisplay === 'top' ? 0 : 16, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); -} - -function top() { - window.scroll({ top: 0, behavior: 'smooth' }); -} - -function onContextmenu(ev: MouseEvent) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(ev.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; - if (window.getSelection().toString() !== '') return; - const path = mainRouter.getCurrentPath(); - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: fullView ? 'fas fa-compress' : 'fas fa-expand', - text: fullView ? i18n.ts.quitFullView : i18n.ts.fullView, - action: () => { - fullView = !fullView; - }, - }, { - icon: 'ti ti-window-maximize', - text: i18n.ts.openInWindow, - action: () => { - os.pageWindow(path); - }, - }], ev); -} - -function onAiClick(ev) { - //if (this.live2d) this.live2d.click(ev); -} - -if (window.innerWidth < 1024) { - localStorage.setItem('ui', 'default'); - location.reload(); -} - -document.documentElement.style.overflowY = 'scroll'; - -if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: null, data: {}, - }, { - name: 'notifications', - id: 'b', place: null, data: {}, - }, { - name: 'trends', - id: 'c', place: null, data: {}, - }]); -} - -onMounted(() => { - window.addEventListener('resize', () => { - isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD); - }, { passive: true }); - - if (defaultStore.state.aiChanMode) { - const iframeRect = live2d.getBoundingClientRect(); - window.addEventListener('mousemove', ev => { - live2d.contentWindow.postMessage({ - type: 'moveCursor', - body: { - x: ev.clientX - iframeRect.left, - y: ev.clientY - iframeRect.top, - }, - }, '*'); - }, { passive: true }); - window.addEventListener('touchmove', ev => { - live2d.contentWindow.postMessage({ - type: 'moveCursor', - body: { - x: ev.touches[0].clientX - iframeRect.left, - y: ev.touches[0].clientY - iframeRect.top, - }, - }, '*'); - }, { passive: true }); - } -}); -</script> - -<style lang="scss" scoped> -.tray-enter-active, -.tray-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-enter-from, -.tray-leave-active { - opacity: 0; - transform: translateX(240px); -} - -.tray-back-enter-active, -.tray-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-back-enter-from, -.tray-back-leave-active { - opacity: 0; -} - -.gbhvwtnk { - $ui-font-size: 1em; - $widgets-hide-threshold: 1200px; - - min-height: 100dvh; - box-sizing: border-box; - - &.wallpaper { - background: var(--wallpaperOverlay); - //backdrop-filter: var(--blur, blur(4px)); - } - - > .columns { - display: flex; - justify-content: center; - max-width: 100%; - //margin: 32px 0; - - &.fullView { - margin: 0; - - > .sidebar { - display: none; - } - - > .widgets { - display: none; - } - - > .main { - margin: 0; - border-radius: 0; - box-shadow: none; - width: 100%; - } - } - - > .main { - min-width: 0; - width: 750px; - margin: 0 16px 0 0; - background: var(--panel); - border-left: solid 1px var(--divider); - border-right: solid 1px var(--divider); - border-radius: 0; - overflow: clip; - --margin: 12px; - } - - > .widgets { - //--panelBorder: none; - width: 300px; - margin-top: 16px; - - @media (max-width: $widgets-hide-threshold) { - display: none; - } - - &.left { - margin-right: 16px; - } - } - - > .sidebar { - margin-top: 16px; - } - - &.withGlobalHeader { - > .main { - margin-top: 0; - border: solid 1px var(--divider); - border-radius: var(--radius); - --stickyTop: var(--globalHeaderHeight); - } - - > .widgets { - --stickyTop: var(--globalHeaderHeight); - margin-top: 0; - } - } - - @media (max-width: 850px) { - margin: 0; - - > .sidebar { - border-right: solid 0.5px var(--divider); - } - - > .main { - margin: 0; - border-radius: 0; - box-shadow: none; - width: 100%; - } - } - } - - > .tray-back { - z-index: 1001; - } - - > .tray { - position: fixed; - top: 0; - right: 0; - z-index: 1001; - height: 100dvh; - padding: var(--margin); - box-sizing: border-box; - overflow: auto; - background: var(--bg); - } - - > .ivnzpscs { - position: fixed; - bottom: 0; - right: 0; - width: 300px; - height: 600px; - border: none; - pointer-events: none; - } -} -</style> diff --git a/packages/client/src/ui/classic.widgets.vue b/packages/client/src/ui/classic.widgets.vue deleted file mode 100644 index 163ec982ce..0000000000 --- a/packages/client/src/ui/classic.widgets.vue +++ /dev/null @@ -1,84 +0,0 @@ -<template> -<div class="ddiqwdnk"> - <XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> - <MkAd class="a" :prefer="['square']"/> - - <button v-if="editMode" class="_textButton edit" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ $ts.editWidgetsExit }}</button> - <button v-else class="_textButton edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ $ts.editWidgets }}</button> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import XWidgets from '@/components/MkWidgets.vue'; - -export default defineComponent({ - components: { - XWidgets, - }, - - props: { - place: { - type: String, - }, - }, - - emits: ['mounted'], - - data() { - return { - editMode: false, - }; - }, - - mounted() { - this.$emit('mounted', this.$el); - }, - - methods: { - addWidget(widget) { - this.$store.set('widgets', [{ - ...widget, - place: this.place, - }, ...this.$store.state.widgets]); - }, - - removeWidget(widget) { - this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id !== widget.id)); - }, - - updateWidget({ id, data }) { - this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { - ...w, - data, - } : w)); - }, - - updateWidgets(widgets) { - this.$store.set('widgets', [ - ...this.$store.state.widgets.filter(w => w.place !== this.place), - ...widgets, - ]); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.ddiqwdnk { - position: sticky; - height: min-content; - box-sizing: border-box; - padding-bottom: 8px; - - > .widgets, - > .a { - width: 300px; - } - - > .edit { - display: block; - margin: 16px auto; - } -} -</style> diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue deleted file mode 100644 index f3415cfd09..0000000000 --- a/packages/client/src/ui/deck.vue +++ /dev/null @@ -1,435 +0,0 @@ -<template> -<div - class="mk-deck" :class="[{ isMobile }]" -> - <XSidebar v-if="!isMobile"/> - - <div class="main"> - <XStatusBars class="statusbars"/> - <div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu"> - <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="layout.length === 0" class="intro _panel"> - <div>{{ i18n.ts._deck.introduction }}</div> - <MkButton primary class="add" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton> - <div>{{ i18n.ts._deck.introduction2 }}</div> - </div> - <div class="sideMenu"> - <div class="top"> - <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" class="_button button" @click="changeProfile"><i class="ti ti-caret-down"></i></button> - <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ti ti-trash"></i></button> - </div> - <div class="middle"> - <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="ti ti-plus"></i></button> - </div> - <div class="bottom"> - <button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ti ti-settings"></i></button> - </div> - </div> - </div> - </div> - - <div v-if="isMobile" class="buttons"> - <button class="button nav _button" @click="drawerMenuShowing = true"><i class="ti ti-menu-2"></i><span v-if="menuIndicated" class="indicator"><i class="_indicatorCircle"></i></span></button> - <button class="button home _button" @click="mainRouter.push('/')"><i class="ti ti-home"></i></button> - <button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="_indicatorCircle"></i></span></button> - <button class="button post _button" @click="os.post()"><i class="ti ti-pencil"></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 './deck/deck-store'; -import DeckColumnCore from '@/ui/deck/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'; -const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); - -mainRouter.navHook = (path, flag): boolean => { - if (flag === 'forcePage') return false; - const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main'); - if (deckStore.state.navWindow || noMainColumn) { - 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; -}); - -function showSettings() { - os.pageWindow('/settings/deck'); -} - -let columnsEl = $ref<HTMLElement>(); - -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); -}; - -document.documentElement.style.overflowY = 'hidden'; -document.documentElement.style.scrollBehavior = 'auto'; -window.addEventListener('wheel', (ev) => { - if (ev.target === columnsEl && ev.deltaX === 0) { - columnsEl.scrollLeft += ev.deltaY; - } else if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) { - columnsEl.scrollLeft += ev.deltaY; - } -}); -loadDeck(); - -function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') { - // 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> - -<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); - - --deckDividerThickness: 5px; - - display: flex; - height: 100dvh; - box-sizing: border-box; - flex: 1; - - &.isMobile { - padding-bottom: 100px; - } - - > .main { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - - > .columns { - flex: 1; - display: flex; - overflow-x: auto; - overflow-y: clip; - - &.center { - > .column:first-of-type { - margin-left: auto; - } - - > .column:last-of-type { - margin-right: auto; - } - } - - > .column { - flex-shrink: 0; - border-right: solid var(--deckDividerThickness) var(--deckDivider); - - &:first-of-type { - border-left: solid var(--deckDividerThickness) var(--deckDivider); - } - - &.folder { - display: flex; - flex-direction: column; - - > *:not(:last-of-type) { - border-bottom: solid var(--deckDividerThickness) var(--deckDivider); - } - } - } - - > .intro { - padding: 32px; - height: min-content; - text-align: center; - margin: auto; - - > .add { - margin: 1em auto; - } - } - - > .sideMenu { - flex-shrink: 0; - margin-right: 0; - margin-left: auto; - display: flex; - flex-direction: column; - justify-content: center; - width: 32px; - - > .top, > .middle, > .bottom { - > .button { - display: block; - width: 100%; - aspect-ratio: 1; - } - } - - > .top { - margin-bottom: auto; - } - - > .middle { - margin-top: auto; - margin-bottom: auto; - } - - > .bottom { - margin-top: auto; - } - } - } - } - - > .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; - height: 100dvh; - width: 240px; - box-sizing: border-box; - contain: strict; - overflow: auto; - overscroll-behavior: contain; - background: var(--navBg); - } -} -</style> diff --git a/packages/client/src/ui/deck/antenna-column.vue b/packages/client/src/ui/deck/antenna-column.vue deleted file mode 100644 index ba14530662..0000000000 --- a/packages/client/src/ui/deck/antenna-column.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header> - <i class="ti ti-antenna"></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 { updateColumn, Column } from './deck-store'; -import XTimeline from '@/components/MkTimeline.vue'; -import * as os from '@/os'; -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, - }); -} - -const menu = [{ - icon: 'ti ti-pencil', - text: i18n.ts.selectAntenna, - action: setAntenna, -}]; - -/* -function focus() { - timeline.focus(); -} - -defineExpose({ - focus, -}); -*/ -</script> - -<style lang="scss" scoped> -</style> diff --git a/packages/client/src/ui/deck/column-core.vue b/packages/client/src/ui/deck/column-core.vue deleted file mode 100644 index 30c0dc5e1c..0000000000 --- a/packages/client/src/ui/deck/column-core.vue +++ /dev/null @@ -1,34 +0,0 @@ -<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; -}>(); -</script> diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue deleted file mode 100644 index 2a99b621e6..0000000000 --- a/packages/client/src/ui/deck/column.vue +++ /dev/null @@ -1,398 +0,0 @@ -<template> -<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> -<section - v-hotkey="keymap" class="dnpfarvg _narrow_" - :class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }" - @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="ti ti-chevron-up"></i></template> - <template v-else><i class="ti ti-chevron-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="ti ti-dots"></i></button> - </header> - <div v-show="active" ref="body"> - <slot></slot> - </div> -</section> -</template> - -<script lang="ts" setup> -import { onBeforeUnmount, onMounted, provide, Ref, 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'; -import { MenuItem } from '@/types/menu'; - -provide('shouldHeaderThin', true); -provide('shouldOmitHeaderTitle', true); -provide('shouldSpacerMin', true); - -const props = withDefaults(defineProps<{ - column: Column; - isStacked?: boolean; - naked?: boolean; - indicated?: boolean; - menu?: MenuItem[]; -}>(), { - isStacked: false, - 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() { - let items = [{ - icon: 'ti ti-settings', - text: i18n.ts._deck.configureColumn, - 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); - }, - }, { - type: 'parent', - text: i18n.ts.move + '...', - icon: 'ti ti-arrows-move', - children: [{ - icon: 'ti ti-arrow-left', - text: i18n.ts._deck.swapLeft, - action: () => { - swapLeftColumn(props.column.id); - }, - }, { - icon: 'ti ti-arrow-right', - text: i18n.ts._deck.swapRight, - action: () => { - swapRightColumn(props.column.id); - }, - }, props.isStacked ? { - icon: 'ti ti-arrow-up', - text: i18n.ts._deck.swapUp, - 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, - action: () => { - stackLeftColumn(props.column.id); - }, - }, props.isStacked ? { - icon: 'ti ti-window-maximize', - text: i18n.ts._deck.popRight, - action: () => { - popRightColumn(props.column.id); - }, - } : undefined, null, { - icon: 'ti ti-trash', - text: i18n.ts.remove, - danger: true, - action: () => { - removeColumn(props.column.id); - }, - }]; - - if (props.menu) { - items.unshift(null); - items = props.menu.concat(items); - } - - 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: 40px; - - height: 100%; - overflow: hidden; - contain: strict; - - &.draghover { - &:after { - content: ""; - display: block; - position: absolute; - z-index: 1000; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--focus); - } - } - - &.dragging { - &:after { - content: ""; - display: block; - position: absolute; - z-index: 1000; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--focus); - opacity: 0.5; - } - } - - &.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; - -webkit-backdrop-filter: var(--blur, blur(10px)); - backdrop-filter: var(--blur, blur(10px)); - - > 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; - background-color: var(--bg); - } -} -</style> diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts deleted file mode 100644 index 56db7398e5..0000000000 --- a/packages/client/src/ui/deck/deck-store.ts +++ /dev/null @@ -1,296 +0,0 @@ -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'; -import { deepClone } from '@/scripts/clone'; - -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'; -}; - -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, - }, -})); - -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', []); - deckStore.set('layout', []); - 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 = deepClone(deckStore.state.layout); - layout[aX][aY] = b; - layout[bX][bY] = a; - deckStore.set('layout', layout); - saveDeck(); -} - -export function swapLeftColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const left = deckStore.state.layout[i - 1]; - if (left) { - layout[i - 1] = deckStore.state.layout[i]; - layout[i] = left; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapRightColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const right = deckStore.state.layout[i + 1]; - if (right) { - layout[i + 1] = deckStore.state.layout[i]; - layout[i] = right; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapUpColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const up = ids[i - 1]; - if (up) { - ids[i - 1] = id; - ids[i] = up; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapDownColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const down = ids[i + 1]; - if (down) { - ids[i + 1] = id; - ids[i] = down; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function stackLeftColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout[i - 1].push(id); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - saveDeck(); -} - -export function popRightColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const affected = layout[i]; - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout.splice(i + 1, 0, [id]); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - - const columns = deepClone(deckStore.state.columns); - for (const column of columns) { - if (affected.includes(column.id)) { - column.active = true; - } - } - deckStore.set('columns', columns); - - saveDeck(); -} - -export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - if (column.widgets == null) column.widgets = []; - column.widgets.unshift(widget); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = column.widgets.filter(w => w.id !== widget.id); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = widgets; - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) 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 = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const currentColumn = deepClone(deckStore.state.columns[columnIndex]); - if (currentColumn == null) return; - for (const [k, v] of Object.entries(column)) { - currentColumn[k] = v; - } - columns[columnIndex] = currentColumn; - deckStore.set('columns', columns); - saveDeck(); -} diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue deleted file mode 100644 index 75b018cacd..0000000000 --- a/packages/client/src/ui/deck/direct-column.vue +++ /dev/null @@ -1,31 +0,0 @@ -<template> -<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header><i class="ti ti-mail" 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> diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue deleted file mode 100644 index d9f3f7b4e7..0000000000 --- a/packages/client/src/ui/deck/list-column.vue +++ /dev/null @@ -1,58 +0,0 @@ -<template> -<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header> - <i class="ti ti-list"></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 { updateColumn, Column } from './deck-store'; -import XTimeline from '@/components/MkTimeline.vue'; -import * as os from '@/os'; -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, - }); -} - -const menu = [{ - icon: 'ti ti-pencil', - text: i18n.ts.selectList, - action: setList, -}]; -</script> - -<style lang="scss" scoped> -</style> diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue deleted file mode 100644 index 0c66172397..0000000000 --- a/packages/client/src/ui/deck/main-column.vue +++ /dev/null @@ -1,68 +0,0 @@ -<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: 'ti ti-window-maximize', - text: i18n.ts.openInWindow, - action: () => { - os.pageWindow(path); - }, - }], ev); -} -</script> diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue deleted file mode 100644 index 16962956a0..0000000000 --- a/packages/client/src/ui/deck/mentions-column.vue +++ /dev/null @@ -1,28 +0,0 @@ -<template> -<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header><i class="ti ti-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> diff --git a/packages/client/src/ui/deck/notifications-column.vue b/packages/client/src/ui/deck/notifications-column.vue deleted file mode 100644 index 9d133035fe..0000000000 --- a/packages/client/src/ui/deck/notifications-column.vue +++ /dev/null @@ -1,44 +0,0 @@ -<template> -<XColumn :column="column" :is-stacked="isStacked" :menu="menu" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header><i class="ti ti-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 { updateColumn, Column } from './deck-store'; -import XNotifications from '@/components/MkNotifications.vue'; -import * as os from '@/os'; -import { i18n } from '@/i18n'; - -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'); -} - -const menu = [{ - icon: 'ti ti-pencil', - text: i18n.ts.notificationSetting, - action: func, -}]; -</script> diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue deleted file mode 100644 index 49b29145ff..0000000000 --- a/packages/client/src/ui/deck/tl-column.vue +++ /dev/null @@ -1,119 +0,0 @@ -<template> -<XColumn :menu="menu" :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="ti ti-home"></i> - <i v-else-if="column.tl === 'local'" class="ti ti-messages"></i> - <i v-else-if="column.tl === 'social'" class="ti ti-share"></i> - <i v-else-if="column.tl === 'global'" class="ti ti-world"></i> - <span style="margin-left: 8px;">{{ column.name }}</span> - </template> - - <div v-if="disabled" class="iwaalbte"> - <p> - <i class="ti ti-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 { removeColumn, updateColumn, Column } from './deck-store'; -import XTimeline from '@/components/MkTimeline.vue'; -import * as os from '@/os'; -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; - } -} - -const menu = [{ - icon: 'ti ti-pencil', - text: i18n.ts.timeline, - action: setType, -}]; -</script> - -<style lang="scss" scoped> -.iwaalbte { - text-align: center; - - > p { - margin: 16px; - - &.desc { - font-size: 14px; - } - } -} -</style> diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue deleted file mode 100644 index fc61d18ff6..0000000000 --- a/packages/client/src/ui/deck/widgets-column.vue +++ /dev/null @@ -1,69 +0,0 @@ -<template> -<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)"> - <template #header><i class="ti ti-apps" 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; -} - -const menu = [{ - icon: 'ti ti-pencil', - text: i18n.ts.editWidgets, - action: func, -}]; -</script> - -<style lang="scss" scoped> -.wtdtxvec { - --margin: 8px; - --panelBorder: none; - - padding: 0 var(--margin); - - > .intro { - padding: 16px; - text-align: center; - } -} -</style> diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue deleted file mode 100644 index b91bf476e8..0000000000 --- a/packages/client/src/ui/universal.vue +++ /dev/null @@ -1,390 +0,0 @@ -<template> -<div class="dkgtipfy" :class="{ wallpaper }"> - <XSidebar v-if="!isMobile" class="sidebar"/> - - <MkStickyContainer class="contents"> - <template #header><XStatusBars :class="$style.statusbars"/></template> - <main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> - <div :class="$style.content"> - <RouterView/> - </div> - <div :class="$style.spacer"></div> - </main> - </MkStickyContainer> - - <div v-if="isDesktop" ref="widgetsEl" class="widgets"> - <XWidgets @mounted="attachSticky"/> - </div> - - <button v-if="!isDesktop && !isMobile" class="widgetButton _button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> - - <div v-if="isMobile" class="buttons"> - <button class="button nav _button" @click="drawerMenuShowing = true"><i class="ti ti-menu-2"></i><span v-if="menuIndicated" class="indicator"><i class="_indicatorCircle"></i></span></button> - <button class="button home _button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i class="ti ti-home"></i></button> - <button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="_indicatorCircle"></i></span></button> - <button class="button widget _button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> - <button class="button post _button" @click="os.post()"><i class="ti ti-pencil"></i></button> - </div> - - <transition :name="$store.state.animation ? 'menuDrawer-back' : ''"> - <div - v-if="drawerMenuShowing" - class="menuDrawer-back _modalBg" - @click="drawerMenuShowing = false" - @touchstart.passive="drawerMenuShowing = false" - ></div> - </transition> - - <transition :name="$store.state.animation ? 'menuDrawer' : ''"> - <XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/> - </transition> - - <transition :name="$store.state.animation ? 'widgetsDrawer-back' : ''"> - <div - v-if="widgetsShowing" - class="widgetsDrawer-back _modalBg" - @click="widgetsShowing = false" - @touchstart.passive="widgetsShowing = false" - ></div> - </transition> - - <transition :name="$store.state.animation ? 'widgetsDrawer' : ''"> - <XWidgets v-if="widgetsShowing" class="widgetsDrawer"/> - </transition> - - <XCommon/> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, provide, onMounted, computed, ref, watch, ComputedRef } from 'vue'; -import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config'; -import { StickySidebar } from '@/scripts/sticky-sidebar'; -import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; -import * as os from '@/os'; -import { defaultStore } from '@/store'; -import { navbarItemDef } from '@/navbar'; -import { i18n } from '@/i18n'; -import { $i } from '@/account'; -import { Router } from '@/nirax'; -import { mainRouter } from '@/router'; -import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; -import { deviceKind } from '@/scripts/device-kind'; -const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); -const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); -const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); - -const DESKTOP_THRESHOLD = 1100; -const MOBILE_THRESHOLD = 500; - -// デスクトップでウィンドウを狭くしたときモバイルUIが表示されて欲しいことはあるので deviceKind === 'desktop' の判定は行わない -const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); -const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); -window.addEventListener('resize', () => { - isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD; -}); - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -const widgetsEl = $ref<HTMLElement>(); -const widgetsShowing = $ref(false); - -provide('router', mainRouter); -provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; - } -}); - -const menuIndicated = computed(() => { - for (const def in navbarItemDef) { - if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから - if (navbarItemDef[def].indicated) return true; - } - return false; -}); - -const drawerMenuShowing = ref(false); - -mainRouter.on('change', () => { - drawerMenuShowing.value = false; -}); - -document.documentElement.style.overflowY = 'scroll'; - -if (defaultStore.state.widgets.length === 0) { - defaultStore.set('widgets', [{ - name: 'calendar', - id: 'a', place: 'right', data: {}, - }, { - name: 'notifications', - id: 'b', place: 'right', data: {}, - }, { - name: 'trends', - id: 'c', place: 'right', data: {}, - }]); -} - -onMounted(() => { - if (!isDesktop.value) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true; - }, { passive: true }); - } -}); - -const onContextmenu = (ev) => { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; - if (isLink(ev.target)) return; - if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; - if (window.getSelection()?.toString() !== '') return; - const path = mainRouter.getCurrentPath(); - os.contextMenu([{ - type: 'label', - text: path, - }, { - icon: 'ti ti-window-maximize', - text: i18n.ts.openInWindow, - action: () => { - os.pageWindow(path); - }, - }], ev); -}; - -const attachSticky = (el) => { - const sticky = new StickySidebar(widgetsEl); - window.addEventListener('scroll', () => { - sticky.calc(window.scrollY); - }, { passive: true }); -}; - -function top() { - window.scroll({ top: 0, behavior: 'smooth' }); -} - -const wallpaper = localStorage.getItem('wallpaper') != null; -</script> - -<style lang="scss" scoped> -.widgetsDrawer-enter-active, -.widgetsDrawer-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); -} -.widgetsDrawer-enter-from, -.widgetsDrawer-leave-active { - opacity: 0; - transform: translateX(240px); -} - -.widgetsDrawer-back-enter-active, -.widgetsDrawer-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.widgetsDrawer-back-enter-from, -.widgetsDrawer-back-leave-active { - opacity: 0; -} - -.menuDrawer-enter-active, -.menuDrawer-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); -} -.menuDrawer-enter-from, -.menuDrawer-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.menuDrawer-back-enter-active, -.menuDrawer-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.menuDrawer-back-enter-from, -.menuDrawer-back-leave-active { - opacity: 0; -} - -.dkgtipfy { - $ui-font-size: 1em; // TODO: どこかに集約したい - $widgets-hide-threshold: 1090px; - - min-height: 100dvh; - box-sizing: border-box; - display: flex; - - &.wallpaper { - background: var(--wallpaperOverlay); - //backdrop-filter: var(--blur, blur(4px)); - } - - > .sidebar { - border-right: solid 0.5px var(--divider); - } - - > .contents { - width: 100%; - min-width: 0; - background: var(--bg); - } - - > .widgets { - padding: 0 var(--margin); - border-left: solid 0.5px var(--divider); - background: var(--bg); - - @media (max-width: $widgets-hide-threshold) { - display: none; - } - } - - > .widgetButton { - display: block; - position: fixed; - z-index: 1000; - bottom: 32px; - right: 32px; - width: 64px; - height: 64px; - border-radius: 100%; - box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); - font-size: 22px; - background: var(--panel); - } - - > .widgetsDrawer-back { - z-index: 1001; - } - - > .widgetsDrawer { - position: fixed; - top: 0; - right: 0; - z-index: 1001; - height: 100dvh; - padding: var(--margin); - box-sizing: border-box; - overflow: auto; - overscroll-behavior: contain; - background: var(--bg); - } - - > .buttons { - position: fixed; - z-index: 1000; - bottom: 0; - left: 0; - padding: 16px 16px calc(env(safe-area-inset-bottom, 0px) + 16px) 16px; - display: flex; - 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); - - > .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; - } - } - } - } - - > .menuDrawer-back { - z-index: 1001; - } - - > .menuDrawer { - 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); - } -} -</style> - -<style lang="scss" module> -.statusbars { - position: sticky; - top: 0; - left: 0; -} - -.spacer { - $widgets-hide-threshold: 1090px; - - height: calc(env(safe-area-inset-bottom, 0px) + 96px); - - @media (min-width: ($widgets-hide-threshold + 1px)) { - display: none; - } -} -</style> diff --git a/packages/client/src/ui/universal.widgets.vue b/packages/client/src/ui/universal.widgets.vue deleted file mode 100644 index 33fb492836..0000000000 --- a/packages/client/src/ui/universal.widgets.vue +++ /dev/null @@ -1,71 +0,0 @@ -<template> -<div class="efzpzdvf"> - <XWidgets :edit="editMode" :widgets="defaultStore.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> - - <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> - <button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> -</div> -</template> - -<script lang="ts" setup> -import { onMounted } from 'vue'; -import XWidgets from '@/components/MkWidgets.vue'; -import { i18n } from '@/i18n'; -import { defaultStore } from '@/store'; - -const emit = defineEmits<{ - (ev: 'mounted', el: Element): void; -}>(); - -let editMode = $ref(false); -let rootEl = $ref<HTMLDivElement>(); - -onMounted(() => { - emit('mounted', rootEl); -}); - -function addWidget(widget) { - defaultStore.set('widgets', [{ - ...widget, - place: null, - }, ...defaultStore.state.widgets]); -} - -function removeWidget(widget) { - defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id)); -} - -function updateWidget({ id, data }) { - defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? { - ...w, - data, - } : w)); -} - -function updateWidgets(widgets) { - defaultStore.set('widgets', widgets); -} -</script> - -<style lang="scss" scoped> -.efzpzdvf { - position: sticky; - height: min-content; - min-height: 100vh; - padding: var(--margin) 0; - box-sizing: border-box; - - > * { - margin: var(--margin) 0; - width: 300px; - - &:first-child { - margin-top: 0; - } - } - - > .add { - margin: 0 auto; - } -} -</style> diff --git a/packages/client/src/ui/visitor.vue b/packages/client/src/ui/visitor.vue deleted file mode 100644 index ec9150d346..0000000000 --- a/packages/client/src/ui/visitor.vue +++ /dev/null @@ -1,19 +0,0 @@ -<template> -<DesignB/> -<XCommon/> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import DesignA from './visitor/a.vue'; -import DesignB from './visitor/b.vue'; -import XCommon from './_common_/common.vue'; - -export default defineComponent({ - components: { - XCommon, - DesignA, - DesignB, - }, -}); -</script> diff --git a/packages/client/src/ui/visitor/a.vue b/packages/client/src/ui/visitor/a.vue deleted file mode 100644 index f8db7a9d09..0000000000 --- a/packages/client/src/ui/visitor/a.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<div class="mk-app"> - <div v-if="mainRouter.currentRoute?.name === 'index'" class="banner" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> - <div> - <h1 v-if="meta"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1> - <div v-if="meta" class="about"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div class="desc" v-html="meta.description || $ts.introMisskey"></div> - </div> - <div class="action"> - <button class="_button primary" @click="signup()">{{ $ts.signup }}</button> - <button class="_button" @click="signin()">{{ $ts.login }}</button> - </div> - </div> - </div> - <div v-else class="banner-mini" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> - <div> - <h1 v-if="meta"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1> - </div> - </div> - - <div class="main"> - <div ref="contents" class="contents" :class="{ wallpaper }"> - <header v-show="mainRouter.currentRoute?.name !== 'index'" ref="header" class="header"> - <XHeader :info="pageInfo"/> - </header> - <main ref="main"> - <RouterView/> - </main> - <div class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import XHeader from './header.vue'; -import { host, instanceName } from '@/config'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import MkPagination from '@/components/MkPagination.vue'; -import MkButton from '@/components/MkButton.vue'; -import { ColdDeviceStorage } from '@/store'; -import { mainRouter } from '@/router'; - -const DESKTOP_THRESHOLD = 1100; - -export default defineComponent({ - components: { - XHeader, - MkPagination, - MkButton, - }, - - data() { - return { - host, - instanceName, - pageInfo: null, - meta: null, - narrow: window.innerWidth < 1280, - announcements: { - endpoint: 'announcements', - limit: 10, - }, - mainRouter, - isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, - }; - }, - - computed: { - keymap(): any { - return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - this.$store.set('darkMode', !this.$store.state.darkMode); - }, - 's': search, - 'h|/': this.help, - }; - }, - }, - - created() { - document.documentElement.style.overflowY = 'scroll'; - - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - }, - - mounted() { - if (!this.isDesktop) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true; - }, { passive: true }); - } - }, - - methods: { - // @ThatOneCalculator: Are these methods even used? - // I can't find references to them anywhere else in the code... - - // setParallax(el) { - // new simpleParallax(el); - // }, - - changePage(page) { - if (page == null) return; - // eslint-disable-next-line no-undef - if (page[symbols.PAGE_INFO]) { - // eslint-disable-next-line no-undef - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, - - top() { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - - help() { - window.open('https://misskey-hub.net/docs/keyboard-shortcut.md', '_blank'); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.mk-app { - min-height: 100vh; - - > .banner { - position: relative; - width: 100%; - text-align: center; - background-position: center; - background-size: cover; - - > div { - height: 100%; - background: rgba(0, 0, 0, 0.3); - - * { - color: #fff; - } - - > h1 { - margin: 0; - padding: 96px 32px 0 32px; - text-shadow: 0 0 8px black; - - > .logo { - vertical-align: bottom; - max-height: 150px; - } - } - - > .about { - padding: 32px; - max-width: 580px; - margin: 0 auto; - box-sizing: border-box; - text-shadow: 0 0 8px black; - } - - > .action { - padding-bottom: 64px; - - > button { - display: inline-block; - padding: 10px 20px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - background: var(--panel); - color: var(--fg); - - &.primary { - background: var(--accent); - color: #fff; - } - - &:first-child { - margin-right: 16px; - } - } - } - } - } - - > .banner-mini { - position: relative; - width: 100%; - text-align: center; - background-position: center; - background-size: cover; - - > div { - position: relative; - z-index: 1; - height: 100%; - background: rgba(0, 0, 0, 0.3); - - * { - color: #fff !important; - } - - > header { - - } - - > h1 { - margin: 0; - padding: 32px; - text-shadow: 0 0 8px black; - - > .logo { - vertical-align: bottom; - max-height: 100px; - } - } - } - } - - > .main { - > .contents { - position: relative; - z-index: 1; - - > .header { - position: sticky; - top: 0; - left: 0; - z-index: 1000; - } - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid var(--divider); - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } -} -</style> - -<style lang="scss"> -</style> diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue deleted file mode 100644 index 275008a8f8..0000000000 --- a/packages/client/src/ui/visitor/b.vue +++ /dev/null @@ -1,248 +0,0 @@ -<template> -<div class="mk-app"> - <a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> - - <div v-if="!narrow && !root" class="side"> - <XKanban class="kanban" full/> - </div> - - <div class="main"> - <XKanban v-if="narrow && !root" class="banner" :powered-by="root"/> - - <div class="contents"> - <XHeader v-if="!root" class="header" :info="pageInfo"/> - <main> - <RouterView/> - </main> - <div v-if="!root" class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </div> - </div> - - <transition :name="$store.state.animation ? 'tray-back' : ''"> - <div - v-if="showMenu" - class="menu-back _modalBg" - @click="showMenu = false" - @touchstart.passive="showMenu = false" - ></div> - </transition> - - <transition :name="$store.state.animation ? 'tray' : ''"> - <div v-if="showMenu" class="menu"> - <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> - <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> - <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> - <div class="action"> - <button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> - <button class="_button" @click="signin()">{{ $ts.login }}</button> - </div> - </div> - </transition> -</div> -</template> - -<script lang="ts" setup> -import { ComputedRef, onMounted, provide } from 'vue'; -import XHeader from './header.vue'; -import XKanban from './kanban.vue'; -import { host, instanceName } from '@/config'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import MkPagination from '@/components/MkPagination.vue'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import MkButton from '@/components/MkButton.vue'; -import { ColdDeviceStorage, defaultStore } from '@/store'; -import { mainRouter } from '@/router'; -import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; - -const DESKTOP_THRESHOLD = 1100; - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); - -provide('router', mainRouter); -provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; - } -}); - -const announcements = { - endpoint: 'announcements', - limit: 10, -}; -let showMenu = $ref(false); -let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); -let narrow = $ref(window.innerWidth < 1280); -let meta = $ref(); - -const keymap = $computed(() => { - return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': search, - }; -}); - -const root = $computed(() => mainRouter.currentRoute.value.name === 'index'); - -os.api('meta', { detail: true }).then(res => { - meta = res; -}); - -function signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); -} - -function signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); -} - -onMounted(() => { - if (!isDesktop) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop = true; - }, { passive: true }); - } -}); - -defineExpose({ - showMenu: $$(showMenu), -}); -</script> - -<style> -.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} -</style> - -<style lang="scss" scoped> -.tray-enter-active, -.tray-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-enter-from, -.tray-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.tray-back-enter-active, -.tray-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-back-enter-from, -.tray-back-leave-active { - opacity: 0; -} - -.mk-app { - display: flex; - min-height: 100vh; - background-position: center; - background-size: cover; - background-attachment: fixed; - - > .side { - width: 500px; - height: 100vh; - - > .kanban { - position: fixed; - top: 0; - left: 0; - width: 500px; - height: 100vh; - overflow: auto; - } - } - - > .main { - flex: 1; - min-width: 0; - - > .banner { - } - - > .contents { - position: relative; - z-index: 1; - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid var(--divider); - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } - - > .menu-back { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - } - - > .menu { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 240px; - height: 100vh; - background: var(--panel); - - > .link { - display: block; - padding: 16px; - - > .icon { - margin-right: 1em; - } - } - - > .action { - padding: 16px; - - > button { - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - - &._button { - background: var(--panel); - } - - &:first-child { - margin-bottom: 16px; - } - } - } - } -} -</style> diff --git a/packages/client/src/ui/visitor/header.vue b/packages/client/src/ui/visitor/header.vue deleted file mode 100644 index 7300b12a75..0000000000 --- a/packages/client/src/ui/visitor/header.vue +++ /dev/null @@ -1,228 +0,0 @@ -<template> -<div class="sqxihjet"> - <div v-if="narrow === false" class="wide"> - <div class="content"> - <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> - <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> - <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> - <div v-if="info" class="page active link"> - <div class="title"> - <i v-if="info.icon" class="icon" :class="info.icon"></i> - <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> - <span v-if="info.title" class="text">{{ info.title }}</span> - <MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> - </div> - <button v-if="info.action" class="_button action" @click.stop="info.action.handler"><!-- TODO --></button> - </div> - <div class="right"> - <button class="_button search" @click="search()"><i class="ti ti-search icon"></i><span>{{ $ts.search }}</span></button> - <button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button> - <button class="_button login" @click="signin()">{{ $ts.login }}</button> - </div> - </div> - </div> - <div v-else-if="narrow === true" class="narrow"> - <button class="menu _button" @click="$parent.showMenu = true"> - <i class="ti ti-menu-2 icon"></i> - </button> - <div v-if="info" class="title"> - <i v-if="info.icon" class="icon" :class="info.icon"></i> - <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> - <span v-if="info.title" class="text">{{ info.title }}</span> - <MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> - </div> - <button v-if="info && info.action" class="action _button" @click.stop="info.action.handler"> - <!-- TODO --> - </button> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import * as os from '@/os'; -import { search } from '@/scripts/search'; - -export default defineComponent({ - props: { - info: { - required: true, - }, - }, - - data() { - return { - narrow: null, - showMenu: false, - }; - }, - - mounted() { - this.narrow = this.$el.clientWidth < 1300; - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); - }, - - search, - }, -}); -</script> - -<style lang="scss" scoped> -.sqxihjet { - $height: 60px; - position: sticky; - top: 0; - left: 0; - z-index: 1000; - line-height: $height; - -webkit-backdrop-filter: var(--blur, blur(32px)); - backdrop-filter: var(--blur, blur(32px)); - background-color: var(--X16); - - > .wide { - > .content { - max-width: 1400px; - margin: 0 auto; - display: flex; - align-items: center; - - > .link { - $line: 3px; - display: inline-block; - padding: 0 16px; - line-height: $height - ($line * 2); - border-top: solid $line transparent; - border-bottom: solid $line transparent; - - > .icon { - margin-right: 0.5em; - } - - &.page { - border-bottom-color: var(--accent); - } - } - - > .page { - > .title { - display: inline-block; - vertical-align: bottom; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; - - > .icon + .text { - margin-left: 8px; - } - - > .avatar { - $size: 32px; - display: inline-block; - width: $size; - height: $size; - vertical-align: middle; - margin-right: 8px; - pointer-events: none; - } - - &._button { - &:hover { - color: var(--fgHighlighted); - } - } - - &.selected { - box-shadow: 0 -2px 0 0 var(--accent) inset; - color: var(--fgHighlighted); - } - } - - > .action { - padding: 0 0 0 16px; - } - } - - > .right { - margin-left: auto; - - > .search { - background: var(--bg); - border-radius: 999px; - width: 230px; - line-height: $height - 20px; - margin-right: 16px; - text-align: left; - - > * { - opacity: 0.7; - } - - > .icon { - padding: 0 16px; - } - } - - > .signup { - border-radius: 999px; - padding: 0 24px; - line-height: $height - 20px; - } - - > .login { - padding: 0 16px; - } - } - } - } - - > .narrow { - display: flex; - - > .menu, - > .action { - width: $height; - height: $height; - font-size: 20px; - } - - > .title { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; - text-align: center; - - > .icon + .text { - margin-left: 8px; - } - - > .avatar { - $size: 32px; - display: inline-block; - width: $size; - height: $size; - vertical-align: middle; - margin-right: 8px; - pointer-events: none; - } - } - } -} -</style> diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue deleted file mode 100644 index 51e47f277d..0000000000 --- a/packages/client/src/ui/visitor/kanban.vue +++ /dev/null @@ -1,257 +0,0 @@ -<!-- eslint-disable vue/no-v-html --> -<template> -<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ $instance.backgroundImageUrl })` }"> - <div class="back" :class="{ transparent }"></div> - <div class="contents"> - <div class="wrapper"> - <h1 v-if="meta" :class="{ full }"> - <MkA to="/" class="link"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl" alt="logo"><span v-else class="text">{{ instanceName }}</span></MkA> - </h1> - <template v-if="full"> - <div v-if="meta" class="about"> - <div class="desc" v-html="meta.description || $ts.introMisskey"></div> - </div> - <div class="action"> - <button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> - <button class="_button" @click="signin()">{{ $ts.login }}</button> - </div> - <div class="announcements panel"> - <header>{{ $ts.announcements }}</header> - <MkPagination v-slot="{items}" :pagination="announcements" class="list"> - <section v-for="announcement in items" :key="announcement.id" class="item"> - <div class="title">{{ announcement.title }}</div> - <div class="content"> - <Mfm :text="announcement.text"/> - <img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt="announcement image"/> - </div> - </section> - </MkPagination> - </div> - <div v-if="poweredBy" class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </template> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent, defineAsyncComponent } from 'vue'; -import { host, instanceName } from '@/config'; -import * as os from '@/os'; -import MkPagination from '@/components/MkPagination.vue'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import MkButton from '@/components/MkButton.vue'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - props: { - full: { - type: Boolean, - required: false, - default: false, - }, - transparent: { - type: Boolean, - required: false, - default: false, - }, - poweredBy: { - type: Boolean, - required: false, - default: false, - }, - }, - - data() { - return { - host, - instanceName, - pageInfo: null, - meta: null, - narrow: window.innerWidth < 1280, - announcements: { - endpoint: 'announcements', - limit: 10, - }, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.rwqkcmrc { - position: relative; - text-align: center; - background-position: center; - background-size: cover; - // TODO: パララックスにしたい - - > .back { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.3); - - &.transparent { - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); - } - } - - > .contents { - position: relative; - z-index: 1; - height: inherit; - overflow: auto; - - > .wrapper { - max-width: 380px; - padding: 0 16px; - box-sizing: border-box; - margin: 0 auto; - - > .panel { - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - background: rgba(0, 0, 0, 0.5); - border-radius: var(--radius); - - &, * { - color: #fff !important; - } - } - - > h1 { - display: block; - margin: 0; - padding: 32px 0 32px 0; - color: #fff; - - &.full { - padding: 64px 0 0 0; - - > .link { - > ::v-deep(.logo) { - max-height: 130px; - } - } - } - - > .link { - display: block; - - > ::v-deep(.logo) { - vertical-align: bottom; - max-height: 100px; - } - } - } - - > .about { - display: block; - margin: 24px 0; - text-align: center; - box-sizing: border-box; - text-shadow: 0 0 8px black; - color: #fff; - } - - > .action { - > button { - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - - &._button { - background: var(--panel); - } - - &:first-child { - margin-bottom: 16px; - } - } - } - - > .announcements { - margin: 32px 0; - text-align: left; - - > header { - padding: 12px 16px; - border-bottom: solid 1px rgba(255, 255, 255, 0.5); - } - - > .list { - max-height: 300px; - overflow: auto; - - > .item { - padding: 12px 16px; - - & + .item { - border-top: solid 1px rgba(255, 255, 255, 0.5); - } - - > .title { - font-weight: bold; - } - - > .content { - > img { - max-width: 100%; - } - } - } - } - } - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid rgba(255, 255, 255, 0.5); - color: #fff; - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } -} -</style> diff --git a/packages/client/src/ui/zen.vue b/packages/client/src/ui/zen.vue deleted file mode 100644 index 84c96a1dae..0000000000 --- a/packages/client/src/ui/zen.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="mk-app"> - <RouterView/> - - <XCommon/> -</div> -</template> - -<script lang="ts" setup> -import { provide, ComputedRef } from 'vue'; -import XCommon from './_common_/common.vue'; -import { mainRouter } from '@/router'; -import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; -import { instanceName } from '@/config'; - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); - -provide('router', mainRouter); -provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; - } -}); - -document.documentElement.style.overflowY = 'scroll'; -</script> - -<style lang="scss" scoped> -.mk-app { - min-height: 100dvh; - box-sizing: border-box; -} -</style> |