summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-09-22 22:53:41 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-09-22 22:53:41 +0900
commit338793d891d1657f158cd4dc83f998e124bd7e45 (patch)
treed47080ad4fcff61ad5eafdb8eb1e3ca997739115 /src/client
parentMerge branch 'develop' (diff)
parent12.91.0 (diff)
downloadmisskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.gz
misskey-338793d891d1657f158cd4dc83f998e124bd7e45.tar.bz2
misskey-338793d891d1657f158cd4dc83f998e124bd7e45.zip
Merge branch 'develop'
Diffstat (limited to 'src/client')
-rw-r--r--src/client/account.ts22
-rw-r--r--src/client/components/global/avatar.vue26
-rw-r--r--src/client/components/mfm.ts14
-rw-r--r--src/client/components/note-header.vue12
-rw-r--r--src/client/components/page-window.vue2
-rwxr-xr-xsrc/client/components/signin.vue41
-rw-r--r--src/client/components/sparkle.vue180
-rw-r--r--src/client/components/ui/folder.vue3
-rw-r--r--src/client/components/ui/input.vue2
-rw-r--r--src/client/components/ui/menu.vue33
-rw-r--r--src/client/components/ui/textarea.vue2
-rw-r--r--src/client/init.ts9
-rw-r--r--src/client/menu.ts45
-rw-r--r--src/client/os.ts2
-rw-r--r--src/client/pages/antenna-timeline.vue147
-rw-r--r--src/client/pages/emojis.category.vue134
-rw-r--r--src/client/pages/emojis.emoji.vue92
-rw-r--r--src/client/pages/emojis.vue143
-rw-r--r--src/client/pages/favorites.vue3
-rw-r--r--src/client/pages/mfm-cheat-sheet.vue13
-rw-r--r--src/client/pages/note.vue134
-rw-r--r--src/client/pages/notifications.vue1
-rw-r--r--src/client/pages/settings/index.vue3
-rw-r--r--src/client/pages/settings/other.vue2
-rw-r--r--src/client/pages/timeline.vue198
-rw-r--r--src/client/pages/user-list-timeline.vue147
-rw-r--r--src/client/pages/user/index.vue93
-rw-r--r--src/client/router.ts2
-rw-r--r--src/client/scripts/show-suspended-dialog.ts10
-rw-r--r--src/client/style.scss1
-rw-r--r--src/client/themes/_dark.json53
-rw-r--r--src/client/themes/_light.json53
-rw-r--r--src/client/ui/_common_/header.vue110
-rw-r--r--src/client/ui/_common_/sidebar.vue86
-rw-r--r--src/client/ui/default.vue11
-rw-r--r--src/client/ui/universal.vue6
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;