diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-09-22 22:53:41 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-09-22 22:53:41 +0900 |
| commit | 338793d891d1657f158cd4dc83f998e124bd7e45 (patch) | |
| tree | d47080ad4fcff61ad5eafdb8eb1e3ca997739115 /src/client | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.91.0 (diff) | |
| download | misskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.gz misskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.bz2 misskey-338793d891d1657f158cd4dc83f998e124bd7e45.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/client')
36 files changed, 1240 insertions, 495 deletions
diff --git a/src/client/account.ts b/src/client/account.ts index e469bae5a2..6e26ac1f7d 100644 --- a/src/client/account.ts +++ b/src/client/account.ts @@ -3,6 +3,7 @@ import { reactive } from 'vue'; import { apiUrl } from '@client/config'; import { waiting } from '@client/os'; import { unisonReload, reloadChannel } from '@client/scripts/unison-reload'; +import { showSuspendedDialog } from './scripts/show-suspended-dialog'; // TODO: 他のタブと永続化されたstateを同期 @@ -82,17 +83,20 @@ function fetchAccount(token): Promise<Account> { i: token }) }) + .then(res => res.json()) .then(res => { - // When failed to authenticate user - if (res.status !== 200 && res.status < 500) { - return signout(); + if (res.error) { + if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { + showSuspendedDialog().then(() => { + signout(); + }); + } else { + signout(); + } + } else { + res.token = token; + done(res); } - - // Parse response - res.json().then(i => { - i.token = token; - done(i); - }); }) .catch(fail); }); 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/mfm.ts b/src/client/components/mfm.ts index c248f934df..a228ca4b8d 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -8,6 +8,7 @@ import { concat } from '@client/../prelude/array'; import MkFormula from '@client/components/formula.vue'; import MkCode from '@client/components/code.vue'; import MkGoogle from '@client/components/google.vue'; +import MkSparkle from '@client/components/sparkle.vue'; import MkA from '@client/components/global/a.vue'; import { host } from '@client/config'; @@ -169,6 +170,19 @@ export default defineComponent({ style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : ''; break; } + case 'sparkle': { + if (!this.$store.state.animatedMfm) { + return genEl(token.children); + } + let count = token.props.args.count ? parseInt(token.props.args.count) : 10; + if (count > 100) { + count = 100; + } + const speed = token.props.args.speed ? parseFloat(token.props.args.speed) : 1; + return h(MkSparkle, { + count, speed, + }, genEl(token.children)); + } } if (style == null) { return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']); diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue index 7758dea3ae..80bfea9b07 100644 --- a/src/client/components/note-header.vue +++ b/src/client/components/note-header.vue @@ -3,10 +3,10 @@ <MkA class="name" :to="userPage(note.user)" v-user-preview="note.user.id"> <MkUserName :user="note.user"/> </MkA> - <span class="is-bot" v-if="note.user.isBot">bot</span> - <span class="username"><MkAcct :user="note.user"/></span> - <span class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></span> - <span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></span> + <div class="is-bot" v-if="note.user.isBot">bot</div> + <div class="username"><MkAcct :user="note.user"/></div> + <div class="admin" v-if="note.user.isAdmin"><i class="fas fa-bookmark"></i></div> + <div class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><i class="far fa-bookmark"></i></div> <div class="info"> <span class="mobile" v-if="note.viaMobile"><i class="fas fa-mobile-alt"></i></span> <MkA class="created-at" :to="notePage(note)"> @@ -55,6 +55,7 @@ export default defineComponent({ white-space: nowrap; > .name { + flex-shrink: 1; display: block; margin: 0 .5em 0 0; padding: 0; @@ -81,17 +82,20 @@ export default defineComponent({ > .admin, > .moderator { + flex-shrink: 0; margin-right: 0.5em; color: var(--badge); } > .username { + flex-shrink: 9999999; margin: 0 .5em 0 0; overflow: hidden; text-overflow: ellipsis; } > .info { + flex-shrink: 0; margin-left: auto; font-size: 0.9em; diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue index c83b040dd8..fbc9f0b7fd 100644 --- a/src/client/components/page-window.vue +++ b/src/client/components/page-window.vue @@ -8,7 +8,7 @@ @closed="$emit('closed')" > <template #header> - <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/> + <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()" :title-only="true"/> </template> <div class="yrolvcoq _flat_"> <component :is="component" v-bind="props" :ref="changePage"/> diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue index c051288d0a..69f527b7d6 100755 --- a/src/client/components/signin.vue +++ b/src/client/components/signin.vue @@ -54,6 +54,7 @@ import { apiUrl, host } from '@client/config'; import { byteify, hexify } from '@client/scripts/2fa'; import * as os from '@client/os'; import { login } from '@client/account'; +import { showSuspendedDialog } from '../scripts/show-suspended-dialog'; export default defineComponent({ components: { @@ -169,15 +170,7 @@ export default defineComponent({ this.signing = false; this.challengeData = res; return this.queryKey(); - }).catch(() => { - os.dialog({ - type: 'error', - text: this.$ts.signinFailed - }); - this.challengeData = null; - this.totpLogin = false; - this.signing = false; - }); + }).catch(this.loginFailed); } else { this.totpLogin = true; this.signing = false; @@ -190,14 +183,36 @@ export default defineComponent({ }).then(res => { this.$emit('login', res); this.onLogin(res); - }).catch(() => { + }).catch(this.loginFailed); + } + }, + + loginFailed(err) { + switch (err.id) { + case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { os.dialog({ type: 'error', - text: this.$ts.loginFailed + title: this.$ts.loginFailed, + text: this.$ts.noSuchUser }); - this.signing = false; - }); + break; + } + case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { + showSuspendedDialog(); + break; + } + default: { + os.dialog({ + type: 'error', + title: this.$ts.loginFailed, + text: JSON.stringify(err) + }); + } } + + this.challengeData = null; + this.totpLogin = false; + this.signing = false; }, resetPassword() { diff --git a/src/client/components/sparkle.vue b/src/client/components/sparkle.vue new file mode 100644 index 0000000000..942412b445 --- /dev/null +++ b/src/client/components/sparkle.vue @@ -0,0 +1,180 @@ +<template> +<span class="mk-sparkle"> + <span ref="content"> + <slot></slot> + </span> + <canvas ref="canvas"></canvas> +</span> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import * as os from '@client/os'; + +const sprite = new Image(); +sprite.src = "/static-assets/client/sparkle-spritesheet.png"; + + +export default defineComponent({ + props: { + count: { + type: Number, + required: true, + }, + speed: { + type: Number, + required: true, + }, + }, + data() { + return { + sprites: [0,6,13,20], + particles: [], + anim: null, + ctx: null, + }; + }, + methods: { + createSparkles(w, h, count) { + var holder = []; + + for (var i = 0; i < count; i++) { + + const color = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6); + + holder[i] = { + position: { + x: Math.floor(Math.random() * w), + y: Math.floor(Math.random() * h) + }, + style: this.sprites[ Math.floor(Math.random() * 4) ], + delta: { + x: Math.floor(Math.random() * 1000) - 500, + y: Math.floor(Math.random() * 1000) - 500 + }, + color: color, + opacity: Math.random(), + }; + + } + + return holder; + }, + draw(time) { + this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height); + this.ctx.beginPath(); + + const particleSize = Math.floor(this.fontSize / 2); + this.particles.forEach((particle) => { + var modulus = Math.floor(Math.random()*7); + + if (Math.floor(time) % modulus === 0) { + particle.style = this.sprites[ Math.floor(Math.random()*4) ]; + } + + this.ctx.save(); + this.ctx.globalAlpha = particle.opacity; + this.ctx.drawImage(sprite, particle.style, 0, 7, 7, particle.position.x, particle.position.y, particleSize, particleSize); + + this.ctx.globalCompositeOperation = "source-atop"; + this.ctx.globalAlpha = 0.5; + this.ctx.fillStyle = particle.color; + this.ctx.fillRect(particle.position.x, particle.position.y, particleSize, particleSize); + + this.ctx.restore(); + }); + this.ctx.stroke(); + }, + tick() { + this.anim = window.requestAnimationFrame((time) => { + if (!this.$refs.canvas) { + return; + } + this.particles.forEach((particle) => { + if (!particle) { + return; + } + var randX = Math.random() > Math.random() * 2; + var randY = Math.random() > Math.random() * 3; + + if (randX) { + particle.position.x += (particle.delta.x * this.speed) / 1500; + } + + if (!randY) { + particle.position.y -= (particle.delta.y * this.speed) / 800; + } + + if( particle.position.x > this.$refs.canvas.width ) { + particle.position.x = -7; + } else if (particle.position.x < -7) { + particle.position.x = this.$refs.canvas.width; + } + + if (particle.position.y > this.$refs.canvas.height) { + particle.position.y = -7; + particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width); + } else if (particle.position.y < -7) { + particle.position.y = this.$refs.canvas.height; + particle.position.x = Math.floor(Math.random() * this.$refs.canvas.width); + } + + particle.opacity -= 0.005; + + if (particle.opacity <= 0) { + particle.opacity = 1; + } + }); + + this.draw(time); + + this.tick(); + }); + }, + resize() { + if (this.$refs.content) { + const contentRect = this.$refs.content.getBoundingClientRect(); + this.fontSize = parseFloat(getComputedStyle(this.$refs.content).fontSize); + const padding = this.fontSize * 0.2; + + this.$refs.canvas.width = parseInt(contentRect.width + padding); + this.$refs.canvas.height = parseInt(contentRect.height + padding); + + this.particles = this.createSparkles(this.$refs.canvas.width, this.$refs.canvas.height, this.count); + } + }, + }, + mounted() { + this.ctx = this.$refs.canvas.getContext('2d'); + + new ResizeObserver(this.resize).observe(this.$refs.content); + + this.resize(); + this.tick(); + }, + updated() { + this.resize(); + }, + destroyed() { + window.cancelAnimationFrame(this.anim); + }, +}); +</script> + +<style lang="scss" scoped> +.mk-sparkle { + position: relative; + display: inline-block; + + > span { + display: inline-block; + } + + > canvas { + position: absolute; + top: -0.1em; + left: -0.1em; + pointer-events: none; + } +} +</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/menu.vue b/src/client/components/ui/menu.vue index d652d9b84f..26b4b04b11 100644 --- a/src/client/components/ui/menu.vue +++ b/src/client/components/ui/menu.vue @@ -41,7 +41,7 @@ </template> <script lang="ts"> -import { defineComponent, ref } from 'vue'; +import { defineComponent, ref, unref } from 'vue'; import { focusPrev, focusNext } from '@client/scripts/focus'; import contains from '@client/scripts/contains'; @@ -79,21 +79,26 @@ export default defineComponent({ }; }, }, - created() { - const items = ref(this.items.filter(item => item !== undefined)); + watch: { + items: { + handler() { + const items = ref(unref(this.items).filter(item => item !== undefined)); - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - - if (item && item.then) { // if item is Promise - items.value[i] = { type: 'pending' }; - item.then(actualItem => { - items.value[i] = actualItem; - }); - } - } + for (let i = 0; i < items.value.length; i++) { + const item = items.value[i]; + + if (item && item.then) { // if item is Promise + items.value[i] = { type: 'pending' }; + item.then(actualItem => { + items.value[i] = actualItem; + }); + } + } - this._items = items; + this._items = items; + }, + immediate: true + } }, mounted() { if (this.viaKeyboard) { 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..c15374e49b 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; @@ -215,7 +217,10 @@ if (lastVersion !== version) { try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため if (lastVersion != null && compareVersions(version, lastVersion) === 1) { - popup(import('@client/components/updated.vue'), {}, {}, 'closed'); + // ログインしてる場合だけ + if ($i) { + popup(import('@client/components/updated.vue'), {}, {}, 'closed'); + } } } catch (e) { } diff --git a/src/client/menu.ts b/src/client/menu.ts index 8e65496cf3..0a9e2b5475 100644 --- a/src/client/menu.ts +++ b/src/client/menu.ts @@ -1,9 +1,10 @@ -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { search } from '@client/scripts/search'; import * as os from '@client/os'; import { i18n } from '@client/i18n'; import { $i } from './account'; import { unisonReload } from '@client/scripts/unison-reload'; +import { router } from './router'; export const menuDef = { notifications: { @@ -58,7 +59,26 @@ export const menuDef = { title: 'lists', icon: 'fas fa-list-ul', show: computed(() => $i != null), - to: '/my/lists', + active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')), + action: (ev) => { + const items = ref([{ + type: 'pending' + }]); + os.api('users/lists/list').then(lists => { + const _items = [...lists.map(list => ({ + type: 'link', + text: list.name, + to: `/timeline/list/${list.id}` + })), null, { + type: 'link', + to: '/my/lists', + text: i18n.locale.manageLists, + icon: 'fas fa-cog', + }]; + items.value = _items; + }); + os.popupMenu(items, ev.currentTarget || ev.target); + }, }, groups: { title: 'groups', @@ -70,7 +90,26 @@ export const menuDef = { title: 'antennas', icon: 'fas fa-satellite', show: computed(() => $i != null), - to: '/my/antennas', + active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')), + action: (ev) => { + const items = ref([{ + type: 'pending' + }]); + os.api('antennas/list').then(antennas => { + const _items = [...antennas.map(antenna => ({ + type: 'link', + text: antenna.name, + to: `/timeline/antenna/${antenna.id}` + })), null, { + type: 'link', + to: '/my/antennas', + text: i18n.locale.manageAntennas, + icon: 'fas fa-cog', + }]; + items.value = _items; + }); + os.popupMenu(items, ev.currentTarget || ev.target); + }, }, mentions: { title: 'mentions', diff --git a/src/client/os.ts b/src/client/os.ts index 8125332798..7ae774dd92 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -372,7 +372,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: }); } -export function popupMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { +export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { return new Promise((resolve, reject) => { let dispose; popup(import('@client/components/ui/popup-menu.vue'), { diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue new file mode 100644 index 0000000000..425bec6987 --- /dev/null +++ b/src/client/pages/antenna-timeline.vue @@ -0,0 +1,147 @@ +<template> +<div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }"> + <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> + <div class="tl _block"> + <XTimeline ref="tl" class="tl" + :key="antennaId" + src="antenna" + :antenna="antennaId" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent, computed } from 'vue'; +import Progress from '@client/scripts/loading'; +import XTimeline from '@client/components/timeline.vue'; +import { scroll } from '@client/scripts/scroll'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + XTimeline, + }, + + props: { + antennaId: { + type: String, + required: true + } + }, + + data() { + return { + antenna: null, + queue: 0, + [symbols.PAGE_INFO]: computed(() => this.antenna ? { + title: this.antenna.name, + icon: 'fas fa-satellite', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-calendar-alt', + text: this.$ts.jumpToSpecifiedDate, + handler: this.timetravel + }, { + icon: 'fas fa-cog', + text: this.$ts.settings, + handler: this.settings + }], + } : null), + }; + }, + + computed: { + keymap(): any { + return { + 't': this.focus + }; + }, + }, + + watch: { + antennaId: { + async handler() { + this.antenna = await os.api('antennas/show', { + antennaId: this.antennaId + }); + }, + immediate: true + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + }, + + queueUpdated(q) { + this.queue = q; + }, + + top() { + scroll(this.$el, 0); + }, + + async timetravel() { + const { canceled, result: date } = await os.dialog({ + title: this.$ts.date, + input: { + type: 'date' + } + }); + if (canceled) return; + + this.$refs.tl.timetravel(new Date(date)); + }, + + settings() { + this.$router.push(`/my/antennas/${this.antennaId}`); + }, + + focus() { + (this.$refs.tl as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.tqmomfks { + padding: var(--margin); + + > .new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + + > button { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; + } + } + + > .tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; + } + + &.min-width_800px { + max-width: 800px; + margin: 0 auto; + } +} +</style> diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue new file mode 100644 index 0000000000..091c3f20a9 --- /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(--accentedBg); + border-radius: 5px; + + &.active { + background: var(--accent); + color: var(--fgOnAccent); + } + } + } + } + + > .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..8918de2338 100644 --- a/src/client/pages/emojis.vue +++ b/src/client/pages/emojis.vue @@ -1,151 +1,36 @@ <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 :class="$style.root"> + <XCategory v-if="tab === 'category'"/> </div> </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, - } - }, - - 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)); + icon: 'fas fa-laugh', + bg: 'var(--bg)', + })), + tab: 'category', } }, - - 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 lang="scss" module> +.root { + max-width: 1000px; + margin: 0 auto; } </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/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue index 95ddc1cbd1..314b5e2a5f 100644 --- a/src/client/pages/mfm-cheat-sheet.vue +++ b/src/client/pages/mfm-cheat-sheet.vue @@ -271,6 +271,16 @@ </div> </div> </div> + <div class="section _block"> + <div class="title">{{ $ts._mfm.sparkle }}</div> + <div class="content"> + <p>{{ $ts._mfm.sparkleDescription }}</p> + <div class="preview"> + <Mfm :text="preview_sparkle"/> + <MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea> + </div> + </div> + </div> </div> </template> @@ -294,7 +304,7 @@ export default defineComponent({ preview_hashtag: '#test', preview_url: `https://example.com`, preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`, - preview_emoji: `:${this.$instance.emojis[0].name}:`, + preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`, preview_bold: `**${this.$ts._mfm.dummy}**`, preview_small: `<small>${this.$ts._mfm.dummy}</small>`, preview_center: `<center>${this.$ts._mfm.dummy}</center>`, @@ -317,6 +327,7 @@ export default defineComponent({ preview_x4: `$[x4 🍮]`, preview_blur: `$[blur ${this.$ts._mfm.dummy}]`, preview_rainbow: `$[rainbow 🍮]`, + preview_sparkle: `$[sparkle 🍮]`, } }, }); 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 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> - <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="_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); - } + &.next { + margin-bottom: var(--margin); + } - &.prev { - margin-top: var(--margin); + &.prev { + margin-top: var(--margin); + } } - } - > .note { > .note { - border-radius: var(--radius); - background: var(--panel); + > .note { + border-radius: var(--radius); + background: var(--panel); + } } - } - > .clips { - > .title { - font-weight: bold; - padding: 12px; - } + > .clips { + > .title { + font-weight: bold; + padding: 12px; + } - > .item { - display: block; - padding: 16px; + > .item { + display: block; + padding: 16px; - > .description { - padding: 8px 0; - } + > .description { + padding: 8px 0; + } - > .user { - $height: 32px; - padding-top: 16px; - border-top: solid 0.5px var(--divider); - line-height: $height; + > .user { + $height: 32px; + padding-top: 16px; + border-top: solid 0.5px var(--divider); + line-height: $height; - > .avatar { - width: $height; - 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/settings/other.vue b/src/client/pages/settings/other.vue index 6857950350..21b5439041 100644 --- a/src/client/pages/settings/other.vue +++ b/src/client/pages/settings/other.vue @@ -26,7 +26,7 @@ <FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink> <FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink> - <FormLink to="./delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> + <FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink> </FormBase> </template> diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue index f54549b982..9dda82462d 100644 --- a/src/client/pages/timeline.vue +++ b/src/client/pages/timeline.vue @@ -1,31 +1,13 @@ <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" + :key="src" :src="src" - :list="list ? list.id : null" - :antenna="antenna ? antenna.id : null" - :channel="channel ? channel.id : null" :sound="true" @before="before()" @after="after()" @@ -56,19 +38,52 @@ export default defineComponent({ data() { return { src: 'home', - list: null, - antenna: null, - channel: null, - menuOpened: false, 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-list-ul', + text: this.$ts.lists, + handler: this.chooseList + }, { + icon: 'fas fa-satellite', + text: this.$ts.antennas, + handler: this.chooseAntenna + }, { + icon: 'fas fa-satellite-dish', + text: this.$ts.channel, + handler: this.chooseChannel + }, { 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(); }, }] })), }; @@ -94,32 +109,10 @@ export default defineComponent({ src() { this.showNav = false; }, - list(x) { - this.showNav = false; - if (x != null) this.antenna = null; - if (x != null) this.channel = null; - }, - antenna(x) { - this.showNav = false; - if (x != null) this.list = null; - if (x != null) this.channel = null; - }, - channel(x) { - this.showNav = false; - if (x != null) this.antenna = null; - if (x != null) this.list = null; - }, }, created() { this.src = this.$store.state.tl.src; - if (this.src === 'list') { - this.list = this.$store.state.tl.arg; - } else if (this.src === 'antenna') { - this.antenna = this.$store.state.tl.arg; - } else if (this.src === 'channel') { - this.channel = this.$store.state.tl.arg; - } }, methods: { @@ -142,12 +135,9 @@ export default defineComponent({ async chooseList(ev) { const lists = await os.api('users/lists/list'); const items = lists.map(list => ({ + type: 'link', text: list.name, - action: () => { - this.list = list; - this.src = 'list'; - this.saveSrc(); - } + to: `/timeline/list/${list.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -155,13 +145,10 @@ export default defineComponent({ async chooseAntenna(ev) { const antennas = await os.api('antennas/list'); const items = antennas.map(antenna => ({ + type: 'link', text: antenna.name, indicate: antenna.hasUnreadNote, - action: () => { - this.antenna = antenna; - this.src = 'antenna'; - this.saveSrc(); - } + to: `/timeline/antenna/${antenna.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -169,15 +156,10 @@ export default defineComponent({ async chooseChannel(ev) { const channels = await os.api('channels/followed'); const items = channels.map(channel => ({ + type: 'link', text: channel.name, indicate: channel.hasUnreadNote, - action: () => { - // NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で - //this.channel = channel; - //this.src = 'channel'; - //this.saveSrc(); - this.$router.push(`/channels/${channel.id}`); - } + to: `/channels/${channel.id}` })); os.popupMenu(items, ev.currentTarget || ev.target); }, @@ -185,10 +167,6 @@ export default defineComponent({ saveSrc() { this.$store.set('tl', { src: this.src, - arg: - this.src === 'list' ? this.list : - this.src === 'antenna' ? this.antenna : - this.channel }); }, @@ -213,6 +191,8 @@ export default defineComponent({ <style lang="scss" scoped> .cmuxhskf { + padding: var(--margin); + > .new { position: sticky; top: calc(var(--stickyTop, 0px) + 16px); @@ -227,79 +207,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-list-timeline.vue b/src/client/pages/user-list-timeline.vue new file mode 100644 index 0000000000..491fe948c1 --- /dev/null +++ b/src/client/pages/user-list-timeline.vue @@ -0,0 +1,147 @@ +<template> +<div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }"> + <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> + <div class="tl _block"> + <XTimeline ref="tl" class="tl" + :key="listId" + src="list" + :list="listId" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, defineAsyncComponent, computed } from 'vue'; +import Progress from '@client/scripts/loading'; +import XTimeline from '@client/components/timeline.vue'; +import { scroll } from '@client/scripts/scroll'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + XTimeline, + }, + + props: { + listId: { + type: String, + required: true + } + }, + + data() { + return { + list: null, + queue: 0, + [symbols.PAGE_INFO]: computed(() => this.list ? { + title: this.list.name, + icon: 'fas fa-list-ul', + bg: 'var(--bg)', + actions: [{ + icon: 'fas fa-calendar-alt', + text: this.$ts.jumpToSpecifiedDate, + handler: this.timetravel + }, { + icon: 'fas fa-cog', + text: this.$ts.settings, + handler: this.settings + }], + } : null), + }; + }, + + computed: { + keymap(): any { + return { + 't': this.focus + }; + }, + }, + + watch: { + listId: { + async handler() { + this.list = await os.api('users/lists/show', { + listId: this.listId + }); + }, + immediate: true + } + }, + + methods: { + before() { + Progress.start(); + }, + + after() { + Progress.done(); + }, + + queueUpdated(q) { + this.queue = q; + }, + + top() { + scroll(this.$el, 0); + }, + + settings() { + this.$router.push(`/my/lists/${this.listId}`); + }, + + async timetravel() { + const { canceled, result: date } = await os.dialog({ + title: this.$ts.date, + input: { + type: 'date' + } + }); + if (canceled) return; + + this.$refs.tl.timetravel(new Date(date)); + }, + + focus() { + (this.$refs.tl as any).focus(); + } + } +}); +</script> + +<style lang="scss" scoped> +.eqqrhokj { + padding: var(--margin); + + > .new { + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + + > button { + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; + } + } + + > .tl { + background: var(--bg); + border-radius: var(--radius); + overflow: clip; + } + + &.min-width_800px { + 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/router.ts b/src/client/router.ts index 225ee44e32..573f285c79 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -48,6 +48,8 @@ const defaultRoutes = [ { path: '/channels/:channelId/edit', component: page('channel-editor'), props: true }, { path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) }, { path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) }, + { path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) }, + { path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) }, { path: '/my/notifications', component: page('notifications') }, { path: '/my/favorites', component: page('favorites') }, { path: '/my/messages', component: page('messages') }, diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts new file mode 100644 index 0000000000..dde829cdae --- /dev/null +++ b/src/client/scripts/show-suspended-dialog.ts @@ -0,0 +1,10 @@ +import * as os from '@client/os'; +import { i18n } from '@client/i18n'; + +export function showSuspendedDialog() { + return os.dialog({ + type: 'error', + title: i18n.locale.yourAccountSuspendedTitle, + text: i18n.locale.yourAccountSuspendedDescription + }); +} 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..e1d5779a80 100644 --- a/src/client/themes/_dark.json5 +++ b/src/client/themes/_dark.json5 @@ -12,6 +12,7 @@ accent: '#86b300', accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', + accentedBg: ':alpha<0.15<@accent', focus: ':alpha<0.3<@accent', bg: '#000', acrylicBg: ':alpha<0.5<@bg', @@ -36,7 +37,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..87895e6406 100644 --- a/src/client/themes/_light.json5 +++ b/src/client/themes/_light.json5 @@ -12,6 +12,7 @@ accent: '#86b300', accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', + accentedBg: ':alpha<0.15<@accent', focus: ':alpha<0.3<@accent', bg: '#fff', acrylicBg: ':alpha<0.5<@bg', @@ -36,7 +37,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..1e0db9a3a1 100644 --- a/src/client/ui/_common_/header.vue +++ b/src/client/ui/_common_/header.vue @@ -1,31 +1,41 @@ <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="buttons right"> - <template v-if="info.actions && showActions"> - <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> - <button v-if="closeButton" class="_button button" @click.stop="$emit('close')" @touchstart="preventDrag" v-tooltip="$ts.close"><i class="fas fa-times"></i></button> + <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> </template> + <div class="buttons right"> + <template v-if="info && 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> + <button v-if="closeButton" class="_button button" @click.stop="$emit('close')" @touchstart="preventDrag" v-tooltip="$ts.close"><i class="fas fa-times"></i></button> + </div> </div> </template> @@ -52,24 +62,29 @@ 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 == null) return false; + 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 +100,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 +117,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 +139,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 +162,7 @@ export default defineComponent({ .fdidabkb { display: flex; - &.center { + &.slim { text-align: center; > .titleContainer { @@ -190,6 +217,7 @@ export default defineComponent({ overflow: auto; white-space: nowrap; text-align: left; + font-weight: bold; > .avatar { $size: 32px; @@ -219,6 +247,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/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue index 333d0ac392..9817a46e30 100644 --- a/src/client/ui/_common_/sidebar.vue +++ b/src/client/ui/_common_/sidebar.vue @@ -11,28 +11,28 @@ <transition name="nav"> <nav class="nav" :class="{ iconOnly, hidden }" v-show="showing"> <div> - <button class="item _button account" @click="openAccountMenu"> + <button class="item _button account" @click="openAccountMenu" v-click-anime> <MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/> </button> - <MkA class="item index" active-class="active" to="/" exact> + <MkA class="item index" active-class="active" to="/" exact v-click-anime> <i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> - <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to"> + <component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime> <i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span> <span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span> </component> </template> <div class="divider"></div> - <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance"> + <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime> <i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span> </MkA> - <button class="item _button" @click="more"> + <button class="item _button" @click="more" v-click-anime> <i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span> <span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span> </button> - <MkA class="item" active-class="active" to="/settings"> + <MkA class="item" active-class="active" to="/settings" v-click-anime> <i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span> </MkA> <button class="item _button post" @click="post"> @@ -263,24 +263,32 @@ export default defineComponent({ > .item { padding-left: 0; + padding: 18px 0; width: 100%; text-align: center; font-size: $ui-font-size * 1.1; - line-height: 3.7rem; + line-height: initial; > i, > .avatar { - margin-right: 0; + display: block; + margin: 0 auto; } > i { - left: 10px; + opacity: 0.7; } > .text { display: none; } + &:hover, &.active { + > i, > .text { + opacity: 1; + } + } + &:first-child { margin-bottom: 8px; } @@ -314,10 +322,11 @@ export default defineComponent({ height: calc(var(--vh, 1vh) * 100); box-sizing: border-box; overflow: auto; + overflow-x: clip; background: var(--navBg); > .divider { - margin: 16px 0; + margin: 16px 16px; border-top: solid 0.5px var(--divider); } @@ -326,7 +335,7 @@ export default defineComponent({ display: block; padding-left: 24px; font-size: $ui-font-size; - line-height: 3rem; + line-height: 2.85rem; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -336,6 +345,7 @@ export default defineComponent({ color: var(--navFg); > i { + position: relative; width: 32px; } @@ -359,6 +369,11 @@ export default defineComponent({ animation: blink 1s infinite; } + > .text { + position: relative; + font-size: 0.9em; + } + &:hover { text-decoration: none; color: var(--navHoverFg); @@ -368,6 +383,23 @@ export default defineComponent({ 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: 8px; + background: var(--accentedBg); + } + } + &:first-child, &:last-child { position: sticky; z-index: 1; @@ -380,14 +412,38 @@ export default defineComponent({ &:first-child { top: 0; - margin-bottom: 16px; - border-bottom: solid 0.5px var(--divider); + + &:hover, &.active { + &:before { + content: none; + } + } } &:last-child { bottom: 0; - margin-top: 16px; - border-top: solid 0.5px var(--divider); + color: var(--fgOnAccent); + + &:before { + content: ""; + display: block; + width: calc(100% - 20px); + height: calc(100% - 20px); + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accent); + } + + &:hover, &.active { + &:before { + background: var(--accentLighten); + } + } } } } 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..ec9254b697 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 { @@ -314,6 +313,7 @@ export default defineComponent({ > .widgets { padding: 0 var(--margin); border-left: solid 0.5px var(--divider); + background: var(--bg); @media (max-width: $widgets-hide-threshold) { display: none; |