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/_common_ | |
| parent | wip: retention for dashboard (diff) | |
| download | misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/ui/_common_')
| -rw-r--r-- | packages/client/src/ui/_common_/common.vue | 139 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/navbar-for-mobile.vue | 314 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/navbar.vue | 521 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/statusbar-federation.vue | 108 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/statusbar-rss.vue | 93 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/statusbar-user-list.vue | 113 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/statusbars.vue | 92 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/stream-indicator.vue | 61 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/sw-inject.ts | 35 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/upload.vue | 129 |
10 files changed, 0 insertions, 1605 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> |