diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue index eea970ec9a..395ed5d8ce 100644 --- a/src/client/components/global/avatar.vue +++ b/src/client/components/global/avatar.vue @@ -73,6 +73,22 @@ export default defineComponent({ </script> <style lang="scss" scoped> +@keyframes earwiggleleft { + from { transform: rotate(37.6deg) skew(30deg); } + 25% { transform: rotate(10deg) skew(30deg); } + 50% { transform: rotate(20deg) skew(30deg); } + 75% { transform: rotate(0deg) skew(30deg); } + to { transform: rotate(37.6deg) skew(30deg); } +} + +@keyframes earwiggleright { + from { transform: rotate(-37.6deg) skew(-30deg); } + 30% { transform: rotate(-10deg) skew(-30deg); } + 55% { transform: rotate(-20deg) skew(-30deg); } + 75% { transform: rotate(0deg) skew(-30deg); } + to { transform: rotate(-37.6deg) skew(-30deg); } +} + .eiwwqkts { position: relative; display: inline-block; @@ -132,6 +148,16 @@ export default defineComponent({ border-radius: 75% 0 75% 75%; transform: rotate(-37.5deg) skew(-30deg); } + + &:hover { + &:before { + animation: earwiggleleft 1s infinite; + } + + &:after { + animation: earwiggleright 1s infinite; + } + } } } </style> diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue index 1f3593a74a..eecf1d8be1 100644 --- a/src/client/components/ui/folder.vue +++ b/src/client/components/ui/folder.vue @@ -99,7 +99,8 @@ export default defineComponent({ z-index: 10; position: sticky; top: var(--stickyTop, 0px); - background: var(--panel); + padding: var(--x-padding); + background: var(--x-header, var(--panel)); /* TODO panelの半透明バージョンをプログラマティックに作りたい background: var(--X17); -webkit-backdrop-filter: var(--blur, blur(8px)); diff --git a/src/client/components/ui/input.vue b/src/client/components/ui/input.vue index 05ce5d3e15..a916a0b035 100644 --- a/src/client/components/ui/input.vue +++ b/src/client/components/ui/input.vue @@ -245,7 +245,7 @@ export default defineComponent({ font-size: 1em; color: var(--fg); background: var(--panel); - border: solid 1px var(--inputBorder); + border: solid 0.5px var(--inputBorder); border-radius: 6px; outline: none; box-shadow: none; diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue index 53a141f011..08ac3182a9 100644 --- a/src/client/components/ui/textarea.vue +++ b/src/client/components/ui/textarea.vue @@ -212,7 +212,7 @@ export default defineComponent({ font-size: 1em; color: var(--fg); background: var(--panel); - border: solid 1px var(--inputBorder); + border: solid 0.5px var(--inputBorder); border-radius: 6px; outline: none; box-shadow: none; diff --git a/src/client/init.ts b/src/client/init.ts index 4d2170e03f..aa9cd817c4 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -15,7 +15,7 @@ if (localStorage.getItem('accounts') != null) { import * as Sentry from '@sentry/browser'; import { Integrations } from '@sentry/tracing'; -import { computed, createApp, watch, markRaw } from 'vue'; +import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue'; import compareVersions from 'compare-versions'; import widgets from '@client/widgets'; @@ -47,6 +47,8 @@ window.onunhandledrejection = null; if (_DEV_) { console.warn('Development mode!!!'); + console.info(`vue ${vueVersion}`); + (window as any).$i = $i; (window as any).$store = defaultStore; diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue new file mode 100644 index 0000000000..0c24b06d16 --- /dev/null +++ b/src/client/pages/emojis.category.vue @@ -0,0 +1,134 @@ +<template> +<div class="driuhtrh"> + <div class="query"> + <MkInput v-model="q" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search"> + <template #prefix><i class="fas fa-search"></i></template> + </MkInput> + + <div class="tags"> + <span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> + </div> + </div> + + <MkFolder class="emojis" v-if="searchEmojis"> + <template #header>{{ $ts.searchResult }}</template> + <div class="zuvgdzyt"> + <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/> + </div> + </MkFolder> + + <MkFolder class="emojis" v-for="category in customEmojiCategories" :key="category"> + <template #header>{{ category || $ts.other }}</template> + <div class="zuvgdzyt"> + <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> + </div> + </MkFolder> +</div> +</template> + +<script lang="ts"> +import { defineComponent, computed } from 'vue'; +import MkButton from '@client/components/ui/button.vue'; +import MkInput from '@client/components/ui/input.vue'; +import MkSelect from '@client/components/ui/select.vue'; +import MkFolder from '@client/components/ui/folder.vue'; +import MkTab from '@client/components/tab.vue'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; +import { emojiCategories, emojiTags } from '@client/instance'; +import XEmoji from './emojis.emoji.vue'; + +export default defineComponent({ + components: { + MkButton, + MkInput, + MkSelect, + MkFolder, + MkTab, + XEmoji, + }, + + data() { + return { + q: '', + customEmojiCategories: emojiCategories, + customEmojis: this.$instance.emojis, + tags: emojiTags, + selectedTags: new Set(), + searchEmojis: null, + } + }, + + watch: { + q() { this.search(); }, + selectedTags: { + handler() { + this.search(); + }, + deep: true + }, + }, + + methods: { + search() { + if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) { + this.searchEmojis = null; + return; + } + + if (this.selectedTags.size === 0) { + this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q)); + } else { + this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t))); + } + }, + + toggleTag(tag) { + if (this.selectedTags.has(tag)) { + this.selectedTags.delete(tag); + } else { + this.selectedTags.add(tag); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.driuhtrh { + background: var(--bg); + + > .query { + background: var(--bg); + padding: 16px; + + > .tags { + > .tag { + display: inline-block; + margin: 8px 8px 0 0; + padding: 4px 8px; + font-size: 0.9em; + background: var(--panel); + border: solid 0.5px var(--divider); + border-radius: 5px; + + &.active { + border-color: var(--accent); + } + } + } + } + + > .emojis { + --x-header: var(--bg); + --x-padding: 0 16px; + + .zuvgdzyt { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + grid-gap: 12px; + margin: 0 var(--margin) var(--margin) var(--margin); + } + } +} +</style> diff --git a/src/client/pages/emojis.emoji.vue b/src/client/pages/emojis.emoji.vue new file mode 100644 index 0000000000..3c9bb4debe --- /dev/null +++ b/src/client/pages/emojis.emoji.vue @@ -0,0 +1,92 @@ +<template> +<button class="zuvgdzyu _button" @click="menu"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <div class="name _monospace">{{ emoji.name }}</div> + <div class="info">{{ emoji.aliases.join(' ') }}</div> + </div> +</button> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import * as os from '@client/os'; +import copyToClipboard from '@client/scripts/copy-to-clipboard'; +import VanillaTilt from 'vanilla-tilt'; + +export default defineComponent({ + props: { + emoji: { + type: Object, + required: true, + } + }, + + mounted() { + VanillaTilt.init(this.$el, { + reverse: true, + gyroscope: false, + scale: 1.1, + speed: 500, + }); + }, + + methods: { + menu(ev) { + os.popupMenu([{ + type: 'label', + text: ':' + this.emoji.name + ':', + }, { + text: this.$ts.copy, + icon: 'fas fa-copy', + action: () => { + copyToClipboard(`:${this.emoji.name}:`); + os.success(); + } + }], ev.currentTarget || ev.target); + } + } +}); +</script> + +<style lang="scss" scoped> +.zuvgdzyu { + display: flex; + align-items: center; + padding: 12px; + text-align: left; + background: var(--panel); + border-radius: 8px; + transform-style: preserve-3d; + transform: perspective(1000px); + + &:hover { + border-color: var(--accent); + } + + > .img { + width: 42px; + height: 42px; + transform: translateZ(20px); + } + + > .body { + padding: 0 0 0 8px; + white-space: nowrap; + overflow: hidden; + transform: translateZ(10px); + + > .name { + text-overflow: ellipsis; + overflow: hidden; + } + + > .info { + opacity: 0.5; + font-size: 0.9em; + text-overflow: ellipsis; + overflow: hidden; + } + } +} +</style> diff --git a/src/client/pages/emojis.vue b/src/client/pages/emojis.vue index 391aff8297..c1f87047d3 100644 --- a/src/client/pages/emojis.vue +++ b/src/client/pages/emojis.vue @@ -1,151 +1,30 @@ <template> -<div class="driuhtrh"> - <div class="query"> - <MkInput v-model="q" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search"> - <template #prefix><i class="fas fa-search"></i></template> - </MkInput> - </div> - - <div class="emojis"> - <MkFolder v-if="searchEmojis"> - <template #header>{{ $ts.searchResult }}</template> - <div class="zuvgdzyt"> - <button v-for="emoji in searchEmojis" :key="emoji.name" class="emoji _button" @click="menu(emoji, $event)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.aliases.join(' ') }}</div> - </div> - </button> - </div> - </MkFolder> - <MkFolder v-for="category in customEmojiCategories" :key="category"> - <template #header>{{ category || $ts.other }}</template> - <div class="zuvgdzyt"> - <button v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji _button" @click="menu(emoji, $event)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.aliases.join(' ') }}</div> - </div> - </button> - </div> - </MkFolder> - </div> -</div> +<XCategory v-if="tab === 'category'"/> </template> <script lang="ts"> -import { defineComponent } from 'vue'; -import MkButton from '@client/components/ui/button.vue'; -import MkInput from '@client/components/ui/input.vue'; -import MkSelect from '@client/components/ui/select.vue'; -import MkFolder from '@client/components/ui/folder.vue'; +import { defineComponent, computed } from 'vue'; import * as os from '@client/os'; import * as symbols from '@client/symbols'; -import { emojiCategories } from '@client/instance'; -import copyToClipboard from '@client/scripts/copy-to-clipboard'; +import XCategory from './emojis.category.vue'; export default defineComponent({ components: { - MkButton, - MkInput, - MkSelect, - MkFolder, + XCategory, }, data() { return { - [symbols.PAGE_INFO]: { + [symbols.PAGE_INFO]: computed(() => ({ title: this.$ts.customEmojis, - icon: 'fas fa-laugh' - }, - q: '', - customEmojiCategories: emojiCategories, - customEmojis: this.$instance.emojis, - searchEmojis: null, + icon: 'fas fa-laugh', + bg: 'var(--bg)', + })), + tab: 'category', } }, - - watch: { - q() { - if (this.q === '' || this.q == null) { - this.searchEmojis = null; - return; - } - - this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q)); - } - }, - - methods: { - menu(emoji, ev) { - os.popupMenu([{ - type: 'label', - text: ':' + emoji.name + ':', - }, { - text: this.$ts.copy, - icon: 'fas fa-copy', - action: () => { - copyToClipboard(`:${emoji.name}:`); - os.success(); - } - }], ev.currentTarget || ev.target); - } - } }); </script> <style lang="scss" scoped> -.driuhtrh { - > .query { - background: var(--bg); - padding: 16px; - } - - > .emojis { - .zuvgdzyt { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); - grid-gap: 12px; - margin: 0 var(--margin) var(--margin) var(--margin); - - > .emoji { - display: flex; - align-items: center; - padding: 12px; - text-align: left; - border: solid 1px var(--divider); - border-radius: 8px; - - &:hover { - border-color: var(--accent); - } - - > .img { - width: 42px; - height: 42px; - } - - > .body { - padding: 0 0 0 8px; - white-space: nowrap; - overflow: hidden; - - > .name { - text-overflow: ellipsis; - overflow: hidden; - } - - > .info { - opacity: 0.5; - font-size: 0.9em; - text-overflow: ellipsis; - overflow: hidden; - } - } - } - } - } -} </style> diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue index a2d61b98d9..f13723c2d1 100644 --- a/src/client/pages/favorites.vue +++ b/src/client/pages/favorites.vue @@ -22,7 +22,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.favorites, - icon: 'fas fa-star' + icon: 'fas fa-star', + bg: 'var(--bg)', }, pagination: { endpoint: 'i/favorites', diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue index 7725ca14b4..fe85d7364e 100644 --- a/src/client/pages/note.vue +++ b/src/client/pages/note.vue @@ -1,37 +1,39 @@ <template> -<div class="fcuexfpr _root"> - <transition name="fade" mode="out-in"> - <div v-if="note" class="note"> - <div class="_gap" v-if="showNext"> - <XNotes class="_content" :pagination="next" :no-gap="true"/> - </div> - - <div class="main _gap"> - <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton> - <div class="note _gap"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/> - <XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/> +<div class="fcuexfpr"> + <div class="_root"> + <transition name="fade" mode="out-in"> + <div v-if="note" class="note"> + <div class="_gap" v-if="showNext"> + <XNotes class="_content" :pagination="next" :no-gap="true"/> </div> - <div class="_content clips _gap" v-if="clips && clips.length > 0"> - <div class="title">{{ $ts.clip }}</div> - <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> - <b>{{ item.name }}</b> - <div v-if="item.description" class="description">{{ item.description }}</div> - <div class="user"> - <MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/> - </div> - </MkA> - </div> - <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton> - </div> - <div class="_gap" v-if="showPrev"> - <XNotes class="_content" :pagination="prev" :no-gap="true"/> + <div class="main _gap"> + <MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton> + <div class="note _gap"> + <MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_isolated"/> + <XNoteDetailed v-model:note="note" :key="note.id" class="_isolated note"/> + </div> + <div class="_content clips _gap" v-if="clips && clips.length > 0"> + <div class="title">{{ $ts.clip }}</div> + <MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap"> + <b>{{ item.name }}</b> + <div v-if="item.description" class="description">{{ item.description }}</div> + <div class="user"> + <MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/> + </div> + </MkA> + </div> + <MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton> + </div> + + <div class="_gap" v-if="showPrev"> + <XNotes class="_content" :pagination="prev" :no-gap="true"/> + </div> </div> - </div> - <MkError v-else-if="error" @retry="fetch()"/> - <MkLoading v-else/> - </transition> + <MkError v-else-if="error" @retry="fetch()"/> + <MkLoading v-else/> + </transition> + </div> </div> </template> @@ -63,12 +65,14 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: computed(() => this.note ? { title: this.$ts.note, + subtitle: new Date(this.note.createdAt).toLocaleString(), avatar: this.note.user, path: `/notes/${this.note.id}`, share: { title: this.$t('noteOf', { user: this.note.user.name }), text: this.note.text, }, + bg: 'var(--bg)', } : null), note: null, clips: null, @@ -149,52 +153,54 @@ export default defineComponent({ .fcuexfpr { background: var(--bg); - > .note { - > .main { - > .load { - min-width: 0; - margin: 0 auto; - border-radius: 999px; + > ._root { + > .note { + > .main { + > .load { + min-width: 0; + margin: 0 auto; + border-radius: 999px; - &.next { - margin-bottom: var(--margin); - } - - &.prev { - margin-top: var(--margin); - } - } - - > .note { - > .note { - border-radius: var(--radius); - background: var(--panel); - } - } - - > .clips { - > .title { - font-weight: bold; - padding: 12px; - } - - > .item { - display: block; - padding: 16px; - - > .description { - padding: 8px 0; + &.next { + margin-bottom: var(--margin); } - > .user { - $height: 32px; - padding-top: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; + &.prev { + margin-top: var(--margin); + } + } - > .avatar { - width: $height; - height: $height; + > .note { + > .note { + border-radius: var(--radius); + background: var(--panel); + } + } + + > .clips { + > .title { + font-weight: bold; + padding: 12px; + } + + > .item { + display: block; + padding: 16px; + + > .description { + padding: 8px 0; + } + + > .user { + $height: 32px; + padding-top: 16px; + border-top: solid 0.5px var(--divider); + line-height: $height; + + > .avatar { + width: $height; + height: $height; + } } } } diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue index 633718a90b..06f8ad3cba 100644 --- a/src/client/pages/notifications.vue +++ b/src/client/pages/notifications.vue @@ -21,6 +21,7 @@ export default defineComponent({ [symbols.PAGE_INFO]: { title: this.$ts.notifications, icon: 'fas fa-bell', + bg: 'var(--bg)', actions: [{ text: this.$ts.markAllAsRead, icon: 'fas fa-check', diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue index e7e2506020..3fb5f5f1e6 100644 --- a/src/client/pages/settings/index.vue +++ b/src/client/pages/settings/index.vue @@ -86,7 +86,8 @@ export default defineComponent({ setup(props, context) { const indexInfo = { title: i18n.locale.settings, - icon: 'fas fa-cog' + icon: 'fas fa-cog', + bg: 'var(--bg)', }; const INFO = ref(indexInfo); const page = ref(props.initialPage); diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue index f54549b982..125191223c 100644 --- a/src/client/pages/timeline.vue +++ b/src/client/pages/timeline.vue @@ -1,25 +1,10 @@ <template> <div class="cmuxhskf" v-hotkey.global="keymap" v-size="{ min: [800] }"> - <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block _isolated"/> - <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block _isolated" fixed/> - <div class="tabs"> - <div class="left"> - <button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><i class="fas fa-home"></i></button> - <button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><i class="fas fa-comments"></i></button> - <button class="_button tab" @click="() => { src = 'social'; saveSrc(); }" :class="{ active: src === 'social' }" v-tooltip="$ts._timelines.social" v-if="isLocalTimelineAvailable"><i class="fas fa-share-alt"></i></button> - <button class="_button tab" @click="() => { src = 'global'; saveSrc(); }" :class="{ active: src === 'global' }" v-tooltip="$ts._timelines.global" v-if="isGlobalTimelineAvailable"><i class="fas fa-globe"></i></button> - <span class="divider"></span> - <button class="_button tab" @click="() => { src = 'mentions'; saveSrc(); }" :class="{ active: src === 'mentions' }" v-tooltip="$ts.mentions"><i class="fas fa-at"></i><i v-if="$i.hasUnreadMentions" class="fas fa-circle i"></i></button> - <button class="_button tab" @click="() => { src = 'directs'; saveSrc(); }" :class="{ active: src === 'directs' }" v-tooltip="$ts.directNotes"><i class="fas fa-envelope"></i><i v-if="$i.hasUnreadSpecifiedNotes" class="fas fa-circle i"></i></button> - </div> - <div class="right"> - <button class="_button tab" @click="chooseChannel" :class="{ active: src === 'channel' }" v-tooltip="$ts.channel"><i class="fas fa-satellite-dish"></i><i v-if="$i.hasUnreadChannel" class="fas fa-circle i"></i></button> - <button class="_button tab" @click="chooseAntenna" :class="{ active: src === 'antenna' }" v-tooltip="$ts.antennas"><i class="fas fa-satellite"></i><i v-if="$i.hasUnreadAntenna" class="fas fa-circle i"></i></button> - <button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><i class="fas fa-list-ul"></i></button> - </div> - </div> + <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> + <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> + <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="tl"> + <div class="tl _block"> <XTimeline ref="tl" class="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" :src="src" @@ -63,12 +48,37 @@ export default defineComponent({ queue: 0, [symbols.PAGE_INFO]: computed(() => ({ title: this.$ts.timeline, - subtitle: this.src === 'local' ? this.$ts._timelines.local : this.src === 'social' ? this.$ts._timelines.social : this.src === 'global' ? this.$ts._timelines.global : this.$ts._timelines.home, icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home', + bg: 'var(--bg)', actions: [{ icon: 'fas fa-calendar-alt', text: this.$ts.jumpToSpecifiedDate, handler: this.timetravel + }], + tabs: [{ + active: this.src === 'home', + title: this.$ts._timelines.home, + icon: 'fas fa-home', + iconOnly: true, + onClick: () => { this.src = 'home'; this.saveSrc(); }, + }, { + active: this.src === 'local', + title: this.$ts._timelines.local, + icon: 'fas fa-comments', + iconOnly: true, + onClick: () => { this.src = 'local'; this.saveSrc(); }, + }, { + active: this.src === 'social', + title: this.$ts._timelines.social, + icon: 'fas fa-share-alt', + iconOnly: true, + onClick: () => { this.src = 'social'; this.saveSrc(); }, + }, { + active: this.src === 'global', + title: this.$ts._timelines.global, + icon: 'fas fa-globe', + iconOnly: true, + onClick: () => { this.src = 'global'; this.saveSrc(); }, }] })), }; @@ -213,6 +223,8 @@ export default defineComponent({ <style lang="scss" scoped> .cmuxhskf { + padding: var(--margin); + > .new { position: sticky; top: calc(var(--stickyTop, 0px) + 16px); @@ -227,79 +239,15 @@ export default defineComponent({ } } - > .tabs { - display: flex; - box-sizing: border-box; - padding: 0 8px; - white-space: nowrap; - overflow: auto; - border-bottom: solid 0.5px var(--divider); - - // 影の都合上 - position: relative; - - > .right { - margin-left: auto; - } - - > .left, > .right { - > .tab { - position: relative; - height: 50px; - padding: 0 12px; - - &:hover { - color: var(--fgHighlighted); - } - - &.active { - color: var(--fgHighlighted); - - &:after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0 auto; - width: 100%; - height: 2px; - background: var(--accent); - } - } - - > .i { - position: absolute; - top: 16px; - right: 8px; - color: var(--indicator); - font-size: 8px; - animation: blink 1s infinite; - } - } - - > .divider { - display: inline-block; - width: 1px; - height: 28px; - vertical-align: middle; - margin: 0 8px; - background: var(--divider); - } - } + > .tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; } &.min-width_800px { - > .tl { - background: var(--bg); - padding: 32px 0; - - > .tl { - max-width: 800px; - margin: 0 auto; - } - } + max-width: 800px; + margin: 0 auto; } } </style> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue index 4145c86d56..86dc7361b5 100644 --- a/src/client/pages/user/index.vue +++ b/src/client/pages/user/index.vue @@ -60,23 +60,9 @@ <XPhotos :user="user" :key="user.id" class="_gap"/> </div> <div class="main"> - <div class="nav _gap"> - <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link"> - <i class="fas fa-comment-alt icon"></i> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link"> - <i class="fas fa-paperclip icon"></i> - <span>{{ $ts.clips }}</span> - </MkA> - <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link"> - <i class="fas fa-file-alt icon"></i> - <span>{{ $ts.pages }}</span> - </MkA> - <div class="actions"> - <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button> - <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> - </div> + <div class="actions"> + <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button> + <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> </div> <template v-if="page === 'index'"> <div v-if="user.pinnedNotes.length > 0" class="_gap"> @@ -178,25 +164,6 @@ </div> <div class="contents"> - <div class="nav _gap"> - <MkA :to="userPage(user)" :class="{ active: page === 'index' }" class="link" v-click-anime> - <i class="fas fa-comment-alt icon"></i> - <span>{{ $ts.notes }}</span> - </MkA> - <MkA :to="userPage(user, 'clips')" :class="{ active: page === 'clips' }" class="link" v-click-anime> - <i class="fas fa-paperclip icon"></i> - <span>{{ $ts.clips }}</span> - </MkA> - <MkA :to="userPage(user, 'pages')" :class="{ active: page === 'pages' }" class="link" v-click-anime> - <i class="fas fa-file-alt icon"></i> - <span>{{ $ts.pages }}</span> - </MkA> - <MkA :to="userPage(user, 'gallery')" :class="{ active: page === 'gallery' }" class="link" v-click-anime> - <i class="fas fa-icons icon"></i> - <span>{{ $ts.gallery }}</span> - </MkA> - </div> - <template v-if="page === 'index'"> <div> <div v-if="user.pinnedNotes.length > 0" class="_gap"> @@ -283,6 +250,27 @@ export default defineComponent({ share: { title: this.user.name, }, + bg: 'var(--bg)', + tabs: [{ + active: this.page === 'index', + title: this.$ts.overview, + icon: 'fas fa-home', + }, { + active: this.page === 'clips', + title: this.$ts.clips, + icon: 'fas fa-paperclip', + onClick: () => { this.page = 'clips'; }, + }, { + active: this.page === 'pages', + title: this.$ts.pages, + icon: 'fas fa-file-alt', + onClick: () => { this.page = 'pages'; }, + }, { + active: this.page === 'gallery', + title: this.$ts.gallery, + icon: 'fas fa-icons', + onClick: () => { this.page = 'gallery'; }, + }] } : null), user: null, error: null, @@ -314,7 +302,7 @@ export default defineComponent({ mounted() { window.requestAnimationFrame(this.parallaxLoop); - this.narrow = this.$el.clientWidth < 1000; + this.narrow = true//this.$el.clientWidth < 1000; }, beforeUnmount() { @@ -772,37 +760,6 @@ export default defineComponent({ } > .contents { - > .nav { - display: flex; - align-items: center; - font-size: 90%; - - > .link { - flex: 1; - display: inline-block; - padding: 16px; - text-align: center; - border-bottom: solid 3px transparent; - - &:hover { - text-decoration: none; - } - - &.active { - color: var(--accent); - border-bottom-color: var(--accent); - } - - &:not(.active):hover { - color: var(--fgHighlighted); - } - - > .icon { - margin-right: 6px; - } - } - } - > .content { margin-bottom: var(--margin); } diff --git a/src/client/style.scss b/src/client/style.scss index 6ab5e796bd..0318013f60 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -245,7 +245,6 @@ hr { ._panel { background: var(--panel); border-radius: var(--radius); - border: var(--panelBorder); overflow: clip; } diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5 index ca9994d5e9..b4553ee812 100644 --- a/src/client/themes/_dark.json5 +++ b/src/client/themes/_dark.json5 @@ -36,7 +36,7 @@ navFg: '@fg', navHoverFg: ':lighten<17<@fg', navActive: '@accent', - navIndicator: '@accent', + navIndicator: '@indicator', link: '#44a4c1', hashtag: '#ff9156', mention: '@accent', diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5 index 973a6251f0..104f5a85af 100644 --- a/src/client/themes/_light.json5 +++ b/src/client/themes/_light.json5 @@ -36,7 +36,7 @@ navFg: '@fg', navHoverFg: ':darken<17<@fg', navActive: '@accent', - navIndicator: '@accent', + navIndicator: '@indicator', link: '#44a4c1', hashtag: '#ff9156', mention: '@accent', diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue index 115f70a540..eb8a1b9c0a 100644 --- a/src/client/ui/_common_/header.vue +++ b/src/client/ui/_common_/header.vue @@ -1,25 +1,35 @@ <template> -<div class="fdidabkb" :class="{ center }" :style="`--height:${height};`" :key="key"> +<div class="fdidabkb" :class="{ slim: titleOnly || narrow }" :style="`--height:${height};`" :key="key"> <transition :name="$store.state.animation ? 'header' : ''" mode="out-in" appear> <div class="buttons left" v-if="backButton"> <button class="_button button back" @click.stop="$emit('back')" @touchstart="preventDrag" v-tooltip="$ts.goBack"><i class="fas fa-chevron-left"></i></button> </div> </transition> <template v-if="info"> - <div class="titleContainer"> + <div class="titleContainer" @click="showTabsPopup"> <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"/> <div class="title"> <MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/> <div v-else-if="info.title" class="title">{{ info.title }}</div> - <div class="subtitle" v-if="info.subtitle"> + <div class="subtitle" v-if="!narrow && info.subtitle"> {{ info.subtitle }} </div> + <div class="subtitle activeTab" v-if="narrow && hasTabs"> + {{ info.tabs.find(tab => tab.active)?.title }} + <i class="chevron fas fa-chevron-down"></i> + </div> </div> </div> + <div class="tabs" v-if="!narrow"> + <button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title"> + <i v-if="tab.icon" class="icon" :class="tab.icon"></i> + <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> + </button> + </div> <div class="buttons right"> - <template v-if="info.actions && showActions"> + <template v-if="info.actions && !narrow"> <button v-for="action in info.actions" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button> </template> <button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button> @@ -52,24 +62,28 @@ export default defineComponent({ required: false, default: false, }, - center: { + titleOnly: { type: Boolean, required: false, - default: true, + default: false, }, }, data() { return { - showActions: false, + narrow: false, height: 0, key: 0, }; }, computed: { + hasTabs(): boolean { + return this.info.tabs && this.info.tabs.length > 0; + }, + shouldShowMenu() { - if (this.info.actions != null && !this.showActions) return true; + if (this.info.actions != null && this.narrow) return true; if (this.info.menu != null) return true; if (this.info.share != null) return true; if (this.menu != null) return true; @@ -85,10 +99,10 @@ export default defineComponent({ mounted() { this.height = this.$el.parentElement.offsetHeight + 'px'; - this.showActions = this.$el.parentElement.offsetWidth >= 500; + this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500; new ResizeObserver((entries, observer) => { this.height = this.$el.parentElement.offsetHeight + 'px'; - this.showActions = this.$el.parentElement.offsetWidth >= 500; + this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500; }).observe(this.$el); }, @@ -102,7 +116,7 @@ export default defineComponent({ showMenu(ev) { let menu = this.info.menu ? this.info.menu() : []; - if (!this.showActions && this.info.actions) { + if (this.narrow && this.info.actions) { menu = [...this.info.actions.map(x => ({ text: x.text, icon: x.icon, @@ -124,6 +138,18 @@ export default defineComponent({ popupMenu(menu, ev.currentTarget || ev.target); }, + showTabsPopup(ev) { + if (!this.hasTabs) return; + ev.preventDefault(); + ev.stopPropagation(); + const menu = this.info.tabs.map(tab => ({ + text: tab.title, + icon: tab.icon, + action: tab.onClick, + })); + popupMenu(menu, ev.currentTarget || ev.target); + }, + preventDrag(ev) { ev.stopPropagation(); } @@ -135,7 +161,7 @@ export default defineComponent({ .fdidabkb { display: flex; - &.center { + &.slim { text-align: center; > .titleContainer { @@ -190,6 +216,7 @@ export default defineComponent({ overflow: auto; white-space: nowrap; text-align: left; + font-weight: bold; > .avatar { $size: 32px; @@ -219,6 +246,54 @@ export default defineComponent({ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + &.activeTab { + text-align: center; + + > .chevron { + display: inline-block; + margin-left: 6px; + } + } + } + } + } + + > .tabs { + margin-left: 16px; + font-size: 0.8em; + + > .tab { + display: inline-block; + position: relative; + padding: 0 10px; + height: 100%; + font-weight: normal; + opacity: 0.7; + + &:hover { + opacity: 1; + } + + &.active { + opacity: 1; + + &:after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: 0 auto; + width: 100%; + height: 3px; + background: var(--accent); + } + } + + > .icon + .title { + margin-left: 8px; } } } diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue index eef693faef..a5ec243e9e 100644 --- a/src/client/ui/default.vue +++ b/src/client/ui/default.vue @@ -12,7 +12,7 @@ </div> </template> - <main class="main" @contextmenu.stop="onContextmenu"> + <main class="main" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }"> <header class="header" @click="onHeaderClick"> <XHeader :info="pageInfo" :back-button="true" @back="back()"/> </header> @@ -145,6 +145,15 @@ export default defineComponent({ } }, '*'); }, { passive: true }); + window.addEventListener('touchmove', ev => { + this.$refs.live2d.contentWindow.postMessage({ + type: 'moveCursor', + body: { + x: ev.touches[0].clientX - iframeRect.left, + y: ev.touches[0].clientY - iframeRect.top, + } + }, '*'); + }, { passive: true }); } }, diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue index d6cace0f41..cc754cba70 100644 --- a/src/client/ui/universal.vue +++ b/src/client/ui/universal.vue @@ -2,8 +2,8 @@ <div class="mk-app" :class="{ wallpaper }"> <XSidebar ref="nav" class="sidebar"/> - <div class="contents" ref="contents" @contextmenu.stop="onContextmenu"> - <header class="header" ref="header" @click="onHeaderClick"> + <div class="contents" ref="contents" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }"> + <header class="header" ref="header" @click="onHeaderClick" :style="{ background: pageInfo?.bg }"> <XHeader :info="pageInfo" :back-button="true" @back="back()"/> </header> <main ref="main"> @@ -258,7 +258,6 @@ export default defineComponent({ } > .sidebar { - border-right: solid 0.5px var(--divider); } > .contents { @@ -313,7 +312,8 @@ export default defineComponent({ > .widgets { padding: 0 var(--margin); - border-left: solid 0.5px var(--divider); + //border-left: solid 0.5px var(--divider); + background: var(--navBg); @media (max-width: $widgets-hide-threshold) { display: none;