mirror of
https://github.com/misskey-dev/misskey
synced 2025-07-09 21:22:51 +02:00
Compare commits
24 commits
2025.7.0-b
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
c165749a29 | ||
|
c4fdf5a47c | ||
|
288f0abeac | ||
|
89ed8be8ff | ||
|
a8abb03d17 | ||
|
c2a01551a7 | ||
|
553ccff77c | ||
|
9dddc84750 | ||
|
004cfd5e4b | ||
|
40a35968f0 | ||
|
e6ec15e397 | ||
|
8430256f22 | ||
|
abde15979b | ||
|
f128682200 | ||
|
cc4cdd1ec0 | ||
|
075df75afa | ||
|
64eb338d65 | ||
|
d986da745b | ||
|
50f5b29290 | ||
|
a460bb7913 | ||
|
7cf1eccd04 | ||
|
73397e1b7e | ||
|
bf17092b41 | ||
|
a45ccc18b4 |
100 changed files with 856 additions and 696 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -6,8 +6,12 @@
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.test.ts": "typescript"
|
"*.test.ts": "typescript"
|
||||||
},
|
},
|
||||||
"jest.jestCommandLine": "pnpm run jest",
|
|
||||||
"jest.runMode": "on-demand",
|
"jest.runMode": "on-demand",
|
||||||
|
"jest.virtualFolders": [
|
||||||
|
{ "name": "backend unit", "jestCommandLine": "pnpm -F backend run test" },
|
||||||
|
{ "name": "backend e2e", "jestCommandLine": "pnpm -F backend run test:e2e"},
|
||||||
|
{ "name": "misskey-js", "jestCommandLine": "pnpm -F misskey-js run jest" }
|
||||||
|
],
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
- Feat: ノートの下書き機能
|
- Feat: ノートの下書き機能
|
||||||
- Feat: クリップ内でノートを検索できるように
|
- Feat: クリップ内でノートを検索できるように
|
||||||
- Feat: Playを検索できるように
|
- Feat: Playを検索できるように
|
||||||
|
- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Feat: モデログを検索できるように
|
- Feat: モデログを検索できるように
|
||||||
|
@ -13,13 +14,16 @@
|
||||||
- Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善
|
- Enhance: 投稿フォームにファイルをペースト/ドロップした際のUXを改善
|
||||||
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように
|
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように
|
||||||
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように
|
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように
|
||||||
|
- Enhance: レンダリングパフォーマンスの向上
|
||||||
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
|
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
|
||||||
- Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正
|
- Fix: プラグインをアンインストールしてもセーブデータが残る問題を修正
|
||||||
- Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正
|
- Fix: 数時間後Misskeyのタブに戻った際に、タブがスロットリングされている間の更新アニメーションを延々見せ続けられる問題を修正
|
||||||
- Fix: 非ログイン時のハイライトノートの画像がCWの有無を考慮せず表示される問題を修正
|
- Fix: 非ログイン時のハイライトノートの画像がCWの有無を考慮せず表示される問題を修正
|
||||||
|
- Fix: レンジ選択・ドロップダウンにて、操作を無効にすべきところで無効にならない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に
|
- Enhance: sinceId/untilIdが指定可能なエンドポイントにおいて、sinceDate/untilDateも指定可能に
|
||||||
|
- Enhance: メールの送信者としてサーバー名を表示するように (サーバー名が設定されている場合)
|
||||||
- Fix: ジョブキューのProgressの値を正しく計算する
|
- Fix: ジョブキューのProgressの値を正しく計算する
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "Esborrany "
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "Seleccionar esborrany"
|
select: "Seleccionar esborrany"
|
||||||
cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear."
|
cannotCreateDraftAnymore: "S'ha sobrepassat el nombre màxim d'esborranys que es poden crear."
|
||||||
cannotCreateDraftOfRenote: "No es poden crear esborranys de remotes."
|
cannotCreateDraft: "Amb aquest contingut no es poden crear esborranys."
|
||||||
delete: "Esborrar esborranys"
|
delete: "Esborrar esborranys"
|
||||||
deleteAreYouSure: "Vols esborrar els esborranys?"
|
deleteAreYouSure: "Vols esborrar els esborranys?"
|
||||||
noDrafts: "No hi ha esborranys"
|
noDrafts: "No hi ha esborranys"
|
||||||
|
|
|
@ -3182,7 +3182,6 @@ drafts: "Drafts"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "Select Draft"
|
select: "Select Draft"
|
||||||
cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded."
|
cannotCreateDraftAnymore: "The number of drafts that can be created has been exceeded."
|
||||||
cannotCreateDraftOfRenote: "You cannot create a draft of a renote."
|
|
||||||
delete: "Delete Draft"
|
delete: "Delete Draft"
|
||||||
deleteAreYouSure: "Delete draft?"
|
deleteAreYouSure: "Delete draft?"
|
||||||
noDrafts: "No drafts"
|
noDrafts: "No drafts"
|
||||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "Borrador"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "Seleccionar borradores"
|
select: "Seleccionar borradores"
|
||||||
cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear."
|
cannotCreateDraftAnymore: "Se ha superado el número de borradores que se pueden crear."
|
||||||
cannotCreateDraftOfRenote: "No se pueden crear borradores de renotas."
|
cannotCreateDraft: "No se pueden crear borradores con este contenido."
|
||||||
delete: "Eliminar borrador"
|
delete: "Eliminar borrador"
|
||||||
deleteAreYouSure: "¿Quieres borrar el borrador?"
|
deleteAreYouSure: "¿Quieres borrar el borrador?"
|
||||||
noDrafts: "No hay borradores disponibles."
|
noDrafts: "No hay borradores disponibles."
|
||||||
|
|
|
@ -5,9 +5,13 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif
|
||||||
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
|
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
|
||||||
monthAndDay: "{day} {month}"
|
monthAndDay: "{day} {month}"
|
||||||
search: "Penelusuran"
|
search: "Penelusuran"
|
||||||
|
reset: "Reset"
|
||||||
notifications: "Notifikasi"
|
notifications: "Notifikasi"
|
||||||
username: "Nama Pengguna"
|
username: "Nama Pengguna"
|
||||||
password: "Kata sandi"
|
password: "Kata sandi"
|
||||||
|
initialPasswordForSetup: "Kata sandi untuk memulai konfigurasi awal"
|
||||||
|
initialPasswordIsIncorrect: "Kata sandi untuk memulai konfigurasi awal salah."
|
||||||
|
initialPasswordForSetupDescription: "Jika Anda menginstal Misskey sendiri, gunakan kata sandi yang Anda masukkan di berkas konfigurasi.\nJika Anda menggunakan layanan hosting Misskey, gunakan kata sandi yang diberikan.\nJika Anda belum mengatur kata sandi, biarkan kosong dan lanjutkan."
|
||||||
forgotPassword: "Lupa Kata Sandi"
|
forgotPassword: "Lupa Kata Sandi"
|
||||||
fetchingAsApObject: "Mengambil data dari Fediverse..."
|
fetchingAsApObject: "Mengambil data dari Fediverse..."
|
||||||
ok: "OK"
|
ok: "OK"
|
||||||
|
@ -45,6 +49,7 @@ pin: "Sematkan ke profil"
|
||||||
unpin: "Lepas sematan dari profil"
|
unpin: "Lepas sematan dari profil"
|
||||||
copyContent: "Salin konten"
|
copyContent: "Salin konten"
|
||||||
copyLink: "Salin tautan"
|
copyLink: "Salin tautan"
|
||||||
|
copyRemoteLink: "Salin tautan jarak jauh"
|
||||||
copyLinkRenote: "Salin tautan renote"
|
copyLinkRenote: "Salin tautan renote"
|
||||||
delete: "Hapus"
|
delete: "Hapus"
|
||||||
deleteAndEdit: "Hapus dan sunting"
|
deleteAndEdit: "Hapus dan sunting"
|
||||||
|
@ -212,8 +217,10 @@ perDay: "per Hari"
|
||||||
stopActivityDelivery: "Berhenti mengirim aktivitas"
|
stopActivityDelivery: "Berhenti mengirim aktivitas"
|
||||||
blockThisInstance: "Blokir instansi ini"
|
blockThisInstance: "Blokir instansi ini"
|
||||||
silenceThisInstance: "Senyapkan instansi ini"
|
silenceThisInstance: "Senyapkan instansi ini"
|
||||||
|
mediaSilenceThisInstance: "Server media senyap"
|
||||||
operations: "Tindakan"
|
operations: "Tindakan"
|
||||||
software: "Perangkat lunak"
|
software: "Perangkat lunak"
|
||||||
|
softwareName: "Nama Perangkat Lunak"
|
||||||
version: "Versi"
|
version: "Versi"
|
||||||
metadata: "Metadata"
|
metadata: "Metadata"
|
||||||
withNFiles: "{n} berkas"
|
withNFiles: "{n} berkas"
|
||||||
|
@ -1040,7 +1047,7 @@ disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi
|
||||||
disableFederationOk: "Matikan federasi"
|
disableFederationOk: "Matikan federasi"
|
||||||
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
|
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
|
||||||
emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
|
emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
|
||||||
postToTheChannel: "Catat ke kanal"
|
postToTheChannel: "Buat Catatan ke Kanal"
|
||||||
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
|
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
|
||||||
reactionAcceptance: "Penerimaan reaksi"
|
reactionAcceptance: "Penerimaan reaksi"
|
||||||
likeOnly: "Hanya suka"
|
likeOnly: "Hanya suka"
|
||||||
|
@ -2400,7 +2407,7 @@ _deck:
|
||||||
main: "Utama"
|
main: "Utama"
|
||||||
widgets: "Widget"
|
widgets: "Widget"
|
||||||
notifications: "Notifikasi"
|
notifications: "Notifikasi"
|
||||||
tl: "Lini masa"
|
tl: "Beranda"
|
||||||
antenna: "Antena"
|
antenna: "Antena"
|
||||||
list: "Daftar"
|
list: "Daftar"
|
||||||
channel: "Kanal"
|
channel: "Kanal"
|
||||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -10890,6 +10890,10 @@ export interface Locale extends ILocale {
|
||||||
* 添付されているノート
|
* 添付されているノート
|
||||||
*/
|
*/
|
||||||
"attachedNotes": string;
|
"attachedNotes": string;
|
||||||
|
/**
|
||||||
|
* 利用
|
||||||
|
*/
|
||||||
|
"usage": string;
|
||||||
/**
|
/**
|
||||||
* このページは、このファイルをアップロードしたユーザーしか閲覧できません。
|
* このページは、このファイルをアップロードしたユーザーしか閲覧できません。
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2885,6 +2885,7 @@ _fileViewer:
|
||||||
url: "URL"
|
url: "URL"
|
||||||
uploadedAt: "追加日"
|
uploadedAt: "追加日"
|
||||||
attachedNotes: "添付されているノート"
|
attachedNotes: "添付されているノート"
|
||||||
|
usage: "利用"
|
||||||
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
|
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
|
||||||
|
|
||||||
_externalResourceInstaller:
|
_externalResourceInstaller:
|
||||||
|
|
|
@ -3182,7 +3182,6 @@ drafts: "초안"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "초안 선택"
|
select: "초안 선택"
|
||||||
cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다."
|
cannotCreateDraftAnymore: "초안 작성 가능 수를 초과했습니다."
|
||||||
cannotCreateDraftOfRenote: "리노트 초안은 작성할 수 없습니다."
|
|
||||||
delete: "초안 삭제\n"
|
delete: "초안 삭제\n"
|
||||||
deleteAreYouSure: "초안을 삭제하시겠습니까?"
|
deleteAreYouSure: "초안을 삭제하시겠습니까?"
|
||||||
noDrafts: "초안 없음\n"
|
noDrafts: "초안 없음\n"
|
||||||
|
|
|
@ -3182,6 +3182,5 @@ drafts: "Rascunhos"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "Selecionar Rascunho"
|
select: "Selecionar Rascunho"
|
||||||
cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido."
|
cannotCreateDraftAnymore: "O número máximo de rascunhos foi excedido."
|
||||||
cannotCreateDraftOfRenote: "Você não pode criar o rascunho de uma repostagem."
|
|
||||||
delete: "Excluir Rascunho"
|
delete: "Excluir Rascunho"
|
||||||
restore: "Redefinir"
|
restore: "Redefinir"
|
||||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "草稿"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "选择草稿"
|
select: "选择草稿"
|
||||||
cannotCreateDraftAnymore: "已超过可创建的草稿数量。"
|
cannotCreateDraftAnymore: "已超过可创建的草稿数量。"
|
||||||
cannotCreateDraftOfRenote: "无法创建转帖草稿。"
|
cannotCreateDraft: "此内容无法创建草稿。"
|
||||||
delete: "删除草稿"
|
delete: "删除草稿"
|
||||||
deleteAreYouSure: "要删除草稿吗?"
|
deleteAreYouSure: "要删除草稿吗?"
|
||||||
noDrafts: "没有草稿"
|
noDrafts: "没有草稿"
|
||||||
|
|
|
@ -3182,7 +3182,7 @@ drafts: "草稿\n"
|
||||||
_drafts:
|
_drafts:
|
||||||
select: "選擇草槁"
|
select: "選擇草槁"
|
||||||
cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n"
|
cannotCreateDraftAnymore: "已超出可建立的草稿數量上限。\n"
|
||||||
cannotCreateDraftOfRenote: "無法建立轉發的草稿。\n"
|
cannotCreateDraft: "無法以此內容建立草稿。\n"
|
||||||
delete: "刪除草稿"
|
delete: "刪除草稿"
|
||||||
deleteAreYouSure: "確定要刪除草稿嗎?\n"
|
deleteAreYouSure: "確定要刪除草稿嗎?\n"
|
||||||
noDrafts: "沒有草稿。\n"
|
noDrafts: "沒有草稿。\n"
|
||||||
|
|
|
@ -145,7 +145,10 @@ export class EmailService {
|
||||||
try {
|
try {
|
||||||
// TODO: htmlサニタイズ
|
// TODO: htmlサニタイズ
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: this.meta.email!,
|
from: this.meta.name ? {
|
||||||
|
name: this.meta.name,
|
||||||
|
address: this.meta.email!,
|
||||||
|
} : this.meta.email!,
|
||||||
to: to,
|
to: to,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
text: text,
|
text: text,
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const packedFlashSchema = {
|
||||||
},
|
},
|
||||||
likedCount: {
|
likedCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
isLiked: {
|
isLiked: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
@ -168,6 +168,7 @@ export * as 'clips/update' from './endpoints/clips/update.js';
|
||||||
export * as 'drive' from './endpoints/drive.js';
|
export * as 'drive' from './endpoints/drive.js';
|
||||||
export * as 'drive/files' from './endpoints/drive/files.js';
|
export * as 'drive/files' from './endpoints/drive/files.js';
|
||||||
export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
|
export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
|
||||||
|
export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js';
|
||||||
export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
|
export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
|
||||||
export * as 'drive/files/create' from './endpoints/drive/files/create.js';
|
export * as 'drive/files/create' from './endpoints/drive/files/create.js';
|
||||||
export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
|
export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
|
||||||
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['drive', 'chat'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:drive',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'ChatMessage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: 'No such file.',
|
||||||
|
code: 'NO_SUCH_FILE',
|
||||||
|
id: '485ce26d-f5d2-4313-9783-e689d131eafb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
sinceDate: { type: 'integer' },
|
||||||
|
untilDate: { type: 'integer' },
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
fileId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['fileId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.driveFilesRepository)
|
||||||
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.chatMessagesRepository)
|
||||||
|
private chatMessagesRepository: ChatMessagesRepository,
|
||||||
|
|
||||||
|
private chatEntityService: ChatEntityService,
|
||||||
|
private queryService: QueryService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const file = await this.driveFilesRepository.findOneBy({
|
||||||
|
id: ps.fileId,
|
||||||
|
userId: await this.roleService.isModerator(me) ? undefined : me.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
|
||||||
|
query.andWhere('message.fileId = :fileId', { fileId: file.id });
|
||||||
|
|
||||||
|
const messages = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
return await this.chatEntityService.packMessagesDetailed(messages, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,73 +21,73 @@ describe('ReactionService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('normalize', () => {
|
describe('normalize', () => {
|
||||||
test('絵文字リアクションはそのまま', async () => {
|
test('絵文字リアクションはそのまま', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('👍'), '👍');
|
assert.strictEqual(reactionService.normalize('👍'), '👍');
|
||||||
assert.strictEqual(await reactionService.normalize('🍅'), '🍅');
|
assert.strictEqual(reactionService.normalize('🍅'), '🍅');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する pudding', async () => {
|
test('既存のリアクションは絵文字化する pudding', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('pudding'), '🍮');
|
assert.strictEqual(reactionService.normalize('pudding'), '🍮');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する like', async () => {
|
test('既存のリアクションは絵文字化する like', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('like'), '👍');
|
assert.strictEqual(reactionService.normalize('like'), '👍');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する love', async () => {
|
test('既存のリアクションは絵文字化する love', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('love'), '❤');
|
assert.strictEqual(reactionService.normalize('love'), '❤');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する laugh', async () => {
|
test('既存のリアクションは絵文字化する laugh', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('laugh'), '😆');
|
assert.strictEqual(reactionService.normalize('laugh'), '😆');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する hmm', async () => {
|
test('既存のリアクションは絵文字化する hmm', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('hmm'), '🤔');
|
assert.strictEqual(reactionService.normalize('hmm'), '🤔');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する surprise', async () => {
|
test('既存のリアクションは絵文字化する surprise', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('surprise'), '😮');
|
assert.strictEqual(reactionService.normalize('surprise'), '😮');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する congrats', async () => {
|
test('既存のリアクションは絵文字化する congrats', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('congrats'), '🎉');
|
assert.strictEqual(reactionService.normalize('congrats'), '🎉');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する angry', async () => {
|
test('既存のリアクションは絵文字化する angry', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('angry'), '💢');
|
assert.strictEqual(reactionService.normalize('angry'), '💢');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する confused', async () => {
|
test('既存のリアクションは絵文字化する confused', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('confused'), '😥');
|
assert.strictEqual(reactionService.normalize('confused'), '😥');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する rip', async () => {
|
test('既存のリアクションは絵文字化する rip', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('rip'), '😇');
|
assert.strictEqual(reactionService.normalize('rip'), '😇');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既存のリアクションは絵文字化する star', async () => {
|
test('既存のリアクションは絵文字化する star', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('star'), '⭐');
|
assert.strictEqual(reactionService.normalize('star'), '⭐');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('異体字セレクタ除去', async () => {
|
test('異体字セレクタ除去', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('㊗️'), '㊗');
|
assert.strictEqual(reactionService.normalize('㊗️'), '㊗');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('異体字セレクタ除去 必要なし', async () => {
|
test('異体字セレクタ除去 必要なし', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('㊗'), '㊗');
|
assert.strictEqual(reactionService.normalize('㊗'), '㊗');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fallback - null', async () => {
|
test('fallback - null', () => {
|
||||||
assert.strictEqual(await reactionService.normalize(null), '❤');
|
assert.strictEqual(reactionService.normalize(null), '❤');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fallback - empty', async () => {
|
test('fallback - empty', () => {
|
||||||
assert.strictEqual(await reactionService.normalize(''), '❤');
|
assert.strictEqual(reactionService.normalize(''), '❤');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fallback - unknown', async () => {
|
test('fallback - unknown', () => {
|
||||||
assert.strictEqual(await reactionService.normalize('unknown'), '❤');
|
assert.strictEqual(reactionService.normalize('unknown'), '❤');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import MkDateSeparatedList from './MkDateSeparatedList.vue';
|
|
||||||
void MkDateSeparatedList;
|
|
|
@ -1,254 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) -->
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
|
||||||
import MkAd from '@/components/global/MkAd.vue';
|
|
||||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
|
||||||
import * as os from '@/os.js';
|
|
||||||
import { instance } from '@/instance.js';
|
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
import { getDateText } from '@/utility/timeline-date-separate.js';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
direction: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'down',
|
|
||||||
},
|
|
||||||
reversed: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
noGap: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
ad: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props, { slots, expose }) {
|
|
||||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
|
||||||
|
|
||||||
if (props.items.length === 0) return;
|
|
||||||
|
|
||||||
const renderChildrenImpl = () => props.items.map((item, i) => {
|
|
||||||
if (!slots || !slots.default) return;
|
|
||||||
|
|
||||||
const el = slots.default({
|
|
||||||
item: item,
|
|
||||||
})[0];
|
|
||||||
if (el.key == null && item.id) el.key = item.id;
|
|
||||||
|
|
||||||
const date = new Date(item.createdAt);
|
|
||||||
const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
i !== props.items.length - 1 &&
|
|
||||||
nextDate != null && (
|
|
||||||
date.getFullYear() !== nextDate.getFullYear() ||
|
|
||||||
date.getMonth() !== nextDate.getMonth() ||
|
|
||||||
date.getDate() !== nextDate.getDate()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const separator = h('div', {
|
|
||||||
class: $style['separator'],
|
|
||||||
key: item.id + ':separator',
|
|
||||||
}, h('p', {
|
|
||||||
class: $style['date'],
|
|
||||||
}, [
|
|
||||||
h('span', {
|
|
||||||
class: $style['date-1'],
|
|
||||||
}, [
|
|
||||||
h('i', {
|
|
||||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
|
||||||
}),
|
|
||||||
getDateText(date),
|
|
||||||
]),
|
|
||||||
h('span', {
|
|
||||||
class: $style['date-2'],
|
|
||||||
}, [
|
|
||||||
getDateText(nextDate),
|
|
||||||
h('i', {
|
|
||||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]));
|
|
||||||
|
|
||||||
return [el, separator];
|
|
||||||
} else {
|
|
||||||
if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
|
|
||||||
return [h('div', {
|
|
||||||
key: item.id + ':ad',
|
|
||||||
class: $style['ad-wrapper'],
|
|
||||||
}, [h(MkAd, {
|
|
||||||
prefer: ['horizontal', 'horizontal-big'],
|
|
||||||
})]), el];
|
|
||||||
} else {
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderChildren = () => {
|
|
||||||
const children = renderChildrenImpl();
|
|
||||||
if (isDebuggerEnabled(6864)) {
|
|
||||||
const nodes = children.flatMap((node) => node ?? []);
|
|
||||||
const keys = new Set(nodes.map((node) => node.key));
|
|
||||||
if (keys.size !== nodes.length) {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const instances = stackTraceInstances();
|
|
||||||
os.toast(instances.reduce((a, c) => `${a} at ${c.type.name}`, `[DEBUG_6864 (${id})]: ${nodes.length - keys.size} duplicated keys found`));
|
|
||||||
console.warn({ id, debugId: 6864, stack: instances });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
function onBeforeLeave(el: Element) {
|
|
||||||
if (!(el instanceof HTMLElement)) return;
|
|
||||||
el.style.top = `${el.offsetTop}px`;
|
|
||||||
el.style.left = `${el.offsetLeft}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLeaveCancelled(el: Element) {
|
|
||||||
if (!(el instanceof HTMLElement)) return;
|
|
||||||
el.style.top = '';
|
|
||||||
el.style.left = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
|
||||||
const classes = {
|
|
||||||
[$style['date-separated-list']]: true,
|
|
||||||
[$style['date-separated-list-nogap']]: props.noGap,
|
|
||||||
[$style['reversed']]: props.reversed,
|
|
||||||
[$style['direction-down']]: props.direction === 'down',
|
|
||||||
[$style['direction-up']]: props.direction === 'up',
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => prefer.s.animation ? h(TransitionGroup, {
|
|
||||||
class: classes,
|
|
||||||
name: 'list',
|
|
||||||
tag: 'div',
|
|
||||||
onBeforeLeave,
|
|
||||||
onLeaveCancelled,
|
|
||||||
}, { default: renderChildren }) : h('div', {
|
|
||||||
class: classes,
|
|
||||||
}, { default: renderChildren });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.date-separated-list {
|
|
||||||
container-type: inline-size;
|
|
||||||
|
|
||||||
&:global {
|
|
||||||
> .list-move {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .list-enter-active {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
|
||||||
margin-bottom: var(--MI-margin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-separated-list-nogap {
|
|
||||||
> * {
|
|
||||||
margin: 0 !important;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.direction-up {
|
|
||||||
&:global {
|
|
||||||
> .list-enter-from,
|
|
||||||
> .list-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.direction-down {
|
|
||||||
&:global {
|
|
||||||
> .list-enter-from,
|
|
||||||
> .list-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-64px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reversed {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 16px;
|
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--MI_THEME-dateLabelFg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-1 {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-1-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-2 {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-2-icon {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ad-wrapper {
|
|
||||||
padding: 8px;
|
|
||||||
background-size: auto auto;
|
|
||||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -684,13 +684,8 @@ defineExpose({
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .group {
|
> .group {
|
||||||
&:not(.index) {
|
&:not(.index) {
|
||||||
padding: 4px 0 8px 0;
|
padding: 4px 0 8px 0;
|
||||||
|
|
|
@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import { instanceName as localInstanceName } from '@@/js/config.js';
|
import { instanceName as localInstanceName } from '@@/js/config.js';
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { instance as localInstance } from '@/instance.js';
|
import { instance as localInstance } from '@/instance.js';
|
||||||
|
@ -44,33 +43,10 @@ const faviconUrl = computed(() => {
|
||||||
return getProxiedImageUrlNullable(imageSrc);
|
return getProxiedImageUrlNullable(imageSrc);
|
||||||
});
|
});
|
||||||
|
|
||||||
type ITickerColors = {
|
|
||||||
readonly bg: string;
|
|
||||||
readonly fg: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TICKER_YUV_THRESHOLD = 191 as const;
|
|
||||||
const TICKER_FG_COLOR_LIGHT = '#ffffff' as const;
|
|
||||||
const TICKER_FG_COLOR_DARK = '#2f2f2fcc' as const;
|
|
||||||
|
|
||||||
function getTickerColors(bgHex: string): ITickerColors {
|
|
||||||
const tinycolorInstance = tinycolor(bgHex);
|
|
||||||
const { r, g, b } = tinycolorInstance.toRgb();
|
|
||||||
const yuv = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
||||||
const fgHex = yuv > TICKER_YUV_THRESHOLD ? TICKER_FG_COLOR_DARK : TICKER_FG_COLOR_LIGHT;
|
|
||||||
|
|
||||||
return {
|
|
||||||
fg: fgHex,
|
|
||||||
bg: bgHex,
|
|
||||||
} as const satisfies ITickerColors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeColorStyle = computed<CSSProperties>(() => {
|
const themeColorStyle = computed<CSSProperties>(() => {
|
||||||
const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
|
const themeColor = (props.host == null ? localInstance.themeColor : props.instance?.themeColor) ?? '#777777';
|
||||||
const colors = getTickerColors(themeColor);
|
|
||||||
return {
|
return {
|
||||||
background: `linear-gradient(90deg, ${colors.bg}, ${colors.bg}00)`,
|
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
|
||||||
color: colors.fg,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -84,6 +60,7 @@ $height: 2ex;
|
||||||
height: $height;
|
height: $height;
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
// text-shadowは重いから使うな
|
// text-shadowは重いから使うな
|
||||||
|
|
||||||
|
@ -106,5 +83,10 @@ $height: 2ex;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
|
// text-shadowは重いから使うな
|
||||||
|
color: var(--MI_THEME-fg);
|
||||||
|
-webkit-text-stroke: var(--MI_THEME-panel) .225em;
|
||||||
|
paint-order: stroke fill;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -729,7 +729,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .article > .main > .footer > .footerButton {
|
&:hover > .article > .main > .footer > .footerButton {
|
||||||
opacity: 1;
|
color: var(--MI_THEME-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.showActionsOnlyHover {
|
&.showActionsOnlyHover {
|
||||||
|
@ -1004,7 +1004,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
.footerButton {
|
.footerButton {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
opacity: 0.7;
|
color: color-mix(in srgb, var(--MI_THEME-panel), var(--MI_THEME-fg) 70%); // opacityなど不透明度で表現するとレンダリングパフォーマンスに影響するので通常の色の混合で代用
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
margin-right: 28px;
|
margin-right: 28px;
|
||||||
|
@ -1018,7 +1018,6 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||||
.footerButtonCount {
|
.footerButtonCount {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0 0 0 8px;
|
margin: 0 0 0 8px;
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 580px) {
|
@container (max-width: 580px) {
|
||||||
|
|
|
@ -56,10 +56,12 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
|
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
|
||||||
if (event.touches && event.touches[0] && event.touches[0].screenY != null) {
|
if (('touches' in event) && event.touches[0] && event.touches[0].screenY != null) {
|
||||||
return event.touches[0].screenY;
|
return event.touches[0].screenY;
|
||||||
} else {
|
} else if ('screenY' in event) {
|
||||||
return event.screenY;
|
return event.screenY;
|
||||||
|
} else {
|
||||||
|
return 0; // TSを黙らせるため
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-adaptive-border class="body">
|
<div v-adaptive-border class="body" :class="{ 'disabled': disabled }">
|
||||||
<slot name="prefix"></slot>
|
<slot name="prefix"></slot>
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
|
@ -180,6 +180,8 @@ function onMouseenter() {
|
||||||
let lastClickTime: number | null = null;
|
let lastClickTime: number | null = null;
|
||||||
|
|
||||||
function onMousedown(ev: MouseEvent | TouchEvent) {
|
function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
|
if (props.disabled) return; // Prevent interaction if disabled
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
tooltipForDragShowing.value = true;
|
tooltipForDragShowing.value = true;
|
||||||
|
@ -292,6 +294,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
border: solid 1px var(--MI_THEME-panel);
|
border: solid 1px var(--MI_THEME-panel);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -24,6 +24,7 @@ const elRef = useTemplateRef('elRef');
|
||||||
|
|
||||||
if (props.withTooltip) {
|
if (props.withTooltip) {
|
||||||
useTooltip(elRef, (showing) => {
|
useTooltip(elRef, (showing) => {
|
||||||
|
if (elRef.value == null) return;
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
|
||||||
showing,
|
showing,
|
||||||
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
|
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
|
||||||
|
|
|
@ -41,7 +41,7 @@ import { i18n } from '@/i18n.js';
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
role: Misskey.entities.Role;
|
role: Misskey.entities.Role;
|
||||||
forModeration: boolean;
|
forModeration: boolean;
|
||||||
detailed: boolean;
|
detailed?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
detailed: true,
|
detailed: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -174,7 +174,7 @@ watch([modelValue, () => props.items], () => {
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (opening.value) return;
|
if (opening.value || props.disabled || props.readonly) return;
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
opening.value = true;
|
opening.value = true;
|
||||||
|
|
|
@ -63,6 +63,7 @@ import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||||
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
||||||
import type { BasicTimelineType } from '@/timelines.js';
|
import type { BasicTimelineType } from '@/timelines.js';
|
||||||
import type { SoundStore } from '@/preferences/def.js';
|
import type { SoundStore } from '@/preferences/def.js';
|
||||||
|
import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js';
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import * as sound from '@/utility/sound.js';
|
import * as sound from '@/utility/sound.js';
|
||||||
|
@ -76,7 +77,6 @@ import { i18n } from '@/i18n.js';
|
||||||
import { globalEvents, useGlobalEvent } from '@/events.js';
|
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||||
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||||
|
@ -524,7 +524,6 @@ defineExpose({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
opacity: 0.75;
|
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
|
|
@ -46,8 +46,8 @@ import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, mark
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||||
import type { notificationTypes } from '@@/js/const.js';
|
|
||||||
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
||||||
|
import type { notificationTypes } from '@@/js/const.js';
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
@ -235,7 +235,6 @@ defineExpose({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
opacity: 0.75;
|
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
|
|
|
@ -169,10 +169,6 @@ onUnmounted(() => {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsInner {
|
.tabsInner {
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/>
|
<MkUserInfo v-for="item in items" :key="item.id" :user="extractor(item)"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
|
|
|
@ -194,10 +194,6 @@ onUnmounted(() => {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsInner {
|
.tabsInner {
|
||||||
|
|
|
@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { defineAsyncComponent, ref } from 'vue';
|
import { defineAsyncComponent, ref } from 'vue';
|
||||||
import { toUnicode as decodePunycode } from 'punycode.js';
|
import { toUnicode as decodePunycode } from 'punycode.js';
|
||||||
import { url as local } from '@@/js/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
|
import { maybeMakeRelative } from '@@/js/url.js';
|
||||||
|
import type { MkABehavior } from '@/components/global/MkA.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useTooltip } from '@/composables/use-tooltip.js';
|
import { useTooltip } from '@/composables/use-tooltip.js';
|
||||||
import { isEnabledUrlPreview } from '@/utility/url-preview.js';
|
import { isEnabledUrlPreview } from '@/utility/url-preview.js';
|
||||||
import type { MkABehavior } from '@/components/global/MkA.vue';
|
|
||||||
import { maybeMakeRelative } from '@@/js/url.js';
|
|
||||||
|
|
||||||
function safeURIDecode(str: string): string {
|
function safeURIDecode(str: string): string {
|
||||||
try {
|
try {
|
||||||
|
@ -94,7 +94,7 @@ const target = self ? null : '_blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema {
|
.schema {
|
||||||
opacity: 0.5;
|
color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||||
}
|
}
|
||||||
|
|
||||||
.hostname {
|
.hostname {
|
||||||
|
@ -102,11 +102,11 @@ const target = self ? null : '_blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
.pathname {
|
.pathname {
|
||||||
opacity: 0.8;
|
color: color(from currentcolor srgb r g b / 0.8); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||||
}
|
}
|
||||||
|
|
||||||
.query {
|
.query {
|
||||||
opacity: 0.5;
|
color: color(from currentcolor srgb r g b / 0.5); // DOMノード全体をopacityで半透明化するより文字色を半透明化した方が若干レンダリングパフォーマンスが良い
|
||||||
}
|
}
|
||||||
|
|
||||||
.hash {
|
.hash {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
import type { Form, GetFormResultType } from '@/utility/form.js';
|
import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { PostFormProps } from '@/types/post-form.js';
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
|
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||||
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
||||||
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -837,7 +837,7 @@ export function launchUploader(
|
||||||
options?: {
|
options?: {
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
features?: UploaderDialogFeatures;
|
features?: UploaderFeatures;
|
||||||
},
|
},
|
||||||
): Promise<Misskey.entities.DriveFile[]> {
|
): Promise<Misskey.entities.DriveFile[]> {
|
||||||
return new Promise(async (res, rej) => {
|
return new Promise(async (res, rej) => {
|
||||||
|
|
|
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
|
<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkInfo>
|
<MkInfo>
|
||||||
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
|
{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name ?? host }) }}
|
||||||
</MkInfo>
|
</MkInfo>
|
||||||
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
|
<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
|
||||||
<template #icon><i class="ti ti-code"></i></template>
|
<template #icon><i class="ti ti-code"></i></template>
|
||||||
|
@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue';
|
import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue';
|
||||||
import { version } from '@@/js/config.js';
|
import { host, version } from '@@/js/config.js';
|
||||||
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 MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -414,6 +414,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
|
||||||
const containerEl = useTemplateRef('containerEl');
|
const containerEl = useTemplateRef('containerEl');
|
||||||
|
|
||||||
function iconLoaded() {
|
function iconLoaded() {
|
||||||
|
if (containerEl.value == null) return;
|
||||||
const emojis = prefer.s.emojiPalettes[0].emojis;
|
const emojis = prefer.s.emojiPalettes[0].emojis;
|
||||||
const containerWidth = containerEl.value.offsetWidth;
|
const containerWidth = containerEl.value.offsetWidth;
|
||||||
for (let i = 0; i < 32; i++) {
|
for (let i = 0; i < 32; i++) {
|
||||||
|
@ -431,6 +432,7 @@ function iconLoaded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gravity() {
|
function gravity() {
|
||||||
|
if (containerEl.value == null) return;
|
||||||
if (!easterEggReady) return;
|
if (!easterEggReady) return;
|
||||||
easterEggReady = false;
|
easterEggReady = false;
|
||||||
easterEggEngine.value = physics(containerEl.value);
|
easterEggEngine.value = physics(containerEl.value);
|
||||||
|
|
38
packages/frontend/src/pages/admin-file.chat.vue
Normal file
38
packages/frontend/src/pages/admin-file.chat.vue
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
||||||
|
|
||||||
|
<MkPagination :paginator="paginator">
|
||||||
|
<template #default="{ items }">
|
||||||
|
<XMessage v-for="item in items" :key="item.id" :message="item" :isSearchResult="true"/>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, markRaw } from 'vue';
|
||||||
|
import XMessage from './chat/XMessage.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fileId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const realFileId = computed(() => props.fileId);
|
||||||
|
|
||||||
|
const paginator = markRaw(new Paginator('drive/files/attached-chat-messages', {
|
||||||
|
limit: 10,
|
||||||
|
params: {
|
||||||
|
fileId: realFileId.value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
</script>
|
|
@ -44,8 +44,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'notes' && info" class="_gaps_m">
|
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
|
||||||
<XNotes :fileId="fileId"/>
|
<MkTabs
|
||||||
|
v-model:tab="usageTab"
|
||||||
|
:tabs="[{
|
||||||
|
key: 'note',
|
||||||
|
title: 'Note',
|
||||||
|
}, {
|
||||||
|
key: 'chat',
|
||||||
|
title: 'Chat',
|
||||||
|
}]"
|
||||||
|
/>
|
||||||
|
<XNotes v-if="usageTab === 'note'" :fileId="fileId"/>
|
||||||
|
<XChat v-else-if="usageTab === 'chat'" :fileId="fileId"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
|
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
|
||||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||||
|
@ -86,12 +97,15 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { iAmAdmin, iAmModerator } from '@/i.js';
|
import { iAmAdmin, iAmModerator } from '@/i.js';
|
||||||
|
import MkTabs from '@/components/MkTabs.vue';
|
||||||
|
|
||||||
const tab = ref('overview');
|
const tab = ref('overview');
|
||||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||||
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
|
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
|
||||||
const isSensitive = ref<boolean>(false);
|
const isSensitive = ref<boolean>(false);
|
||||||
|
const usageTab = ref<'note' | 'chat'>('note');
|
||||||
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
|
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
|
||||||
|
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fileId: string,
|
fileId: string,
|
||||||
|
@ -147,9 +161,9 @@ const headerTabs = computed(() => [{
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
}, iAmModerator ? {
|
}, iAmModerator ? {
|
||||||
key: 'notes',
|
key: 'usage',
|
||||||
title: i18n.ts._fileViewer.attachedNotes,
|
title: i18n.ts._fileViewer.usage,
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-plus',
|
||||||
} : null, iAmModerator ? {
|
} : null, iAmModerator ? {
|
||||||
key: 'ip',
|
key: 'ip',
|
||||||
title: 'IP',
|
title: 'IP',
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
messageId?: string;
|
messageId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const initializing = ref(true);
|
const initializing = ref(true);
|
||||||
|
|
|
@ -197,7 +197,7 @@ async function initialize() {
|
||||||
connection.value.on('deleted', onDeleted);
|
connection.value.on('deleted', onDeleted);
|
||||||
connection.value.on('react', onReact);
|
connection.value.on('react', onReact);
|
||||||
connection.value.on('unreact', onUnreact);
|
connection.value.on('unreact', onUnreact);
|
||||||
} else {
|
} else if (props.roomId) {
|
||||||
const [rResult, mResult] = await Promise.allSettled([
|
const [rResult, mResult] = await Promise.allSettled([
|
||||||
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
|
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
|
||||||
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
|
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
|
||||||
|
|
|
@ -76,7 +76,8 @@ watch(() => props.clipId, async () => {
|
||||||
clip.value = await misskeyApi('clips/show', {
|
clip.value = await misskeyApi('clips/show', {
|
||||||
clipId: props.clipId,
|
clipId: props.clipId,
|
||||||
});
|
});
|
||||||
favorited.value = clip.value.isFavorited;
|
|
||||||
|
favorited.value = clip.value!.isFavorited ?? false;
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
@ -108,6 +109,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.edit,
|
text: i18n.ts.edit,
|
||||||
handler: async (): Promise<void> => {
|
handler: async (): Promise<void> => {
|
||||||
|
if (clip.value == null) return;
|
||||||
|
|
||||||
const { canceled, result } = await os.form(clip.value.name, {
|
const { canceled, result } = await os.form(clip.value.name, {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -128,6 +131,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
||||||
default: clip.value.isPublic,
|
default: clip.value.isPublic,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('clips/update', {
|
os.apiWithDialog('clips/update', {
|
||||||
|
@ -178,6 +182,8 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
||||||
text: i18n.ts.delete,
|
text: i18n.ts.delete,
|
||||||
danger: true,
|
danger: true,
|
||||||
handler: async (): Promise<void> => {
|
handler: async (): Promise<void> => {
|
||||||
|
if (clip.value == null) return;
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
|
text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
|
||||||
|
|
|
@ -10,9 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||||
|
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
<MkNote v-for="item in items" :key="item.id" :note="item.note" :class="$style.note"/>
|
||||||
<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
|
||||||
</MkDateSeparatedList>
|
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
|
@ -67,7 +67,7 @@ const router = useRouter();
|
||||||
const tab = ref('featured');
|
const tab = ref('featured');
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const searchPaginator = shallowRef<IPaginator | null>(null);
|
const searchPaginator = shallowRef<Paginator<'flash/search'> | null>(null);
|
||||||
const searchKey = ref(0);
|
const searchKey = ref(0);
|
||||||
|
|
||||||
const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
|
const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
|
||||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkKeyValue>
|
<MkKeyValue>
|
||||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
|
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
|
||||||
<template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template>
|
<template #value><MkUrl v-if="url" :url="url" :showUrlPreview="false"></MkUrl></template>
|
||||||
</MkKeyValue>
|
</MkKeyValue>
|
||||||
<MkKeyValue>
|
<MkKeyValue>
|
||||||
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
|
<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
|
||||||
|
@ -151,7 +151,7 @@ async function fetch() {
|
||||||
case 'theme':
|
case 'theme':
|
||||||
try {
|
try {
|
||||||
const metaRaw = parseThemeCode(res.data);
|
const metaRaw = parseThemeCode(res.data);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { id, props, desc: description, ...meta } = metaRaw;
|
const { id, props, desc: description, ...meta } = metaRaw;
|
||||||
data.value = {
|
data.value = {
|
||||||
type: 'theme',
|
type: 'theme',
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
<div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||||
<div :class="$style.faviconAndName">
|
<div :class="$style.faviconAndName">
|
||||||
<img :src="faviconUrl" alt="" :class="$style.icon"/>
|
<img v-if="faviconUrl" :src="faviconUrl" alt="" :class="$style.icon"/>
|
||||||
<span :class="$style.name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
<span :class="$style.name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||||
|
|
|
@ -62,24 +62,29 @@ function fetchList(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function like() {
|
function like() {
|
||||||
|
if (list.value == null) return;
|
||||||
os.apiWithDialog('users/lists/favorite', {
|
os.apiWithDialog('users/lists/favorite', {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
if (list.value == null) return;
|
||||||
list.value.isLiked = true;
|
list.value.isLiked = true;
|
||||||
list.value.likedCount++;
|
list.value.likedCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function unlike() {
|
function unlike() {
|
||||||
|
if (list.value == null) return;
|
||||||
os.apiWithDialog('users/lists/unfavorite', {
|
os.apiWithDialog('users/lists/unfavorite', {
|
||||||
listId: list.value.id,
|
listId: list.value.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
if (list.value == null) return;
|
||||||
list.value.isLiked = false;
|
list.value.isLiked = false;
|
||||||
list.value.likedCount--;
|
list.value.likedCount--;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
|
if (list.value == null) return;
|
||||||
const { canceled, result: name } = await os.inputText({
|
const { canceled, result: name } = await os.inputText({
|
||||||
title: i18n.ts.enterListName,
|
title: i18n.ts.enterListName,
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,6 +64,7 @@ async function create() {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('clips/create', result);
|
os.apiWithDialog('clips/create', result);
|
||||||
|
|
|
@ -79,7 +79,9 @@ async function createKey() {
|
||||||
default: scope.value.join('/'),
|
default: scope.value.join('/'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('i/registry/set', {
|
os.apiWithDialog('i/registry/set', {
|
||||||
scope: result.scope.split('/'),
|
scope: result.scope.split('/'),
|
||||||
key: result.key,
|
key: result.key,
|
||||||
|
|
|
@ -56,7 +56,9 @@ async function createKey() {
|
||||||
label: i18n.ts._registry.scope,
|
label: i18n.ts._registry.scope,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('i/registry/set', {
|
os.apiWithDialog('i/registry/set', {
|
||||||
scope: result.scope.split('/'),
|
scope: result.scope.split('/'),
|
||||||
key: result.key,
|
key: result.key,
|
||||||
|
|
|
@ -25,7 +25,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkUserName class="name" :user="user" :nowrap="true"/>
|
<MkUserName class="name" :user="user" :nowrap="true"/>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
|
||||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||||
|
@ -44,7 +43,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
<span class="username"><MkAcct :user="user" :detail="true"/></span>
|
||||||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-badge);"><i class="ti ti-shield"></i></span>
|
|
||||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,12 +14,13 @@ import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import type { FormWithDefault } from '@/utility/form.js';
|
||||||
|
|
||||||
export type Plugin = {
|
export type Plugin = {
|
||||||
installId: string;
|
installId: string;
|
||||||
name: string;
|
name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
config?: Record<string, { default: any }>;
|
config?: FormWithDefault;
|
||||||
configData: Record<string, any>;
|
configData: Record<string, any>;
|
||||||
src: string | null;
|
src: string | null;
|
||||||
version: string;
|
version: string;
|
||||||
|
@ -240,7 +241,7 @@ async function launchPlugin(id: Plugin['installId']): Promise<void> {
|
||||||
pluginLogs.value.set(plugin.installId, []);
|
pluginLogs.value.set(plugin.installId, []);
|
||||||
|
|
||||||
function systemLog(message: string, isError = false): void {
|
function systemLog(message: string, isError = false): void {
|
||||||
pluginLogs.value.get(plugin.installId)?.push({
|
pluginLogs.value.get(plugin!.installId)?.push({
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
message,
|
message,
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const store = markRaw(new Pizzax('base', {
|
||||||
},
|
},
|
||||||
memo: {
|
memo: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
default: null,
|
default: null as string | null,
|
||||||
},
|
},
|
||||||
reactionAcceptance: {
|
reactionAcceptance: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
|
|
|
@ -22,11 +22,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
color: var(--MI_THEME-fgOnAccent);
|
|
||||||
background-color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
@ -45,27 +40,6 @@ html {
|
||||||
&, * {
|
&, * {
|
||||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--MI_THEME-scrollbarHandle);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--MI_THEME-scrollbarHandleHover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.f-1 {
|
&.f-1 {
|
||||||
|
@ -110,6 +84,11 @@ html::view-transition-old(theme-changing) {
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html::selection {
|
||||||
|
color: var(--MI_THEME-fgOnAccent);
|
||||||
|
background-color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes themeChangingOld {
|
@keyframes themeChangingOld {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { Component, ComputedRef, Ref } from 'vue';
|
import type { Component, ComputedRef, Ref, MaybeRef } from 'vue';
|
||||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
|
|
||||||
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
|
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: MaybeRef<CP<T>[K]> };
|
||||||
|
|
||||||
type MenuRadioOptionsDef = Record<string, any>;
|
type MenuRadioOptionsDef = Record<string, any>;
|
||||||
|
|
||||||
|
@ -15,22 +15,107 @@ type Text = string | ComputedRef<string>;
|
||||||
|
|
||||||
export type MenuAction = (ev: MouseEvent) => void;
|
export type MenuAction = (ev: MouseEvent) => void;
|
||||||
|
|
||||||
export type MenuDivider = { type: 'divider' };
|
export interface MenuButton {
|
||||||
export type MenuNull = undefined;
|
type?: 'button';
|
||||||
export type MenuLabel = { type: 'label', text: Text, caption?: Text };
|
text: Text;
|
||||||
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
caption?: Text;
|
||||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean };
|
icon?: string;
|
||||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
indicate?: boolean;
|
||||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> };
|
danger?: boolean;
|
||||||
export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
|
active?: boolean | ComputedRef<boolean>;
|
||||||
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
avatar?: Misskey.entities.User;
|
||||||
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
action: MenuAction;
|
||||||
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
|
}
|
||||||
export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
|
||||||
|
|
||||||
export type MenuPending = { type: 'pending' };
|
interface MenuBase {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
|
export interface MenuDivider extends MenuBase {
|
||||||
|
type: 'divider';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuLabel extends MenuBase {
|
||||||
|
type: 'label';
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuLink extends MenuBase {
|
||||||
|
type: 'link';
|
||||||
|
to: string;
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
icon?: string;
|
||||||
|
indicate?: boolean;
|
||||||
|
avatar?: Misskey.entities.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuA extends MenuBase {
|
||||||
|
type: 'a';
|
||||||
|
href: string;
|
||||||
|
target?: string;
|
||||||
|
download?: string;
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
icon?: string;
|
||||||
|
indicate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuUser extends MenuBase {
|
||||||
|
type: 'user';
|
||||||
|
user: Misskey.entities.User;
|
||||||
|
active?: boolean;
|
||||||
|
indicate?: boolean;
|
||||||
|
action: MenuAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuSwitch extends MenuBase {
|
||||||
|
type: 'switch';
|
||||||
|
ref: Ref<boolean>;
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean | Ref<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuRadio extends MenuBase {
|
||||||
|
type: 'radio';
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
icon?: string;
|
||||||
|
ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>;
|
||||||
|
options: MenuRadioOptionsDef;
|
||||||
|
disabled?: boolean | Ref<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuRadioOption extends MenuBase {
|
||||||
|
type: 'radioOption';
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
action: MenuAction;
|
||||||
|
active?: boolean | ComputedRef<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuComponent<T extends Component = any> extends MenuBase {
|
||||||
|
type: 'component';
|
||||||
|
component: T;
|
||||||
|
props?: ComponentProps<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuParent extends MenuBase {
|
||||||
|
type: 'parent';
|
||||||
|
text: Text;
|
||||||
|
caption?: Text;
|
||||||
|
icon?: string;
|
||||||
|
children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuPending extends MenuBase {
|
||||||
|
type: 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
type OuterMenuItem = MenuDivider | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
|
||||||
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuComponent | MenuParent>;
|
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuComponent | MenuParent>;
|
||||||
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
|
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
|
||||||
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
|
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuComponent | MenuParent;
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export interface PostFormProps {
|
export interface PostFormProps {
|
||||||
reply?: Misskey.entities.Note;
|
reply?: Misskey.entities.Note | null;
|
||||||
renote?: Misskey.entities.Note;
|
renote?: Misskey.entities.Note | null;
|
||||||
channel?: Misskey.entities.Channel; // TODO
|
channel?: Misskey.entities.Channel | null; // TODO
|
||||||
mention?: Misskey.entities.User;
|
mention?: Misskey.entities.User;
|
||||||
specified?: Misskey.entities.UserDetailed;
|
specified?: Misskey.entities.UserDetailed;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
|
|
@ -368,10 +368,6 @@ function onDrop(ev) {
|
||||||
> .body {
|
> .body {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,10 +393,6 @@ function onDrop(ev) {
|
||||||
> .body {
|
> .body {
|
||||||
background: var(--MI_THEME-bg) !important;
|
background: var(--MI_THEME-bg) !important;
|
||||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -487,9 +479,5 @@ function onDrop(ev) {
|
||||||
container-type: size;
|
container-type: size;
|
||||||
background-color: var(--MI_THEME-bg);
|
background-color: var(--MI_THEME-bg);
|
||||||
scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
|
scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: var(--MI_THEME-panel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -29,7 +29,8 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
|
||||||
label: i18n.ts.sound,
|
label: i18n.ts.sound,
|
||||||
default: soundSetting.value.type ?? 'none',
|
default: soundSetting.value.type ?? 'none',
|
||||||
enum: soundsTypes.map(f => ({
|
enum: soundsTypes.map(f => ({
|
||||||
value: f ?? 'none', label: getSoundTypeName(f),
|
value: f ?? 'none' as Exclude<SoundType, null> | 'none',
|
||||||
|
label: getSoundTypeName(f),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
soundFile: {
|
soundFile: {
|
||||||
|
@ -81,16 +82,17 @@ export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promis
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
const res = buildSoundStore(result);
|
const res = buildSoundStore(result);
|
||||||
if (res) soundSetting.value = res;
|
if (res) soundSetting.value = res;
|
||||||
|
|
||||||
function buildSoundStore(result: any): SoundStore | null {
|
function buildSoundStore(r: NonNullable<typeof result>): SoundStore | null {
|
||||||
const type = (result.type === 'none' ? null : result.type) as SoundType;
|
const type = (r.type === 'none' ? null : r.type);
|
||||||
const volume = result.volume as number;
|
const volume = r.volume;
|
||||||
const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
|
const fileId = r.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined);
|
||||||
const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
|
const fileUrl = r.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined);
|
||||||
|
|
||||||
if (type === '_driveFile_') {
|
if (type === '_driveFile_') {
|
||||||
if (!fileUrl || !fileId) {
|
if (!fileUrl || !fileId) {
|
||||||
|
|
|
@ -126,7 +126,6 @@ const onContextmenu = (ev) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
$ui-font-size: 1em; // TODO: どこかに集約したい
|
|
||||||
$widgets-hide-threshold: 1090px;
|
$widgets-hide-threshold: 1090px;
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
|
|
@ -5,55 +5,59 @@
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
type EnumItem = string | {
|
export type EnumItem = string | {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Hidden = boolean | ((v: any) => boolean);
|
type Hidden = boolean | ((v: any) => boolean);
|
||||||
|
|
||||||
export type FormItem = {
|
interface FormItemBase {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
hidden?: Hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringFormItem extends FormItemBase {
|
||||||
type: 'string';
|
type: 'string';
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
treatAsMfm?: boolean;
|
treatAsMfm?: boolean;
|
||||||
} | {
|
}
|
||||||
label?: string;
|
|
||||||
|
export interface NumberFormItem extends FormItemBase {
|
||||||
type: 'number';
|
type: 'number';
|
||||||
default?: number | null;
|
default?: number | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
|
||||||
step?: number;
|
step?: number;
|
||||||
} | {
|
}
|
||||||
label?: string;
|
|
||||||
|
export interface BooleanFormItem extends FormItemBase {
|
||||||
type: 'boolean';
|
type: 'boolean';
|
||||||
default?: boolean | null;
|
default?: boolean | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
hidden?: Hidden;
|
}
|
||||||
} | {
|
|
||||||
label?: string;
|
export interface EnumFormItem extends FormItemBase {
|
||||||
type: 'enum';
|
type: 'enum';
|
||||||
default?: string | null;
|
default?: string | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
|
||||||
enum: EnumItem[];
|
enum: EnumItem[];
|
||||||
} | {
|
}
|
||||||
label?: string;
|
|
||||||
|
export interface RadioFormItem extends FormItemBase {
|
||||||
type: 'radio';
|
type: 'radio';
|
||||||
default?: unknown | null;
|
default?: unknown | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: Hidden;
|
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
}[];
|
}[];
|
||||||
} | {
|
}
|
||||||
label?: string;
|
|
||||||
|
export interface RangeFormItem extends FormItemBase {
|
||||||
type: 'range';
|
type: 'range';
|
||||||
default?: number | null;
|
default?: number | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -62,42 +66,80 @@ export type FormItem = {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
textConverter?: (value: number) => string;
|
textConverter?: (value: number) => string;
|
||||||
hidden?: Hidden;
|
}
|
||||||
} | {
|
|
||||||
label?: string;
|
export interface ObjectFormItem extends FormItemBase {
|
||||||
type: 'object';
|
type: 'object';
|
||||||
default?: Record<string, unknown> | null;
|
default?: Record<string, unknown> | null;
|
||||||
hidden: Hidden;
|
}
|
||||||
} | {
|
|
||||||
label?: string;
|
export interface ArrayFormItem extends FormItemBase {
|
||||||
type: 'array';
|
type: 'array';
|
||||||
default?: unknown[] | null;
|
default?: unknown[] | null;
|
||||||
hidden: Hidden;
|
}
|
||||||
} | {
|
|
||||||
|
export interface ButtonFormItem extends FormItemBase {
|
||||||
type: 'button';
|
type: 'button';
|
||||||
content?: string;
|
content?: string;
|
||||||
hidden?: Hidden;
|
|
||||||
action: (ev: MouseEvent, v: any) => void;
|
action: (ev: MouseEvent, v: any) => void;
|
||||||
} | {
|
}
|
||||||
|
|
||||||
|
export interface DriveFileFormItem extends FormItemBase {
|
||||||
type: 'drive-file';
|
type: 'drive-file';
|
||||||
defaultFileId?: string | null;
|
defaultFileId?: string | null;
|
||||||
hidden?: Hidden;
|
|
||||||
validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
|
validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export type FormItem =
|
||||||
|
StringFormItem |
|
||||||
|
NumberFormItem |
|
||||||
|
BooleanFormItem |
|
||||||
|
EnumFormItem |
|
||||||
|
RadioFormItem |
|
||||||
|
RangeFormItem |
|
||||||
|
ObjectFormItem |
|
||||||
|
ArrayFormItem |
|
||||||
|
ButtonFormItem |
|
||||||
|
DriveFileFormItem;
|
||||||
|
|
||||||
export type Form = Record<string, FormItem>;
|
export type Form = Record<string, FormItem>;
|
||||||
|
|
||||||
|
export type FormItemWithDefault = FormItem & {
|
||||||
|
default: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormWithDefault = Record<string, FormItemWithDefault>;
|
||||||
|
|
||||||
|
type GetRadioItemType<Item extends RadioFormItem = RadioFormItem> = Item['options'][number]['value'];
|
||||||
|
type GetEnumItemType<Item extends EnumFormItem, E = Item['enum'][number]> = E extends { value: unknown } ? E['value'] : E;
|
||||||
|
|
||||||
|
type InferDefault<T, Fallback> = T extends { default: infer D }
|
||||||
|
? D extends undefined ? Fallback : D
|
||||||
|
: Fallback;
|
||||||
|
|
||||||
|
type NonNullableIfRequired<T, Item extends FormItem> =
|
||||||
|
Item extends { required: false } ? T | null | undefined : NonNullable<T>;
|
||||||
|
|
||||||
type GetItemType<Item extends FormItem> =
|
type GetItemType<Item extends FormItem> =
|
||||||
Item['type'] extends 'string' ? string :
|
Item extends StringFormItem
|
||||||
Item['type'] extends 'number' ? number :
|
? NonNullableIfRequired<InferDefault<Item, string>, Item>
|
||||||
Item['type'] extends 'boolean' ? boolean :
|
: Item extends NumberFormItem
|
||||||
Item['type'] extends 'radio' ? unknown :
|
? NonNullableIfRequired<InferDefault<Item, number>, Item>
|
||||||
Item['type'] extends 'range' ? number :
|
: Item extends BooleanFormItem
|
||||||
Item['type'] extends 'enum' ? string :
|
? boolean
|
||||||
Item['type'] extends 'array' ? unknown[] :
|
: Item extends RadioFormItem
|
||||||
Item['type'] extends 'object' ? Record<string, unknown> :
|
? GetRadioItemType<Item>
|
||||||
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
|
: Item extends RangeFormItem
|
||||||
never;
|
? NonNullableIfRequired<InferDefault<RangeFormItem, number>, Item>
|
||||||
|
: Item extends EnumFormItem
|
||||||
|
? GetEnumItemType<Item>
|
||||||
|
: Item extends ArrayFormItem
|
||||||
|
? NonNullableIfRequired<InferDefault<ArrayFormItem, unknown[]>, Item>
|
||||||
|
: Item extends ObjectFormItem
|
||||||
|
? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item>
|
||||||
|
: Item extends DriveFileFormItem
|
||||||
|
? Misskey.entities.DriveFile | undefined
|
||||||
|
: never;
|
||||||
|
|
||||||
export type GetFormResultType<F extends Form> = {
|
export type GetFormResultType<F extends Form> = {
|
||||||
[P in keyof F]: GetItemType<F[P]>;
|
[P in keyof F]: GetItemType<F[P]>;
|
||||||
|
|
|
@ -101,7 +101,7 @@ export async function getNoteClipMenu(props: {
|
||||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: null,
|
default: null as string | null,
|
||||||
label: i18n.ts.name,
|
label: i18n.ts.name,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
|
@ -180,6 +180,7 @@ export function getNoteMenu(props: {
|
||||||
currentClip?: Misskey.entities.Clip;
|
currentClip?: Misskey.entities.Clip;
|
||||||
}) {
|
}) {
|
||||||
const appearNote = getAppearNote(props.note);
|
const appearNote = getAppearNote(props.note);
|
||||||
|
const link = appearNote.url ?? appearNote.uri;
|
||||||
|
|
||||||
const cleanups = [] as (() => void)[];
|
const cleanups = [] as (() => void)[];
|
||||||
|
|
||||||
|
@ -189,6 +190,7 @@ export function getNoteMenu(props: {
|
||||||
text: i18n.ts.noteDeleteConfirm,
|
text: i18n.ts.noteDeleteConfirm,
|
||||||
}).then(({ canceled }) => {
|
}).then(({ canceled }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
if ($i == null) return;
|
||||||
|
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -208,6 +210,7 @@ export function getNoteMenu(props: {
|
||||||
text: i18n.ts.deleteAndEditConfirm,
|
text: i18n.ts.deleteAndEditConfirm,
|
||||||
}).then(({ canceled }) => {
|
}).then(({ canceled }) => {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
if ($i == null) return;
|
||||||
|
|
||||||
misskeyApi('notes/delete', {
|
misskeyApi('notes/delete', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -317,22 +320,25 @@ export function getNoteMenu(props: {
|
||||||
action: copyContent,
|
action: copyContent,
|
||||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||||
|
|
||||||
if (appearNote.url || appearNote.uri) {
|
if (link) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
icon: 'ti ti-link',
|
icon: 'ti ti-link',
|
||||||
text: i18n.ts.copyRemoteLink,
|
text: i18n.ts.copyRemoteLink,
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(appearNote.url ?? appearNote.uri);
|
copyToClipboard(link);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-external-link',
|
icon: 'ti ti-external-link',
|
||||||
text: i18n.ts.showOnRemote,
|
text: i18n.ts.showOnRemote,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
window.open(link, '_blank', 'noopener');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed));
|
const embedMenu = getNoteEmbedCodeMenu(appearNote, i18n.ts.embed);
|
||||||
|
if (embedMenu != null) {
|
||||||
|
menuItems.push(embedMenu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSupportShare()) {
|
if (isSupportShare()) {
|
||||||
|
@ -475,22 +481,25 @@ export function getNoteMenu(props: {
|
||||||
action: copyContent,
|
action: copyContent,
|
||||||
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink));
|
||||||
|
|
||||||
if (appearNote.url || appearNote.uri) {
|
if (link != null) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
icon: 'ti ti-link',
|
icon: 'ti ti-link',
|
||||||
text: i18n.ts.copyRemoteLink,
|
text: i18n.ts.copyRemoteLink,
|
||||||
action: () => {
|
action: () => {
|
||||||
copyToClipboard(appearNote.url ?? appearNote.uri);
|
copyToClipboard(link);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-external-link',
|
icon: 'ti ti-external-link',
|
||||||
text: i18n.ts.showOnRemote,
|
text: i18n.ts.showOnRemote,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
|
window.open(link, '_blank', 'noopener');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed));
|
const embedMenu = getNoteEmbedCodeMenu(appearNote, i18n.ts.embed);
|
||||||
|
if (embedMenu != null) {
|
||||||
|
menuItems.push(embedMenu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,7 +634,7 @@ export function getRenoteMenu(props: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, (props.mock) ? undefined : {
|
}, ...(props.mock ? [] : [{
|
||||||
text: i18n.ts.quote,
|
text: i18n.ts.quote,
|
||||||
icon: 'ti ti-quote',
|
icon: 'ti ti-quote',
|
||||||
action: () => {
|
action: () => {
|
||||||
|
@ -633,7 +642,7 @@ export function getRenoteMenu(props: {
|
||||||
renote: appearNote,
|
renote: appearNote,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}]);
|
}])]);
|
||||||
|
|
||||||
normalExternalChannelRenoteItems.push({
|
normalExternalChannelRenoteItems.push({
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
|
|
|
@ -132,6 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
const userDetailed = await misskeyApi('users/show', {
|
const userDetailed = await misskeyApi('users/show', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { canceled, result } = await os.form(i18n.ts.editMemo, {
|
const { canceled, result } = await os.form(i18n.ts.editMemo, {
|
||||||
memo: {
|
memo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -141,6 +142,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
|
||||||
default: userDetailed.memo,
|
default: userDetailed.memo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
os.apiWithDialog('users/update-memo', {
|
os.apiWithDialog('users/update-memo', {
|
||||||
|
|
|
@ -35,6 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
activity: {
|
activity: {
|
||||||
total: number;
|
total: number;
|
||||||
|
@ -44,10 +46,21 @@ const props = defineProps<{
|
||||||
}[]
|
}[]
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
for (const d of props.activity) {
|
const activity = deepClone(props.activity).map(d => ({
|
||||||
d.total = d.notes + d.replies + d.renotes;
|
...d,
|
||||||
}
|
total: d.notes + d.replies + d.renotes,
|
||||||
const peak = Math.max(...props.activity.map(d => d.total));
|
x: 0,
|
||||||
|
date: {
|
||||||
|
year: 0,
|
||||||
|
month: 0,
|
||||||
|
day: 0,
|
||||||
|
weekday: 0,
|
||||||
|
},
|
||||||
|
v: 0,
|
||||||
|
color: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const peak = Math.max(...activity.map(d => d.total));
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const year = now.getFullYear();
|
const year = now.getFullYear();
|
||||||
|
@ -55,7 +68,7 @@ const month = now.getMonth();
|
||||||
const day = now.getDate();
|
const day = now.getDate();
|
||||||
|
|
||||||
let x = 20;
|
let x = 20;
|
||||||
props.activity.slice().forEach((d, i) => {
|
activity.slice().forEach((d, i) => {
|
||||||
d.x = x;
|
d.x = x;
|
||||||
|
|
||||||
const date = new Date(year, month, day - i);
|
const date = new Date(year, month, day - i);
|
||||||
|
|
|
@ -25,29 +25,31 @@ import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
||||||
import XCalendar from './WidgetActivity.calendar.vue';
|
import XCalendar from './WidgetActivity.calendar.vue';
|
||||||
import XChart from './WidgetActivity.chart.vue';
|
import XChart from './WidgetActivity.chart.vue';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { $i } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const name = 'activity';
|
const name = 'activity';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 0,
|
default: 0,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
|
|
||||||
const name = 'ai';
|
const name = 'ai';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ const touched = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMousemove = (ev: MouseEvent) => {
|
const onMousemove = (ev: MouseEvent) => {
|
||||||
|
if (!live2d.value || !live2d.value.contentWindow) return;
|
||||||
|
|
||||||
const iframeRect = live2d.value.getBoundingClientRect();
|
const iframeRect = live2d.value.getBoundingClientRect();
|
||||||
live2d.value.contentWindow.postMessage({
|
live2d.value.contentWindow.postMessage({
|
||||||
type: 'moveCursor',
|
type: 'moveCursor',
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { ref } from 'vue';
|
||||||
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||||
|
@ -35,16 +35,16 @@ const name = 'aiscript';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
script: {
|
script: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
multiline: true,
|
multiline: true,
|
||||||
default: '(1 + 1)',
|
default: '(1 + 1)',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ const run = async () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: err,
|
text: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@ import type { Ref } from 'vue';
|
||||||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
|
@ -31,15 +31,15 @@ const name = 'aiscriptApp';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
script: {
|
script: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
multiline: true,
|
multiline: true,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ async function run() {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'AiScript Error',
|
title: 'AiScript Error',
|
||||||
text: err.message,
|
text: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.bdayFRoot">
|
<div :class="$style.bdayFRoot">
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
<div v-else-if="users.length > 0" :class="$style.bdayFGrid">
|
||||||
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
|
<MkAvatar v-for="user in users" :key="user.id" :user="user.followee!" link preview></MkAvatar>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.bdayFFallback">
|
<div v-else :class="$style.bdayFFallback">
|
||||||
<MkResult type="empty"/>
|
<MkResult type="empty"/>
|
||||||
|
@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -37,10 +37,10 @@ const name = i18n.ts._widgets.birthdayFollowings;
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { Interpreter, Parser } from '@syuilo/aiscript';
|
import { Interpreter, Parser } from '@syuilo/aiscript';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
|
@ -25,19 +25,19 @@ const name = 'button';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
label: {
|
label: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: 'BUTTON',
|
default: 'BUTTON',
|
||||||
},
|
},
|
||||||
colored: {
|
colored: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
script: {
|
script: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
multiline: true,
|
multiline: true,
|
||||||
default: 'Mk:dialog("hello" "world")',
|
default: 'Mk:dialog("hello" "world")',
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ const run = async () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: err,
|
text: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
|
||||||
|
@ -49,10 +49,10 @@ const name = 'calendar';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkChatHistories from '@/components/MkChatHistories.vue';
|
import MkChatHistories from '@/components/MkChatHistories.vue';
|
||||||
|
@ -28,10 +28,10 @@ const name = 'chat';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkClickerGame from '@/components/MkClickerGame.vue';
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ const name = 'clicker';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkAnalogClock from '@/components/MkAnalogClock.vue';
|
import MkAnalogClock from '@/components/MkAnalogClock.vue';
|
||||||
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
||||||
|
@ -43,76 +43,92 @@ const name = 'clock';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: 'radio' as const,
|
type: 'radio',
|
||||||
default: 'medium',
|
default: 'medium',
|
||||||
options: [{
|
options: [{
|
||||||
value: 'small', label: i18n.ts.small,
|
value: 'small' as const,
|
||||||
|
label: i18n.ts.small,
|
||||||
}, {
|
}, {
|
||||||
value: 'medium', label: i18n.ts.medium,
|
value: 'medium' as const,
|
||||||
|
label: i18n.ts.medium,
|
||||||
}, {
|
}, {
|
||||||
value: 'large', label: i18n.ts.large,
|
value: 'large' as const,
|
||||||
|
label: i18n.ts.large,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
thickness: {
|
thickness: {
|
||||||
type: 'radio' as const,
|
type: 'radio',
|
||||||
default: 0.2,
|
default: 0.2,
|
||||||
options: [{
|
options: [{
|
||||||
value: 0.1, label: 'thin',
|
value: 0.1 as const,
|
||||||
|
label: 'thin',
|
||||||
}, {
|
}, {
|
||||||
value: 0.2, label: 'medium',
|
value: 0.2 as const,
|
||||||
|
label: 'medium',
|
||||||
}, {
|
}, {
|
||||||
value: 0.3, label: 'thick',
|
value: 0.3 as const,
|
||||||
|
label: 'thick',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
graduations: {
|
graduations: {
|
||||||
type: 'radio' as const,
|
type: 'radio',
|
||||||
default: 'numbers',
|
default: 'numbers',
|
||||||
options: [{
|
options: [{
|
||||||
value: 'none', label: 'None',
|
value: 'none' as const,
|
||||||
|
label: 'None',
|
||||||
}, {
|
}, {
|
||||||
value: 'dots', label: 'Dots',
|
value: 'dots' as const,
|
||||||
|
label: 'Dots',
|
||||||
}, {
|
}, {
|
||||||
value: 'numbers', label: 'Numbers',
|
value: 'numbers' as const,
|
||||||
|
label: 'Numbers',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
fadeGraduations: {
|
fadeGraduations: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
sAnimation: {
|
sAnimation: {
|
||||||
type: 'radio' as const,
|
type: 'radio',
|
||||||
default: 'elastic',
|
default: 'elastic',
|
||||||
options: [{
|
options: [{
|
||||||
value: 'none', label: 'None',
|
value: 'none' as const,
|
||||||
|
label: 'None',
|
||||||
}, {
|
}, {
|
||||||
value: 'elastic', label: 'Elastic',
|
value: 'elastic' as const,
|
||||||
|
label: 'Elastic',
|
||||||
}, {
|
}, {
|
||||||
value: 'easeOut', label: 'Ease out',
|
value: 'easeOut' as const,
|
||||||
|
label: 'Ease out',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
twentyFour: {
|
twentyFour: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: 'radio' as const,
|
type: 'radio',
|
||||||
default: 'none',
|
default: 'none',
|
||||||
options: [{
|
options: [{
|
||||||
value: 'none', label: 'None',
|
value: 'none' as const,
|
||||||
|
label: 'None',
|
||||||
}, {
|
}, {
|
||||||
value: 'time', label: 'Time',
|
value: 'time' as const,
|
||||||
|
label: 'Time',
|
||||||
}, {
|
}, {
|
||||||
value: 'tz', label: 'TZ',
|
value: 'tz' as const,
|
||||||
|
label: 'TZ',
|
||||||
}, {
|
}, {
|
||||||
value: 'timeAndTz', label: 'Time + TZ',
|
value: 'timeAndTz' as const,
|
||||||
|
label: 'Time + TZ',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
timezone: {
|
timezone: {
|
||||||
type: 'enum' as const,
|
type: 'enum',
|
||||||
default: null,
|
default: null,
|
||||||
enum: [...timezones.map((tz) => ({
|
enum: [...timezones.map((tz) => ({
|
||||||
label: tz.name,
|
label: tz.name,
|
||||||
|
@ -122,7 +138,7 @@ const widgetPropsDef = {
|
||||||
value: null,
|
value: null,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { timezones } from '@/utility/timezones.js';
|
import { timezones } from '@/utility/timezones.js';
|
||||||
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
import MkDigitalClock from '@/components/MkDigitalClock.vue';
|
||||||
|
|
||||||
|
@ -25,24 +25,24 @@ const name = 'digitalClock';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 1.5,
|
default: 1.5,
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
},
|
},
|
||||||
showMs: {
|
showMs: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
showLabel: {
|
showLabel: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
timezone: {
|
timezone: {
|
||||||
type: 'enum' as const,
|
type: 'enum',
|
||||||
default: null,
|
default: null,
|
||||||
enum: [...timezones.map((tz) => ({
|
enum: [...timezones.map((tz) => ({
|
||||||
label: tz.name,
|
label: tz.name,
|
||||||
|
@ -52,7 +52,7 @@ const widgetPropsDef = {
|
||||||
value: null,
|
value: null,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation">
|
<MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-federation class="mkw-federation">
|
||||||
<template #icon><i class="ti ti-whirl"></i></template>
|
<template #icon><i class="ti ti-whirl"></i></template>
|
||||||
<template #header>{{ i18n.ts._widgets.federation }}</template>
|
<template #header>{{ i18n.ts._widgets.federation }}</template>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
|
@ -42,10 +42,10 @@ const name = 'federation';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkTagCloud from '@/components/MkTagCloud.vue';
|
import MkTagCloud from '@/components/MkTagCloud.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -34,10 +34,10 @@ const name = 'instanceCloud';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_panel">
|
<div class="_panel">
|
||||||
<div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : null }">
|
<div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : undefined }">
|
||||||
<div :class="$style.iconContainer">
|
<div :class="$style.iconContainer">
|
||||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
|
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,14 +22,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
const name = 'instanceInfo';
|
const name = 'instanceInfo';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onUnmounted, reactive, ref } from 'vue';
|
import { onUnmounted, reactive, ref } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import kmg from '@/filters/kmg.js';
|
import kmg from '@/filters/kmg.js';
|
||||||
import * as sound from '@/utility/sound.js';
|
import * as sound from '@/utility/sound.js';
|
||||||
|
@ -66,14 +66,14 @@ const name = 'jobQueue';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
sound: {
|
sound: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header>{{ i18n.ts._widgets.memo }}</template>
|
<template #header>{{ i18n.ts._widgets.memo }}</template>
|
||||||
|
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea>
|
<textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.memo" @input="onChange"></textarea>
|
||||||
<button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button>
|
<button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -28,14 +28,14 @@ const name = 'memo';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 100,
|
default: 100,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import type { notificationTypes as notificationTypes_typeReferenceOnly } from '@@/js/const.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
|
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -29,19 +30,19 @@ const name = 'notifications';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 300,
|
default: 300,
|
||||||
},
|
},
|
||||||
excludeTypes: {
|
excludeTypes: {
|
||||||
type: 'array' as const,
|
type: 'array',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
default: [],
|
default: [] as (typeof notificationTypes_typeReferenceOnly[number])[],
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
|
@ -27,10 +27,10 @@ const name = 'onlineUsers';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { onUnmounted, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -38,14 +38,14 @@ const name = 'photos';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
|
||||||
const name = 'postForm';
|
const name = 'postForm';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_panel">
|
<div class="_panel">
|
||||||
<div :class="$style.container" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
|
<div :class="$style.container" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : undefined }">
|
||||||
<div :class="$style.avatarContainer">
|
<div :class="$style.avatarContainer">
|
||||||
<MkAvatar :class="$style.avatar" :user="$i"/>
|
<MkAvatar :class="$style.avatar" :user="$i"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,14 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { $i } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const name = 'profile';
|
const name = 'profile';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { url as base } from '@@/js/config.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
@ -34,22 +34,22 @@ const name = 'rss';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
url: {
|
url: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
||||||
},
|
},
|
||||||
refreshIntervalSec: {
|
refreshIntervalSec: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 60,
|
default: 60,
|
||||||
},
|
},
|
||||||
maxEntries: {
|
maxEntries: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 15,
|
default: 15,
|
||||||
},
|
},
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import MarqueeText from '@/components/MkMarqueeText.vue';
|
import MarqueeText from '@/components/MkMarqueeText.vue';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import { shuffle } from '@/utility/shuffle.js';
|
import { shuffle } from '@/utility/shuffle.js';
|
||||||
import { url as base } from '@@/js/config.js';
|
import { url as base } from '@@/js/config.js';
|
||||||
|
@ -42,41 +42,41 @@ const name = 'rssTicker';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
url: {
|
url: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
|
||||||
},
|
},
|
||||||
shuffle: {
|
shuffle: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
refreshIntervalSec: {
|
refreshIntervalSec: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 60,
|
default: 60,
|
||||||
},
|
},
|
||||||
maxEntries: {
|
maxEntries: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 15,
|
default: 15,
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: 'range' as const,
|
type: 'range',
|
||||||
default: 70,
|
default: 70,
|
||||||
step: 1,
|
step: 1,
|
||||||
min: 5,
|
min: 5,
|
||||||
max: 200,
|
max: 200,
|
||||||
},
|
},
|
||||||
reverse: {
|
reverse: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -32,15 +32,15 @@ const name = 'slideshow';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
height: {
|
height: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 300,
|
default: 300,
|
||||||
},
|
},
|
||||||
folderId: {
|
folderId: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: null,
|
default: null as string | null,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ const slideA = useTemplateRef('slideA');
|
||||||
const slideB = useTemplateRef('slideB');
|
const slideB = useTemplateRef('slideB');
|
||||||
|
|
||||||
const change = () => {
|
const change = () => {
|
||||||
if (images.value.length === 0) return;
|
if (images.value.length === 0 || slideA.value == null || slideB.value == null) return;
|
||||||
|
|
||||||
const index = Math.floor(Math.random() * images.value.length);
|
const index = Math.floor(Math.random() * images.value.length);
|
||||||
const img = `url(${ images.value[index].url })`;
|
const img = `url(${ images.value[index].url })`;
|
||||||
|
@ -73,11 +73,12 @@ const change = () => {
|
||||||
|
|
||||||
slideA.value.style.backgroundImage = img;
|
slideA.value.style.backgroundImage = img;
|
||||||
|
|
||||||
slideB.value.classList.remove('anime');
|
slideB.value!.classList.remove('anime');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetch = () => {
|
const fetch = () => {
|
||||||
|
if (slideA.value == null || slideB.value == null) return;
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
misskeyApi('drive/files', {
|
misskeyApi('drive/files', {
|
||||||
|
@ -87,8 +88,8 @@ const fetch = () => {
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
images.value = res;
|
images.value = res;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
slideA.value.style.backgroundImage = '';
|
slideA.value!.style.backgroundImage = '';
|
||||||
slideB.value.style.backgroundImage = '';
|
slideB.value!.style.backgroundImage = '';
|
||||||
change();
|
change();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<button class="_button" @click="choose">
|
<button class="_button" @click="choose">
|
||||||
<span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span>
|
<span>{{ headerTitle }}</span>
|
||||||
<i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
|
<i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -25,51 +25,59 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
|
<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkStreamingNotesTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/>
|
<MkStreamingNotesTimeline
|
||||||
|
:key="widgetProps.src === 'list' ? `list:${widgetProps.list?.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna?.id}` : widgetProps.src"
|
||||||
|
:src="widgetProps.src"
|
||||||
|
:list="widgetProps.list ? widgetProps.list.id : undefined"
|
||||||
|
:antenna="widgetProps.antenna ? widgetProps.antenna.id : undefined"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
|
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
|
import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
const name = 'timeline';
|
const name = 'timeline';
|
||||||
|
|
||||||
|
type TlSrc = typeof basicTimelineTypes[number] | 'list' | 'antenna';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 300,
|
default: 300,
|
||||||
},
|
},
|
||||||
src: {
|
src: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: 'home',
|
default: 'home' as TlSrc,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
antenna: {
|
antenna: {
|
||||||
type: 'object' as const,
|
type: 'object',
|
||||||
default: null,
|
default: null as Misskey.entities.Antenna | null,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
type: 'object' as const,
|
type: 'object',
|
||||||
default: null,
|
default: null as Misskey.entities.UserList | null,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -84,12 +92,22 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
||||||
|
|
||||||
const menuOpened = ref(false);
|
const menuOpened = ref(false);
|
||||||
|
|
||||||
const setSrc = (src) => {
|
const headerTitle = computed<string>(() => {
|
||||||
|
if (widgetProps.src === 'list' && widgetProps.list != null) {
|
||||||
|
return widgetProps.list.name;
|
||||||
|
} else if (widgetProps.src === 'antenna' && widgetProps.antenna != null) {
|
||||||
|
return widgetProps.antenna.name;
|
||||||
|
} else {
|
||||||
|
return i18n.ts._timelines[widgetProps.src];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setSrc = (src: TlSrc) => {
|
||||||
widgetProps.src = src;
|
widgetProps.src = src;
|
||||||
save();
|
save();
|
||||||
};
|
};
|
||||||
|
|
||||||
const choose = async (ev) => {
|
const choose = async (ev: MouseEvent) => {
|
||||||
menuOpened.value = true;
|
menuOpened.value = true;
|
||||||
const [antennas, lists] = await Promise.all([
|
const [antennas, lists] = await Promise.all([
|
||||||
misskeyApi('antennas/list'),
|
misskeyApi('antennas/list'),
|
||||||
|
|
|
@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
|
@ -40,10 +40,10 @@ const name = 'hashtags';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -19,29 +19,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
import { onUnmounted, ref, watch } from 'vue';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
|
|
||||||
const name = 'unixClock';
|
const name = 'unixClock';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 1.5,
|
default: 1.5,
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
},
|
},
|
||||||
showMs: {
|
showMs: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
showLabel: {
|
showLabel: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||||
emit,
|
emit,
|
||||||
);
|
);
|
||||||
|
|
||||||
let intervalId;
|
let intervalId: number | null = null;
|
||||||
const ss = ref('');
|
const ss = ref('');
|
||||||
const ms = ref('');
|
const ms = ref('');
|
||||||
const showColon = ref(false);
|
const showColon = ref(false);
|
||||||
|
@ -84,7 +84,10 @@ watch(() => widgetProps.showMs, () => {
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.clearInterval(intervalId);
|
if (intervalId) {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose<WidgetComponentExpose>({
|
defineExpose<WidgetComponentExpose>({
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager } from './widget.js';
|
import { useWidgetPropsManager } from './widget.js';
|
||||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -40,15 +40,15 @@ const name = 'userList';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
listId: {
|
listId: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
default: null,
|
default: null as string | null,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
|
||||||
emit,
|
emit,
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = ref<Misskey.entities.UserList>();
|
const list = ref<Misskey.entities.UserList | null>(null);
|
||||||
const users = ref<Misskey.entities.UserDetailed[]>([]);
|
const users = ref<Misskey.entities.UserDetailed[]>([]);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ async function chooseList() {
|
||||||
})),
|
})),
|
||||||
default: widgetProps.listId,
|
default: widgetProps.listId,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || list == null) return;
|
||||||
|
|
||||||
widgetProps.listId = list.id;
|
widgetProps.listId = list.id;
|
||||||
save();
|
save();
|
||||||
|
@ -92,7 +92,7 @@ const fetch = () => {
|
||||||
}).then(_list => {
|
}).then(_list => {
|
||||||
list.value = _list;
|
list.value = _list;
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
userIds: list.value.userIds,
|
userIds: list.value.userIds ?? [],
|
||||||
}).then(_users => {
|
}).then(_users => {
|
||||||
users.value = _users;
|
users.value = _users;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
|
|
|
@ -80,7 +80,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import XPie from './pie.vue';
|
import XPie from './pie.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
|
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
|
||||||
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
|
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
|
||||||
<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
|
<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
|
||||||
<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
|
<XDisk v-else-if="widgetProps.view === 4" :meta="meta"/>
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,7 +30,7 @@ import XCpu from './cpu.vue';
|
||||||
import XMemory from './mem.vue';
|
import XMemory from './mem.vue';
|
||||||
import XDisk from './disk.vue';
|
import XDisk from './disk.vue';
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
import type { GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -39,19 +39,19 @@ const name = 'serverMetric';
|
||||||
|
|
||||||
const widgetPropsDef = {
|
const widgetPropsDef = {
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
transparent: {
|
transparent: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
type: 'number' as const,
|
type: 'number',
|
||||||
default: 0,
|
default: 0,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
};
|
} satisfies FormWithDefault;
|
||||||
|
|
||||||
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import XPie from './pie.vue';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ import bytes from '@/filters/bytes.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
connection: Misskey.IChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { reactive, watch } from 'vue';
|
import { reactive, watch } from 'vue';
|
||||||
|
import type { Reactive } from 'vue';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
import type { Form, GetFormResultType } from '@/utility/form.js';
|
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
|
||||||
|
@ -28,17 +29,17 @@ export type WidgetComponentExpose = {
|
||||||
configure: () => void;
|
configure: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>(
|
export const useWidgetPropsManager = <F extends FormWithDefault>(
|
||||||
name: string,
|
name: string,
|
||||||
propsDef: F,
|
propsDef: F,
|
||||||
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
|
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
|
||||||
emit: WidgetComponentEmits<GetFormResultType<F>>,
|
emit: WidgetComponentEmits<GetFormResultType<F>>,
|
||||||
): {
|
): {
|
||||||
widgetProps: GetFormResultType<F>;
|
widgetProps: Reactive<GetFormResultType<F>>;
|
||||||
save: () => void;
|
save: () => void;
|
||||||
configure: () => void;
|
configure: () => void;
|
||||||
} => {
|
} => {
|
||||||
const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {});
|
const widgetProps = reactive<GetFormResultType<F>>((props.widget ? deepClone(props.widget.data) : {}) as GetFormResultType<F>);
|
||||||
|
|
||||||
const mergeProps = () => {
|
const mergeProps = () => {
|
||||||
for (const prop of Object.keys(propsDef)) {
|
for (const prop of Object.keys(propsDef)) {
|
||||||
|
@ -47,12 +48,13 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(widgetProps, () => {
|
watch(widgetProps, () => {
|
||||||
mergeProps();
|
mergeProps();
|
||||||
}, { deep: true, immediate: true });
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
const save = throttle(3000, () => {
|
const save = throttle(3000, () => {
|
||||||
emit('updateProps', widgetProps);
|
emit('updateProps', widgetProps as GetFormResultType<F>);
|
||||||
});
|
});
|
||||||
|
|
||||||
const configure = async () => {
|
const configure = async () => {
|
||||||
|
|
|
@ -1226,6 +1226,12 @@ type DateString = string;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type DriveFile = components['schemas']['DriveFile'];
|
type DriveFile = components['schemas']['DriveFile'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json'];
|
type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1740,6 +1746,8 @@ declare namespace entities {
|
||||||
DriveResponse,
|
DriveResponse,
|
||||||
DriveFilesRequest,
|
DriveFilesRequest,
|
||||||
DriveFilesResponse,
|
DriveFilesResponse,
|
||||||
|
DriveFilesAttachedChatMessagesRequest,
|
||||||
|
DriveFilesAttachedChatMessagesResponse,
|
||||||
DriveFilesAttachedNotesRequest,
|
DriveFilesAttachedNotesRequest,
|
||||||
DriveFilesAttachedNotesResponse,
|
DriveFilesAttachedNotesResponse,
|
||||||
DriveFilesCheckExistenceRequest,
|
DriveFilesCheckExistenceRequest,
|
||||||
|
|
|
@ -2018,6 +2018,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:drive*
|
||||||
|
*/
|
||||||
|
request<E extends 'drive/files/attached-chat-messages', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the notes to which the given file is attached.
|
* Find the notes to which the given file is attached.
|
||||||
*
|
*
|
||||||
|
|
|
@ -275,6 +275,8 @@ import type {
|
||||||
DriveResponse,
|
DriveResponse,
|
||||||
DriveFilesRequest,
|
DriveFilesRequest,
|
||||||
DriveFilesResponse,
|
DriveFilesResponse,
|
||||||
|
DriveFilesAttachedChatMessagesRequest,
|
||||||
|
DriveFilesAttachedChatMessagesResponse,
|
||||||
DriveFilesAttachedNotesRequest,
|
DriveFilesAttachedNotesRequest,
|
||||||
DriveFilesAttachedNotesResponse,
|
DriveFilesAttachedNotesResponse,
|
||||||
DriveFilesCheckExistenceRequest,
|
DriveFilesCheckExistenceRequest,
|
||||||
|
@ -833,6 +835,7 @@ export type Endpoints = {
|
||||||
'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse };
|
'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse };
|
||||||
'drive': { req: EmptyRequest; res: DriveResponse };
|
'drive': { req: EmptyRequest; res: DriveResponse };
|
||||||
'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse };
|
'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse };
|
||||||
|
'drive/files/attached-chat-messages': { req: DriveFilesAttachedChatMessagesRequest; res: DriveFilesAttachedChatMessagesResponse };
|
||||||
'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse };
|
'drive/files/attached-notes': { req: DriveFilesAttachedNotesRequest; res: DriveFilesAttachedNotesResponse };
|
||||||
'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse };
|
'drive/files/check-existence': { req: DriveFilesCheckExistenceRequest; res: DriveFilesCheckExistenceResponse };
|
||||||
'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse };
|
'drive/files/create': { req: DriveFilesCreateRequest; res: DriveFilesCreateResponse };
|
||||||
|
|
|
@ -278,6 +278,8 @@ export type ClipsUpdateResponse = operations['clips___update']['responses']['200
|
||||||
export type DriveResponse = operations['drive']['responses']['200']['content']['application/json'];
|
export type DriveResponse = operations['drive']['responses']['200']['content']['application/json'];
|
||||||
export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json'];
|
export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json'];
|
||||||
export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json'];
|
export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json'];
|
||||||
|
export type DriveFilesAttachedChatMessagesRequest = operations['drive___files___attached-chat-messages']['requestBody']['content']['application/json'];
|
||||||
|
export type DriveFilesAttachedChatMessagesResponse = operations['drive___files___attached-chat-messages']['responses']['200']['content']['application/json'];
|
||||||
export type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json'];
|
export type DriveFilesAttachedNotesRequest = operations['drive___files___attached-notes']['requestBody']['content']['application/json'];
|
||||||
export type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json'];
|
export type DriveFilesAttachedNotesResponse = operations['drive___files___attached-notes']['responses']['200']['content']['application/json'];
|
||||||
export type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json'];
|
export type DriveFilesCheckExistenceRequest = operations['drive___files___check-existence']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -1653,6 +1653,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['drive___files'];
|
post: operations['drive___files'];
|
||||||
};
|
};
|
||||||
|
'/drive/files/attached-chat-messages': {
|
||||||
|
/**
|
||||||
|
* drive/files/attached-chat-messages
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *read:drive*
|
||||||
|
*/
|
||||||
|
post: operations['drive___files___attached-chat-messages'];
|
||||||
|
};
|
||||||
'/drive/files/attached-notes': {
|
'/drive/files/attached-notes': {
|
||||||
/**
|
/**
|
||||||
* drive/files/attached-notes
|
* drive/files/attached-notes
|
||||||
|
@ -5077,7 +5086,7 @@ export type components = {
|
||||||
script: string;
|
script: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
visibility: 'private' | 'public';
|
visibility: 'private' | 'public';
|
||||||
likedCount: number | null;
|
likedCount: number;
|
||||||
isLiked?: boolean;
|
isLiked?: boolean;
|
||||||
};
|
};
|
||||||
Signin: {
|
Signin: {
|
||||||
|
@ -18748,6 +18757,80 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
'drive___files___attached-chat-messages': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
sinceId?: string;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
untilId?: string;
|
||||||
|
sinceDate?: number;
|
||||||
|
untilDate?: number;
|
||||||
|
/** @default 10 */
|
||||||
|
limit?: number;
|
||||||
|
/** Format: misskey:id */
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['ChatMessage'][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
'drive___files___attached-notes': {
|
'drive___files___attached-notes': {
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue