Compare commits
37 Commits
13.0.0-bet
...
13.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
eba6b326fa | ||
![]() |
4c9b93a12f | ||
![]() |
dfee79f841 | ||
![]() |
962373cf06 | ||
![]() |
13aa4b64b4 | ||
![]() |
5ce56886a1 | ||
![]() |
2817ca03f5 | ||
![]() |
e633c3b84b | ||
![]() |
8524e9d735 | ||
![]() |
91ced90fb2 | ||
![]() |
2acb3917ba | ||
![]() |
dd78ac089c | ||
![]() |
10e526ba56 | ||
![]() |
7ed905f76b | ||
![]() |
5d13e2744f | ||
![]() |
1d7e0293a8 | ||
![]() |
8977d87021 | ||
![]() |
809400ff23 | ||
![]() |
4c8dbcc20d | ||
![]() |
416dcf884d | ||
![]() |
09d3ce444a | ||
![]() |
27c2ca5048 | ||
![]() |
fceeb1b108 | ||
![]() |
b442c38f41 | ||
![]() |
7c2d2676f7 | ||
![]() |
1f6a41cea7 | ||
![]() |
0d7ee20a77 | ||
![]() |
dcca2350dd | ||
![]() |
1cfdd4c41a | ||
![]() |
25f4ee7030 | ||
![]() |
5320f23017 | ||
![]() |
4ffbbbe6d8 | ||
![]() |
132e45dff4 | ||
![]() |
01652b72b3 | ||
![]() |
8b1fdb5a3b | ||
![]() |
192add376c | ||
![]() |
244ea9593a |
@@ -12,7 +12,7 @@ You should also include the user name that made the change.
|
|||||||
## 13.0.0 (unreleased)
|
## 13.0.0 (unreleased)
|
||||||
|
|
||||||
### TL;DR
|
### TL;DR
|
||||||
- New features (Play, new widgets, new charts, etc)
|
- New features (Play, new widgets, new charts, 🍪👈, etc)
|
||||||
- Rewriten backend
|
- Rewriten backend
|
||||||
- Better performance (backend and frontend)
|
- Better performance (backend and frontend)
|
||||||
- Various usability improvements
|
- Various usability improvements
|
||||||
@@ -79,13 +79,16 @@ You should also include the user name that made the change.
|
|||||||
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
||||||
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
||||||
- Client: Support remote objects in search @SoniEx2
|
- Client: Support remote objects in search @SoniEx2
|
||||||
|
- Client: user activity page @syuilo
|
||||||
- Client: add user list widget @syuilo
|
- Client: add user list widget @syuilo
|
||||||
- Client: add heatmap of daily active users to about page @syuilo
|
- Client: add heatmap of daily active users to about page @syuilo
|
||||||
- Client: introduce fluent emoji @syuilo
|
- Client: introduce fluent emoji @syuilo
|
||||||
|
- Client: add new theme @syuilo
|
||||||
- Client: show fireworks when visit user who today is birthday @syuilo
|
- Client: show fireworks when visit user who today is birthday @syuilo
|
||||||
- Client: show bot warning on screen when logged in as bot account @syuilo
|
- Client: show bot warning on screen when logged in as bot account @syuilo
|
||||||
- Client: improve overall performance of client @syuilo
|
- Client: improve overall performance of client @syuilo
|
||||||
- Client: ui tweaks @syuilo
|
- Client: ui tweaks @syuilo
|
||||||
|
- Client: clicker game @syuilo
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
||||||
@@ -96,6 +99,8 @@ You should also include the user name that made the change.
|
|||||||
- Server: アンテナの作成数上限を追加 @syuilo
|
- Server: アンテナの作成数上限を追加 @syuilo
|
||||||
- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
|
- Server: pages/likeのエラーIDが重複しているのを修正 @syuilo
|
||||||
- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
|
- Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo
|
||||||
|
- Server: Escape SQL LIKE @mei23
|
||||||
|
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
|
||||||
- Client: case insensitive emoji search @saschanaz
|
- Client: case insensitive emoji search @saschanaz
|
||||||
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
|
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
|
||||||
- Client: use proxied image for instance icon @syuilo
|
- Client: use proxied image for instance icon @syuilo
|
||||||
|
@@ -1361,6 +1361,7 @@ _widgets:
|
|||||||
userList: "ユーザーリスト"
|
userList: "ユーザーリスト"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
|
clicker: "クリッカー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.0.0-beta.28",
|
"version": "13.0.0-beta.32",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
Font Awesome Icons
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Ⓒ Font Awesome
|
|
||||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 577 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 844 B |
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 689 B |
Before Width: | Height: | Size: 772 B |
Before Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 991 B |
24
packages/backend/assets/tabler-badges/LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Tabler Icons
|
||||||
|
https://github.com/tabler/tabler-icons/blob/master/LICENSE
|
||||||
|
====
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2022 Paweł Kuna
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
BIN
packages/backend/assets/tabler-badges/antenna.png
Normal file
After Width: | Height: | Size: 516 B |
BIN
packages/backend/assets/tabler-badges/arrow-back-up.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
packages/backend/assets/tabler-badges/at.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/backend/assets/tabler-badges/chart-arrows.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
packages/backend/assets/tabler-badges/circle-check.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/backend/assets/tabler-badges/messages.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/tabler-badges/plus.png
Normal file
After Width: | Height: | Size: 414 B |
BIN
packages/backend/assets/tabler-badges/quote.png
Normal file
After Width: | Height: | Size: 1011 B |
BIN
packages/backend/assets/tabler-badges/repeat.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/backend/assets/tabler-badges/user-plus.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/tabler-badges/users.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
3
packages/backend/src/misc/sql-like-escape.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function sqlLikeEscape(s: string) {
|
||||||
|
return s.replace(/([%_])/g, '\\$1');
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.query) {
|
if (ps.query) {
|
||||||
q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' });
|
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = await q
|
const emojis = await q
|
||||||
|
@@ -5,6 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js';
|
|||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
//import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -82,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
let emojis: Emoji[];
|
let emojis: Emoji[];
|
||||||
|
|
||||||
if (ps.query) {
|
if (ps.query) {
|
||||||
//q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` });
|
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||||
//const emojis = await q.take(ps.limit).getMany();
|
//const emojis = await q.take(ps.limit).getMany();
|
||||||
|
|
||||||
emojis = await q.getMany();
|
emojis = await q.getMany();
|
||||||
|
@@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.username) {
|
if (ps.username) {
|
||||||
query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
|
query.andWhere('user.usernameLower like :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.hostname) {
|
if (ps.hostname) {
|
||||||
|
@@ -4,6 +4,7 @@ import type { InstancesRepository } from '@/models/index.js';
|
|||||||
import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
|
import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
@@ -120,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ps.host) {
|
if (ps.host) {
|
||||||
query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' });
|
query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
|
const instances = await query.take(ps.limit).skip(ps.offset).getMany();
|
||||||
|
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { HashtagsRepository } from '@/models/index.js';
|
import type { HashtagsRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['hashtags'],
|
tags: ['hashtags'],
|
||||||
@@ -37,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
|
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
|
||||||
.where('tag.name like :q', { q: ps.query.toLowerCase() + '%' })
|
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
|
||||||
.orderBy('tag.count', 'DESC')
|
.orderBy('tag.count', 'DESC')
|
||||||
.groupBy('tag.id')
|
.groupBy('tag.id')
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
|
@@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js';
|
|||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -70,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query
|
query
|
||||||
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
|
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||||
.leftJoinAndSelect('user.banner', 'banner')
|
.leftJoinAndSelect('user.banner', 'banner')
|
||||||
|
@@ -6,6 +6,7 @@ import type { User } from '@/models/entities/User.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
@@ -59,10 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
if (ps.host) {
|
if (ps.host) {
|
||||||
const q = this.usersRepository.createQueryBuilder('user')
|
const q = this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.isSuspended = FALSE')
|
.where('user.isSuspended = FALSE')
|
||||||
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
|
.andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' });
|
||||||
|
|
||||||
if (ps.username) {
|
if (ps.username) {
|
||||||
q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
|
q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
|
|
||||||
q.andWhere('user.updatedAt IS NOT NULL');
|
q.andWhere('user.updatedAt IS NOT NULL');
|
||||||
@@ -83,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
||||||
.andWhere('user.id != :meId', { meId: me.id })
|
.andWhere('user.id != :meId', { meId: me.id })
|
||||||
.andWhere('user.isSuspended = FALSE')
|
.andWhere('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where('user.updatedAt IS NULL')
|
.where('user.updatedAt IS NULL')
|
||||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||||
@@ -101,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
||||||
.andWhere('user.id != :meId', { meId: me.id })
|
.andWhere('user.id != :meId', { meId: me.id })
|
||||||
.andWhere('user.isSuspended = FALSE')
|
.andWhere('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere('user.updatedAt IS NOT NULL');
|
.andWhere('user.updatedAt IS NOT NULL');
|
||||||
|
|
||||||
otherQuery.setParameters(followingQuery.getParameters());
|
otherQuery.setParameters(followingQuery.getParameters());
|
||||||
@@ -116,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
} else {
|
} else {
|
||||||
users = await this.usersRepository.createQueryBuilder('user')
|
users = await this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.isSuspended = FALSE')
|
.where('user.isSuspended = FALSE')
|
||||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
|
||||||
.andWhere('user.updatedAt IS NOT NULL')
|
.andWhere('user.updatedAt IS NOT NULL')
|
||||||
.orderBy('user.updatedAt', 'DESC')
|
.orderBy('user.updatedAt', 'DESC')
|
||||||
.take(ps.limit - users.length)
|
.take(ps.limit - users.length)
|
||||||
|
@@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
@@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
if (isUsername) {
|
if (isUsername) {
|
||||||
const usernameQuery = this.usersRepository.createQueryBuilder('user')
|
const usernameQuery = this.usersRepository.createQueryBuilder('user')
|
||||||
.where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
|
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where('user.updatedAt IS NULL')
|
.where('user.updatedAt IS NULL')
|
||||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||||
@@ -78,11 +79,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
} else {
|
} else {
|
||||||
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
||||||
.where(new Brackets(qb => {
|
.where(new Brackets(qb => {
|
||||||
qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' });
|
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
|
|
||||||
// Also search username if it qualifies as username
|
// Also search username if it qualifies as username
|
||||||
if (this.userEntityService.validateLocalUsername(ps.query)) {
|
if (this.userEntityService.validateLocalUsername(ps.query)) {
|
||||||
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' });
|
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
@@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
if (users.length < ps.limit) {
|
if (users.length < ps.limit) {
|
||||||
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
|
||||||
.select('prof.userId')
|
.select('prof.userId')
|
||||||
.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
|
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
|
||||||
|
|
||||||
if (ps.origin === 'local') {
|
if (ps.origin === 'local') {
|
||||||
profQuery.andWhere('prof.userHost IS NULL');
|
profQuery.andWhere('prof.userHost IS NULL');
|
||||||
|
@@ -312,7 +312,7 @@ export class ClientServerService {
|
|||||||
fastify.get('/opensearch.xml', async (request, reply) => {
|
fastify.get('/opensearch.xml', async (request, reply) => {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
const name = meta.name || 'Misskey';
|
const name = meta.name ?? 'Misskey';
|
||||||
let content = '';
|
let content = '';
|
||||||
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
||||||
content += `<ShortName>${name}</ShortName>`;
|
content += `<ShortName>${name}</ShortName>`;
|
||||||
@@ -533,13 +533,12 @@ export class ClientServerService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Clip
|
// Clip
|
||||||
// TODO: 非publicなclipのハンドリング
|
|
||||||
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
|
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
|
||||||
const clip = await this.clipsRepository.findOneBy({
|
const clip = await this.clipsRepository.findOneBy({
|
||||||
id: request.params.clip,
|
id: request.params.clip,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (clip) {
|
if (clip && clip.isPublic) {
|
||||||
const _clip = await this.clipEntityService.pack(clip);
|
const _clip = await this.clipEntityService.pack(clip);
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
BIN
packages/frontend/assets/cookie.png
Normal file
After Width: | Height: | Size: 38 KiB |
@@ -33,12 +33,12 @@
|
|||||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
||||||
<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
<MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
||||||
<template #label>{{ c.title }}</template>
|
<template #label>{{ c.title }}</template>
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
@@ -56,7 +56,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: AsUiComponent;
|
component: AsUiComponent;
|
||||||
|
92
packages/frontend/src/components/MkClickerGame.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="game.ready" :class="$style.game">
|
||||||
|
<div :class="$style.cps" class="">{{ number(cps) }}cps</div>
|
||||||
|
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||||
|
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
|
||||||
|
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<MkLoading/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
||||||
|
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import * as game from '@/scripts/clicker-game';
|
||||||
|
import number from '@/filters/number';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const saveData = game.saveData;
|
||||||
|
const cookies = computed(() => saveData.value?.cookies);
|
||||||
|
let cps = $ref(0);
|
||||||
|
let prevCookies = $ref(0);
|
||||||
|
|
||||||
|
function onClick(ev: MouseEvent) {
|
||||||
|
saveData.value!.cookies++;
|
||||||
|
saveData.value!.totalCookies++;
|
||||||
|
saveData.value!.totalHandmadeCookies++;
|
||||||
|
saveData.value!.clicked++;
|
||||||
|
|
||||||
|
const x = ev.clientX;
|
||||||
|
const y = ev.clientY;
|
||||||
|
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
const diff = saveData.value!.cookies - prevCookies;
|
||||||
|
cps = diff;
|
||||||
|
prevCookies = saveData.value!.cookies;
|
||||||
|
}, 1000, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
useInterval(game.save, 1000 * 5, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await game.load();
|
||||||
|
prevCookies = saveData.value!.cookies;
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
game.save();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.game {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cps {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
</style>
|
154
packages/frontend/src/components/MkFoldableSection.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ssazuxis">
|
||||||
|
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||||
|
<div class="title"><div><slot name="header"></slot></div></div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<button class="_button">
|
||||||
|
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||||
|
<template v-else><i class="ti ti-chevron-down"></i></template>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<Transition
|
||||||
|
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||||
|
@enter="enter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
@leave="leave"
|
||||||
|
@after-leave="afterLeave"
|
||||||
|
>
|
||||||
|
<div v-show="showBody">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { miLocalStorage } from '@/local-storage';
|
||||||
|
|
||||||
|
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
expanded: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
persistKey: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bg: null,
|
||||||
|
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showBody() {
|
||||||
|
if (this.persistKey) {
|
||||||
|
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
function getParentBg(el: Element | null): string {
|
||||||
|
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
||||||
|
const bg = el.style.background || el.style.backgroundColor;
|
||||||
|
if (bg) {
|
||||||
|
return bg;
|
||||||
|
} else {
|
||||||
|
return getParentBg(el.parentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rawBg = getParentBg(this.$el);
|
||||||
|
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||||
|
bg.setAlpha(0.85);
|
||||||
|
this.bg = bg.toRgbString();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleContent(show: boolean) {
|
||||||
|
this.showBody = show;
|
||||||
|
},
|
||||||
|
|
||||||
|
enter(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = 0;
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = elementHeight + 'px';
|
||||||
|
},
|
||||||
|
afterEnter(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
},
|
||||||
|
leave(el) {
|
||||||
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
el.style.height = elementHeight + 'px';
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
},
|
||||||
|
afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||||
|
overflow-y: clip;
|
||||||
|
transition: opacity 0.5s, height 0.5s !important;
|
||||||
|
}
|
||||||
|
.folder-toggle-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.folder-toggle-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssazuxis {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> header {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--stickyTop, 0px);
|
||||||
|
padding: var(--x-padding);
|
||||||
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(20px));
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 16px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .divider {
|
||||||
|
flex: 1;
|
||||||
|
margin: auto;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: 12px 0 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.ssazuxis {
|
||||||
|
> header {
|
||||||
|
> .title {
|
||||||
|
padding: 8px 10px 8px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,161 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ssazuxis">
|
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
|
||||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
<div class="header _button" @click="toggle">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<span class="icon"><slot name="icon"></slot></span>
|
||||||
<div class="divider"></div>
|
<span class="text"><slot name="label"></slot></span>
|
||||||
<button class="_button">
|
<span class="right">
|
||||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
<span class="text"><slot name="suffix"></slot></span>
|
||||||
<template v-else><i class="ti ti-chevron-down"></i></template>
|
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||||
</button>
|
<i v-else class="ti ti-chevron-down icon"></i>
|
||||||
</header>
|
</span>
|
||||||
<Transition
|
</div>
|
||||||
:name="$store.state.animation ? 'folder-toggle' : ''"
|
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
|
||||||
@enter="enter"
|
<Transition
|
||||||
@after-enter="afterEnter"
|
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||||
@leave="leave"
|
@enter="enter"
|
||||||
@after-leave="afterLeave"
|
@after-enter="afterEnter"
|
||||||
>
|
@leave="leave"
|
||||||
<div v-show="showBody">
|
@after-leave="afterLeave"
|
||||||
<slot></slot>
|
>
|
||||||
</div>
|
<KeepAlive>
|
||||||
</Transition>
|
<div v-show="opened">
|
||||||
|
<MkSpacer :margin-min="14" :margin-max="22">
|
||||||
|
<slot></slot>
|
||||||
|
</MkSpacer>
|
||||||
|
</div>
|
||||||
|
</KeepAlive>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { nextTick, onMounted } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import { miLocalStorage } from '@/local-storage';
|
|
||||||
|
|
||||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
const props = withDefaults(defineProps<{
|
||||||
|
defaultOpen: boolean;
|
||||||
|
maxHeight: number | null;
|
||||||
|
}>(), {
|
||||||
|
defaultOpen: false,
|
||||||
|
maxHeight: null,
|
||||||
|
});
|
||||||
|
|
||||||
export default defineComponent({
|
const getBgColor = (el: HTMLElement) => {
|
||||||
props: {
|
const style = window.getComputedStyle(el);
|
||||||
expanded: {
|
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
|
||||||
type: Boolean,
|
return style.backgroundColor;
|
||||||
required: false,
|
} else {
|
||||||
default: true,
|
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||||
},
|
}
|
||||||
persistKey: {
|
};
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bg: null,
|
|
||||||
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showBody() {
|
|
||||||
if (this.persistKey) {
|
|
||||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
function getParentBg(el: Element | null): string {
|
|
||||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
|
||||||
const bg = el.style.background || el.style.backgroundColor;
|
|
||||||
if (bg) {
|
|
||||||
return bg;
|
|
||||||
} else {
|
|
||||||
return getParentBg(el.parentElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rawBg = getParentBg(this.$el);
|
|
||||||
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
|
||||||
bg.setAlpha(0.85);
|
|
||||||
this.bg = bg.toRgbString();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleContent(show: boolean) {
|
|
||||||
this.showBody = show;
|
|
||||||
},
|
|
||||||
|
|
||||||
enter(el) {
|
let rootEl = $ref<HTMLElement>();
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
let bgSame = $ref(false);
|
||||||
el.style.height = 0;
|
let opened = $ref(props.defaultOpen);
|
||||||
el.offsetHeight; // reflow
|
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
},
|
function enter(el) {
|
||||||
afterEnter(el) {
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = null;
|
el.style.height = 0;
|
||||||
},
|
el.offsetHeight; // reflow
|
||||||
leave(el) {
|
el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
}
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
el.offsetHeight; // reflow
|
function afterEnter(el) {
|
||||||
el.style.height = 0;
|
el.style.height = null;
|
||||||
},
|
}
|
||||||
afterLeave(el) {
|
|
||||||
el.style.height = null;
|
function leave(el) {
|
||||||
},
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
},
|
el.style.height = elementHeight + 'px';
|
||||||
|
el.offsetHeight; // reflow
|
||||||
|
el.style.height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterLeave(el) {
|
||||||
|
el.style.height = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (!opened) {
|
||||||
|
openedAtLeastOnce = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
opened = !opened;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
const parentBg = getBgColor(rootEl.parentElement);
|
||||||
|
const myBg = computedStyle.getPropertyValue('--panel');
|
||||||
|
bgSame = parentBg === myBg;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
transition: opacity 0.5s, height 0.5s !important;
|
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
|
||||||
}
|
}
|
||||||
.folder-toggle-enter-from {
|
.folder-toggle-enter-from, .folder-toggle-leave-to {
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.folder-toggle-leave-to {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ssazuxis {
|
.dwzlatin {
|
||||||
position: relative;
|
display: block;
|
||||||
|
|
||||||
> header {
|
> .header {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
align-items: center;
|
||||||
z-index: 10;
|
width: 100%;
|
||||||
position: sticky;
|
box-sizing: border-box;
|
||||||
top: var(--stickyTop, 0px);
|
padding: 10px 14px 10px 14px;
|
||||||
padding: var(--x-padding);
|
background: var(--buttonBg);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
border-radius: 6px;
|
||||||
backdrop-filter: var(--blur, blur(20px));
|
|
||||||
|
|
||||||
> .title {
|
&:hover {
|
||||||
display: grid;
|
text-decoration: none;
|
||||||
place-content: center;
|
background: var(--buttonHoverBg);
|
||||||
margin: 0;
|
}
|
||||||
padding: 12px 16px 12px 0;
|
|
||||||
|
|
||||||
> i {
|
&.active {
|
||||||
margin-right: 6px;
|
color: var(--accent);
|
||||||
}
|
background: var(--buttonHoverBg);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
margin-right: 0.75em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
& + .text {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .divider {
|
> .text {
|
||||||
flex: 1;
|
white-space: nowrap;
|
||||||
margin: auto;
|
text-overflow: ellipsis;
|
||||||
height: 1px;
|
overflow: hidden;
|
||||||
background: var(--divider);
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
> .right {
|
||||||
padding: 12px 0 12px 16px;
|
margin-left: auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
> .text:not(:empty) {
|
||||||
|
margin-right: 0.75em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
> .body {
|
||||||
.ssazuxis {
|
background: var(--panel);
|
||||||
> header {
|
border-radius: 0 0 6px 6px;
|
||||||
> .title {
|
container-type: inline-size;
|
||||||
padding: 8px 10px 8px 0;
|
overflow: auto;
|
||||||
}
|
|
||||||
|
&.bgSame {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opened {
|
||||||
|
> .header {
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Chart</template>
|
<template #header>Chart</template>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Active users heatmap</template>
|
<template #header>Active users heatmap</template>
|
||||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
||||||
<option value="active-users">Active users</option>
|
<option value="active-users">Active users</option>
|
||||||
@@ -48,16 +48,16 @@
|
|||||||
<div class="_panel" :class="$style.heatmap">
|
<div class="_panel" :class="$style.heatmap">
|
||||||
<MkHeatmap :src="heatmapSrc"/>
|
<MkHeatmap :src="heatmapSrc"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Retention rate</template>
|
<template #header>Retention rate</template>
|
||||||
<div class="_panel" :class="$style.retention">
|
<div class="_panel" :class="$style.retention">
|
||||||
<MkRetentionHeatmap/>
|
<MkRetentionHeatmap/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Federation</template>
|
<template #header>Federation</template>
|
||||||
<div :class="$style.federation">
|
<div :class="$style.federation">
|
||||||
<div class="pies">
|
<div class="pies">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
|||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||||
import { initChart } from '@/scripts/init-chart';
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
|
||||||
|
@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
|
|||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let content = $shallowRef<HTMLElement>();
|
let content = $shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
|
let useSendAnime = $ref(false);
|
||||||
const type = $computed<ModalTypes>(() => {
|
const type = $computed<ModalTypes>(() => {
|
||||||
if (props.preferType === 'auto') {
|
if (props.preferType === 'auto') {
|
||||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||||
@@ -76,29 +77,32 @@ const type = $computed<ModalTypes>(() => {
|
|||||||
});
|
});
|
||||||
let transitionName = $computed((() =>
|
let transitionName = $computed((() =>
|
||||||
defaultStore.state.animation
|
defaultStore.state.animation
|
||||||
? (type === 'drawer')
|
? useSendAnime
|
||||||
? 'modal-drawer'
|
? 'send'
|
||||||
: (type === 'popup')
|
: type === 'drawer'
|
||||||
? 'modal-popup'
|
? 'modal-drawer'
|
||||||
: 'modal'
|
: type === 'popup'
|
||||||
|
? 'modal-popup'
|
||||||
|
: 'modal'
|
||||||
: ''
|
: ''
|
||||||
));
|
));
|
||||||
let transitionDuration = $computed((() =>
|
let transitionDuration = $computed((() =>
|
||||||
transitionName === 'modal-popup'
|
transitionName === 'send'
|
||||||
? 100
|
? 400
|
||||||
: transitionName === 'modal'
|
: transitionName === 'modal-popup'
|
||||||
? 200
|
? 100
|
||||||
: transitionName === 'modal-drawer'
|
: transitionName === 'modal'
|
||||||
? 200
|
? 200
|
||||||
: 0
|
: transitionName === 'modal-drawer'
|
||||||
|
? 200
|
||||||
|
: 0
|
||||||
));
|
));
|
||||||
|
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
if (opts.useSendAnimation) {
|
if (opts.useSendAnimation) {
|
||||||
transitionName = 'send';
|
useSendAnime = true;
|
||||||
transitionDuration = 400;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
@@ -27,12 +27,12 @@
|
|||||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article class="article" @contextmenu.stop="onContextmenu">
|
||||||
|
@@ -25,12 +25,12 @@
|
|||||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
<article class="article" @contextmenu.stop="onContextmenu">
|
||||||
@@ -43,12 +43,12 @@
|
|||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]">
|
<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
|
||||||
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i>
|
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||||
|
@@ -9,12 +9,12 @@
|
|||||||
<MkA class="created-at" :to="notePage(note)">
|
<MkA class="created-at" :to="notePage(note)">
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
|
||||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
|
||||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
||||||
<div class="head">
|
<div v-once class="head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
||||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<XReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||||
@@ -31,37 +31,39 @@
|
|||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
||||||
</header>
|
</header>
|
||||||
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<div v-once class="content">
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
</MkA>
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
</MkA>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
</MkA>
|
||||||
<i class="ti ti-quote"></i>
|
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<i class="ti ti-quote"></i>
|
||||||
<i class="ti ti-quote"></i>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
<i class="ti ti-quote"></i>
|
||||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
</MkA>
|
||||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
||||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||||
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
<span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
<span v-if="notification.type === 'app'" class="text">
|
<span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
<span v-else-if="notification.type === 'app'" class="text">
|
||||||
</span>
|
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -69,7 +71,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
@@ -263,23 +265,25 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .text {
|
> .content {
|
||||||
white-space: nowrap;
|
> .text {
|
||||||
overflow: hidden;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
font-size: 50%;
|
font-size: 50%;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
> i:first-child {
|
> i:first-child {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> i:last-child {
|
> i:last-child {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||||
<span class="text" :class="{ up }">
|
<span class="text" :class="{ up }">+1</span>
|
||||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
reaction: string;
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
@@ -23,8 +19,8 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let up = $ref(false);
|
let up = $ref(false);
|
||||||
const zIndex = os.claimZIndex('veryLow');
|
const zIndex = os.claimZIndex('middle');
|
||||||
const angle = (90 - (Math.random() * 180)) + 'deg';
|
const angle = (45 - (Math.random() * 90)) + 'deg';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -55,10 +51,11 @@ onMounted(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: var(--accent);
|
color: #fff;
|
||||||
|
text-shadow: 0 0 6px #000;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transform: translateY(-30px);
|
transform: translateY(0px);
|
||||||
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||||
will-change: opacity, transform;
|
will-change: opacity, transform;
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
|
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
|
||||||
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
|
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
|
||||||
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
|
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
|
||||||
<span v-if="visibility === 'followers'"><i class="ti ti-lock-open"></i></span>
|
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
|
||||||
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||||
|
72
packages/frontend/src/components/MkReactionEffect.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||||
|
<span class="text" :class="{ up }">
|
||||||
|
<MkReactionIcon class="icon" :reaction="reaction"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
reaction: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>(), {
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'end'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let up = $ref(false);
|
||||||
|
const zIndex = os.claimZIndex('middle');
|
||||||
|
const angle = (90 - (Math.random() * 180)) + 'deg';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
up = true;
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
emit('end');
|
||||||
|
}, 1100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
|
||||||
|
&:global {
|
||||||
|
> .text {
|
||||||
|
display: block;
|
||||||
|
height: 1em;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) rotateZ(v-bind(angle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||||
<div class="beeadbfb">
|
<div class="beeadbfb">
|
||||||
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||||
<div class="name">{{ reaction.replace('@.', '') }}</div>
|
<div class="name">{{ reaction.replace('@.', '') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</MkTooltip>
|
</MkTooltip>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||||
<div class="bqxuuuey">
|
<div class="bqxuuuey">
|
||||||
<div class="reaction">
|
<div class="reaction">
|
||||||
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||||
<div class="name">{{ getReactionName(reaction) }}</div>
|
<div class="name">{{ getReactionName(reaction) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="users">
|
<div class="users">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import { getEmojiName } from '@/scripts/emojilist';
|
import { getEmojiName } from '@/scripts/emojilist';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
ref="buttonRef"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="hkzvhatu _button"
|
class="hkzvhatu _button"
|
||||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
<MkReactionIcon class="icon" :reaction="reaction"/>
|
||||||
<span class="count">{{ count }}</span>
|
<span class="count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
@@ -28,7 +28,7 @@ const props = defineProps<{
|
|||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const buttonRef = shallowRef<HTMLElement>();
|
const buttonEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ const toggleReaction = () => {
|
|||||||
const anime = () => {
|
const anime = () => {
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
|
|
||||||
const rect = buttonRef.value.getBoundingClientRect();
|
const rect = buttonEl.value.getBoundingClientRect();
|
||||||
const x = rect.left + (buttonRef.value.offsetWidth / 2);
|
const x = rect.left + 16;
|
||||||
const y = rect.top + (buttonRef.value.offsetHeight / 2);
|
const y = rect.top + (buttonEl.value.offsetHeight / 2);
|
||||||
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.count, (newCount, oldCount) => {
|
watch(() => props.count, (newCount, oldCount) => {
|
||||||
@@ -72,7 +72,7 @@ onMounted(() => {
|
|||||||
if (!props.isInitial) anime();
|
if (!props.isInitial) anime();
|
||||||
});
|
});
|
||||||
|
|
||||||
useTooltip(buttonRef, async (showing) => {
|
useTooltip(buttonEl, async (showing) => {
|
||||||
const reactions = await os.apiGet('notes/reactions', {
|
const reactions = await os.apiGet('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
@@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => {
|
|||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
users,
|
users,
|
||||||
count: props.count,
|
count: props.count,
|
||||||
targetElement: buttonRef.value,
|
targetElement: buttonEl.value,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
}, 100);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
<template #prefix><i class="ti ti-lock"></i></template>
|
<template #prefix><i class="ti ti-lock"></i></template>
|
||||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<template #label>{{ i18n.ts.token }}</template>
|
<template #label>{{ i18n.ts.token }}</template>
|
||||||
<template #prefix><i class="ti ti-123"></i></template>
|
<template #prefix><i class="ti ti-123"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
<button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
||||||
<div class="icon"><i class="ti ti-lock-open"></i></div>
|
<div class="icon"><i class="ti ti-lock"></i></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.followers }}</span>
|
<span>{{ i18n.ts._visibility.followers }}</span>
|
||||||
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
||||||
|
@@ -1,175 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
|
|
||||||
<div class="header _button" @click="toggle">
|
|
||||||
<span class="icon"><slot name="icon"></slot></span>
|
|
||||||
<span class="text"><slot name="label"></slot></span>
|
|
||||||
<span class="right">
|
|
||||||
<span class="text"><slot name="suffix"></slot></span>
|
|
||||||
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
|
||||||
<i v-else class="ti ti-chevron-down icon"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }">
|
|
||||||
<Transition
|
|
||||||
:name="$store.state.animation ? 'folder-toggle' : ''"
|
|
||||||
@enter="enter"
|
|
||||||
@after-enter="afterEnter"
|
|
||||||
@leave="leave"
|
|
||||||
@after-leave="afterLeave"
|
|
||||||
>
|
|
||||||
<KeepAlive>
|
|
||||||
<div v-show="opened">
|
|
||||||
<MkSpacer :margin-min="14" :margin-max="22">
|
|
||||||
<slot></slot>
|
|
||||||
</MkSpacer>
|
|
||||||
</div>
|
|
||||||
</KeepAlive>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onMounted } from 'vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
defaultOpen: boolean;
|
|
||||||
}>(), {
|
|
||||||
defaultOpen: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getBgColor = (el: HTMLElement) => {
|
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
|
|
||||||
return style.backgroundColor;
|
|
||||||
} else {
|
|
||||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let rootEl = $ref<HTMLElement>();
|
|
||||||
let bgSame = $ref(false);
|
|
||||||
let opened = $ref(props.defaultOpen);
|
|
||||||
let openedAtLeastOnce = $ref(props.defaultOpen);
|
|
||||||
|
|
||||||
function enter(el) {
|
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
|
||||||
el.style.height = 0;
|
|
||||||
el.offsetHeight; // reflow
|
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterEnter(el) {
|
|
||||||
el.style.height = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function leave(el) {
|
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
|
||||||
el.style.height = elementHeight + 'px';
|
|
||||||
el.offsetHeight; // reflow
|
|
||||||
el.style.height = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterLeave(el) {
|
|
||||||
el.style.height = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (!opened) {
|
|
||||||
openedAtLeastOnce = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
opened = !opened;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
|
||||||
const parentBg = getBgColor(rootEl.parentElement);
|
|
||||||
const myBg = computedStyle.getPropertyValue('--panel');
|
|
||||||
bgSame = parentBg === myBg;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.folder-toggle-enter-active, .folder-toggle-leave-active {
|
|
||||||
overflow-y: clip;
|
|
||||||
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
|
|
||||||
}
|
|
||||||
.folder-toggle-enter-from, .folder-toggle-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dwzlatin {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
> .header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px 14px 10px 14px;
|
|
||||||
background: var(--buttonBg);
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
background: var(--buttonHoverBg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--accent);
|
|
||||||
background: var(--buttonHoverBg);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
margin-right: 0.75em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
text-align: center;
|
|
||||||
opacity: 0.8;
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
& + .text {
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .right {
|
|
||||||
margin-left: auto;
|
|
||||||
opacity: 0.7;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
> .text:not(:empty) {
|
|
||||||
margin-right: 0.75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
container-type: inline-size;
|
|
||||||
|
|
||||||
&.bgSame {
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.opened {
|
|
||||||
> .header {
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -12,6 +12,8 @@ export default {
|
|||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
|
|
||||||
el.addEventListener('mousedown', () => {
|
el.addEventListener('mousedown', () => {
|
||||||
|
target.classList.remove('_anime_bounce');
|
||||||
|
|
||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
target.classList.add('_anime_bounce_ready');
|
target.classList.add('_anime_bounce_ready');
|
||||||
|
|
||||||
@@ -22,10 +24,10 @@ export default {
|
|||||||
|
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
target.classList.add('_anime_bounce');
|
target.classList.add('_anime_bounce');
|
||||||
|
target.classList.remove('_anime_bounce_ready');
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('animationend', () => {
|
el.addEventListener('animationend', () => {
|
||||||
target.classList.remove('_anime_bounce_ready');
|
|
||||||
target.classList.remove('_anime_bounce');
|
target.classList.remove('_anime_bounce');
|
||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
});
|
});
|
||||||
|
@@ -12,19 +12,19 @@
|
|||||||
-->
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkFolder v-if="searchEmojis" class="emojis">
|
<MkFoldableSection v-if="searchEmojis" class="emojis">
|
||||||
<template #header>{{ $ts.searchResult }}</template>
|
<template #header>{{ $ts.searchResult }}</template>
|
||||||
<div class="zuvgdzyt">
|
<div class="zuvgdzyt">
|
||||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category" class="emojis">
|
||||||
<template #header>{{ category || $ts.other }}</template>
|
<template #header>{{ category || $ts.other }}</template>
|
||||||
<div class="zuvgdzyt">
|
<div class="zuvgdzyt">
|
||||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ import XEmoji from './emojis.emoji.vue';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { emojiCategories, emojiTags } from '@/instance';
|
import { emojiCategories, emojiTags } from '@/instance';
|
||||||
@@ -44,7 +44,7 @@ export default defineComponent({
|
|||||||
MkButton,
|
MkButton,
|
||||||
MkInput,
|
MkInput,
|
||||||
MkSelect,
|
MkSelect,
|
||||||
MkFolder,
|
MkFoldableSection,
|
||||||
MkTab,
|
MkTab,
|
||||||
XEmoji,
|
XEmoji,
|
||||||
},
|
},
|
||||||
|
123
packages/frontend/src/pages/admin/federation.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><XHeader :actions="headerActions"/></template>
|
||||||
|
<MkSpacer :content-max="900">
|
||||||
|
<div class="taeiyrib">
|
||||||
|
<div class="query">
|
||||||
|
<MkInput v-model="host" :debounce="true" class="">
|
||||||
|
<template #prefix><i class="ti ti-search"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.host }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<FormSplit style="margin-top: var(--margin);">
|
||||||
|
<MkSelect v-model="state">
|
||||||
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
|
<option value="all">{{ i18n.ts.all }}</option>
|
||||||
|
<option value="federating">{{ i18n.ts.federating }}</option>
|
||||||
|
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
||||||
|
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||||
|
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||||
|
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||||
|
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSelect v-model="sort">
|
||||||
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
|
<option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="+caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="-caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
</MkSelect>
|
||||||
|
</FormSplit>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||||
|
<div class="dqokceoj">
|
||||||
|
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||||
|
<MkInstanceCardMini :instance="instance"/>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||||
|
import FormSplit from '@/components/form/split.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { dateString } from '@/filters/date';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
let host = $ref('');
|
||||||
|
let state = $ref('federating');
|
||||||
|
let sort = $ref('+pubSub');
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'federation/instances' as const,
|
||||||
|
limit: 10,
|
||||||
|
offsetMode: true,
|
||||||
|
params: computed(() => ({
|
||||||
|
sort: sort,
|
||||||
|
host: host !== '' ? host : null,
|
||||||
|
...(
|
||||||
|
state === 'federating' ? { federating: true } :
|
||||||
|
state === 'subscribing' ? { subscribing: true } :
|
||||||
|
state === 'publishing' ? { publishing: true } :
|
||||||
|
state === 'suspended' ? { suspended: true } :
|
||||||
|
state === 'blocked' ? { blocked: true } :
|
||||||
|
state === 'notResponding' ? { notResponding: true } :
|
||||||
|
{}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStatus(instance) {
|
||||||
|
if (instance.isSuspended) return 'Suspended';
|
||||||
|
if (instance.isBlocked) return 'Blocked';
|
||||||
|
if (instance.isNotResponding) return 'Error';
|
||||||
|
return 'Alive';
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
definePageMetadata(computed(() => ({
|
||||||
|
title: i18n.ts.federation,
|
||||||
|
icon: 'ti ti-whirl',
|
||||||
|
})));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.taeiyrib {
|
||||||
|
> .query {
|
||||||
|
background: var(--bg);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dqokceoj {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
|
|
||||||
|
> .instance:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -104,7 +104,7 @@ const menuDef = $computed(() => [{
|
|||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-whirl',
|
icon: 'ti ti-whirl',
|
||||||
text: i18n.ts.federation,
|
text: i18n.ts.federation,
|
||||||
to: '/about#federation',
|
to: '/admin/federation',
|
||||||
active: currentPage?.route.name === 'federation',
|
active: currentPage?.route.name === 'federation',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-clock-play',
|
icon: 'ti ti-clock-play',
|
||||||
|
@@ -4,24 +4,24 @@
|
|||||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-brand-twitter"></i></template>
|
<template #icon><i class="ti ti-brand-twitter"></i></template>
|
||||||
<template #label>Twitter</template>
|
<template #label>Twitter</template>
|
||||||
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||||
<XTwitter/>
|
<XTwitter/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-brand-github"></i></template>
|
<template #icon><i class="ti ti-brand-github"></i></template>
|
||||||
<template #label>GitHub</template>
|
<template #label>GitHub</template>
|
||||||
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||||
<XGithub/>
|
<XGithub/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-brand-discord"></i></template>
|
<template #icon><i class="ti ti-brand-discord"></i></template>
|
||||||
<template #label>Discord</template>
|
<template #label>Discord</template>
|
||||||
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||||
<XDiscord/>
|
<XDiscord/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@@ -34,7 +34,7 @@ import XTwitter from './integrations.twitter.vue';
|
|||||||
import XGithub from './integrations.github.vue';
|
import XGithub from './integrations.github.vue';
|
||||||
import XDiscord from './integrations.discord.vue';
|
import XDiscord from './integrations.discord.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
@@ -1,60 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkSpacer :content-max="1000">
|
<MkSpacer :content-max="1000">
|
||||||
<div ref="rootEl" class="edbbcaef">
|
<div ref="rootEl" class="edbbcaef">
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Stats</template>
|
<template #header>Stats</template>
|
||||||
<XStats/>
|
<XStats/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Active users</template>
|
<template #header>Active users</template>
|
||||||
<XActiveUsers/>
|
<XActiveUsers/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Heatmap</template>
|
<template #header>Heatmap</template>
|
||||||
<XHeatmap/>
|
<XHeatmap/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Retention rate</template>
|
<template #header>Retention rate</template>
|
||||||
<XRetention/>
|
<XRetention/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Moderators</template>
|
<template #header>Moderators</template>
|
||||||
<XModerators/>
|
<XModerators/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Federation</template>
|
<template #header>Federation</template>
|
||||||
<XFederation/>
|
<XFederation/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Instances</template>
|
<template #header>Instances</template>
|
||||||
<XInstances/>
|
<XInstances/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Ap requests</template>
|
<template #header>Ap requests</template>
|
||||||
<XApRequests/>
|
<XApRequests/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>New users</template>
|
<template #header>New users</template>
|
||||||
<XUsers/>
|
<XUsers/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Deliver queue</template>
|
<template #header>Deliver queue</template>
|
||||||
<XQueue domain="deliver"/>
|
<XQueue domain="deliver"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Inbox queue</template>
|
<template #header>Inbox queue</template>
|
||||||
<XQueue domain="inbox"/>
|
<XQueue domain="inbox"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,7 +79,7 @@ import { i18n } from '@/i18n';
|
|||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
|
|
||||||
const rootEl = $shallowRef<HTMLElement>();
|
const rootEl = $shallowRef<HTMLElement>();
|
||||||
let serverInfo: any = $ref(null);
|
let serverInfo: any = $ref(null);
|
||||||
|
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pumxzjhg">
|
<div class="pumxzjhg _gaps">
|
||||||
<div class="_table status">
|
<div :class="$style.status">
|
||||||
<div class="_row">
|
<div class="item _panel"><div class="label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
<div class="item _panel"><div class="label">Active</div>{{ number(active) }}</div>
|
||||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
<div class="item _panel"><div class="label">Waiting</div>{{ number(waiting) }}</div>
|
||||||
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
<div class="item _panel"><div class="label">Delayed</div>{{ number(delayed) }}</div>
|
||||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
@@ -26,15 +24,21 @@
|
|||||||
<XChart ref="chartWaiting" type="waiting"/>
|
<XChart ref="chartWaiting" type="waiting"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jobs">
|
<MkFolder :default-open="true" :max-height="250">
|
||||||
<div v-if="jobs.length > 0">
|
<template #icon><i class="ti ti-alert-triangle"></i></template>
|
||||||
<div v-for="job in jobs" :key="job[0]">
|
<template #label>Errored instances</template>
|
||||||
<span>{{ job[0] }}</span>
|
<template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template>
|
||||||
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
|
||||||
|
<div :class="$style.jobs">
|
||||||
|
<div v-if="jobs.length > 0">
|
||||||
|
<div v-for="job in jobs" :key="job[0]">
|
||||||
|
<MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA>
|
||||||
|
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span>
|
</MkFolder>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,6 +49,7 @@ import number from '@/filters/number';
|
|||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const connection = markRaw(stream.useChannel('queueStats'));
|
const connection = markRaw(stream.useChannel('queueStats'));
|
||||||
|
|
||||||
@@ -115,14 +120,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.pumxzjhg {
|
.pumxzjhg {
|
||||||
> .status {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .charts {
|
> .charts {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
|
|
||||||
> .chart {
|
> .chart {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -135,15 +136,27 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
> .jobs {
|
</style>
|
||||||
margin-top: 16px;
|
|
||||||
padding: 16px;
|
<style lang="scss" module>
|
||||||
max-height: 180px;
|
.status {
|
||||||
overflow: auto;
|
display: grid;
|
||||||
background: var(--panel);
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
border-radius: var(--radius);
|
grid-gap: 10px;
|
||||||
}
|
|
||||||
|
&:global {
|
||||||
|
> .item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
font-size: 80%;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs {
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-shield"></i></template>
|
<template #icon><i class="ti ti-shield"></i></template>
|
||||||
<template #label>{{ i18n.ts.botProtection }}</template>
|
<template #label>{{ i18n.ts.botProtection }}</template>
|
||||||
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
|
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
||||||
|
|
||||||
<XBotProtection/>
|
<XBotProtection/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-eye-off"></i></template>
|
<template #icon><i class="ti ti-eye-off"></i></template>
|
||||||
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
|
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
|
||||||
<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
|
<template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
|
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>Active Email Validation</template>
|
<template #label>Active Email Validation</template>
|
||||||
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
|
<template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
|
||||||
<template v-else #suffix>Disabled</template>
|
<template v-else #suffix>Disabled</template>
|
||||||
@@ -69,9 +69,9 @@
|
|||||||
<template #label>Enable</template>
|
<template #label>Enable</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>Log IP address</template>
|
<template #label>Log IP address</template>
|
||||||
<template v-if="enableIpLogging" #suffix>Enabled</template>
|
<template v-if="enableIpLogging" #suffix>Enabled</template>
|
||||||
<template v-else #suffix>Disabled</template>
|
<template v-else #suffix>Disabled</template>
|
||||||
@@ -81,9 +81,9 @@
|
|||||||
<template #label>Enable</template>
|
<template #label>Enable</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>Summaly Proxy</template>
|
<template #label>Summaly Proxy</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import XBotProtection from './bot-protection.vue';
|
import XBotProtection from './bot-protection.vue';
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
import FormInfo from '@/components/MkInfo.vue';
|
||||||
|
24
packages/frontend/src/pages/clicker.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><MkPageHeader/></template>
|
||||||
|
<MkSpacer :content-max="800">
|
||||||
|
<MkClickerGame/>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
|
||||||
|
</style>
|
@@ -6,52 +6,52 @@
|
|||||||
</MkTab>
|
</MkTab>
|
||||||
<div v-if="origin === 'local'">
|
<div v-if="origin === 'local'">
|
||||||
<template v-if="tag == null">
|
<template v-if="tag == null">
|
||||||
<MkFolder class="_margin" persist-key="explore-pinned-users">
|
<MkFoldableSection class="_margin" persist-key="explore-pinned-users">
|
||||||
<template #header><i class="fas fa-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
<template #header><i class="fas fa-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||||
<XUserList :pagination="pinnedUsers"/>
|
<XUserList :pagination="pinnedUsers"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin" persist-key="explore-popular-users">
|
<MkFoldableSection class="_margin" persist-key="explore-popular-users">
|
||||||
<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||||
<XUserList :pagination="popularUsers"/>
|
<XUserList :pagination="popularUsers"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin" persist-key="explore-recently-updated-users">
|
<MkFoldableSection class="_margin" persist-key="explore-recently-updated-users">
|
||||||
<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin" persist-key="explore-recently-registered-users">
|
<MkFoldableSection class="_margin" persist-key="explore-recently-registered-users">
|
||||||
<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
|
<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
|
||||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
|
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
|
||||||
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
||||||
|
|
||||||
<div class="vxjfqztj">
|
<div class="vxjfqztj">
|
||||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||||
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_margin">
|
<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
|
||||||
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||||
<XUserList :pagination="tagUsers"/>
|
<XUserList :pagination="tagUsers"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<template v-if="tag == null">
|
<template v-if="tag == null">
|
||||||
<MkFolder class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
<template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||||
<XUserList :pagination="popularUsersF"/>
|
<XUserList :pagination="popularUsersF"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
<template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="fas fa-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
<template #header><i class="fas fa-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
||||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import XUserList from '@/components/MkUserList.vue';
|
import XUserList from '@/components/MkUserList.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
@@ -72,7 +72,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let origin = $ref('local');
|
let origin = $ref('local');
|
||||||
let tagsEl = $shallowRef<InstanceType<typeof MkFolder>>();
|
let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>();
|
||||||
let tagsLocal = $ref([]);
|
let tagsLocal = $ref([]);
|
||||||
let tagsRemote = $ref([]);
|
let tagsRemote = $ref([]);
|
||||||
|
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import XFeatured from './explore.featured.vue';
|
import XFeatured from './explore.featured.vue';
|
||||||
import XUsers from './explore.users.vue';
|
import XUsers from './explore.users.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
@@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
let tab = $ref(props.initialTab);
|
let tab = $ref(props.initialTab);
|
||||||
let tagsEl = $shallowRef<InstanceType<typeof MkFolder>>();
|
let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>();
|
||||||
let searchQuery = $ref(null);
|
let searchQuery = $ref(null);
|
||||||
let searchOrigin = $ref('combined');
|
let searchOrigin = $ref('combined');
|
||||||
|
|
||||||
|
@@ -69,7 +69,7 @@ const headerActions = $computed(() => [{
|
|||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'featured',
|
key: 'featured',
|
||||||
title: i18n.ts._play.featured,
|
title: i18n.ts._play.featured,
|
||||||
icon: 'fas fa-fire-alt',
|
icon: 'ti ti-flare',
|
||||||
}, {
|
}, {
|
||||||
key: 'my',
|
key: 'my',
|
||||||
title: i18n.ts._play.my,
|
title: i18n.ts._play.my,
|
||||||
|
@@ -27,12 +27,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<FormFolder class="_margin">
|
<MkFolder class="_margin">
|
||||||
<template #icon><i class="ti ti-code"></i></template>
|
<template #icon><i class="ti ti-code"></i></template>
|
||||||
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
<template #label>{{ i18n.ts._play.viewSource }}</template>
|
||||||
|
|
||||||
<MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea>
|
<MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<Mfm :text="`By @${flash.user.username}`"/>
|
<Mfm :text="`By @${flash.user.username}`"/>
|
||||||
<div class="date">
|
<div class="date">
|
||||||
@@ -65,7 +65,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
|||||||
import MkAsUi from '@/components/MkAsUi.vue';
|
import MkAsUi from '@/components/MkAsUi.vue';
|
||||||
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui';
|
||||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@@ -4,22 +4,22 @@
|
|||||||
<MkSpacer :content-max="1400">
|
<MkSpacer :content-max="1400">
|
||||||
<div class="_root">
|
<div class="_root">
|
||||||
<div v-if="tab === 'explore'">
|
<div v-if="tab === 'explore'">
|
||||||
<MkFolder class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
|
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
|
||||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||||
<div class="vfpdbgtk">
|
<div class="vfpdbgtk">
|
||||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||||
</div>
|
</div>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
|
<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
|
||||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||||
<div class="vfpdbgtk">
|
<div class="vfpdbgtk">
|
||||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||||
</div>
|
</div>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'liked'">
|
<div v-else-if="tab === 'liked'">
|
||||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent, watch } from 'vue';
|
import { computed, defineComponent, watch } from 'vue';
|
||||||
import XUserList from '@/components/MkUserList.vue';
|
import XUserList from '@/components/MkUserList.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
|
@@ -63,7 +63,7 @@ const headerActions = $computed(() => [{
|
|||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'featured',
|
key: 'featured',
|
||||||
title: i18n.ts._pages.featured,
|
title: i18n.ts._pages.featured,
|
||||||
icon: 'fas fa-fire-alt',
|
icon: 'ti ti-flare',
|
||||||
}, {
|
}, {
|
||||||
key: 'my',
|
key: 'my',
|
||||||
title: i18n.ts._pages.my,
|
title: i18n.ts._pages.my,
|
||||||
|
@@ -1,18 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
|
<div class="_gaps">
|
||||||
|
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
|
||||||
|
|
||||||
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
|
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<MkAvatar :user="account" class="avatar"/>
|
<MkAvatar :user="account" class="avatar"/>
|
||||||
</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="name">
|
|
||||||
<MkUserName :user="account"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="acct">
|
<div class="body">
|
||||||
<MkAcct :user="account"/>
|
<div class="name">
|
||||||
|
<MkUserName :user="account"/>
|
||||||
|
</div>
|
||||||
|
<div class="acct">
|
||||||
|
<MkAcct :user="account"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,24 +2,24 @@
|
|||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormSection first>
|
<FormSection first>
|
||||||
<template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
|
<template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template>
|
<template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
|
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
@@ -31,57 +31,57 @@
|
|||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
|
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
|
<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
|
<template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.export }}</template>
|
<template #label>{{ i18n.ts.export }}</template>
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { selectFile } from '@/scripts/select-file';
|
import { selectFile } from '@/scripts/select-file';
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
<FormSection>
|
<FormSection>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSwitch v-model="rememberNoteVisibility" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
|
<MkSwitch v-model="rememberNoteVisibility" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
|
||||||
<FormFolder v-if="!rememberNoteVisibility">
|
<MkFolder v-if="!rememberNoteVisibility">
|
||||||
<template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
|
<template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
|
||||||
<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
|
<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
|
||||||
<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
|
<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.localOnly }}</MkSwitch>
|
<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.localOnly }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ import { } from 'vue';
|
|||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<FormSlot>
|
<FormSlot>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-list"></i></template>
|
<template #icon><i class="ti ti-list"></i></template>
|
||||||
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
|
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
|
||||||
|
|
||||||
@@ -51,18 +51,18 @@
|
|||||||
<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
||||||
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
|
<MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +76,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import FormSlot from '@/components/form/slot.vue';
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
import { host } from '@/config';
|
import { host } from '@/config';
|
||||||
import { selectFile } from '@/scripts/select-file';
|
import { selectFile } from '@/scripts/select-file';
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts.sounds }}</template>
|
<template #label>{{ i18n.ts.sounds }}</template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<FormFolder v-for="type in Object.keys(sounds)" :key="type">
|
<MkFolder v-for="type in Object.keys(sounds)" :key="type">
|
||||||
<template #label>{{ $t('_sfx.' + type) }}</template>
|
<template #label>{{ $t('_sfx.' + type) }}</template>
|
||||||
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
|
<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template>
|
||||||
|
|
||||||
<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
|
<XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ import MkRange from '@/components/MkRange.vue';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { ColdDeviceStorage } from '@/store';
|
import { ColdDeviceStorage } from '@/store';
|
||||||
import { playFile } from '@/scripts/sound';
|
import { playFile } from '@/scripts/sound';
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormFolder v-for="x in statusbars" :key="x.id">
|
<MkFolder v-for="x in statusbars" :key="x.id">
|
||||||
<template #label>{{ x.type ?? i18n.ts.notSet }}</template>
|
<template #label>{{ x.type ?? i18n.ts.notSet }}</template>
|
||||||
<template #suffix>{{ x.name }}</template>
|
<template #suffix>{{ x.name }}</template>
|
||||||
<XStatusbar :_id="x.id" :user-lists="userLists"/>
|
<XStatusbar :_id="x.id" :user-lists="userLists"/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<MkButton primary @click="add">{{ i18n.ts.add }}</MkButton>
|
<MkButton primary @click="add">{{ i18n.ts.add }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -14,7 +14,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import XStatusbar from './statusbar.statusbar.vue';
|
import XStatusbar from './statusbar.statusbar.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||||
<div class="cwepdizn _gaps_m">
|
<div class="cwepdizn _gaps_m">
|
||||||
<FormFolder :default-open="true">
|
<MkFolder :default-open="true">
|
||||||
<template #label>{{ i18n.ts.backgroundColor }}</template>
|
<template #label>{{ i18n.ts.backgroundColor }}</template>
|
||||||
<div class="cwepdizn-colors">
|
<div class="cwepdizn-colors">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder :default-open="true">
|
<MkFolder :default-open="true">
|
||||||
<template #label>{{ i18n.ts.accentColor }}</template>
|
<template #label>{{ i18n.ts.accentColor }}</template>
|
||||||
<div class="cwepdizn-colors">
|
<div class="cwepdizn-colors">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder :default-open="true">
|
<MkFolder :default-open="true">
|
||||||
<template #label>{{ i18n.ts.textColor }}</template>
|
<template #label>{{ i18n.ts.textColor }}</template>
|
||||||
<div class="cwepdizn-colors">
|
<div class="cwepdizn-colors">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder :default-open="false">
|
<MkFolder :default-open="false">
|
||||||
<template #icon><i class="ti ti-code"></i></template>
|
<template #icon><i class="ti ti-code"></i></template>
|
||||||
<template #label>{{ i18n.ts.editCode }}</template>
|
<template #label>{{ i18n.ts.editCode }}</template>
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@
|
|||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton>
|
<MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<FormFolder :default-open="false">
|
<MkFolder :default-open="false">
|
||||||
<template #label>{{ i18n.ts.addDescription }}</template>
|
<template #label>{{ i18n.ts.addDescription }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<template #label>{{ i18n.ts._theme.description }}</template>
|
<template #label>{{ i18n.ts._theme.description }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
@@ -76,7 +76,7 @@ import JSON5 from 'json5';
|
|||||||
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { Theme, applyTheme } from '@/scripts/theme';
|
import { Theme, applyTheme } from '@/scripts/theme';
|
||||||
|
@@ -77,12 +77,12 @@
|
|||||||
|
|
||||||
<MkButton v-if="user.host != null" @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
|
<MkButton v-if="user.host != null" @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
|
||||||
|
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>Raw</template>
|
<template #label>Raw</template>
|
||||||
|
|
||||||
<MkObjectView v-if="ap" tall :value="ap">
|
<MkObjectView v-if="ap" tall :value="ap">
|
||||||
</MkObjectView>
|
</MkObjectView>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<MkTextarea v-model="moderationNote" manual-save>
|
<MkTextarea v-model="moderationNote" manual-save>
|
||||||
<template #label>Moderation note</template>
|
<template #label>Moderation note</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>IP</template>
|
<template #label>IP</template>
|
||||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||||
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
|
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
|
||||||
@@ -108,12 +108,12 @@
|
|||||||
<span class="ip">{{ record.ip }}</span>
|
<span class="ip">{{ record.ip }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.files }}</template>
|
<template #label>{{ i18n.ts.files }}</template>
|
||||||
|
|
||||||
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
|
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
|
||||||
</FormFolder>
|
</MkFolder>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>Drive Capacity Override</template>
|
<template #label>Drive Capacity Override</template>
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ import FormSection from '@/components/form/section.vue';
|
|||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
|
175
packages/frontend/src/pages/user/activity.following.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MkLoading v-if="fetching"/>
|
||||||
|
<div v-show="!fetching" :class="$style.root" class="_panel">
|
||||||
|
<canvas ref="chartEl"></canvas>
|
||||||
|
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { Chart, ChartDataset } from 'chart.js';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
|
import { satisfies } from 'compare-versions';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
import { chartVLine } from '@/scripts/chart-vline';
|
||||||
|
import { alpha } from '@/scripts/color';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
import { chartLegend } from '@/scripts/chart-legend';
|
||||||
|
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||||
|
|
||||||
|
initChart();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: misskey.entities.User;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||||
|
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||||
|
const now = new Date();
|
||||||
|
let chartInstance: Chart = null;
|
||||||
|
const chartLimit = 30;
|
||||||
|
let fetching = $ref(true);
|
||||||
|
|
||||||
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
|
async function renderChart() {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDate = (ago: number) => {
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = now.getMonth();
|
||||||
|
const d = now.getDate();
|
||||||
|
|
||||||
|
return new Date(y, m, d - ago);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (arr) => {
|
||||||
|
return arr.map((v, i) => ({
|
||||||
|
x: getDate(i).getTime(),
|
||||||
|
y: v,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
|
||||||
|
|
||||||
|
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
|
const colorFollowLocal = '#008FFB';
|
||||||
|
const colorFollowRemote = '#008FFB88';
|
||||||
|
const colorFollowedLocal = '#2ecc71';
|
||||||
|
const colorFollowedRemote = '#2ecc7188';
|
||||||
|
|
||||||
|
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||||
|
return Object.assign({
|
||||||
|
label: label,
|
||||||
|
data: data,
|
||||||
|
parsing: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderRadius: 4,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
categoryPercentage: 0.7,
|
||||||
|
fill: true,
|
||||||
|
} satisfies ChartDataset, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartInstance = new Chart(chartEl, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
datasets: [
|
||||||
|
makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal, stack: 'follow' }),
|
||||||
|
makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote, stack: 'follow' }),
|
||||||
|
makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal, stack: 'followed' }),
|
||||||
|
makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote, stack: 'followed' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 3,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 8,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
offset: true,
|
||||||
|
stacked: true,
|
||||||
|
time: {
|
||||||
|
stepSize: 1,
|
||||||
|
unit: 'day',
|
||||||
|
displayFormats: {
|
||||||
|
day: 'M/d',
|
||||||
|
month: 'Y/M',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkipPadding: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
stacked: true,
|
||||||
|
suggestedMax: 10,
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
//mirror: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
gradient,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
|
||||||
|
});
|
||||||
|
|
||||||
|
fetching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
renderChart();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
174
packages/frontend/src/pages/user/activity.notes.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MkLoading v-if="fetching"/>
|
||||||
|
<div v-show="!fetching" :class="$style.root" class="_panel">
|
||||||
|
<canvas ref="chartEl"></canvas>
|
||||||
|
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { Chart, ChartDataset } from 'chart.js';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
|
import { satisfies } from 'compare-versions';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
import { chartVLine } from '@/scripts/chart-vline';
|
||||||
|
import { alpha } from '@/scripts/color';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
import { chartLegend } from '@/scripts/chart-legend';
|
||||||
|
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||||
|
|
||||||
|
initChart();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: misskey.entities.User;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||||
|
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||||
|
const now = new Date();
|
||||||
|
let chartInstance: Chart = null;
|
||||||
|
const chartLimit = 50;
|
||||||
|
let fetching = $ref(true);
|
||||||
|
|
||||||
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
|
async function renderChart() {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDate = (ago: number) => {
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = now.getMonth();
|
||||||
|
const d = now.getDate();
|
||||||
|
|
||||||
|
return new Date(y, m, d - ago);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (arr) => {
|
||||||
|
return arr.map((v, i) => ({
|
||||||
|
x: getDate(i).getTime(),
|
||||||
|
y: v,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
|
||||||
|
|
||||||
|
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
|
const colorNormal = '#008FFB';
|
||||||
|
const colorReply = '#FEB019';
|
||||||
|
const colorRenote = '#00E396';
|
||||||
|
const colorFile = '#e300db';
|
||||||
|
|
||||||
|
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||||
|
return Object.assign({
|
||||||
|
label: label,
|
||||||
|
data: data,
|
||||||
|
parsing: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderRadius: 4,
|
||||||
|
barPercentage: 0.9,
|
||||||
|
fill: true,
|
||||||
|
} satisfies ChartDataset, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartInstance = new Chart(chartEl, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
datasets: [
|
||||||
|
makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }),
|
||||||
|
makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }),
|
||||||
|
makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }),
|
||||||
|
makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 3,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 8,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
offset: true,
|
||||||
|
stacked: true,
|
||||||
|
time: {
|
||||||
|
stepSize: 1,
|
||||||
|
unit: 'day',
|
||||||
|
displayFormats: {
|
||||||
|
day: 'M/d',
|
||||||
|
month: 'Y/M',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkipPadding: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
stacked: true,
|
||||||
|
suggestedMax: 10,
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: true,
|
||||||
|
//mirror: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
gradient,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
|
||||||
|
});
|
||||||
|
|
||||||
|
fetching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
renderChart();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart, ChartDataset } from 'chart.js';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import gradient from 'chartjs-plugin-gradient';
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
@@ -67,65 +67,33 @@ async function renderChart() {
|
|||||||
const colorUser2 = '#3498db88';
|
const colorUser2 = '#3498db88';
|
||||||
const colorVisitor2 = '#2ecc7188';
|
const colorVisitor2 = '#2ecc7188';
|
||||||
|
|
||||||
|
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||||
|
return Object.assign({
|
||||||
|
label: label,
|
||||||
|
data: data,
|
||||||
|
parsing: false,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderRadius: 4,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
categoryPercentage: 0.7,
|
||||||
|
fill: true,
|
||||||
|
} satisfies ChartDataset, extra);
|
||||||
|
}
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl, {
|
chartInstance = new Chart(chartEl, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [
|
||||||
parsing: false,
|
makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }),
|
||||||
label: 'UPV (user)',
|
makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }),
|
||||||
data: format(raw.upv.user).slice().reverse(),
|
makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }),
|
||||||
pointRadius: 0,
|
makeDataset('NPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }),
|
||||||
borderWidth: 0,
|
],
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: colorUser,
|
|
||||||
barPercentage: 0.7,
|
|
||||||
categoryPercentage: 0.7,
|
|
||||||
fill: true,
|
|
||||||
stack: 'u',
|
|
||||||
}, {
|
|
||||||
parsing: false,
|
|
||||||
label: 'UPV (visitor)',
|
|
||||||
data: format(raw.upv.visitor).slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: colorVisitor,
|
|
||||||
barPercentage: 0.7,
|
|
||||||
categoryPercentage: 0.7,
|
|
||||||
fill: true,
|
|
||||||
stack: 'u',
|
|
||||||
}, {
|
|
||||||
parsing: false,
|
|
||||||
label: 'NPV (user)',
|
|
||||||
data: format(raw.pv.user).slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: colorUser2,
|
|
||||||
barPercentage: 0.7,
|
|
||||||
categoryPercentage: 0.7,
|
|
||||||
fill: true,
|
|
||||||
stack: 'n',
|
|
||||||
}, {
|
|
||||||
parsing: false,
|
|
||||||
label: 'NPV (visitor)',
|
|
||||||
data: format(raw.pv.visitor).slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: colorVisitor2,
|
|
||||||
barPercentage: 0.7,
|
|
||||||
categoryPercentage: 0.7,
|
|
||||||
fill: true,
|
|
||||||
stack: 'n',
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@@ -1,14 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkSpacer :content-max="700">
|
<MkSpacer :content-max="700">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Heatmap</template>
|
<template #header><i class="ti ti-activity"></i> Heatmap</template>
|
||||||
<XHeatmap :user="user" :src="'notes'"/>
|
<XHeatmap :user="user" :src="'notes'"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
<MkFolder class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>PV</template>
|
<template #header><i class="ti ti-pencil"></i> Notes</template>
|
||||||
|
<XNotes :user="user"/>
|
||||||
|
</MkFoldableSection>
|
||||||
|
<MkFoldableSection class="item">
|
||||||
|
<template #header><i class="ti ti-users"></i> Following</template>
|
||||||
|
<XFollowing :user="user"/>
|
||||||
|
</MkFoldableSection>
|
||||||
|
<MkFoldableSection class="item">
|
||||||
|
<template #header><i class="ti ti-eye"></i> PV</template>
|
||||||
<XPv :user="user"/>
|
<XPv :user="user"/>
|
||||||
</MkFolder>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,7 +26,9 @@ import { computed } from 'vue';
|
|||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XHeatmap from './activity.heatmap.vue';
|
import XHeatmap from './activity.heatmap.vue';
|
||||||
import XPv from './activity.pv.vue';
|
import XPv from './activity.pv.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import XNotes from './activity.notes.vue';
|
||||||
|
import XFollowing from './activity.following.vue';
|
||||||
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: misskey.entities.User;
|
user: misskey.entities.User;
|
||||||
|
@@ -115,7 +115,7 @@ import XUserTimeline from './index.timeline.vue';
|
|||||||
import XNote from '@/components/MkNote.vue';
|
import XNote from '@/components/MkNote.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
@@ -339,6 +339,10 @@ export const routes = [{
|
|||||||
path: '/files',
|
path: '/files',
|
||||||
name: 'files',
|
name: 'files',
|
||||||
component: page(() => import('./pages/admin/files.vue')),
|
component: page(() => import('./pages/admin/files.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/federation',
|
||||||
|
name: 'federation',
|
||||||
|
component: page(() => import('./pages/admin/federation.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/announcements',
|
path: '/announcements',
|
||||||
name: 'announcements',
|
name: 'announcements',
|
||||||
@@ -460,6 +464,10 @@ export const routes = [{
|
|||||||
path: '/timeline/antenna/:antennaId',
|
path: '/timeline/antenna/:antennaId',
|
||||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
component: page(() => import('./pages/antenna-timeline.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/clicker',
|
||||||
|
component: page(() => import('./pages/clicker.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
68
packages/frontend/src/scripts/clicker-game.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
type SaveData = {
|
||||||
|
gameVersion: number;
|
||||||
|
cookies: number;
|
||||||
|
totalCookies: number;
|
||||||
|
totalHandmadeCookies: number;
|
||||||
|
clicked: number;
|
||||||
|
achievements: any[];
|
||||||
|
facilities: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveData = ref<SaveData>();
|
||||||
|
export const ready = computed(() => saveData.value != null);
|
||||||
|
|
||||||
|
let prev = '';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
try {
|
||||||
|
saveData.value = await os.api('i/registry/get', {
|
||||||
|
scope: ['clickerGame'],
|
||||||
|
key: 'saveData',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'NO_SUCH_KEY') {
|
||||||
|
saveData.value = {
|
||||||
|
gameVersion: 2,
|
||||||
|
cookies: 0,
|
||||||
|
totalCookies: 0,
|
||||||
|
totalHandmadeCookies: 0,
|
||||||
|
clicked: 0,
|
||||||
|
achievements: [],
|
||||||
|
facilities: [],
|
||||||
|
};
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// migration
|
||||||
|
if (saveData.value.gameVersion === 1) {
|
||||||
|
saveData.value = {
|
||||||
|
gameVersion: 2,
|
||||||
|
cookies: saveData.value.cookies,
|
||||||
|
totalCookies: saveData.value.cookies,
|
||||||
|
totalHandmadeCookies: saveData.value.cookies,
|
||||||
|
clicked: saveData.value.clicked,
|
||||||
|
achievements: [],
|
||||||
|
facilities: [],
|
||||||
|
};
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save() {
|
||||||
|
const current = JSON.stringify(saveData.value);
|
||||||
|
if (current === prev) return;
|
||||||
|
|
||||||
|
await os.api('i/registry/set', {
|
||||||
|
scope: ['clickerGame'],
|
||||||
|
key: 'saveData',
|
||||||
|
value: saveData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = current;
|
||||||
|
}
|
@@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all(
|
|||||||
'l-coffee',
|
'l-coffee',
|
||||||
'l-apricot',
|
'l-apricot',
|
||||||
'l-rainy',
|
'l-rainy',
|
||||||
|
'l-botanical',
|
||||||
'l-vivid',
|
'l-vivid',
|
||||||
'l-cherry',
|
'l-cherry',
|
||||||
'l-sushi',
|
'l-sushi',
|
||||||
|
29
packages/frontend/src/themes/l-botanical.json5
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
id: '1100673c-f902-4ccd-93aa-7cb88be56178',
|
||||||
|
|
||||||
|
name: 'Mi Botanical Light',
|
||||||
|
author: 'ThinaticSystem',
|
||||||
|
|
||||||
|
base: 'light',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
accent: '#77b58c',
|
||||||
|
bg: 'e2deda',
|
||||||
|
fg: '#3d3d3d',
|
||||||
|
fgHighlighted: '#6bc9a0',
|
||||||
|
divider: '#cfcfcf',
|
||||||
|
panel: '@X14',
|
||||||
|
panelHeaderBg: '@panel',
|
||||||
|
panelHeaderDivider: '@divider',
|
||||||
|
header: ':alpha<0.7<@panel',
|
||||||
|
navBg: '@X14',
|
||||||
|
renote: '#229e92',
|
||||||
|
mention: '#da6d35',
|
||||||
|
mentionMe: '#d44c4c',
|
||||||
|
hashtag: '#4cb8d4',
|
||||||
|
link: '@accent',
|
||||||
|
buttonGradateB: ':hue<-70<@accent',
|
||||||
|
success: '#86b300',
|
||||||
|
X14: '#ebe7e5'
|
||||||
|
},
|
||||||
|
}
|
@@ -41,23 +41,18 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||||||
to: '/api-console',
|
to: '/api-console',
|
||||||
text: 'API Console',
|
text: 'API Console',
|
||||||
icon: 'ti ti-terminal-2',
|
icon: 'ti ti-terminal-2',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/clicker',
|
||||||
|
text: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
}],
|
}],
|
||||||
}, null, {
|
}, null, {
|
||||||
type: 'parent',
|
|
||||||
text: i18n.ts.help,
|
text: i18n.ts.help,
|
||||||
icon: 'ti ti-question-circle',
|
icon: 'ti ti-question-circle',
|
||||||
children: [{
|
action: () => {
|
||||||
type: 'link',
|
window.open('https://misskey-hub.net/help.html', '_blank');
|
||||||
to: '/mfm-cheat-sheet',
|
},
|
||||||
text: i18n.ts._mfm.cheatSheet,
|
|
||||||
icon: 'ti ti-code',
|
|
||||||
}, null, {
|
|
||||||
text: i18n.ts.document,
|
|
||||||
icon: 'ti ti-question-circle',
|
|
||||||
action: () => {
|
|
||||||
window.open('https://misskey-hub.net/help.html', '_blank');
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
}, {
|
}, {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
text: i18n.ts.aboutMisskey,
|
text: i18n.ts.aboutMisskey,
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
<div v-if="showMenu" class="menu">
|
<div v-if="showMenu" class="menu">
|
||||||
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
||||||
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
||||||
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
|
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
|
||||||
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
|
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
||||||
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
||||||
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
|
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
|
||||||
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
||||||
<div v-if="info" class="page active link">
|
<div v-if="info" class="page active link">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@@ -11,19 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div>
|
<div>
|
||||||
<p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
|
<p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p>
|
||||||
<div class="meter">
|
<div class="meter">
|
||||||
<div class="val" :style="{ width: `${dayP}%` }"></div>
|
<div class="val" :style="{ width: `${dayP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
|
<p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p>
|
||||||
<div class="meter">
|
<div class="meter">
|
||||||
<div class="val" :style="{ width: `${monthP}%` }"></div>
|
<div class="val" :style="{ width: `${monthP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
|
<p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p>
|
||||||
<div class="meter">
|
<div class="meter">
|
||||||
<div class="val" :style="{ width: `${yearP}%` }"></div>
|
<div class="val" :style="{ width: `${yearP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
|
display: flex;
|
||||||
margin: 0 0 2px 0;
|
margin: 0 0 2px 0;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
> b {
|
> b {
|
||||||
margin-left: 2px;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
packages/frontend/src/widgets/clicker.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<MkContainer :show-header="widgetProps.showHeader" class="mkw-clicker">
|
||||||
|
<template #header><i class="ti ti-cookie"></i>Clicker</template>
|
||||||
|
<MkClickerGame/>
|
||||||
|
</MkContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||||
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||||
|
import { GetFormResultType } from '@/scripts/form';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
|
||||||
|
const name = 'clicker';
|
||||||
|
|
||||||
|
const widgetPropsDef = {
|
||||||
|
showHeader: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
|
||||||
|
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||||
|
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||||
|
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
|
||||||
|
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
|
||||||
|
|
||||||
|
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||||
|
widgetPropsDef,
|
||||||
|
props,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose<WidgetComponentExpose>({
|
||||||
|
name,
|
||||||
|
configure,
|
||||||
|
id: props.widget ? props.widget.id : null,
|
||||||
|
});
|
||||||
|
</script>
|
@@ -25,6 +25,7 @@ export default function(app: App) {
|
|||||||
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
||||||
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
||||||
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
||||||
|
app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgets = [
|
export const widgets = [
|
||||||
@@ -52,4 +53,5 @@ export const widgets = [
|
|||||||
'aiscriptApp',
|
'aiscriptApp',
|
||||||
'aichan',
|
'aichan',
|
||||||
'userList',
|
'userList',
|
||||||
|
'clicker',
|
||||||
];
|
];
|
||||||
|
@@ -3,14 +3,21 @@
|
|||||||
*/
|
*/
|
||||||
import { swLang } from '@/scripts/lang';
|
import { swLang } from '@/scripts/lang';
|
||||||
import { cli } from '@/scripts/operations';
|
import { cli } from '@/scripts/operations';
|
||||||
import { pushNotificationDataMap } from '@/types';
|
import { badgeNames, pushNotificationDataMap } from '@/types';
|
||||||
import getUserName from '@/scripts/get-user-name';
|
import getUserName from '@/scripts/get-user-name';
|
||||||
import { I18n } from '@/scripts/i18n';
|
import { I18n } from '@/scripts/i18n';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
import { char2fileName } from '@/scripts/twemoji-base';
|
import { char2fileName } from '@/scripts/twemoji-base';
|
||||||
import * as url from '@/scripts/url';
|
import * as url from '@/scripts/url';
|
||||||
|
|
||||||
const iconUrl = (name: string) => `/static-assets/notification-badges/${name}.png`;
|
const iconUrl = (name: badgeNames) => `/static-assets/tabler-badges/${name}.png`;
|
||||||
|
/* How to add a new badge:
|
||||||
|
* 1. Find the icon and download png from https://tabler-icons.io/
|
||||||
|
* 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png;
|
||||||
|
* 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/
|
||||||
|
* 4. Add 'icon-name' to badgeNames
|
||||||
|
* 5. Add `badge: iconUrl('icon-name'),`
|
||||||
|
*/
|
||||||
|
|
||||||
export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
|
export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
|
||||||
const n = await composeNotification(data);
|
const n = await composeNotification(data);
|
||||||
@@ -75,7 +82,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('reply'),
|
badge: iconUrl('arrow-back-up'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -89,7 +96,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('retweet'),
|
badge: iconUrl('repeat'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -103,7 +110,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('quote-right'),
|
badge: iconUrl('quote'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -171,7 +178,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
case 'pollEnded':
|
case 'pollEnded':
|
||||||
return [t('_notification.pollEnded'), {
|
return [t('_notification.pollEnded'), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
badge: iconUrl('clipboard-check-solid'),
|
badge: iconUrl('chart-arrows'),
|
||||||
|
tag: `poll:${data.body.note.id}`,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -179,7 +187,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youReceivedFollowRequest'), {
|
return [t('_notification.youReceivedFollowRequest'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('clock'),
|
badge: iconUrl('user-plus'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -197,14 +205,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.yourFollowRequestAccepted'), {
|
return [t('_notification.yourFollowRequestAccepted'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('check'),
|
badge: iconUrl('circle-check'),
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'groupInvited':
|
case 'groupInvited':
|
||||||
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
|
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
|
||||||
body: data.body.invitation.group.name,
|
body: data.body.invitation.group.name,
|
||||||
badge: iconUrl('id-card-alt'),
|
badge: iconUrl('users'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -232,7 +240,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
if (data.body.groupId === null) {
|
if (data.body.groupId === null) {
|
||||||
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('comments'),
|
badge: iconUrl('messages'),
|
||||||
tag: `messaging:user:${data.body.userId}`,
|
tag: `messaging:user:${data.body.userId}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
@@ -240,7 +248,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
}
|
}
|
||||||
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('comments'),
|
badge: iconUrl('messages'),
|
||||||
tag: `messaging:group:${data.body.groupId}`,
|
tag: `messaging:group:${data.body.groupId}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
@@ -249,7 +257,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
||||||
body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`,
|
body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`,
|
||||||
icon: data.body.note.user.avatarUrl,
|
icon: data.body.note.user.avatarUrl,
|
||||||
badge: iconUrl('satellite'),
|
badge: iconUrl('antenna'),
|
||||||
tag: `antenna:${data.body.antenna.id}`,
|
tag: `antenna:${data.body.antenna.id}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
|
@@ -36,3 +36,18 @@ export type pushNotificationData<K extends keyof pushNotificationDataSourceMap>
|
|||||||
export type pushNotificationDataMap = {
|
export type pushNotificationDataMap = {
|
||||||
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
|
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type badgeNames =
|
||||||
|
'null'
|
||||||
|
| 'antenna'
|
||||||
|
| 'arrow-back-up'
|
||||||
|
| 'at'
|
||||||
|
| 'chart-arrows'
|
||||||
|
| 'circle-check'
|
||||||
|
| 'messages'
|
||||||
|
| 'plus'
|
||||||
|
| 'quote'
|
||||||
|
| 'repeat'
|
||||||
|
| 'user-plus'
|
||||||
|
| 'users'
|
||||||
|
;
|
||||||
|