mirror of
https://github.com/misskey-dev/misskey
synced 2025-09-18 09:10:25 +02:00
Compare commits
18 commits
2025.9.0-b
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
8c413d01e6 |
||
|
b231da7c7c | ||
|
df3e44f62e | ||
|
e504560477 |
||
|
bcb2073715 | ||
|
6a80c23a50 |
||
|
2621f468ff | ||
|
d4654dd7bd |
||
|
b7da6cad87 |
||
|
5b4115e21a |
||
|
c174c5c144 |
||
|
aebc3f781e |
||
|
f60b6291d7 |
||
|
7673874675 |
||
|
6e3354f95d | ||
|
b9df928097 | ||
|
0754678144 | ||
|
a8cc51dc77 |
156 changed files with 2366 additions and 1514 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
||||||
|
- Enhance: 画像編集にマスクエフェクトを追加
|
||||||
|
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
||||||
|
|
||||||
|
### Server
|
||||||
|
-
|
||||||
|
|
||||||
|
|
||||||
## 2025.9.0
|
## 2025.9.0
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
104
locales/index.d.ts
vendored
104
locales/index.d.ts
vendored
|
@ -1227,7 +1227,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"noMoreHistory": string;
|
"noMoreHistory": string;
|
||||||
/**
|
/**
|
||||||
* チャットを始める
|
* メッセージを送る
|
||||||
*/
|
*/
|
||||||
"startChat": string;
|
"startChat": string;
|
||||||
/**
|
/**
|
||||||
|
@ -1927,7 +1927,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"markAsReadAllUnreadNotes": string;
|
"markAsReadAllUnreadNotes": string;
|
||||||
/**
|
/**
|
||||||
* すべてのチャットを既読にする
|
* すべてのダイレクトメッセージを既読にする
|
||||||
*/
|
*/
|
||||||
"markAsReadAllTalkMessages": string;
|
"markAsReadAllTalkMessages": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5390,6 +5390,14 @@ export interface Locale extends ILocale {
|
||||||
* チャット
|
* チャット
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
|
/**
|
||||||
|
* ダイレクトメッセージ
|
||||||
|
*/
|
||||||
|
"directMessage": string;
|
||||||
|
/**
|
||||||
|
* メッセージ
|
||||||
|
*/
|
||||||
|
"directMessage_short": string;
|
||||||
/**
|
/**
|
||||||
* 旧設定情報を移行
|
* 旧設定情報を移行
|
||||||
*/
|
*/
|
||||||
|
@ -5529,6 +5537,10 @@ export interface Locale extends ILocale {
|
||||||
* ベータ版の検証にご協力いただきありがとうございます!
|
* ベータ版の検証にご協力いただきありがとうございます!
|
||||||
*/
|
*/
|
||||||
"thankYouForTestingBeta": string;
|
"thankYouForTestingBeta": string;
|
||||||
|
/**
|
||||||
|
* ユーザー指定ノートを作成
|
||||||
|
*/
|
||||||
|
"createUserSpecifiedNote": string;
|
||||||
"_order": {
|
"_order": {
|
||||||
/**
|
/**
|
||||||
* 新しい順
|
* 新しい順
|
||||||
|
@ -5540,6 +5552,10 @@ export interface Locale extends ILocale {
|
||||||
"oldest": string;
|
"oldest": string;
|
||||||
};
|
};
|
||||||
"_chat": {
|
"_chat": {
|
||||||
|
/**
|
||||||
|
* メッセージ
|
||||||
|
*/
|
||||||
|
"messages": string;
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
*/
|
*/
|
||||||
|
@ -5549,36 +5565,36 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"newMessage": string;
|
"newMessage": string;
|
||||||
/**
|
/**
|
||||||
* 個人チャット
|
* 個別
|
||||||
*/
|
*/
|
||||||
"individualChat": string;
|
"individualChat": string;
|
||||||
/**
|
/**
|
||||||
* 特定ユーザーとの一対一のチャットができます。
|
* 特定ユーザーと個別にメッセージのやりとりができます。
|
||||||
*/
|
*/
|
||||||
"individualChat_description": string;
|
"individualChat_description": string;
|
||||||
/**
|
/**
|
||||||
* ルームチャット
|
* グループ
|
||||||
*/
|
*/
|
||||||
"roomChat": string;
|
"roomChat": string;
|
||||||
/**
|
/**
|
||||||
* 複数人でのチャットができます。
|
* 複数人でメッセージのやりとりができます。
|
||||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
* また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。
|
||||||
*/
|
*/
|
||||||
"roomChat_description": string;
|
"roomChat_description": string;
|
||||||
/**
|
/**
|
||||||
* ルームを作成
|
* グループを作成
|
||||||
*/
|
*/
|
||||||
"createRoom": string;
|
"createRoom": string;
|
||||||
/**
|
/**
|
||||||
* ユーザーを招待してチャットを始めましょう
|
* ユーザーを招待してメッセージを送信しましょう
|
||||||
*/
|
*/
|
||||||
"inviteUserToChat": string;
|
"inviteUserToChat": string;
|
||||||
/**
|
/**
|
||||||
* 作成したルーム
|
* 作成したグループ
|
||||||
*/
|
*/
|
||||||
"yourRooms": string;
|
"yourRooms": string;
|
||||||
/**
|
/**
|
||||||
* 参加中のルーム
|
* 参加中のグループ
|
||||||
*/
|
*/
|
||||||
"joiningRooms": string;
|
"joiningRooms": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5598,7 +5614,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"noHistory": string;
|
"noHistory": string;
|
||||||
/**
|
/**
|
||||||
* ルームはありません
|
* グループはありません
|
||||||
*/
|
*/
|
||||||
"noRooms": string;
|
"noRooms": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5618,7 +5634,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"ignore": string;
|
"ignore": string;
|
||||||
/**
|
/**
|
||||||
* ルームから退出
|
* グループから退出
|
||||||
*/
|
*/
|
||||||
"leave": string;
|
"leave": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5642,35 +5658,35 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"newline": string;
|
"newline": string;
|
||||||
/**
|
/**
|
||||||
* このルームをミュート
|
* このグループをミュート
|
||||||
*/
|
*/
|
||||||
"muteThisRoom": string;
|
"muteThisRoom": string;
|
||||||
/**
|
/**
|
||||||
* ルームを削除
|
* グループを削除
|
||||||
*/
|
*/
|
||||||
"deleteRoom": string;
|
"deleteRoom": string;
|
||||||
/**
|
/**
|
||||||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
* このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。
|
||||||
*/
|
*/
|
||||||
"chatNotAvailableForThisAccountOrServer": string;
|
"chatNotAvailableForThisAccountOrServer": string;
|
||||||
/**
|
/**
|
||||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
* このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。
|
||||||
*/
|
*/
|
||||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||||
/**
|
/**
|
||||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
* 相手のアカウントでダイレクトメッセージが使えない状態になっています。
|
||||||
*/
|
*/
|
||||||
"chatNotAvailableInOtherAccount": string;
|
"chatNotAvailableInOtherAccount": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーとのチャットを開始できません
|
* このユーザーとのダイレクトメッセージを開始できません
|
||||||
*/
|
*/
|
||||||
"cannotChatWithTheUser": string;
|
"cannotChatWithTheUser": string;
|
||||||
/**
|
/**
|
||||||
* チャットが使えない状態になっているか、相手がチャットを開放していません。
|
* ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。
|
||||||
*/
|
*/
|
||||||
"cannotChatWithTheUser_description": string;
|
"cannotChatWithTheUser_description": string;
|
||||||
/**
|
/**
|
||||||
* あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
* あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||||
*/
|
*/
|
||||||
"youAreNotAMemberOfThisRoomButInvited": string;
|
"youAreNotAMemberOfThisRoomButInvited": string;
|
||||||
/**
|
/**
|
||||||
|
@ -5678,31 +5694,31 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"doYouAcceptInvitation": string;
|
"doYouAcceptInvitation": string;
|
||||||
/**
|
/**
|
||||||
* チャットする
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chatWithThisUser": string;
|
"chatWithThisUser": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーはフォロワーからのみチャットを受け付けています。
|
* このユーザーはフォロワーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。
|
* このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
|
* このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。
|
||||||
*/
|
*/
|
||||||
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
||||||
/**
|
/**
|
||||||
* このユーザーは誰からもチャットを受け付けていません。
|
* このユーザーは誰からもメッセージを受け付けていません。
|
||||||
*/
|
*/
|
||||||
"thisUserNotAllowedChatAnyone": string;
|
"thisUserNotAllowedChatAnyone": string;
|
||||||
/**
|
/**
|
||||||
* チャットを許可する相手
|
* メッセージを許可する相手
|
||||||
*/
|
*/
|
||||||
"chatAllowedUsers": string;
|
"chatAllowedUsers": string;
|
||||||
/**
|
/**
|
||||||
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
|
* 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。
|
||||||
*/
|
*/
|
||||||
"chatAllowedUsers_note": string;
|
"chatAllowedUsers_note": string;
|
||||||
"_chatAllowedUsers": {
|
"_chatAllowedUsers": {
|
||||||
|
@ -7856,7 +7872,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"canImportUserLists": string;
|
"canImportUserLists": string;
|
||||||
/**
|
/**
|
||||||
* チャットを許可
|
* ダイレクトメッセージを許可
|
||||||
*/
|
*/
|
||||||
"chatAvailability": string;
|
"chatAvailability": string;
|
||||||
/**
|
/**
|
||||||
|
@ -8706,7 +8722,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"badge": string;
|
"badge": string;
|
||||||
/**
|
/**
|
||||||
* チャットの背景
|
* メッセージの背景
|
||||||
*/
|
*/
|
||||||
"messageBg": string;
|
"messageBg": string;
|
||||||
/**
|
/**
|
||||||
|
@ -8733,7 +8749,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"reaction": string;
|
"reaction": string;
|
||||||
/**
|
/**
|
||||||
* チャットのメッセージ
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chatMessage": string;
|
"chatMessage": string;
|
||||||
};
|
};
|
||||||
|
@ -9017,11 +9033,11 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"write:following": string;
|
"write:following": string;
|
||||||
/**
|
/**
|
||||||
* チャットを見る
|
* ダイレクトメッセージを見る
|
||||||
*/
|
*/
|
||||||
"read:messaging": string;
|
"read:messaging": string;
|
||||||
/**
|
/**
|
||||||
* チャットを操作する
|
* ダイレクトメッセージを操作する
|
||||||
*/
|
*/
|
||||||
"write:messaging": string;
|
"write:messaging": string;
|
||||||
/**
|
/**
|
||||||
|
@ -9313,11 +9329,11 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"write:report-abuse": string;
|
"write:report-abuse": string;
|
||||||
/**
|
/**
|
||||||
* チャットを操作する
|
* ダイレクトメッセージを操作する
|
||||||
*/
|
*/
|
||||||
"write:chat": string;
|
"write:chat": string;
|
||||||
/**
|
/**
|
||||||
* チャットを閲覧する
|
* ダイレクトメッセージを閲覧する
|
||||||
*/
|
*/
|
||||||
"read:chat": string;
|
"read:chat": string;
|
||||||
};
|
};
|
||||||
|
@ -9543,7 +9559,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"birthdayFollowings": string;
|
"birthdayFollowings": string;
|
||||||
/**
|
/**
|
||||||
* チャット
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
};
|
};
|
||||||
|
@ -10283,7 +10299,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleAssigned": string;
|
"roleAssigned": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームへ招待されました
|
* ダイレクトメッセージのグループへ招待されました
|
||||||
*/
|
*/
|
||||||
"chatRoomInvitationReceived": string;
|
"chatRoomInvitationReceived": string;
|
||||||
/**
|
/**
|
||||||
|
@ -10396,7 +10412,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleAssigned": string;
|
"roleAssigned": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームへ招待された
|
* ダイレクトメッセージのグループへ招待された
|
||||||
*/
|
*/
|
||||||
"chatRoomInvitationReceived": string;
|
"chatRoomInvitationReceived": string;
|
||||||
/**
|
/**
|
||||||
|
@ -10578,7 +10594,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"roleTimeline": string;
|
"roleTimeline": string;
|
||||||
/**
|
/**
|
||||||
* チャット
|
* ダイレクトメッセージ
|
||||||
*/
|
*/
|
||||||
"chat": string;
|
"chat": string;
|
||||||
};
|
};
|
||||||
|
@ -10945,7 +10961,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"deleteGalleryPost": string;
|
"deleteGalleryPost": string;
|
||||||
/**
|
/**
|
||||||
* チャットルームを削除
|
* ダイレクトメッセージのグループを削除
|
||||||
*/
|
*/
|
||||||
"deleteChatRoom": string;
|
"deleteChatRoom": string;
|
||||||
/**
|
/**
|
||||||
|
@ -12362,6 +12378,10 @@ export interface Locale extends ILocale {
|
||||||
* ティアリング
|
* ティアリング
|
||||||
*/
|
*/
|
||||||
"tearing": string;
|
"tearing": string;
|
||||||
|
/**
|
||||||
|
* 塗りつぶし(四角)
|
||||||
|
*/
|
||||||
|
"fillSquare": string;
|
||||||
};
|
};
|
||||||
"_fxProps": {
|
"_fxProps": {
|
||||||
/**
|
/**
|
||||||
|
@ -12376,6 +12396,10 @@ export interface Locale extends ILocale {
|
||||||
* サイズ
|
* サイズ
|
||||||
*/
|
*/
|
||||||
"size": string;
|
"size": string;
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
"offset": string;
|
||||||
/**
|
/**
|
||||||
* 色
|
* 色
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -302,7 +302,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
|
||||||
explore: "みつける"
|
explore: "みつける"
|
||||||
messageRead: "既読"
|
messageRead: "既読"
|
||||||
noMoreHistory: "これより過去の履歴はありません"
|
noMoreHistory: "これより過去の履歴はありません"
|
||||||
startChat: "チャットを始める"
|
startChat: "メッセージを送る"
|
||||||
nUsersRead: "{n}人が読みました"
|
nUsersRead: "{n}人が読みました"
|
||||||
agreeTo: "{0}に同意"
|
agreeTo: "{0}に同意"
|
||||||
agree: "同意する"
|
agree: "同意する"
|
||||||
|
@ -477,7 +477,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
|
||||||
uploadFolder: "既定アップロード先"
|
uploadFolder: "既定アップロード先"
|
||||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
|
||||||
help: "ヘルプ"
|
help: "ヘルプ"
|
||||||
inputMessageHere: "ここにメッセージを入力"
|
inputMessageHere: "ここにメッセージを入力"
|
||||||
close: "閉じる"
|
close: "閉じる"
|
||||||
|
@ -1343,6 +1343,8 @@ postForm: "投稿フォーム"
|
||||||
textCount: "文字数"
|
textCount: "文字数"
|
||||||
information: "情報"
|
information: "情報"
|
||||||
chat: "チャット"
|
chat: "チャット"
|
||||||
|
directMessage: "ダイレクトメッセージ"
|
||||||
|
directMessage_short: "メッセージ"
|
||||||
migrateOldSettings: "旧設定情報を移行"
|
migrateOldSettings: "旧設定情報を移行"
|
||||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||||
compress: "圧縮"
|
compress: "圧縮"
|
||||||
|
@ -1377,53 +1379,55 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
|
||||||
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
||||||
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
||||||
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
||||||
|
createUserSpecifiedNote: "ユーザー指定ノートを作成"
|
||||||
|
|
||||||
_order:
|
_order:
|
||||||
newest: "新しい順"
|
newest: "新しい順"
|
||||||
oldest: "古い順"
|
oldest: "古い順"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
|
messages: "メッセージ"
|
||||||
noMessagesYet: "まだメッセージはありません"
|
noMessagesYet: "まだメッセージはありません"
|
||||||
newMessage: "新しいメッセージ"
|
newMessage: "新しいメッセージ"
|
||||||
individualChat: "個人チャット"
|
individualChat: "個別"
|
||||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
|
||||||
roomChat: "ルームチャット"
|
roomChat: "グループ"
|
||||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
|
||||||
createRoom: "ルームを作成"
|
createRoom: "グループを作成"
|
||||||
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
|
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
|
||||||
yourRooms: "作成したルーム"
|
yourRooms: "作成したグループ"
|
||||||
joiningRooms: "参加中のルーム"
|
joiningRooms: "参加中のグループ"
|
||||||
invitations: "招待"
|
invitations: "招待"
|
||||||
noInvitations: "招待はありません"
|
noInvitations: "招待はありません"
|
||||||
history: "履歴"
|
history: "履歴"
|
||||||
noHistory: "履歴はありません"
|
noHistory: "履歴はありません"
|
||||||
noRooms: "ルームはありません"
|
noRooms: "グループはありません"
|
||||||
inviteUser: "ユーザーを招待"
|
inviteUser: "ユーザーを招待"
|
||||||
sentInvitations: "送信した招待"
|
sentInvitations: "送信した招待"
|
||||||
join: "参加"
|
join: "参加"
|
||||||
ignore: "無視"
|
ignore: "無視"
|
||||||
leave: "ルームから退出"
|
leave: "グループから退出"
|
||||||
members: "メンバー"
|
members: "メンバー"
|
||||||
searchMessages: "メッセージを検索"
|
searchMessages: "メッセージを検索"
|
||||||
home: "ホーム"
|
home: "ホーム"
|
||||||
send: "送信"
|
send: "送信"
|
||||||
newline: "改行"
|
newline: "改行"
|
||||||
muteThisRoom: "このルームをミュート"
|
muteThisRoom: "このグループをミュート"
|
||||||
deleteRoom: "ルームを削除"
|
deleteRoom: "グループを削除"
|
||||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
|
||||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
|
||||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
|
||||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
|
||||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
|
||||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||||
doYouAcceptInvitation: "招待を承認しますか?"
|
doYouAcceptInvitation: "招待を承認しますか?"
|
||||||
chatWithThisUser: "チャットする"
|
chatWithThisUser: "ダイレクトメッセージ"
|
||||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
|
||||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
|
||||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
|
||||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
|
||||||
chatAllowedUsers: "チャットを許可する相手"
|
chatAllowedUsers: "メッセージを許可する相手"
|
||||||
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
|
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
|
||||||
_chatAllowedUsers:
|
_chatAllowedUsers:
|
||||||
everyone: "誰でも"
|
everyone: "誰でも"
|
||||||
followers: "自分のフォロワーのみ"
|
followers: "自分のフォロワーのみ"
|
||||||
|
@ -2034,7 +2038,7 @@ _role:
|
||||||
canImportFollowing: "フォローのインポートを許可"
|
canImportFollowing: "フォローのインポートを許可"
|
||||||
canImportMuting: "ミュートのインポートを許可"
|
canImportMuting: "ミュートのインポートを許可"
|
||||||
canImportUserLists: "リストのインポートを許可"
|
canImportUserLists: "リストのインポートを許可"
|
||||||
chatAvailability: "チャットを許可"
|
chatAvailability: "ダイレクトメッセージを許可"
|
||||||
uploadableFileTypes: "アップロード可能なファイル種別"
|
uploadableFileTypes: "アップロード可能なファイル種別"
|
||||||
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
||||||
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
||||||
|
@ -2281,7 +2285,7 @@ _theme:
|
||||||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||||
inputBorder: "入力ボックスの縁取り"
|
inputBorder: "入力ボックスの縁取り"
|
||||||
badge: "バッジ"
|
badge: "バッジ"
|
||||||
messageBg: "チャットの背景"
|
messageBg: "メッセージの背景"
|
||||||
fgHighlighted: "強調された文字"
|
fgHighlighted: "強調された文字"
|
||||||
|
|
||||||
_sfx:
|
_sfx:
|
||||||
|
@ -2289,7 +2293,7 @@ _sfx:
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
reaction: "リアクション選択時"
|
reaction: "リアクション選択時"
|
||||||
chatMessage: "チャットのメッセージ"
|
chatMessage: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_soundSettings:
|
_soundSettings:
|
||||||
driveFile: "ドライブの音声を使用"
|
driveFile: "ドライブの音声を使用"
|
||||||
|
@ -2369,8 +2373,8 @@ _permissions:
|
||||||
"write:favorites": "お気に入りを操作する"
|
"write:favorites": "お気に入りを操作する"
|
||||||
"read:following": "フォローの情報を見る"
|
"read:following": "フォローの情報を見る"
|
||||||
"write:following": "フォロー・フォロー解除する"
|
"write:following": "フォロー・フォロー解除する"
|
||||||
"read:messaging": "チャットを見る"
|
"read:messaging": "ダイレクトメッセージを見る"
|
||||||
"write:messaging": "チャットを操作する"
|
"write:messaging": "ダイレクトメッセージを操作する"
|
||||||
"read:mutes": "ミュートを見る"
|
"read:mutes": "ミュートを見る"
|
||||||
"write:mutes": "ミュートを操作する"
|
"write:mutes": "ミュートを操作する"
|
||||||
"write:notes": "ノートを作成・削除する"
|
"write:notes": "ノートを作成・削除する"
|
||||||
|
@ -2443,8 +2447,8 @@ _permissions:
|
||||||
"read:clip-favorite": "クリップのいいねを見る"
|
"read:clip-favorite": "クリップのいいねを見る"
|
||||||
"read:federation": "連合に関する情報を取得する"
|
"read:federation": "連合に関する情報を取得する"
|
||||||
"write:report-abuse": "違反を報告する"
|
"write:report-abuse": "違反を報告する"
|
||||||
"write:chat": "チャットを操作する"
|
"write:chat": "ダイレクトメッセージを操作する"
|
||||||
"read:chat": "チャットを閲覧する"
|
"read:chat": "ダイレクトメッセージを閲覧する"
|
||||||
|
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "アプリへのアクセス許可"
|
shareAccessTitle: "アプリへのアクセス許可"
|
||||||
|
@ -2507,7 +2511,7 @@ _widgets:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
clicker: "クリッカー"
|
clicker: "クリッカー"
|
||||||
birthdayFollowings: "今日誕生日のユーザー"
|
birthdayFollowings: "今日誕生日のユーザー"
|
||||||
chat: "チャット"
|
chat: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
@ -2714,7 +2718,7 @@ _notification:
|
||||||
newNote: "新しい投稿"
|
newNote: "新しい投稿"
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
roleAssigned: "ロールが付与されました"
|
roleAssigned: "ロールが付与されました"
|
||||||
chatRoomInvitationReceived: "チャットルームへ招待されました"
|
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
|
@ -2744,7 +2748,7 @@ _notification:
|
||||||
receiveFollowRequest: "フォロー申請を受け取った"
|
receiveFollowRequest: "フォロー申請を受け取った"
|
||||||
followRequestAccepted: "フォローが受理された"
|
followRequestAccepted: "フォローが受理された"
|
||||||
roleAssigned: "ロールが付与された"
|
roleAssigned: "ロールが付与された"
|
||||||
chatRoomInvitationReceived: "チャットルームへ招待された"
|
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
|
||||||
achievementEarned: "実績の獲得"
|
achievementEarned: "実績の獲得"
|
||||||
exportCompleted: "エクスポートが完了した"
|
exportCompleted: "エクスポートが完了した"
|
||||||
login: "ログイン"
|
login: "ログイン"
|
||||||
|
@ -2794,7 +2798,7 @@ _deck:
|
||||||
mentions: "メンション"
|
mentions: "メンション"
|
||||||
direct: "指名"
|
direct: "指名"
|
||||||
roleTimeline: "ロールタイムライン"
|
roleTimeline: "ロールタイムライン"
|
||||||
chat: "チャット"
|
chat: "ダイレクトメッセージ"
|
||||||
|
|
||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||||
|
@ -2897,7 +2901,7 @@ _moderationLogTypes:
|
||||||
deletePage: "ページを削除"
|
deletePage: "ページを削除"
|
||||||
deleteFlash: "Playを削除"
|
deleteFlash: "Playを削除"
|
||||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||||
deleteChatRoom: "チャットルームを削除"
|
deleteChatRoom: "ダイレクトメッセージのグループを削除"
|
||||||
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
|
@ -3310,11 +3314,13 @@ _imageEffector:
|
||||||
checker: "チェッカー"
|
checker: "チェッカー"
|
||||||
blockNoise: "ブロックノイズ"
|
blockNoise: "ブロックノイズ"
|
||||||
tearing: "ティアリング"
|
tearing: "ティアリング"
|
||||||
|
fillSquare: "塗りつぶし(四角)"
|
||||||
|
|
||||||
_fxProps:
|
_fxProps:
|
||||||
angle: "角度"
|
angle: "角度"
|
||||||
scale: "サイズ"
|
scale: "サイズ"
|
||||||
size: "サイズ"
|
size: "サイズ"
|
||||||
|
offset: "位置"
|
||||||
color: "色"
|
color: "色"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
normalize: "正規化"
|
normalize: "正規化"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.9.0-beta.1",
|
"version": "2025.9.0",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
16
packages/backend/migration/1757823175259-sensitive-ad.js
Normal file
16
packages/backend/migration/1757823175259-sensitive-ad.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SensitiveAd1757823175259 {
|
||||||
|
name = 'SensitiveAd1757823175259'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -101,14 +101,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
userEachUserListsLimit: 50,
|
userEachUserListsLimit: 50,
|
||||||
rateLimitFactor: 1,
|
rateLimitFactor: 1,
|
||||||
avatarDecorationLimit: 1,
|
avatarDecorationLimit: 1,
|
||||||
canImportAntennas: true,
|
canImportAntennas: false,
|
||||||
canImportBlocking: true,
|
canImportBlocking: false,
|
||||||
canImportFollowing: true,
|
canImportFollowing: false,
|
||||||
canImportMuting: true,
|
canImportMuting: false,
|
||||||
canImportUserLists: true,
|
canImportUserLists: false,
|
||||||
chatAvailability: 'available',
|
chatAvailability: 'available',
|
||||||
uploadableFileTypes: [
|
uploadableFileTypes: [
|
||||||
'text/plain',
|
'text/plain',
|
||||||
|
'text/csv',
|
||||||
'application/json',
|
'application/json',
|
||||||
'image/*',
|
'image/*',
|
||||||
'video/*',
|
'video/*',
|
||||||
|
|
|
@ -117,6 +117,7 @@ export class MetaEntityService {
|
||||||
ratio: ad.ratio,
|
ratio: ad.ratio,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive ? true : undefined,
|
||||||
})),
|
})),
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
enableEmail: instance.enableEmail,
|
enableEmail: instance.enableEmail,
|
||||||
|
|
|
@ -54,10 +54,17 @@ export class MiAd {
|
||||||
length: 8192, nullable: false,
|
length: 8192, nullable: false,
|
||||||
})
|
})
|
||||||
public memo: string;
|
public memo: string;
|
||||||
|
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0, nullable: false,
|
default: 0, nullable: false,
|
||||||
})
|
})
|
||||||
public dayOfWeek: number;
|
public dayOfWeek: number;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isSensitive: boolean;
|
||||||
|
|
||||||
constructor(data: Partial<MiAd>) {
|
constructor(data: Partial<MiAd>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -60,5 +60,10 @@ export const packedAdSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isSensitive: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,13 +34,22 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'MeDetailed',
|
allOf: [
|
||||||
properties: {
|
{
|
||||||
token: {
|
type: 'object',
|
||||||
type: 'string',
|
ref: 'MeDetailed',
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
||||||
startsAt: { type: 'integer' },
|
startsAt: { type: 'integer' },
|
||||||
imageUrl: { type: 'string', minLength: 1 },
|
imageUrl: { type: 'string', minLength: 1 },
|
||||||
dayOfWeek: { type: 'integer' },
|
dayOfWeek: { type: 'integer' },
|
||||||
|
isSensitive: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: new Date(ps.expiresAt),
|
expiresAt: new Date(ps.expiresAt),
|
||||||
startsAt: new Date(ps.startsAt),
|
startsAt: new Date(ps.startsAt),
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
|
isSensitive: ps.isSensitive,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
imageUrl: ps.imageUrl,
|
imageUrl: ps.imageUrl,
|
||||||
priority: ps.priority,
|
priority: ps.priority,
|
||||||
|
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ad.expiresAt.toISOString(),
|
expiresAt: ad.expiresAt.toISOString(),
|
||||||
startsAt: ad.startsAt.toISOString(),
|
startsAt: ad.startsAt.toISOString(),
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
priority: ad.priority,
|
priority: ad.priority,
|
||||||
|
|
|
@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ad.expiresAt.toISOString(),
|
expiresAt: ad.expiresAt.toISOString(),
|
||||||
startsAt: ad.startsAt.toISOString(),
|
startsAt: ad.startsAt.toISOString(),
|
||||||
dayOfWeek: ad.dayOfWeek,
|
dayOfWeek: ad.dayOfWeek,
|
||||||
|
isSensitive: ad.isSensitive,
|
||||||
url: ad.url,
|
url: ad.url,
|
||||||
imageUrl: ad.imageUrl,
|
imageUrl: ad.imageUrl,
|
||||||
memo: ad.memo,
|
memo: ad.memo,
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const paramDef = {
|
||||||
expiresAt: { type: 'integer' },
|
expiresAt: { type: 'integer' },
|
||||||
startsAt: { type: 'integer' },
|
startsAt: { type: 'integer' },
|
||||||
dayOfWeek: { type: 'integer' },
|
dayOfWeek: { type: 'integer' },
|
||||||
|
isSensitive: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['id'],
|
required: ['id'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
||||||
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
||||||
dayOfWeek: ps.dayOfWeek,
|
dayOfWeek: ps.dayOfWeek,
|
||||||
|
isSensitive: ps.isSensitive,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||||
|
|
|
@ -29,10 +29,16 @@ export const meta = {
|
||||||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||||
},
|
},
|
||||||
|
|
||||||
signinRequired: {
|
contentRestrictedByUser: {
|
||||||
message: 'Signin required.',
|
message: 'Content restricted by user. Please sign in to view.',
|
||||||
code: 'SIGNIN_REQUIRED',
|
code: 'CONTENT_RESTRICTED_BY_USER',
|
||||||
id: '8e75455b-738c-471d-9f80-62693f33372e',
|
id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab',
|
||||||
|
},
|
||||||
|
|
||||||
|
contentRestrictedByServer: {
|
||||||
|
message: 'Content restricted by server settings. Please sign in to view.',
|
||||||
|
code: 'CONTENT_RESTRICTED_BY_SERVER',
|
||||||
|
id: '145f88d2-b03d-4087-8143-a78928883c4b',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note.user!.requireSigninToViewContents && me == null) {
|
if (note.user!.requireSigninToViewContents && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.pack(note, me, {
|
return await this.noteEntityService.pack(note, me, {
|
||||||
|
|
|
@ -22,17 +22,26 @@ export const meta = {
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'UserList',
|
allOf: [
|
||||||
properties: {
|
{
|
||||||
likedCount: {
|
type: 'object',
|
||||||
type: 'number',
|
ref: 'UserList',
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
},
|
||||||
isLiked: {
|
{
|
||||||
type: 'boolean',
|
type: 'object',
|
||||||
optional: true, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
likedCount: {
|
||||||
|
type: 'number',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
isLiked: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
|
||||||
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
||||||
ADMIN_CACHE.set(host, {
|
ADMIN_CACHE.set(host, {
|
||||||
id: res.id,
|
id: res.id,
|
||||||
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
|
|
||||||
i: res.token,
|
i: res.token,
|
||||||
});
|
});
|
||||||
return res as Misskey.entities.SignupResponse;
|
return res as Misskey.entities.SignupResponse;
|
||||||
|
|
|
@ -20,6 +20,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"magic-string": "0.30.17",
|
"magic-string": "0.30.17",
|
||||||
"vite": "7.0.6"
|
"vite": "7.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,71 @@ export default [
|
||||||
allowSingleExtends: true,
|
allowSingleExtends: true,
|
||||||
}],
|
}],
|
||||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
// close ... window.closeと衝突 or 紛らわしい
|
||||||
|
// open ... window.openと衝突 or 紛らわしい
|
||||||
|
// fetch ... window.fetchと衝突 or 紛らわしい
|
||||||
|
// location ... window.locationと衝突 or 紛らわしい
|
||||||
|
// document ... window.documentと衝突 or 紛らわしい
|
||||||
|
// history ... window.historyと衝突 or 紛らわしい
|
||||||
|
// scroll ... window.scrollと衝突 or 紛らわしい
|
||||||
|
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
|
||||||
|
// setInterval ... window.setIntervalと衝突 or 紛らわしい
|
||||||
|
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
|
||||||
|
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
|
||||||
|
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'name': 'open',
|
||||||
|
'message': 'Use `window.open`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'close',
|
||||||
|
'message': 'Use `window.close`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'fetch',
|
||||||
|
'message': 'Use `window.fetch`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'location',
|
||||||
|
'message': 'Use `window.location`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'document',
|
||||||
|
'message': 'Use `window.document`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'history',
|
||||||
|
'message': 'Use `window.history`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'scroll',
|
||||||
|
'message': 'Use `window.scroll`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'setTimeout',
|
||||||
|
'message': 'Use `window.setTimeout`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'setInterval',
|
||||||
|
'message': 'Use `window.setInterval`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'clearTimeout',
|
||||||
|
'message': 'Use `window.clearTimeout`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'clearInterval',
|
||||||
|
'message': 'Use `window.clearInterval`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'name',
|
||||||
|
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
alphabetical: false,
|
alphabetical: false,
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.5",
|
||||||
"vue": "3.5.21"
|
"vue": "3.5.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
|
||||||
console.log('Misskey Embed');
|
console.log('Misskey Embed');
|
||||||
|
|
||||||
//#region Embedパラメータの取得・パース
|
//#region Embedパラメータの取得・パース
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const embedParams = parseEmbedParams(params);
|
const embedParams = parseEmbedParams(params);
|
||||||
if (_DEV_) console.log(embedParams);
|
if (_DEV_) console.log(embedParams);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// サイズの制限
|
// サイズの制限
|
||||||
document.documentElement.style.maxWidth = '500px';
|
window.document.documentElement.style.maxWidth = '500px';
|
||||||
|
|
||||||
// iframeIdの設定
|
// iframeIdの設定
|
||||||
function setIframeIdHandler(event: MessageEvent) {
|
function setIframeIdHandler(event: MessageEvent) {
|
||||||
|
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
|
||||||
const rootEl = ((): HTMLElement => {
|
const rootEl = ((): HTMLElement => {
|
||||||
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
|
||||||
|
|
||||||
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
|
||||||
|
|
||||||
if (currentRoot) {
|
if (currentRoot) {
|
||||||
console.warn('multiple import detected');
|
console.warn('multiple import detected');
|
||||||
return currentRoot;
|
return currentRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = document.createElement('div');
|
const root = window.document.createElement('div');
|
||||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||||
document.body.appendChild(root);
|
window.document.body.appendChild(root);
|
||||||
return root;
|
return root;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
function removeSplash() {
|
function removeSplash() {
|
||||||
const splash = document.getElementById('splash');
|
const splash = window.document.getElementById('splash');
|
||||||
if (splash) {
|
if (splash) {
|
||||||
splash.style.opacity = '0';
|
splash.style.opacity = '0';
|
||||||
splash.style.pointerEvents = 'none';
|
splash.style.pointerEvents = 'none';
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
|
||||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||||
// テスト環境で Web Worker インスタンスは作成できない
|
// テスト環境で Web Worker インスタンスは作成できない
|
||||||
if (import.meta.env.MODE === 'test') {
|
if (import.meta.env.MODE === 'test') {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.document.createElement('canvas');
|
||||||
canvas.width = 64;
|
canvas.width = 64;
|
||||||
canvas.height = 64;
|
canvas.height = 64;
|
||||||
resolve(canvas);
|
resolve(canvas);
|
||||||
|
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
|
||||||
);
|
);
|
||||||
resolve(workers);
|
resolve(workers);
|
||||||
} else {
|
} else {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.document.createElement('canvas');
|
||||||
canvas.width = 64;
|
canvas.width = 64;
|
||||||
canvas.height = 64;
|
canvas.height = 64;
|
||||||
resolve(canvas);
|
resolve(canvas);
|
||||||
|
|
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
||||||
// if no instance data is given, this is for the local instance
|
// if no instance data is given, this is for the local instance
|
||||||
const instance = props.instance ?? {
|
const instance = props.instance ?? {
|
||||||
name: serverMetadata.name,
|
name: serverMetadata.name,
|
||||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
|
themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
|
||||||
};
|
};
|
||||||
|
|
||||||
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
|
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
|
||||||
|
|
|
@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
|
||||||
|
|
||||||
const url = `/${canonical}`;
|
const url = `/${canonical}`;
|
||||||
|
|
||||||
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
|
||||||
bg.setAlpha(0.1);
|
bg.setAlpha(0.1);
|
||||||
const bgCss = bg.toRgbString();
|
const bgCss = bg.toRgbString();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -134,7 +134,7 @@ const isBackTop = ref(false);
|
||||||
const empty = computed(() => items.value.size === 0);
|
const empty = computed(() => items.value.size === 0);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
|
|
||||||
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
|
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
|
||||||
|
|
||||||
const visibility = useDocumentVisibility();
|
const visibility = useDocumentVisibility();
|
||||||
|
|
||||||
|
@ -353,7 +353,7 @@ watch(visibility, () => {
|
||||||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
||||||
} else { // 'visible'
|
} else { // 'visible'
|
||||||
if (timerForSetPause) {
|
if (timerForSetPause) {
|
||||||
clearTimeout(timerForSetPause);
|
window.clearTimeout(timerForSetPause);
|
||||||
timerForSetPause = null;
|
timerForSetPause = null;
|
||||||
} else {
|
} else {
|
||||||
isPausingUpdate = false;
|
isPausingUpdate = false;
|
||||||
|
@ -447,11 +447,11 @@ onBeforeMount(() => {
|
||||||
init().then(() => {
|
init().then(() => {
|
||||||
if (props.pagination.reversed) {
|
if (props.pagination.reversed) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(toBottom, 800);
|
window.setTimeout(toBottom, 800);
|
||||||
|
|
||||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||||
// more = trueを遅らせる
|
// more = trueを遅らせる
|
||||||
setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
@ -461,11 +461,11 @@ onBeforeMount(() => {
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timerForSetPause) {
|
if (timerForSetPause) {
|
||||||
clearTimeout(timerForSetPause);
|
window.clearTimeout(timerForSetPause);
|
||||||
timerForSetPause = null;
|
timerForSetPause = null;
|
||||||
}
|
}
|
||||||
if (preventAppearFetchMoreTimer.value) {
|
if (preventAppearFetchMoreTimer.value) {
|
||||||
clearTimeout(preventAppearFetchMoreTimer.value);
|
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
||||||
preventAppearFetchMoreTimer.value = null;
|
preventAppearFetchMoreTimer.value = null;
|
||||||
}
|
}
|
||||||
scrollObserver.value?.disconnect();
|
scrollObserver.value?.disconnect();
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
const providedContextEl = document.getElementById('misskey_embedCtx');
|
const providedContextEl = window.document.getElementById('misskey_embedCtx');
|
||||||
|
|
||||||
export type ServerContext = {
|
export type ServerContext = {
|
||||||
clip?: Misskey.entities.Clip;
|
clip?: Misskey.entities.Clip;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApi } from '@/misskey-api.js';
|
import { misskeyApi } from '@/misskey-api.js';
|
||||||
|
|
||||||
const providedMetaEl = document.getElementById('misskey_meta');
|
const providedMetaEl = window.document.getElementById('misskey_meta');
|
||||||
|
|
||||||
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
|
||||||
|
|
||||||
|
|
|
@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
|
|
||||||
document.documentElement.classList.add('_themeChanging_');
|
window.document.documentElement.classList.add('_themeChanging_');
|
||||||
|
|
||||||
timeout = window.setTimeout(() => {
|
timeout = window.setTimeout(() => {
|
||||||
document.documentElement.classList.remove('_themeChanging_');
|
window.document.documentElement.classList.remove('_themeChanging_');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
document.documentElement.dataset.colorScheme = colorScheme;
|
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||||
|
|
||||||
// Deep copy
|
// Deep copy
|
||||||
const _theme = JSON.parse(JSON.stringify(theme));
|
const _theme = JSON.parse(JSON.stringify(theme));
|
||||||
|
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
|
|
||||||
const props = compile(_theme);
|
const props = compile(_theme);
|
||||||
|
|
||||||
for (const tag of document.head.children) {
|
for (const tag of window.document.head.children) {
|
||||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||||
tag.setAttribute('content', props['htmlThemeColor']);
|
tag.setAttribute('content', props['htmlThemeColor']);
|
||||||
break;
|
break;
|
||||||
|
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(props)) {
|
for (const [k, v] of Object.entries(props)) {
|
||||||
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
|
||||||
|
|
|
@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = location.pathname.split('/')[2];
|
const page = window.location.pathname.split('/')[2];
|
||||||
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
|
||||||
if (_DEV_) console.log(page, contentId);
|
if (_DEV_) console.log(page, contentId);
|
||||||
|
|
||||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||||
|
|
|
@ -51,9 +51,71 @@ export default [
|
||||||
allowSingleExtends: true,
|
allowSingleExtends: true,
|
||||||
}],
|
}],
|
||||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
// close ... window.closeと衝突 or 紛らわしい
|
||||||
|
// open ... window.openと衝突 or 紛らわしい
|
||||||
|
// fetch ... window.fetchと衝突 or 紛らわしい
|
||||||
|
// location ... window.locationと衝突 or 紛らわしい
|
||||||
|
// document ... window.documentと衝突 or 紛らわしい
|
||||||
|
// history ... window.historyと衝突 or 紛らわしい
|
||||||
|
// scroll ... window.scrollと衝突 or 紛らわしい
|
||||||
|
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
|
||||||
|
// setInterval ... window.setIntervalと衝突 or 紛らわしい
|
||||||
|
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
|
||||||
|
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
|
||||||
|
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'name': 'open',
|
||||||
|
'message': 'Use `window.open`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'close',
|
||||||
|
'message': 'Use `window.close`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'fetch',
|
||||||
|
'message': 'Use `window.fetch`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'location',
|
||||||
|
'message': 'Use `window.location`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'document',
|
||||||
|
'message': 'Use `window.document`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'history',
|
||||||
|
'message': 'Use `window.history`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'scroll',
|
||||||
|
'message': 'Use `window.scroll`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'setTimeout',
|
||||||
|
'message': 'Use `window.setTimeout`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'setInterval',
|
||||||
|
'message': 'Use `window.setInterval`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'clearTimeout',
|
||||||
|
'message': 'Use `window.clearTimeout`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'clearInterval',
|
||||||
|
'message': 'Use `window.clearInterval`.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'name',
|
||||||
|
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
alphabetical: false,
|
alphabetical: false,
|
||||||
|
|
|
@ -4,15 +4,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
|
||||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||||
|
|
||||||
export const host = address.host;
|
export const host = address.host;
|
||||||
export const hostname = address.hostname;
|
export const hostname = address.hostname;
|
||||||
export const url = address.origin;
|
export const url = address.origin;
|
||||||
export const port = address.port;
|
export const port = address.port;
|
||||||
export const apiUrl = location.origin + '/api';
|
export const apiUrl = window.location.origin + '/api';
|
||||||
export const wsOrigin = location.origin;
|
export const wsOrigin = window.location.origin;
|
||||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||||
export const langs = _LANGS_;
|
export const langs = _LANGS_;
|
||||||
export const version = _VERSION_;
|
export const version = _VERSION_;
|
||||||
|
|
|
@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
|
||||||
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
||||||
let prevTopVisible = firstTopVisible;
|
let prevTopVisible = firstTopVisible;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!window.document.body.contains(el)) return;
|
||||||
|
|
||||||
const topVisible = isHeadVisible(el, tolerance);
|
const topVisible = isHeadVisible(el, tolerance);
|
||||||
if (topVisible !== prevTopVisible) {
|
if (topVisible !== prevTopVisible) {
|
||||||
|
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
||||||
|
|
||||||
const containerOrWindow = container ?? window;
|
const containerOrWindow = container ?? window;
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
if (!document.body.contains(el)) return;
|
if (!window.document.body.contains(el)) return;
|
||||||
if (isTailVisible(el, 1, container)) {
|
if (isTailVisible(el, 1, container)) {
|
||||||
cb();
|
cb();
|
||||||
if (once) removeListener();
|
if (once) removeListener();
|
||||||
|
@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr
|
||||||
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
// https://ja.javascript.info/size-and-scroll-window#ref-932
|
||||||
export function getBodyScrollHeight() {
|
export function getBodyScrollHeight() {
|
||||||
return Math.max(
|
return Math.max(
|
||||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
|
||||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
|
||||||
document.body.clientHeight, document.documentElement.clientHeight,
|
window.document.body.clientHeight, window.document.documentElement.clientHeight,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
||||||
const visibility = ref(document.visibilityState);
|
const visibility = ref(window.document.visibilityState);
|
||||||
|
|
||||||
const onChange = (): void => {
|
const onChange = (): void => {
|
||||||
visibility.value = document.visibilityState;
|
visibility.value = window.document.visibilityState;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('visibilitychange', onChange);
|
window.document.addEventListener('visibilitychange', onChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('visibilitychange', onChange);
|
window.document.removeEventListener('visibilitychange', onChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
return visibility;
|
return visibility;
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.5",
|
||||||
"vue": "3.5.21",
|
"vue": "3.5.21",
|
||||||
"vuedraggable": "next",
|
"vuedraggable": "next",
|
||||||
"wanakana": "5.3.1"
|
"wanakana": "5.3.1"
|
||||||
|
|
|
@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="name">
|
<MkInput v-model="name">
|
||||||
<template #label>{{ i18n.ts.name }}</template>
|
<template #label>{{ i18n.ts.name }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="src">
|
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
|
||||||
<template #label>{{ i18n.ts.antennaSource }}</template>
|
<template #label>{{ i18n.ts.antennaSource }}</template>
|
||||||
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
|
|
||||||
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
|
|
||||||
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
|
|
||||||
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
|
|
||||||
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-if="src === 'list'" v-model="userListId">
|
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
|
||||||
<template #label>{{ i18n.ts.userList }}</template>
|
<template #label>{{ i18n.ts.userList }}</template>
|
||||||
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
||||||
<template #label>{{ i18n.ts.users }}</template>
|
<template #label>{{ i18n.ts.users }}</template>
|
||||||
|
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref } from 'vue';
|
import { watch, ref, computed } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { DeepPartial } from '@/utility/merge.js';
|
import type { DeepPartial } from '@/utility/merge.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -64,6 +58,7 @@ 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 { deepMerge } from '@/utility/merge.js';
|
import { deepMerge } from '@/utility/merge.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -99,9 +94,35 @@ const emit = defineEmits<{
|
||||||
(ev: 'deleted'): void,
|
(ev: 'deleted'): void,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
model: src,
|
||||||
|
def: antennaSourcesSelectDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ value: 'all', label: i18n.ts._antennaSources.all },
|
||||||
|
//{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
|
||||||
|
{ value: 'users', label: i18n.ts._antennaSources.users },
|
||||||
|
//{ value: 'list', label: i18n.ts._antennaSources.userList },
|
||||||
|
{ value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
|
||||||
|
],
|
||||||
|
initialValue: initialAntenna.src,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
model: userListId,
|
||||||
|
def: userListsSelectDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => {
|
||||||
|
if (userLists.value == null) return [];
|
||||||
|
return userLists.value.map(list => ({
|
||||||
|
value: list.id,
|
||||||
|
label: list.name,
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
initialValue: initialAntenna.userListId,
|
||||||
|
});
|
||||||
|
|
||||||
const name = ref<string>(initialAntenna.name);
|
const name = ref<string>(initialAntenna.name);
|
||||||
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
|
|
||||||
const userListId = ref<string | null>(initialAntenna.userListId);
|
|
||||||
const users = ref<string>(initialAntenna.users.join('\n'));
|
const users = ref<string>(initialAntenna.users.join('\n'));
|
||||||
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
|
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
|
||||||
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
|
||||||
|
|
|
@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
|
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
|
||||||
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
|
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
|
||||||
|
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
component: AsUiComponent;
|
component: AsUiComponent;
|
||||||
|
@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
|
const {
|
||||||
|
model: valueForSelect,
|
||||||
|
def: selectDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => {
|
||||||
|
if (c.type !== 'select') return [];
|
||||||
|
return (c.items ?? []).map(item => ({
|
||||||
|
value: item.value,
|
||||||
|
label: item.text,
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
|
||||||
|
});
|
||||||
|
|
||||||
function onSelectUpdate(v) {
|
function onSelectUpdate(v) {
|
||||||
valueForSelect.value = v;
|
valueForSelect.value = v;
|
||||||
|
|
|
@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await misskeyApi('users/show', {
|
users.value = await misskeyApi('users/show', {
|
||||||
userIds: props.userIds,
|
userIds: props.userIds,
|
||||||
}) as unknown as Misskey.entities.UserLite[];
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||||
<template v-if="select.items">
|
|
||||||
<template v-for="item in select.items">
|
|
||||||
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
|
|
||||||
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<option v-else :value="item.value">{{ item.text }}</option>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</MkSelect>
|
|
||||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||||
|
@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
|
@ -67,17 +60,9 @@ type Input = {
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectItem = {
|
|
||||||
value: any;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Select = {
|
type Select = {
|
||||||
items: (SelectItem | {
|
items: MkSelectItem[];
|
||||||
sectionTitle: string;
|
default: OptionValue | null;
|
||||||
items: SelectItem[];
|
|
||||||
})[];
|
|
||||||
default: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result = string | number | true | null;
|
type Result = string | number | true | null;
|
||||||
|
@ -115,7 +100,6 @@ const emit = defineEmits<{
|
||||||
const modal = useTemplateRef('modal');
|
const modal = useTemplateRef('modal');
|
||||||
|
|
||||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||||
const selectedValue = ref(props.select?.default ?? null);
|
|
||||||
|
|
||||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||||
if (props.input) {
|
if (props.input) {
|
||||||
|
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
def: selectDef,
|
||||||
|
model: selectedValue,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => props.select?.items ?? []),
|
||||||
|
initialValue: props.select?.default ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
// overload function を使いたいので lint エラーを無視する
|
// overload function を使いたいので lint エラーを無視する
|
||||||
function done(canceled: true): void;
|
function done(canceled: true): void;
|
||||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||||
|
|
|
@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #suffix>px</template>
|
<template #suffix>px</template>
|
||||||
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="colorMode">
|
<MkSelect v-model="colorMode" :items="colorModeDef">
|
||||||
<template #label>{{ i18n.ts.theme }}</template>
|
<template #label>{{ i18n.ts.theme }}</template>
|
||||||
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
|
|
||||||
<option value="light">{{ i18n.ts.light }}</option>
|
|
||||||
<option value="dark">{{ i18n.ts.dark }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
|
||||||
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
|
||||||
|
@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||||
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
||||||
|
|
||||||
|
@ -162,7 +160,18 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro
|
||||||
const header = ref(props.params?.header ?? true);
|
const header = ref(props.params?.header ?? true);
|
||||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
||||||
|
|
||||||
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
const {
|
||||||
|
model: colorMode,
|
||||||
|
def: colorModeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ value: 'auto', label: i18n.ts.syncDeviceDarkMode },
|
||||||
|
{ value: 'light', label: i18n.ts.light },
|
||||||
|
{ value: 'dark', label: i18n.ts.dark },
|
||||||
|
],
|
||||||
|
initialValue: props.params?.colorMode ?? 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
const rounded = ref(props.params?.rounded ?? true);
|
const rounded = ref(props.params?.rounded ?? true);
|
||||||
const border = ref(props.params?.border ?? true);
|
const border = ref(props.params?.border ?? true);
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-text="v.label || k"></span>
|
<span v-text="v.label || k"></span>
|
||||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
|
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
|
@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkRadios from './MkRadios.vue';
|
import MkRadios from './MkRadios.vue';
|
||||||
import XFile from './MkFormDialog.file.vue';
|
import XFile from './MkFormDialog.file.vue';
|
||||||
import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
|
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
|
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
@ -120,16 +120,14 @@ function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnumLabel(e: EnumItem) {
|
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||||
return typeof e === 'string' ? e : e.label;
|
return def.enum.map((v) => {
|
||||||
}
|
if (typeof v === 'string') {
|
||||||
|
return { value: v, label: v };
|
||||||
function getEnumValue(e: EnumItem) {
|
} else {
|
||||||
return typeof e === 'string' ? e : e.value;
|
return { value: v.value, label: v.label };
|
||||||
}
|
}
|
||||||
|
});
|
||||||
function getEnumKey(e: EnumItem) {
|
|
||||||
return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRadioKey(e: RadioFormItem['options'][number]) {
|
function getRadioKey(e: RadioFormItem['options'][number]) {
|
||||||
|
|
|
@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div :class="$style.preview">
|
<div :class="$style.preview">
|
||||||
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
|
||||||
<div :class="$style.previewContainer">
|
<div :class="$style.previewContainer">
|
||||||
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||||
|
<div class="_acrylic" :class="$style.editControls">
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, fillSquare ? $style.active : null]" @click="fillSquare = true"><i class="ti ti-pencil"></i></button>
|
||||||
|
</div>
|
||||||
<div class="_acrylic" :class="$style.previewControls">
|
<div class="_acrylic" :class="$style.previewControls">
|
||||||
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
||||||
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
||||||
|
@ -212,6 +215,100 @@ watch(enabled, () => {
|
||||||
renderer.render();
|
renderer.render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fillSquare = ref(false);
|
||||||
|
|
||||||
|
function onImagePointerdown(ev: PointerEvent) {
|
||||||
|
if (canvasEl.value == null || imageBitmap == null || !fillSquare.value) return;
|
||||||
|
|
||||||
|
const AW = canvasEl.value.clientWidth;
|
||||||
|
const AH = canvasEl.value.clientHeight;
|
||||||
|
const BW = imageBitmap.width;
|
||||||
|
const BH = imageBitmap.height;
|
||||||
|
|
||||||
|
let xOffset = 0;
|
||||||
|
let yOffset = 0;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
yOffset = AH - BH * (AW / BW);
|
||||||
|
} else { // 縦長
|
||||||
|
xOffset = AW - BW * (AH / BH);
|
||||||
|
}
|
||||||
|
|
||||||
|
xOffset /= 2;
|
||||||
|
yOffset /= 2;
|
||||||
|
|
||||||
|
let startX = ev.offsetX - xOffset;
|
||||||
|
let startY = ev.offsetY - yOffset;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
} else { // 縦長
|
||||||
|
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = genId();
|
||||||
|
layers.push({
|
||||||
|
id,
|
||||||
|
fxId: 'fillSquare',
|
||||||
|
params: {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
scaleX: 0.1,
|
||||||
|
scaleY: 0.1,
|
||||||
|
angle: 0,
|
||||||
|
opacity: 1,
|
||||||
|
color: [1, 1, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_move(ev.offsetX, ev.offsetY);
|
||||||
|
|
||||||
|
function _move(pointerX: number, pointerY: number) {
|
||||||
|
let x = pointerX - xOffset;
|
||||||
|
let y = pointerY - yOffset;
|
||||||
|
|
||||||
|
if (AW / AH < BW / BH) { // 横長
|
||||||
|
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
} else { // 縦長
|
||||||
|
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
|
||||||
|
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = Math.abs(x - startX);
|
||||||
|
const scaleY = Math.abs(y - startY);
|
||||||
|
|
||||||
|
const layerIndex = layers.findIndex((l) => l.id === id);
|
||||||
|
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
|
||||||
|
if (layer != null) {
|
||||||
|
layer.params.offsetX = (x + startX) - 1;
|
||||||
|
layer.params.offsetY = (y + startY) - 1;
|
||||||
|
layer.params.scaleX = scaleX;
|
||||||
|
layer.params.scaleY = scaleY;
|
||||||
|
layers[layerIndex] = layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(ev: PointerEvent) {
|
||||||
|
_move(ev.offsetX, ev.offsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function up() {
|
||||||
|
canvasEl.value?.removeEventListener('pointermove', move);
|
||||||
|
canvasEl.value?.removeEventListener('pointerup', up);
|
||||||
|
canvasEl.value?.removeEventListener('pointercancel', up);
|
||||||
|
canvasEl.value?.releasePointerCapture(ev.pointerId);
|
||||||
|
|
||||||
|
fillSquare.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasEl.value.addEventListener('pointermove', move);
|
||||||
|
canvasEl.value.addEventListener('pointerup', up);
|
||||||
|
canvasEl.value.setPointerCapture(ev.pointerId);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
@ -251,6 +348,18 @@ watch(enabled, () => {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editControls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.previewControls {
|
.previewControls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
@ -283,9 +392,11 @@ watch(enabled, () => {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: -webkit-fill-available;
|
||||||
height: 100%;
|
width: stretch;
|
||||||
padding: 20px;
|
height: -webkit-fill-available;
|
||||||
|
height: stretch;
|
||||||
|
margin: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #header>Chart</template>
|
<template #header>Chart</template>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
|
||||||
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
|
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
|
||||||
<option value="federation">{{ i18n.ts._charts.federation }}</option>
|
|
||||||
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="i18n.ts.users">
|
|
||||||
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
|
|
||||||
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
|
|
||||||
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="i18n.ts.notes">
|
|
||||||
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
|
|
||||||
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
|
|
||||||
<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
|
|
||||||
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="i18n.ts.drive">
|
|
||||||
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
|
|
||||||
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</MkSelect>
|
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
|
||||||
<option value="hour">{{ i18n.ts.perHour }}</option>
|
|
||||||
<option value="day">{{ i18n.ts.perDay }}</option>
|
|
||||||
</MkSelect>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chart _panel">
|
<div class="chart _panel">
|
||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||||
|
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkFoldableSection class="item">
|
<MkFoldableSection class="item">
|
||||||
<template #header>Active users heatmap</template>
|
<template #header>Active users heatmap</template>
|
||||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
|
||||||
<option value="active-users">Active users</option>
|
|
||||||
<option value="notes">Notes</option>
|
|
||||||
<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
|
|
||||||
<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
|
|
||||||
<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
|
|
||||||
</MkSelect>
|
|
||||||
<div class="_panel" :class="$style.heatmap">
|
<div class="_panel" :class="$style.heatmap">
|
||||||
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, computed, useTemplateRef } from 'vue';
|
import { onMounted, computed, useTemplateRef } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
|
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from '@/components/MkChart.vue';
|
||||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||||
|
@ -101,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||||
import { initChart } from '@/utility/init-chart.js';
|
import { initChart } from '@/utility/init-chart.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
||||||
|
|
||||||
const chartLimit = 500;
|
const chartLimit = 500;
|
||||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
const {
|
||||||
const chartSrc = ref<ChartSrc>('active-users');
|
model: chartSpan,
|
||||||
const heatmapSrc = ref<HeatmapSource>('active-users');
|
def: chartSpanDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ value: 'hour', label: i18n.ts.perHour },
|
||||||
|
{ value: 'day', label: i18n.ts.perDay },
|
||||||
|
],
|
||||||
|
initialValue: 'hour',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: chartSrc,
|
||||||
|
def: chartSrcDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed<MkSelectItem<ChartSrc>[]>(() => {
|
||||||
|
const items: MkSelectItem<ChartSrc>[] = [];
|
||||||
|
|
||||||
|
if (shouldShowFederation.value) {
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
label: i18n.ts.federation,
|
||||||
|
items: [
|
||||||
|
{ value: 'federation', label: i18n.ts._charts.federation },
|
||||||
|
{ value: 'ap-request', label: i18n.ts._charts.apRequest },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
label: i18n.ts.users,
|
||||||
|
items: [
|
||||||
|
{ value: 'users', label: i18n.ts._charts.usersIncDec },
|
||||||
|
{ value: 'users-total', label: i18n.ts._charts.usersTotal },
|
||||||
|
{ value: 'active-users', label: i18n.ts._charts.activeUsers },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notesItems: ItemOption<ChartSrc>[] = [
|
||||||
|
{ value: 'notes', label: i18n.ts._charts.notesIncDec },
|
||||||
|
{ value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
|
||||||
|
|
||||||
|
notesItems.push(
|
||||||
|
{ value: 'notes-total', label: i18n.ts._charts.notesTotal },
|
||||||
|
);
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
label: i18n.ts.notes,
|
||||||
|
items: notesItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: 'group',
|
||||||
|
label: i18n.ts.drive,
|
||||||
|
items: [
|
||||||
|
{ value: 'drive-files', label: i18n.ts._charts.filesIncDec },
|
||||||
|
{ value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}),
|
||||||
|
initialValue: 'active-users',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: heatmapSrc,
|
||||||
|
def: heatmapSrcDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => [
|
||||||
|
{ value: 'active-users' as const, label: 'Active Users' },
|
||||||
|
{ value: 'notes' as const, label: 'Notes' },
|
||||||
|
...(shouldShowFederation.value ? [
|
||||||
|
{ value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
|
||||||
|
{ value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
|
||||||
|
{ value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
|
||||||
|
] : []),
|
||||||
|
]),
|
||||||
|
initialValue: 'active-users',
|
||||||
|
});
|
||||||
const subDoughnutEl = useTemplateRef('subDoughnutEl');
|
const subDoughnutEl = useTemplateRef('subDoughnutEl');
|
||||||
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.control">
|
<div :class="$style.control">
|
||||||
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
|
<MkSelect v-model="order" :class="$style.order" :items="orderDef">
|
||||||
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
|
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
|
||||||
|
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
paginator: T;
|
paginator: T;
|
||||||
|
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
|
||||||
const searchOpened = ref(false);
|
const searchOpened = ref(false);
|
||||||
const filterOpened = ref(props.filterOpened);
|
const filterOpened = ref(props.filterOpened);
|
||||||
|
|
||||||
const order = ref<'newest' | 'oldest'>('newest');
|
const {
|
||||||
|
model: order,
|
||||||
|
def: orderDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._order.newest, value: 'newest' },
|
||||||
|
{ label: i18n.ts._order.oldest, value: 'oldest' },
|
||||||
|
],
|
||||||
|
initialValue: 'newest',
|
||||||
|
});
|
||||||
const date = ref<number | null>(null);
|
const date = ref<number | null>(null);
|
||||||
const q = ref<string | null>(null);
|
const q = ref<string | null>(null);
|
||||||
|
|
||||||
|
|
|
@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { host } from '@@/js/config.js';
|
import { host } from '@@/js/config.js';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
|
||||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||||
import { sum } from '@/utility/array.js';
|
import { sum } from '@/utility/array.js';
|
||||||
import { pleaseLogin } from '@/utility/please-login.js';
|
import { pleaseLogin } from '@/utility/please-login.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';
|
||||||
|
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
@ -48,7 +48,21 @@ const props = defineProps<{
|
||||||
author?: Misskey.entities.UserLite;
|
author?: Misskey.entities.UserLite;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const remaining = ref(-1);
|
const now = useLowresTime();
|
||||||
|
|
||||||
|
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
|
||||||
|
|
||||||
|
const remaining = computed(() => {
|
||||||
|
if (expiresAtTime.value == null) return -1;
|
||||||
|
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainingWatchStop = watch(remaining, (to) => {
|
||||||
|
if (to <= 0) {
|
||||||
|
showResult.value = true;
|
||||||
|
remainingWatchStop();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
|
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
url: `https://${host}/notes/${props.noteId}`,
|
url: `https://${host}/notes/${props.noteId}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 期限付きアンケート
|
const vote = async (id: number) => {
|
||||||
if (props.expiresAt) {
|
|
||||||
const tick = () => {
|
|
||||||
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
|
||||||
if (remaining.value === 0) {
|
|
||||||
showResult.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useInterval(tick, 3000, {
|
|
||||||
immediate: true,
|
|
||||||
afterMounted: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const vote = async (id) => {
|
|
||||||
if (props.readOnly || closed.value || isVoted.value) return;
|
if (props.readOnly || closed.value || isVoted.value) return;
|
||||||
|
|
||||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||||
|
|
|
@ -22,11 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
||||||
<section>
|
<section>
|
||||||
<div>
|
<div>
|
||||||
<MkSelect v-model="expiration" small>
|
<MkSelect v-model="expiration" :items="expirationDef" small>
|
||||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||||
<option value="infinite">{{ i18n.ts._poll.infinite }}</option>
|
|
||||||
<option value="at">{{ i18n.ts._poll.at }}</option>
|
|
||||||
<option value="after">{{ i18n.ts._poll.after }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<section v-if="expiration === 'at'">
|
<section v-if="expiration === 'at'">
|
||||||
<MkInput v-model="atDate" small type="date" class="input">
|
<MkInput v-model="atDate" small type="date" class="input">
|
||||||
|
@ -40,12 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="after" small type="number" :min="1" class="input">
|
<MkInput v-model="after" small type="number" :min="1" class="input">
|
||||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="unit" small>
|
<MkSelect v-model="unit" :items="unitDef" small></MkSelect>
|
||||||
<option value="second">{{ i18n.ts._time.second }}</option>
|
|
||||||
<option value="minute">{{ i18n.ts._time.minute }}</option>
|
|
||||||
<option value="hour">{{ i18n.ts._time.hour }}</option>
|
|
||||||
<option value="day">{{ i18n.ts._time.day }}</option>
|
|
||||||
</MkSelect>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -61,6 +53,7 @@ import MkButton from './MkButton.vue';
|
||||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||||
import { addTime } from '@/utility/time.js';
|
import { addTime } from '@/utility/time.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
export type PollEditorModelValue = {
|
export type PollEditorModelValue = {
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
|
@ -78,11 +71,32 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const choices = ref(props.modelValue.choices);
|
const choices = ref(props.modelValue.choices);
|
||||||
const multiple = ref(props.modelValue.multiple);
|
const multiple = ref(props.modelValue.multiple);
|
||||||
const expiration = ref('infinite');
|
const {
|
||||||
|
model: expiration,
|
||||||
|
def: expirationDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._poll.infinite, value: 'infinite' },
|
||||||
|
{ label: i18n.ts._poll.at, value: 'at' },
|
||||||
|
{ label: i18n.ts._poll.after, value: 'after' },
|
||||||
|
],
|
||||||
|
initialValue: 'infinite',
|
||||||
|
});
|
||||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||||
const atTime = ref('00:00');
|
const atTime = ref('00:00');
|
||||||
const after = ref(0);
|
const after = ref(0);
|
||||||
const unit = ref('second');
|
const {
|
||||||
|
model: unit,
|
||||||
|
def: unitDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._time.second, value: 'second' },
|
||||||
|
{ label: i18n.ts._time.minute, value: 'minute' },
|
||||||
|
{ label: i18n.ts._time.hour, value: 'hour' },
|
||||||
|
{ label: i18n.ts._time.day, value: 'day' },
|
||||||
|
],
|
||||||
|
initialValue: 'second',
|
||||||
|
});
|
||||||
|
|
||||||
if (props.modelValue.expiresAt) {
|
if (props.modelValue.expiresAt) {
|
||||||
expiration.value = 'at';
|
expiration.value = 'at';
|
||||||
|
|
|
@ -567,11 +567,11 @@ async function toggleReactionAcceptance() {
|
||||||
const select = await os.select({
|
const select = await os.select({
|
||||||
title: i18n.ts.reactionAcceptance,
|
title: i18n.ts.reactionAcceptance,
|
||||||
items: [
|
items: [
|
||||||
{ value: null, text: i18n.ts.all },
|
{ value: null, label: i18n.ts.all },
|
||||||
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
|
{ value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
|
||||||
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
|
{ value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
|
||||||
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
||||||
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
{ value: 'likeOnly' as const, label: i18n.ts.likeOnly },
|
||||||
],
|
],
|
||||||
default: reactionAcceptance.value,
|
default: reactionAcceptance.value,
|
||||||
});
|
});
|
||||||
|
@ -823,17 +823,15 @@ async function saveServerDraft(clearLocal = false) {
|
||||||
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
|
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
|
||||||
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
|
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
|
||||||
text: text.value,
|
text: text.value,
|
||||||
useCw: useCw.value,
|
cw: useCw.value ? cw.value || null : null,
|
||||||
cw: cw.value,
|
|
||||||
visibility: visibility.value,
|
visibility: visibility.value,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
hashtag: hashtags.value,
|
hashtag: hashtags.value,
|
||||||
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
|
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
|
||||||
poll: poll.value,
|
poll: poll.value,
|
||||||
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
|
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
|
||||||
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : undefined,
|
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
|
||||||
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
|
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
|
||||||
quoteId: quoteId.value,
|
|
||||||
channelId: targetChannel.value ? targetChannel.value.id : undefined,
|
channelId: targetChannel.value ? targetChannel.value.id : undefined,
|
||||||
reactionAcceptance: reactionAcceptance.value,
|
reactionAcceptance: reactionAcceptance.value,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
|
@ -90,7 +90,7 @@ function subscribe() {
|
||||||
publickey: encode(subscription.getKey('p256dh')),
|
publickey: encode(subscription.getKey('p256dh')),
|
||||||
});
|
});
|
||||||
}, async err => { // When subscribe failed
|
}, async err => { // When subscribe failed
|
||||||
// 通知が許可されていなかったとき
|
// 通知が許可されていなかったとき
|
||||||
if (err?.name === 'NotAllowedError') {
|
if (err?.name === 'NotAllowedError') {
|
||||||
console.info('User denied the notification permission request.');
|
console.info('User denied the notification permission request.');
|
||||||
return;
|
return;
|
||||||
|
@ -114,14 +114,13 @@ async function unsubscribe() {
|
||||||
|
|
||||||
if ($i && accounts.length >= 2) {
|
if ($i && accounts.length >= 2) {
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog('sw/unregister', {
|
||||||
i: $i.token,
|
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
}, $i.token);
|
||||||
} else {
|
} else {
|
||||||
pushSubscription.value.unsubscribe();
|
pushSubscription.value.unsubscribe();
|
||||||
apiWithDialog('sw/unregister', {
|
apiWithDialog('sw/unregister', {
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
}, null);
|
||||||
pushSubscription.value = null;
|
pushSubscription.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) {
|
||||||
* Convert the URL safe base64 string to a Uint8Array
|
* Convert the URL safe base64 string to a Uint8Array
|
||||||
* @param base64String base64 string
|
* @param base64String base64 string
|
||||||
*/
|
*/
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
function urlBase64ToUint8Array(base64String: string): BufferSource {
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
const base64 = (base64String + padding)
|
const base64 = (base64String + padding)
|
||||||
.replace(/-/g, '+')
|
.replace(/-/g, '+')
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||||
<template v-if="forModeration">
|
<template v-if="forModeration">
|
||||||
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
|
<i v-if="'isPublic' in role && role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
|
||||||
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
|
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span :class="$style.bodyName">{{ role.name }}</span>
|
<span :class="$style.bodyName">{{ role.name }}</span>
|
||||||
<template v-if="detailed">
|
<template v-if="detailed && 'target' in role && 'usersCount' in role">
|
||||||
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
|
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
|
||||||
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
|
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
role: Misskey.entities.Role;
|
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
|
||||||
forModeration: boolean;
|
forModeration: boolean;
|
||||||
detailed?: boolean;
|
detailed?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -102,12 +102,12 @@ async function addRole() {
|
||||||
const items = roles.value
|
const items = roles.value
|
||||||
.filter(r => r.isPublic)
|
.filter(r => r.isPublic)
|
||||||
.filter(r => !selectedRoleIds.value.includes(r.id))
|
.filter(r => !selectedRoleIds.value.includes(r.id))
|
||||||
.map(r => ({ text: r.name, value: r }));
|
.map(r => ({ label: r.name, value: r.id }));
|
||||||
|
|
||||||
const { canceled, result: role } = await os.select({ items });
|
const { canceled, result: roleId } = await os.select({ items });
|
||||||
if (canceled || role == null) return;
|
if (canceled || roleId == null) return;
|
||||||
|
|
||||||
selectedRoleIds.value.push(role.id);
|
selectedRoleIds.value.push(roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRole(roleId: string) {
|
async function removeRole(roleId: string) {
|
||||||
|
|
|
@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type ItemOption = {
|
export type OptionValue = string | number | null;
|
||||||
|
|
||||||
|
export type ItemOption<T extends OptionValue = OptionValue> = {
|
||||||
type?: 'option';
|
type?: 'option';
|
||||||
value: string | number | null;
|
value: T;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemGroup = {
|
export type ItemGroup<T extends OptionValue = OptionValue> = {
|
||||||
type: 'group';
|
type: 'group';
|
||||||
label: string;
|
label?: string;
|
||||||
items: ItemOption[];
|
items: ItemOption<T>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MkSelectItem = ItemOption | ItemGroup;
|
export type MkSelectItem<T extends OptionValue = OptionValue> = ItemOption<T> | ItemGroup<T>;
|
||||||
|
|
||||||
type ValuesOfItems<T> = T extends (infer U)[]
|
export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
|
||||||
? U extends { type: 'group'; items: infer V }
|
? T['items'][number]['value']
|
||||||
? V extends (infer W)[]
|
: T extends ItemOption
|
||||||
? W extends { value: infer X }
|
? T['value']
|
||||||
? X
|
: never;
|
||||||
: never
|
|
||||||
: never
|
export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
|
||||||
: U extends { value: infer Y }
|
? GetMkSelectValueType<T[number]>
|
||||||
? Y
|
|
||||||
: never
|
|
||||||
: never;
|
: never;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup generic="T extends MkSelectItem[]">
|
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
|
||||||
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
|
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
import type { VNode, VNodeChild } from 'vue';
|
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
// TODO: itemsをslot内のoptionで指定する用法は廃止する(props.itemsを必須化する)
|
|
||||||
// see: https://github.com/misskey-dev/misskey/issues/15558
|
|
||||||
// あと型推論と相性が良くない
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: ValuesOfItems<T>;
|
items: ITEMS;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -88,16 +83,17 @@ const props = defineProps<{
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
large?: boolean;
|
large?: boolean;
|
||||||
items?: T;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
type ModelTChecked = MODELT & (
|
||||||
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
|
MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
|
||||||
}>();
|
? unknown
|
||||||
|
: 'Error: The type of model does not match the type of items.'
|
||||||
|
);
|
||||||
|
|
||||||
const slots = useSlots();
|
const model = defineModel<ModelTChecked>({ required: true });
|
||||||
|
|
||||||
const { modelValue, autofocus } = toRefs(props);
|
const { autofocus } = toRefs(props);
|
||||||
const focused = ref(false);
|
const focused = ref(false);
|
||||||
const opening = ref(false);
|
const opening = ref(false);
|
||||||
const currentValueText = ref<string | null>(null);
|
const currentValueText = ref<string | null>(null);
|
||||||
|
@ -140,52 +136,26 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([modelValue, () => props.items], () => {
|
watch([model, () => props.items], () => {
|
||||||
if (props.items) {
|
let found: ItemOption | null = null;
|
||||||
let found: ItemOption | null = null;
|
for (const item of props.items) {
|
||||||
for (const item of props.items) {
|
if (item.type === 'group') {
|
||||||
if (item.type === 'group') {
|
for (const option of item.items) {
|
||||||
for (const option of item.items) {
|
if (option.value === model.value) {
|
||||||
if (option.value === modelValue.value) {
|
found = option;
|
||||||
found = option;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (item.value === modelValue.value) {
|
|
||||||
found = item;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (item.value === model.value) {
|
||||||
|
found = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (found) {
|
|
||||||
currentValueText.value = found.label;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (found) {
|
||||||
const scanOptions = (options: VNodeChild[]) => {
|
currentValueText.value = found.label;
|
||||||
for (const vnode of options) {
|
}
|
||||||
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
|
|
||||||
if (vnode.type === 'optgroup') {
|
|
||||||
const optgroup = vnode;
|
|
||||||
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
|
|
||||||
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
|
||||||
const fragment = vnode;
|
|
||||||
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
|
|
||||||
} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
|
|
||||||
// nop?
|
|
||||||
} else {
|
|
||||||
const option = vnode;
|
|
||||||
if (option.props?.value === modelValue.value) {
|
|
||||||
currentValueText.value = option.children as string;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scanOptions(slots.default!());
|
|
||||||
}, { immediate: true, deep: true });
|
}, { immediate: true, deep: true });
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
|
@ -196,68 +166,32 @@ function show() {
|
||||||
|
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
|
|
||||||
if (props.items) {
|
for (const item of props.items) {
|
||||||
for (const item of props.items) {
|
if (item.type === 'group') {
|
||||||
if (item.type === 'group') {
|
if (item.label != null) {
|
||||||
menu.push({
|
menu.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: item.label,
|
text: item.label,
|
||||||
});
|
});
|
||||||
for (const option of item.items) {
|
}
|
||||||
menu.push({
|
for (const option of item.items) {
|
||||||
text: option.label,
|
|
||||||
active: computed(() => modelValue.value === option.value),
|
|
||||||
action: () => {
|
|
||||||
emit('update:modelValue', option.value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: item.label,
|
text: option.label,
|
||||||
active: computed(() => modelValue.value === item.value),
|
active: computed(() => model.value === option.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
emit('update:modelValue', item.value);
|
model.value = option.value as ModelTChecked;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
|
||||||
let options = slots.default!();
|
|
||||||
|
|
||||||
const pushOption = (option: VNode) => {
|
|
||||||
menu.push({
|
menu.push({
|
||||||
text: option.children as string,
|
text: item.label,
|
||||||
active: computed(() => modelValue.value === option.props?.value),
|
active: computed(() => model.value === item.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
emit('update:modelValue', option.props?.value);
|
model.value = item.value as ModelTChecked;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
const scanOptions = (options: VNodeChild[]) => {
|
|
||||||
for (const vnode of options) {
|
|
||||||
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
|
|
||||||
if (vnode.type === 'optgroup') {
|
|
||||||
const optgroup = vnode;
|
|
||||||
menu.push({
|
|
||||||
type: 'label',
|
|
||||||
text: optgroup.props?.label,
|
|
||||||
});
|
|
||||||
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
|
|
||||||
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
|
||||||
const fragment = vnode;
|
|
||||||
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
|
|
||||||
} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
|
|
||||||
// nop?
|
|
||||||
} else {
|
|
||||||
const option = vnode;
|
|
||||||
pushOption(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scanOptions(options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os.popupMenu(menu, container.value, {
|
os.popupMenu(menu, container.value, {
|
||||||
|
|
|
@ -72,7 +72,7 @@ import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
q: string;
|
q: string | Misskey.entities.UserDetailed;
|
||||||
source: HTMLElement;
|
source: HTMLElement;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -99,10 +99,11 @@ async function fetchUser() {
|
||||||
user.value = props.q;
|
user.value = props.q;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
} else {
|
} else {
|
||||||
const query: Omit<Misskey.entities.UsersShowRequest, 'userIds'> = props.q.startsWith('@') ?
|
const query: Misskey.entities.UsersShowRequest = props.q.startsWith('@') ?
|
||||||
Misskey.acct.parse(props.q.substring(1)) :
|
Misskey.acct.parse(props.q.substring(1)) :
|
||||||
{ userId: props.q };
|
{ userId: props.q };
|
||||||
|
|
||||||
|
// @ts-expect-error payloadの引数側の型が正常に解決されない
|
||||||
misskeyApi('users/show', query).then(res => {
|
misskeyApi('users/show', query).then(res => {
|
||||||
if (!props.showing) return;
|
if (!props.showing) return;
|
||||||
user.value = res;
|
user.value = res;
|
||||||
|
|
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.controls">
|
<div :class="$style.controls">
|
||||||
<div class="_spacer _gaps">
|
<div class="_spacer _gaps">
|
||||||
<MkSelect v-model="type" :items="[{ label: i18n.ts._watermarkEditor.text, value: 'text' }, { label: i18n.ts._watermarkEditor.image, value: 'image' }, { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' }]">
|
<MkSelect v-model="type" :items="typeDef">
|
||||||
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
|
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
|
@ -86,6 +86,7 @@ import * as os from '@/os.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -186,7 +187,18 @@ async function cancel() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = ref(preset.layers.length > 1 ? 'advanced' : preset.layers[0].type);
|
const {
|
||||||
|
model: type,
|
||||||
|
def: typeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._watermarkEditor.text, value: 'text' },
|
||||||
|
{ label: i18n.ts._watermarkEditor.image, value: 'image' },
|
||||||
|
{ label: i18n.ts._watermarkEditor.advanced, value: 'advanced' },
|
||||||
|
],
|
||||||
|
initialValue: preset.layers.length > 1 ? 'advanced' : preset.layers[0].type,
|
||||||
|
});
|
||||||
|
|
||||||
watch(type, () => {
|
watch(type, () => {
|
||||||
if (type.value === 'text') {
|
if (type.value === 'text') {
|
||||||
preset.layers = [createTextLayer()];
|
preset.layers = [createTextLayer()];
|
||||||
|
|
|
@ -7,9 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header :class="$style.editHeader">
|
<header :class="$style.editHeader">
|
||||||
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
<MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
||||||
<template #label>{{ i18n.ts.selectWidget }}</template>
|
<template #label>{{ i18n.ts.selectWidget }}</template>
|
||||||
<option v-for="widget in _widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||||
|
@ -59,6 +58,7 @@ import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||||
|
|
||||||
|
@ -89,7 +89,15 @@ const widgetRefs = {};
|
||||||
const configWidget = (id: string) => {
|
const configWidget = (id: string) => {
|
||||||
widgetRefs[id].configure();
|
widgetRefs[id].configure();
|
||||||
};
|
};
|
||||||
const widgetAdderSelected = ref<string | null>(null);
|
|
||||||
|
const {
|
||||||
|
model: widgetAdderSelected,
|
||||||
|
def: widgetAdderSelectedDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => [{ label: i18n.ts.none, value: null }, ..._widgetDefs.value.map(x => ({ label: i18n.ts._widgets[x], value: x }))]),
|
||||||
|
initialValue: null,
|
||||||
|
});
|
||||||
|
|
||||||
const addWidget = () => {
|
const addWidget = () => {
|
||||||
if (widgetAdderSelected.value == null) return;
|
if (widgetAdderSelected.value == null) return;
|
||||||
|
|
||||||
|
|
|
@ -4,31 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.root, { [$style.inline]: inline }]">
|
<component
|
||||||
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
|
:is="to ? 'div' : 'button'"
|
||||||
|
:class="[
|
||||||
|
$style.root,
|
||||||
|
{
|
||||||
|
[$style.inline]: inline,
|
||||||
|
'_button': !to,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="to ? (external ? 'a' : 'MkA') : 'div'"
|
||||||
|
:class="[$style.main, { [$style.active]: active }]"
|
||||||
|
class="_button"
|
||||||
|
v-bind="to ? (external ? { href: to, target: '_blank' } : { to, behavior }) : {}"
|
||||||
|
>
|
||||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
<span :class="$style.icon"><slot name="icon"></slot></span>
|
||||||
<span :class="$style.text"><slot></slot></span>
|
<div :class="$style.headerText">
|
||||||
|
<div>
|
||||||
|
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span :class="$style.suffix">
|
<span :class="$style.suffix">
|
||||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
||||||
<i class="ti ti-external-link"></i>
|
<i :class="to && external ? 'ti ti-external-link' : 'ti ti-chevron-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</component>
|
||||||
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
|
</component>
|
||||||
<span :class="$style.icon"><slot name="icon"></slot></span>
|
|
||||||
<span :class="$style.text"><slot></slot></span>
|
|
||||||
<span :class="$style.suffix">
|
|
||||||
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
|
|
||||||
<i class="ti ti-chevron-right"></i>
|
|
||||||
</span>
|
|
||||||
</MkA>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
defineProps<{
|
||||||
|
to?: string;
|
||||||
const props = defineProps<{
|
|
||||||
to: string;
|
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
behavior?: null | 'window' | 'browser';
|
behavior?: null | 'window' | 'browser';
|
||||||
|
@ -75,17 +83,18 @@ const props = defineProps<{
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
& + .text {
|
& + .headerText {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.headerText {
|
||||||
flex-shrink: 1;
|
white-space: nowrap;
|
||||||
white-space: normal;
|
text-overflow: ellipsis;
|
||||||
|
text-align: start;
|
||||||
|
overflow: hidden;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.suffix {
|
.suffix {
|
||||||
|
|
|
@ -75,6 +75,7 @@ const common = {
|
||||||
place: '',
|
place: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
dayOfWeek: 7,
|
dayOfWeek: 7,
|
||||||
|
isSensitive: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
@ -14,9 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
import { onMounted, onUnmounted, ref, computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { dateTimeFormat } from '@@/js/intl-const.js';
|
import { dateTimeFormat } from '@@/js/intl-const.js';
|
||||||
|
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
time: Date | string | number | null;
|
time: Date | string | number | null;
|
||||||
|
@ -46,8 +47,10 @@ const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
|
||||||
const invalid = Number.isNaN(_time);
|
const invalid = Number.isNaN(_time);
|
||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||||
|
|
||||||
|
const actualNow = useLowresTime();
|
||||||
|
const now = computed(() => (props.origin ? props.origin.getTime() : actualNow.value));
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
|
||||||
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
|
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
|
||||||
|
|
||||||
const relative = computed<string>(() => {
|
const relative = computed<string>(() => {
|
||||||
|
@ -72,29 +75,6 @@ const relative = computed<string>(() => {
|
||||||
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let tickId: number;
|
|
||||||
let currentInterval: number;
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
now.value = Date.now();
|
|
||||||
const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
|
|
||||||
|
|
||||||
if (currentInterval !== nextInterval) {
|
|
||||||
if (tickId) window.clearInterval(tickId);
|
|
||||||
currentInterval = nextInterval;
|
|
||||||
tickId = window.setInterval(tick, nextInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
|
|
||||||
onMounted(() => {
|
|
||||||
tick();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (tickId) window.clearInterval(tickId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
|
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
|
||||||
</template>
|
</template>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
|
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs ?? []">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</MkSwiper>
|
</MkSwiper>
|
||||||
<slot v-else></slot>
|
<slot v-else></slot>
|
||||||
|
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<PageHeaderProps & {
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageHeaderProps = computed(() => {
|
const pageHeaderProps = computed(() => {
|
||||||
const { reversed, ...rest } = props;
|
const { reversed, tab, ...rest } = props;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,10 +75,6 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.body, .swiper {
|
.body, .swiper {
|
||||||
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,5 +65,12 @@ router.useListener('change', ({ resolved }) => {
|
||||||
.root {
|
.root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--MI_THEME-bg);
|
background-color: var(--MI_THEME-bg);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Safari 26 で contain: layout を指定するとバグるので、hotfixとして _pageContainer の content: strict を上書き
|
||||||
|
* https://github.com/misskey-dev/misskey/issues/16204#issuecomment-3265404776
|
||||||
|
* https://bugs.webkit.org/show_bug.cgi?id=297186
|
||||||
|
*/
|
||||||
|
contain: size style paint !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
34
packages/frontend/src/composables/use-lowres-time.ts
Normal file
34
packages/frontend/src/composables/use-lowres-time.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, readonly, computed } from 'vue';
|
||||||
|
|
||||||
|
const time = ref(Date.now());
|
||||||
|
|
||||||
|
export const TIME_UPDATE_INTERVAL = 10000; // 10秒
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
|
||||||
|
* tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
|
||||||
|
*
|
||||||
|
* ※ マウント前の時刻を返す可能性があるため、通常は`useLowresTime`を使用する
|
||||||
|
*/
|
||||||
|
export const lowresTime = readonly(time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
|
||||||
|
* tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
|
||||||
|
*
|
||||||
|
* 必ず現在時刻以降を返すことを保証するコンポーサブル
|
||||||
|
*/
|
||||||
|
export function useLowresTime() {
|
||||||
|
// lowresTime自体はマウント前の時刻を返す可能性があるため、必ず現在時刻以降を返すことを保証する
|
||||||
|
const now = Date.now();
|
||||||
|
return computed(() => Math.max(time.value, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setInterval(() => {
|
||||||
|
time.value = Date.now();
|
||||||
|
}, TIME_UPDATE_INTERVAL);
|
38
packages/frontend/src/composables/use-mkselect.ts
Normal file
38
packages/frontend/src/composables/use-mkselect.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { Ref, MaybeRefOrGetter } from 'vue';
|
||||||
|
import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
||||||
|
|
||||||
|
type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;
|
||||||
|
|
||||||
|
/** 指定したオプション定義をもとに型を狭めたrefを生成するコンポーサブル */
|
||||||
|
export function useMkSelect<
|
||||||
|
const TItemsInput extends MaybeRefOrGetter<MkSelectItem[]>,
|
||||||
|
const TItems extends TItemsInput extends MaybeRefOrGetter<infer U> ? U : never,
|
||||||
|
TInitialValue extends OptionValue | void = void,
|
||||||
|
TItemsValue = GetMkSelectValueTypesFromDef<UnwrapReadonlyItems<TItems>>,
|
||||||
|
ModelType = TInitialValue extends void
|
||||||
|
? TItemsValue
|
||||||
|
: (TItemsValue | TInitialValue)
|
||||||
|
>(opts: {
|
||||||
|
items: TItemsInput;
|
||||||
|
initialValue?: (TInitialValue | (OptionValue extends TItemsValue ? OptionValue : TInitialValue)) & (
|
||||||
|
TItemsValue extends TInitialValue
|
||||||
|
? unknown
|
||||||
|
: { 'Error: Type of initialValue must include all types of items': TItemsValue }
|
||||||
|
);
|
||||||
|
}): {
|
||||||
|
def: TItemsInput;
|
||||||
|
model: Ref<ModelType>;
|
||||||
|
} {
|
||||||
|
const model = ref(opts.initialValue ?? null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
def: opts.items,
|
||||||
|
model: model as Ref<ModelType>,
|
||||||
|
};
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ export const globalEvents = new EventEmitter<Events>();
|
||||||
|
|
||||||
export function useGlobalEvent<T extends keyof Events>(
|
export function useGlobalEvent<T extends keyof Events>(
|
||||||
event: T,
|
event: T,
|
||||||
callback: Events[T],
|
callback: EventEmitter.EventListener<Events, T>,
|
||||||
): void {
|
): void {
|
||||||
globalEvents.on(event, callback);
|
globalEvents.on(event, callback);
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class Pizzax<T extends StateDef> {
|
||||||
|
|
||||||
private mergeState<X>(value: X, def: X): X {
|
private mergeState<X>(value: X, def: X): X {
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||||
const merged = deepMerge(value, def);
|
const merged = deepMerge<Record<PropertyKey, unknown>>(value, def);
|
||||||
|
|
||||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ export const navbarItemDef = reactive({
|
||||||
to: '/channels',
|
to: '/channels',
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts.directMessage_short,
|
||||||
icon: 'ti ti-messages',
|
icon: 'ti ti-messages',
|
||||||
to: '/chat',
|
to: '/chat',
|
||||||
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),
|
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),
|
||||||
|
|
|
@ -14,6 +14,7 @@ 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 { UploaderFeatures } from '@/composables/use-uploader.js';
|
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||||
|
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||||
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';
|
||||||
|
@ -35,9 +36,9 @@ import { focusParent } from '@/utility/focus.js';
|
||||||
export const openingWindowsCount = ref(0);
|
export const openingWindowsCount = ref(0);
|
||||||
|
|
||||||
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
|
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
|
||||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
export const apiWithDialog = (<E extends keyof Misskey.Endpoints>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P,
|
data: Misskey.Endpoints[E]['req'],
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
customErrors?: ApiWithDialogCustomErrors,
|
customErrors?: ApiWithDialogCustomErrors,
|
||||||
) => {
|
) => {
|
||||||
|
@ -502,50 +503,15 @@ export function authenticateDialog(): Promise<{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectItem<C> = {
|
export function select<C extends OptionValue, D extends C | null = null>(props: {
|
||||||
value: C;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// default が指定されていたら result は null になり得ないことを保証する overload function
|
|
||||||
export function select<C = unknown>(props: {
|
|
||||||
title?: string;
|
title?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
default: string;
|
default?: D;
|
||||||
items: (SelectItem<C> | {
|
items: (MkSelectItem<C> | undefined)[];
|
||||||
sectionTitle: string;
|
|
||||||
items: SelectItem<C>[];
|
|
||||||
} | undefined)[];
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
canceled: true; result: undefined;
|
canceled: true; result: undefined;
|
||||||
} | {
|
} | {
|
||||||
canceled: false; result: C;
|
canceled: false; result: Exclude<D, undefined> extends null ? C | null : C;
|
||||||
}>;
|
|
||||||
export function select<C = unknown>(props: {
|
|
||||||
title?: string;
|
|
||||||
text?: string;
|
|
||||||
default?: string | null;
|
|
||||||
items: (SelectItem<C> | {
|
|
||||||
sectionTitle: string;
|
|
||||||
items: SelectItem<C>[];
|
|
||||||
} | undefined)[];
|
|
||||||
}): Promise<{
|
|
||||||
canceled: true; result: undefined;
|
|
||||||
} | {
|
|
||||||
canceled: false; result: C | null;
|
|
||||||
}>;
|
|
||||||
export function select<C = unknown>(props: {
|
|
||||||
title?: string;
|
|
||||||
text?: string;
|
|
||||||
default?: string | null;
|
|
||||||
items: (SelectItem<C> | {
|
|
||||||
sectionTitle: string;
|
|
||||||
items: SelectItem<C>[];
|
|
||||||
} | undefined)[];
|
|
||||||
}): Promise<{
|
|
||||||
canceled: true; result: undefined;
|
|
||||||
} | {
|
|
||||||
canceled: false; result: C | null;
|
|
||||||
}> {
|
}> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const { dispose } = popup(MkDialog, {
|
const { dispose } = popup(MkDialog, {
|
||||||
|
|
|
@ -11,12 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
|
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
|
||||||
<template #prefix><i class="ti ti-search"></i></template>
|
<template #prefix><i class="ti ti-search"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<!-- たくさんあると邪魔
|
|
||||||
<div class="tags">
|
|
||||||
<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkFoldableSection v-if="searchEmojis">
|
<MkFoldableSection v-if="searchEmojis">
|
||||||
|
@ -42,51 +36,33 @@ import XEmoji from './emojis.emoji.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
|
import { customEmojis, customEmojiCategories } from '@/custom-emojis.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
|
|
||||||
const customEmojiTags = getCustomEmojiTags();
|
|
||||||
const q = ref('');
|
const q = ref('');
|
||||||
const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
|
const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
|
||||||
const selectedTags = ref(new Set());
|
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
|
if (q.value === '' || q.value == null) {
|
||||||
searchEmojis.value = null;
|
searchEmojis.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTags.value.size === 0) {
|
const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
|
||||||
const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
|
|
||||||
|
|
||||||
if (queryarry) {
|
if (queryarry) {
|
||||||
searchEmojis.value = customEmojis.value.filter(emoji =>
|
searchEmojis.value = customEmojis.value.filter(emoji =>
|
||||||
queryarry.includes(`:${emoji.name}:`),
|
queryarry.includes(`:${emoji.name}:`),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
|
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTag(tag) {
|
|
||||||
if (selectedTags.value.has(tag)) {
|
|
||||||
selectedTags.value.delete(tag);
|
|
||||||
} else {
|
|
||||||
selectedTags.value.add(tag);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(q, () => {
|
watch(q, () => {
|
||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(selectedTags, () => {
|
|
||||||
search();
|
|
||||||
}, { deep: true });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -11,56 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.host }}</template>
|
<template #label>{{ i18n.ts.host }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<FormSplit style="margin-top: var(--MI-margin);">
|
<FormSplit style="margin-top: var(--MI-margin);">
|
||||||
<MkSelect v-model="state">
|
<MkSelect v-model="state" :items="stateDef">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="federating">{{ i18n.ts.federating }}</option>
|
|
||||||
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
|
||||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
|
||||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
|
||||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect
|
<MkSelect v-model="sort" :items="sortDef">
|
||||||
v-model="sort" :items="[{
|
|
||||||
label: `${i18n.ts.pubSub} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+pubSub',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.pubSub} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-pubSub',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.notes} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+notes',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.notes} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-notes',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.users} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+users',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.users} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-users',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.following} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+following',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.following} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-following',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.followers} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+followers',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.followers} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-followers',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.registeredAt} (${i18n.ts.descendingOrder})`,
|
|
||||||
value: '+firstRetrievedAt',
|
|
||||||
}, {
|
|
||||||
label: `${i18n.ts.registeredAt} (${i18n.ts.ascendingOrder})`,
|
|
||||||
value: '-firstRetrievedAt',
|
|
||||||
}] as const"
|
|
||||||
>
|
|
||||||
<template #label>{{ i18n.ts.sort }}</template>
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
|
@ -85,11 +39,46 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const host = ref('');
|
const host = ref('');
|
||||||
const state = ref('federating');
|
const {
|
||||||
const sort = ref<NonNullable<Misskey.entities.FederationInstancesRequest['sort']>>('+pubSub');
|
model: state,
|
||||||
|
def: stateDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.federating, value: 'federating' },
|
||||||
|
{ label: i18n.ts.subscribing, value: 'subscribing' },
|
||||||
|
{ label: i18n.ts.publishing, value: 'publishing' },
|
||||||
|
{ label: i18n.ts.suspended, value: 'suspended' },
|
||||||
|
{ label: i18n.ts.silence, value: 'silenced' },
|
||||||
|
{ label: i18n.ts.blocked, value: 'blocked' },
|
||||||
|
{ label: i18n.ts.notResponding, value: 'notResponding' },
|
||||||
|
],
|
||||||
|
initialValue: 'federating',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: sort,
|
||||||
|
def: sortDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: `${i18n.ts.pubSub} (${i18n.ts.descendingOrder})`, value: '+pubSub' },
|
||||||
|
{ label: `${i18n.ts.pubSub} (${i18n.ts.ascendingOrder})`, value: '-pubSub' },
|
||||||
|
{ label: `${i18n.ts.notes} (${i18n.ts.descendingOrder})`, value: '+notes' },
|
||||||
|
{ label: `${i18n.ts.notes} (${i18n.ts.ascendingOrder})`, value: '-notes' },
|
||||||
|
{ label: `${i18n.ts.users} (${i18n.ts.descendingOrder})`, value: '+users' },
|
||||||
|
{ label: `${i18n.ts.users} (${i18n.ts.ascendingOrder})`, value: '-users' },
|
||||||
|
{ label: `${i18n.ts.following} (${i18n.ts.descendingOrder})`, value: '+following' },
|
||||||
|
{ label: `${i18n.ts.following} (${i18n.ts.ascendingOrder})`, value: '-following' },
|
||||||
|
{ label: `${i18n.ts.followers} (${i18n.ts.descendingOrder})`, value: '+followers' },
|
||||||
|
{ label: `${i18n.ts.followers} (${i18n.ts.ascendingOrder})`, value: '-followers' },
|
||||||
|
{ label: `${i18n.ts.registeredAt} (${i18n.ts.descendingOrder})`, value: '+firstRetrievedAt' },
|
||||||
|
{ label: `${i18n.ts.registeredAt} (${i18n.ts.ascendingOrder})`, value: '-firstRetrievedAt' },
|
||||||
|
],
|
||||||
|
initialValue: '+pubSub',
|
||||||
|
});
|
||||||
const paginator = markRaw(new Paginator('federation/instances', {
|
const paginator = markRaw(new Paginator('federation/instances', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offsetMode: true,
|
offsetMode: true,
|
||||||
|
|
|
@ -153,17 +153,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-else-if="tab === 'announcements'" class="_gaps">
|
<div v-else-if="tab === 'announcements'" class="_gaps">
|
||||||
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
|
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
|
||||||
|
|
||||||
<MkSelect v-model="announcementsStatus">
|
<MkSelect v-model="announcementsStatus" :items="announcementsStatusDef">
|
||||||
<template #label>{{ i18n.ts.filter }}</template>
|
<template #label>{{ i18n.ts.filter }}</template>
|
||||||
<option value="active">{{ i18n.ts.active }}</option>
|
|
||||||
<option value="archived">{{ i18n.ts.archived }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkPagination :paginator="announcementsPaginator">
|
<MkPagination :paginator="announcementsPaginator">
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
|
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
|
||||||
<span style="margin-right: 0.5em;">
|
<span v-if="'icon' in announcement" style="margin-right: 0.5em;">
|
||||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
|
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
|
||||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
|
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
|
||||||
|
@ -184,8 +182,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
||||||
<div class="cmhjzshm">
|
<div class="cmhjzshm">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0 10px 0 0; flex: 1;">
|
||||||
<option value="per-user-notes">{{ i18n.ts.notes }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
|
@ -229,10 +226,12 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { acct } from '@/filters/user.js';
|
import { acct } from '@/filters/user.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
|
import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
|
||||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -246,7 +245,15 @@ const props = withDefaults(defineProps<{
|
||||||
const result = await _fetch_();
|
const result = await _fetch_();
|
||||||
|
|
||||||
const tab = ref(props.initialTab);
|
const tab = ref(props.initialTab);
|
||||||
const chartSrc = ref('per-user-notes');
|
const {
|
||||||
|
model: chartSrc,
|
||||||
|
def: chartSrcDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.notes, value: 'per-user-notes' },
|
||||||
|
],
|
||||||
|
initialValue: 'per-user-notes',
|
||||||
|
});
|
||||||
const user = ref(result.user);
|
const user = ref(result.user);
|
||||||
const info = ref(result.info);
|
const info = ref(result.info);
|
||||||
const ips = ref(result.ips);
|
const ips = ref(result.ips);
|
||||||
|
@ -263,7 +270,16 @@ const filesPaginator = markRaw(new Paginator('admin/drive/files', {
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const announcementsStatus = ref<'active' | 'archived'>('active');
|
const {
|
||||||
|
model: announcementsStatus,
|
||||||
|
def: announcementsStatusDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.active, value: 'active' },
|
||||||
|
{ label: i18n.ts.archived, value: 'archived' },
|
||||||
|
],
|
||||||
|
initialValue: 'active',
|
||||||
|
});
|
||||||
|
|
||||||
const announcementsPaginator = markRaw(new Paginator('admin/announcements/list', {
|
const announcementsPaginator = markRaw(new Paginator('admin/announcements/list', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -427,22 +443,22 @@ async function assignRole() {
|
||||||
|
|
||||||
const { canceled, result: roleId } = await os.select({
|
const { canceled, result: roleId } = await os.select({
|
||||||
title: i18n.ts._role.chooseRoleToAssign,
|
title: i18n.ts._role.chooseRoleToAssign,
|
||||||
items: roles.map(r => ({ text: r.name, value: r.id })),
|
items: roles.map(r => ({ label: r.name, value: r.id })),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled || roleId == null) return;
|
||||||
|
|
||||||
const { canceled: canceled2, result: period } = await os.select({
|
const { canceled: canceled2, result: period } = await os.select({
|
||||||
title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name,
|
title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name,
|
||||||
items: [{
|
items: [{
|
||||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
value: 'indefinitely', label: i18n.ts.indefinitely,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneHour', text: i18n.ts.oneHour,
|
value: 'oneHour', label: i18n.ts.oneHour,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneDay', text: i18n.ts.oneDay,
|
value: 'oneDay', label: i18n.ts.oneDay,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
value: 'oneWeek', label: i18n.ts.oneWeek,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneMonth', text: i18n.ts.oneMonth,
|
value: 'oneMonth', label: i18n.ts.oneMonth,
|
||||||
}],
|
}],
|
||||||
default: 'indefinitely',
|
default: 'indefinitely',
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,26 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<MkSelect v-model="type" :class="$style.typeSelect">
|
<MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect">
|
||||||
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
|
|
||||||
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
|
|
||||||
<option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
|
|
||||||
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
|
|
||||||
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
|
|
||||||
<option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
|
|
||||||
<option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
|
|
||||||
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
|
|
||||||
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
|
|
||||||
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
|
|
||||||
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
|
|
||||||
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
|
|
||||||
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
|
|
||||||
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
|
|
||||||
<option value="notesLessThanOrEq">{{ i18n.ts._role._condition.notesLessThanOrEq }}</option>
|
|
||||||
<option value="notesMoreThanOrEq">{{ i18n.ts._role._condition.notesMoreThanOrEq }}</option>
|
|
||||||
<option value="and">{{ i18n.ts._role._condition.and }}</option>
|
|
||||||
<option value="or">{{ i18n.ts._role._condition.or }}</option>
|
|
||||||
<option value="not">{{ i18n.ts._role._condition.not }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
|
<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
|
||||||
<i class="ti ti-menu-2"></i>
|
<i class="ti ti-menu-2"></i>
|
||||||
|
@ -58,8 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq', 'notesLessThanOrEq', 'notesMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId">
|
<MkSelect v-else-if="type === 'roleAssignedTo'" v-model="v.roleId" :items="assignedToDef">
|
||||||
<option v-for="role in roles.filter(r => r.target === 'manual')" :key="role.id" :value="role.id">{{ role.name }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -69,6 +49,7 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
@ -99,7 +80,29 @@ watch(v, () => {
|
||||||
emit('update:modelValue', v.value);
|
emit('update:modelValue', v.value);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
const type = computed({
|
const typeDef = [
|
||||||
|
{ label: i18n.ts._role._condition.isLocal, value: 'isLocal' },
|
||||||
|
{ label: i18n.ts._role._condition.isRemote, value: 'isRemote' },
|
||||||
|
{ label: i18n.ts._role._condition.isSuspended, value: 'isSuspended' },
|
||||||
|
{ label: i18n.ts._role._condition.isLocked, value: 'isLocked' },
|
||||||
|
{ label: i18n.ts._role._condition.isBot, value: 'isBot' },
|
||||||
|
{ label: i18n.ts._role._condition.isCat, value: 'isCat' },
|
||||||
|
{ label: i18n.ts._role._condition.isExplorable, value: 'isExplorable' },
|
||||||
|
{ label: i18n.ts._role._condition.roleAssignedTo, value: 'roleAssignedTo' },
|
||||||
|
{ label: i18n.ts._role._condition.createdLessThan, value: 'createdLessThan' },
|
||||||
|
{ label: i18n.ts._role._condition.createdMoreThan, value: 'createdMoreThan' },
|
||||||
|
{ label: i18n.ts._role._condition.followersLessThanOrEq, value: 'followersLessThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.followersMoreThanOrEq, value: 'followersMoreThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.followingLessThanOrEq, value: 'followingLessThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.followingMoreThanOrEq, value: 'followingMoreThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.notesLessThanOrEq, value: 'notesLessThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.notesMoreThanOrEq, value: 'notesMoreThanOrEq' },
|
||||||
|
{ label: i18n.ts._role._condition.and, value: 'and' },
|
||||||
|
{ label: i18n.ts._role._condition.or, value: 'or' },
|
||||||
|
{ label: i18n.ts._role._condition.not, value: 'not' },
|
||||||
|
] as const satisfies MkSelectItem[];
|
||||||
|
|
||||||
|
const type = computed<GetMkSelectValueTypesFromDef<typeof typeDef>>({
|
||||||
get: () => v.value.type,
|
get: () => v.value.type,
|
||||||
set: (t) => {
|
set: (t) => {
|
||||||
if (t === 'and') v.value.values = [];
|
if (t === 'and') v.value.values = [];
|
||||||
|
@ -118,6 +121,8 @@ const type = computed({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const assignedToDef = computed(() => roles.filter(r => r.target === 'manual').map(r => ({ label: r.name, value: r.id })) satisfies MkSelectItem[]);
|
||||||
|
|
||||||
function addValue() {
|
function addValue() {
|
||||||
v.value.values.push({ id: genId(), type: 'isRemote' });
|
v.value.values.push({ id: genId(), type: 'isRemote' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,27 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-model="title">
|
<MkInput v-model="title">
|
||||||
<template #label>{{ i18n.ts.title }}</template>
|
<template #label>{{ i18n.ts.title }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-model="method">
|
<MkSelect v-model="method" :items="methodDef">
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||||
<option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
|
||||||
<option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
|
||||||
<template #caption>
|
<template #caption>
|
||||||
{{ methodCaption }}
|
{{ methodCaption }}
|
||||||
</template>
|
</template>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div>
|
<div>
|
||||||
<MkSelect v-if="method === 'email'" v-model="userId">
|
<MkSelect v-if="method === 'email'" v-model="userId" :items="userIdDef">
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template>
|
||||||
<option v-for="user in moderators" :key="user.id" :value="user.id">
|
|
||||||
{{ user.name ? `${user.name}(${user.username})` : user.username }}
|
|
||||||
</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<div v-else-if="method === 'webhook'" :class="$style.systemWebhook">
|
<div v-else-if="method === 'webhook'" :class="$style.systemWebhook">
|
||||||
<MkSelect v-model="systemWebhookId" style="flex: 1">
|
<MkSelect v-model="systemWebhookId" :items="systemWebhookIdDef" style="flex: 1">
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
||||||
<option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id">
|
|
||||||
{{ webhook.name }}
|
|
||||||
</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
|
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
|
||||||
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
|
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
|
||||||
|
@ -79,14 +71,13 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkDivider from '@/components/MkDivider.vue';
|
import MkDivider from '@/components/MkDivider.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
type NotificationRecipientMethod = 'email' | 'webhook';
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'submitted'): void;
|
(ev: 'submitted'): void;
|
||||||
(ev: 'canceled'): void;
|
(ev: 'canceled'): void;
|
||||||
|
@ -105,9 +96,28 @@ const dialogEl = useTemplateRef('dialogEl');
|
||||||
const loading = ref<number>(0);
|
const loading = ref<number>(0);
|
||||||
|
|
||||||
const title = ref<string>('');
|
const title = ref<string>('');
|
||||||
const method = ref<NotificationRecipientMethod>('email');
|
const {
|
||||||
const userId = ref<string | null>(null);
|
model: method,
|
||||||
const systemWebhookId = ref<string | null>(null);
|
def: methodDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._abuseReport._notificationRecipient._recipientType.mail, value: 'email' },
|
||||||
|
{ label: i18n.ts._abuseReport._notificationRecipient._recipientType.webhook, value: 'webhook' },
|
||||||
|
],
|
||||||
|
initialValue: 'email',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: userId,
|
||||||
|
def: userIdDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => moderators.value.map(u => ({ label: u.name ? `${u.name}(${u.username})` : u.username, value: u.id as string | null }))),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: systemWebhookId,
|
||||||
|
def: systemWebhookIdDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => systemWebhooks.value.map(w => ({ label: w.name, value: w.id }))),
|
||||||
|
});
|
||||||
const isActive = ref<boolean>(true);
|
const isActive = ref<boolean>(true);
|
||||||
|
|
||||||
const moderators = ref<entities.User[]>([]);
|
const moderators = ref<entities.User[]>([]);
|
||||||
|
|
|
@ -13,11 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.subMenus" class="_gaps_s">
|
<div :class="$style.subMenus" class="_gaps_s">
|
||||||
<MkSelect v-model="filterMethod" style="flex: 1">
|
<MkSelect v-model="filterMethod" :items="filterMethodDef" style="flex: 1">
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template>
|
||||||
<option :value="null">-</option>
|
|
||||||
<option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option>
|
|
||||||
<option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkInput v-model="filterText" type="search" style="flex: 1">
|
<MkInput v-model="filterText" type="search" style="flex: 1">
|
||||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template>
|
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template>
|
||||||
|
@ -51,10 +48,21 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkDivider from '@/components/MkDivider.vue';
|
import MkDivider from '@/components/MkDivider.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]);
|
const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]);
|
||||||
|
|
||||||
const filterMethod = ref<string | null>(null);
|
const {
|
||||||
|
model: filterMethod,
|
||||||
|
def: filterMethodDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: null },
|
||||||
|
{ label: i18n.ts._abuseReport._notificationRecipient._recipientType.mail, value: 'email' },
|
||||||
|
{ label: i18n.ts._abuseReport._notificationRecipient._recipientType.webhook, value: 'webhook' },
|
||||||
|
],
|
||||||
|
initialValue: null,
|
||||||
|
});
|
||||||
const filterText = ref<string>('');
|
const filterText = ref<string>('');
|
||||||
|
|
||||||
const filteredRecipients = computed(() => {
|
const filteredRecipients = computed(() => {
|
||||||
|
|
|
@ -16,23 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkTip>
|
</MkTip>
|
||||||
|
|
||||||
<div :class="$style.inputs" class="_gaps">
|
<div :class="$style.inputs" class="_gaps">
|
||||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
<MkSelect v-model="state" :items="stateDef" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="unresolved">{{ i18n.ts.unresolved }}</option>
|
|
||||||
<option value="resolved">{{ i18n.ts.resolved }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
|
<MkSelect v-model="targetUserOrigin" :items="targetUserOriginDef" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
<template #label>{{ i18n.ts.reporteeOrigin }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
|
<MkSelect v-model="reporterOrigin" :items="reporterOriginDef" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
<template #label>{{ i18n.ts.reporterOrigin }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -64,13 +55,44 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const state = ref('unresolved');
|
const {
|
||||||
const reporterOrigin = ref('combined');
|
model: state,
|
||||||
const targetUserOrigin = ref('combined');
|
def: stateDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.unresolved, value: 'unresolved' },
|
||||||
|
{ label: i18n.ts.resolved, value: 'resolved' },
|
||||||
|
],
|
||||||
|
initialValue: 'unresolved',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: reporterOrigin,
|
||||||
|
def: reporterOriginDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'combined' },
|
||||||
|
{ label: i18n.ts.local, value: 'local' },
|
||||||
|
{ label: i18n.ts.remote, value: 'remote' },
|
||||||
|
],
|
||||||
|
initialValue: 'combined',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: targetUserOrigin,
|
||||||
|
def: targetUserOriginDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'combined' },
|
||||||
|
{ label: i18n.ts.local, value: 'local' },
|
||||||
|
{ label: i18n.ts.remote, value: 'remote' },
|
||||||
|
],
|
||||||
|
initialValue: 'combined',
|
||||||
|
});
|
||||||
const searchUsername = ref('');
|
const searchUsername = ref('');
|
||||||
const searchHost = ref('');
|
const searchHost = ref('');
|
||||||
|
|
||||||
|
|
|
@ -6,27 +6,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||||
<MkSelect v-model="filterType" :class="$style.input" @update:modelValue="filterItems">
|
<MkSelect v-model="filterType" :items="filterTypeDef" :class="$style.input" @update:modelValue="filterItems">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
|
||||||
<option value="expired">{{ i18n.ts.expired }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||||
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
|
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
|
||||||
|
|
||||||
<MkInput v-model="ad.url" type="url">
|
<MkInput v-model="ad.url" type="url">
|
||||||
<template #label>URL</template>
|
<template #label>URL</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="ad.imageUrl" type="url">
|
<MkInput v-model="ad.imageUrl" type="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkRadios v-model="ad.place">
|
<MkRadios v-model="ad.place">
|
||||||
<template #label>Form</template>
|
<template #label>Form</template>
|
||||||
<option value="square">square</option>
|
<option value="square">square</option>
|
||||||
<option value="horizontal">horizontal</option>
|
<option value="horizontal">horizontal</option>
|
||||||
<option value="horizontal-big">horizontal-big</option>
|
<option value="horizontal-big">horizontal-big</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div style="margin: 32px 0;">
|
<div style="margin: 32px 0;">
|
||||||
{{ i18n.ts.priority }}
|
{{ i18n.ts.priority }}
|
||||||
|
@ -35,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
|
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<FormSplit>
|
<FormSplit>
|
||||||
<MkInput v-model="ad.ratio" type="number">
|
<MkInput v-model="ad.ratio" type="number">
|
||||||
<template #label>{{ i18n.ts.ratio }}</template>
|
<template #label>{{ i18n.ts.ratio }}</template>
|
||||||
|
@ -46,6 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.expiration }}</template>
|
<template #label>{{ i18n.ts.expiration }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
|
|
||||||
|
<MkSwitch v-model="ad.isSensitive">
|
||||||
|
<template #label>{{ i18n.ts.sensitive }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||||
<span>
|
<span>
|
||||||
|
@ -59,9 +67,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkTextarea v-model="ad.memo">
|
<MkTextarea v-model="ad.memo">
|
||||||
<template #label>{{ i18n.ts.memo }}</template>
|
<template #label>{{ i18n.ts.memo }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
<div class="_buttons">
|
<div class="_buttons">
|
||||||
<MkButton inline primary style="margin-right: 12px;" @click="save(ad)">
|
<MkButton inline primary style="margin-right: 12px;" @click="save(ad)">
|
||||||
<i
|
<i
|
||||||
|
@ -73,6 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkButton @click="more()">
|
<MkButton @click="more()">
|
||||||
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
|
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
|
@ -91,10 +102,12 @@ import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.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';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const ads = ref<Misskey.entities.Ad[]>([]);
|
const ads = ref<Misskey.entities.Ad[]>([]);
|
||||||
|
|
||||||
|
@ -102,7 +115,17 @@ const ads = ref<Misskey.entities.Ad[]>([]);
|
||||||
const localTime = new Date();
|
const localTime = new Date();
|
||||||
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||||
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
|
||||||
const filterType = ref('all');
|
const {
|
||||||
|
model: filterType,
|
||||||
|
def: filterTypeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.publishing, value: 'publishing' },
|
||||||
|
{ label: i18n.ts.expired, value: 'expired' },
|
||||||
|
],
|
||||||
|
initialValue: 'all',
|
||||||
|
});
|
||||||
let publishing: boolean | null = null;
|
let publishing: boolean | null = null;
|
||||||
|
|
||||||
misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
|
@ -121,7 +144,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterItems = (v) => {
|
const filterItems = (v: typeof filterType.value) => {
|
||||||
if (v === 'publishing') {
|
if (v === 'publishing') {
|
||||||
publishing = true;
|
publishing = true;
|
||||||
} else if (v === 'expired') {
|
} else if (v === 'expired') {
|
||||||
|
@ -134,7 +157,7 @@ const filterItems = (v) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 選択された曜日(index)のビットフラグを操作する
|
// 選択された曜日(index)のビットフラグを操作する
|
||||||
function toggleDayOfWeek(ad, index) {
|
function toggleDayOfWeek(ad: Misskey.entities.Ad, index: number) {
|
||||||
ad.dayOfWeek ^= 1 << index;
|
ad.dayOfWeek ^= 1 << index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,10 +173,11 @@ function add() {
|
||||||
expiresAt: new Date().toISOString(),
|
expiresAt: new Date().toISOString(),
|
||||||
startsAt: new Date().toISOString(),
|
startsAt: new Date().toISOString(),
|
||||||
dayOfWeek: 0,
|
dayOfWeek: 0,
|
||||||
|
isSensitive: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(ad) {
|
function remove(ad: Misskey.entities.Ad) {
|
||||||
os.confirm({
|
os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.tsx.removeAreYouSure({ x: ad.url }),
|
text: i18n.tsx.removeAreYouSure({ x: ad.url }),
|
||||||
|
@ -169,7 +193,7 @@ function remove(ad) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(ad) {
|
function save(ad: Misskey.entities.Ad) {
|
||||||
if (ad.id === '') {
|
if (ad.id === '') {
|
||||||
misskeyApi('admin/ad/create', {
|
misskeyApi('admin/ad/create', {
|
||||||
...ad,
|
...ad,
|
||||||
|
|
|
@ -10,10 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
|
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
|
||||||
<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
|
<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
|
||||||
|
|
||||||
<MkSelect v-model="announcementsStatus">
|
<MkSelect v-model="announcementsStatus" :items="announcementsStatusDef">
|
||||||
<template #label>{{ i18n.ts.filter }}</template>
|
<template #label>{{ i18n.ts.filter }}</template>
|
||||||
<option value="active">{{ i18n.ts.active }}</option>
|
|
||||||
<option value="archived">{{ i18n.ts.archived }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkLoading v-if="loading"/>
|
<MkLoading v-if="loading"/>
|
||||||
|
@ -98,8 +96,18 @@ import { definePage } from '@/page.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const announcementsStatus = ref<'active' | 'archived'>('active');
|
const {
|
||||||
|
model: announcementsStatus,
|
||||||
|
def: announcementsStatusDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.active, value: 'active' },
|
||||||
|
{ label: i18n.ts.archived, value: 'archived' },
|
||||||
|
],
|
||||||
|
initialValue: 'active',
|
||||||
|
});
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const loadingMore = ref(false);
|
const loadingMore = ref(false);
|
||||||
|
|
|
@ -56,20 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect
|
<MkSelect
|
||||||
v-model="model.sensitive"
|
v-model="model.sensitive"
|
||||||
|
:items="[
|
||||||
|
{ label: '-', value: null },
|
||||||
|
{ label: 'true', value: 'true' },
|
||||||
|
{ label: 'false', value: 'false' },
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<template #label>sensitive</template>
|
<template #label>sensitive</template>
|
||||||
<option :value="null">-</option>
|
|
||||||
<option :value="true">true</option>
|
|
||||||
<option :value="false">false</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkSelect
|
<MkSelect
|
||||||
v-model="model.localOnly"
|
v-model="model.localOnly"
|
||||||
|
:items="[
|
||||||
|
{ label: '-', value: null },
|
||||||
|
{ label: 'true', value: 'true' },
|
||||||
|
{ label: 'false', value: 'false' },
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<template #label>localOnly</template>
|
<template #label>localOnly</template>
|
||||||
<option :value="null">-</option>
|
|
||||||
<option :value="true">true</option>
|
|
||||||
<option :value="false">false</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkInput
|
<MkInput
|
||||||
v-model="model.updatedAtFrom"
|
v-model="model.updatedAtFrom"
|
||||||
|
|
|
@ -12,11 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}</template>
|
<template #caption>{{ i18n.ts._customEmojisManager._local._register.uploadSettingDescription }}</template>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkSelect v-model="selectedFolderId">
|
<MkSelect v-model="selectedFolderId" :items="selectedFolderIdDef">
|
||||||
<template #label>{{ i18n.ts.uploadFolder }}</template>
|
<template #label>{{ i18n.ts.uploadFolder }}</template>
|
||||||
<option v-for="folder in uploadFolders" :key="folder.id" :value="folder.id">
|
|
||||||
{{ folder.name }}
|
|
||||||
</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkSwitch v-model="directoryToCategory">
|
<MkSwitch v-model="directoryToCategory">
|
||||||
|
@ -63,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { onMounted, ref, useCssModule } from 'vue';
|
import { computed, onMounted, ref, useCssModule } from 'vue';
|
||||||
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
|
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||||
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||||
import type { DroppedFile } from '@/utility/file-drop.js';
|
import type { DroppedFile } from '@/utility/file-drop.js';
|
||||||
|
@ -87,6 +84,7 @@ import { chooseDriveFile, chooseFileFromPcAndUpload } from '@/utility/drive.js';
|
||||||
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
|
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
|
||||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
||||||
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
|
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
|
@ -229,7 +227,13 @@ function setupGrid(): GridSetting {
|
||||||
|
|
||||||
const uploadFolders = ref<FolderItem[]>([]);
|
const uploadFolders = ref<FolderItem[]>([]);
|
||||||
const gridItems = ref<GridItem[]>([]);
|
const gridItems = ref<GridItem[]>([]);
|
||||||
const selectedFolderId = ref(prefer.s.uploadFolder);
|
const {
|
||||||
|
model: selectedFolderId,
|
||||||
|
def: selectedFolderIdDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: computed(() => uploadFolders.value.map(folder => ({ label: folder.name, value: folder.id || '' }))),
|
||||||
|
initialValue: prefer.s.uploadFolder,
|
||||||
|
});
|
||||||
const directoryToCategory = ref<boolean>(false);
|
const directoryToCategory = ref<boolean>(false);
|
||||||
const registerButtonDisabled = ref<boolean>(false);
|
const registerButtonDisabled = ref<boolean>(false);
|
||||||
const requestLogs = ref<RequestLogItem[]>([]);
|
const requestLogs = ref<RequestLogItem[]>([]);
|
||||||
|
@ -303,8 +307,8 @@ async function onFileSelectClicked() {
|
||||||
const driveFiles = await chooseFileFromPcAndUpload({
|
const driveFiles = await chooseFileFromPcAndUpload({
|
||||||
multiple: true,
|
multiple: true,
|
||||||
folderId: selectedFolderId.value,
|
folderId: selectedFolderId.value,
|
||||||
// 拡張子は消す
|
// // 拡張子は消す
|
||||||
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
// nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
||||||
|
|
|
@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl');
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
let chartInstance: Chart;
|
let chartInstance: Chart | null = null;
|
||||||
|
|
||||||
function setData(values) {
|
function setData(values) {
|
||||||
if (chartInstance == null) return;
|
if (chartInstance == null || chartInstance.data.labels == null) return;
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
chartInstance.data.labels.push('');
|
chartInstance.data.labels.push('');
|
||||||
chartInstance.data.datasets[0].data.push(value);
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
|
@ -42,7 +42,7 @@ function setData(values) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushData(value) {
|
function pushData(value) {
|
||||||
if (chartInstance == null) return;
|
if (chartInstance == null || chartInstance.data.labels == null) return;
|
||||||
chartInstance.data.labels.push('');
|
chartInstance.data.labels.push('');
|
||||||
chartInstance.data.datasets[0].data.push(value);
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
if (chartInstance.data.datasets[0].data.length > 200) {
|
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||||
|
@ -69,6 +69,8 @@ const color =
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -13,31 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.host }}</template>
|
<template #label>{{ i18n.ts.host }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<FormSplit style="margin-top: var(--MI-margin);">
|
<FormSplit style="margin-top: var(--MI-margin);">
|
||||||
<MkSelect v-model="state">
|
<MkSelect v-model="state" :items="stateDef">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="federating">{{ i18n.ts.federating }}</option>
|
|
||||||
<option value="subscribing">{{ i18n.ts.subscribing }}</option>
|
|
||||||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
|
||||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
|
||||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="sort">
|
<MkSelect v-model="sort" :items="sortDef">
|
||||||
<template #label>{{ i18n.ts.sort }}</template>
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
<option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-firstRetrievedAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,11 +44,46 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||||
import FormSplit from '@/components/form/split.vue';
|
import FormSplit from '@/components/form/split.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const host = ref('');
|
const host = ref('');
|
||||||
const state = ref('federating');
|
const {
|
||||||
const sort = ref('+pubSub');
|
model: state,
|
||||||
|
def: stateDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.federating, value: 'federating' },
|
||||||
|
{ label: i18n.ts.subscribing, value: 'subscribing' },
|
||||||
|
{ label: i18n.ts.publishing, value: 'publishing' },
|
||||||
|
{ label: i18n.ts.suspended, value: 'suspended' },
|
||||||
|
{ label: i18n.ts.blocked, value: 'blocked' },
|
||||||
|
{ label: i18n.ts.silence, value: 'silenced' },
|
||||||
|
{ label: i18n.ts.notResponding, value: 'notResponding' },
|
||||||
|
],
|
||||||
|
initialValue: 'federating',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: sort,
|
||||||
|
def: sortDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: `${i18n.ts.pubSub} (${i18n.ts.descendingOrder})`, value: '+pubSub' },
|
||||||
|
{ label: `${i18n.ts.pubSub} (${i18n.ts.ascendingOrder})`, value: '-pubSub' },
|
||||||
|
{ label: `${i18n.ts.notes} (${i18n.ts.descendingOrder})`, value: '+notes' },
|
||||||
|
{ label: `${i18n.ts.notes} (${i18n.ts.ascendingOrder})`, value: '-notes' },
|
||||||
|
{ label: `${i18n.ts.users} (${i18n.ts.descendingOrder})`, value: '+users' },
|
||||||
|
{ label: `${i18n.ts.users} (${i18n.ts.ascendingOrder})`, value: '-users' },
|
||||||
|
{ label: `${i18n.ts.following} (${i18n.ts.descendingOrder})`, value: '+following' },
|
||||||
|
{ label: `${i18n.ts.following} (${i18n.ts.ascendingOrder})`, value: '-following' },
|
||||||
|
{ label: `${i18n.ts.followers} (${i18n.ts.descendingOrder})`, value: '+followers' },
|
||||||
|
{ label: `${i18n.ts.followers} (${i18n.ts.ascendingOrder})`, value: '-followers' },
|
||||||
|
{ label: `${i18n.ts.registeredAt} (${i18n.ts.descendingOrder})`, value: '+firstRetrievedAt' },
|
||||||
|
{ label: `${i18n.ts.registeredAt} (${i18n.ts.ascendingOrder})`, value: '-firstRetrievedAt' },
|
||||||
|
],
|
||||||
|
initialValue: '+pubSub',
|
||||||
|
});
|
||||||
const paginator = markRaw(new Paginator('federation/instances', {
|
const paginator = markRaw(new Paginator('federation/instances', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offsetMode: true,
|
offsetMode: true,
|
||||||
|
|
|
@ -8,11 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
|
<div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
|
||||||
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
|
<MkSelect v-model="origin" :items="originDef" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ i18n.ts.instance }}</template>
|
<template #label>{{ i18n.ts.instance }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams?.value?.origin === 'local'">
|
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams?.value?.origin === 'local'">
|
||||||
<template #label>{{ i18n.ts.host }}</template>
|
<template #label>{{ i18n.ts.host }}</template>
|
||||||
|
@ -42,9 +39,20 @@ import * as os from '@/os.js';
|
||||||
import { lookupFile } from '@/utility/admin-lookup.js';
|
import { lookupFile } from '@/utility/admin-lookup.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const origin = ref<NonNullable<Misskey.entities.AdminDriveFilesRequest['origin']>>('local');
|
const {
|
||||||
|
model: origin,
|
||||||
|
def: originDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'combined' },
|
||||||
|
{ label: i18n.ts.local, value: 'local' },
|
||||||
|
{ label: i18n.ts.remote, value: 'remote' },
|
||||||
|
],
|
||||||
|
initialValue: 'local',
|
||||||
|
});
|
||||||
const type = ref<string | null>(null);
|
const type = ref<string | null>(null);
|
||||||
const searchHost = ref('');
|
const searchHost = ref('');
|
||||||
const userId = ref('');
|
const userId = ref('');
|
||||||
|
|
|
@ -26,19 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<div :class="$style.inputs">
|
<div :class="$style.inputs">
|
||||||
<MkSelect v-model="type" :class="$style.input">
|
<MkSelect v-model="type" :items="typeDef" :class="$style.input">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="unused">{{ i18n.ts.unused }}</option>
|
|
||||||
<option value="used">{{ i18n.ts.used }}</option>
|
|
||||||
<option value="expired">{{ i18n.ts.expired }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="sort" :class="$style.input">
|
<MkSelect v-model="sort" :items="sortDef" :class="$style.input">
|
||||||
<template #label>{{ i18n.ts.sort }}</template>
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
<option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination :paginator="paginator">
|
<MkPagination :paginator="paginator">
|
||||||
|
@ -67,10 +59,33 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const type = ref<NonNullable<Misskey.entities.AdminInviteListRequest['type']>>('all');
|
const {
|
||||||
const sort = ref<NonNullable<Misskey.entities.AdminInviteListRequest['sort']>>('+createdAt');
|
model: type,
|
||||||
|
def: typeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.unused, value: 'unused' },
|
||||||
|
{ label: i18n.ts.used, value: 'used' },
|
||||||
|
{ label: i18n.ts.expired, value: 'expired' },
|
||||||
|
],
|
||||||
|
initialValue: 'all',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: sort,
|
||||||
|
def: sortDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: `${i18n.ts.createdAt} (${i18n.ts.ascendingOrder})`, value: '+createdAt' },
|
||||||
|
{ label: `${i18n.ts.createdAt} (${i18n.ts.descendingOrder})`, value: '-createdAt' },
|
||||||
|
{ label: `${i18n.ts.usedAt} (${i18n.ts.ascendingOrder})`, value: '+usedAt' },
|
||||||
|
{ label: `${i18n.ts.usedAt} (${i18n.ts.descendingOrder})`, value: '-usedAt' },
|
||||||
|
],
|
||||||
|
initialValue: '+createdAt',
|
||||||
|
});
|
||||||
|
|
||||||
const paginator = markRaw(new Paginator('admin/invite/list', {
|
const paginator = markRaw(new Paginator('admin/invite/list', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
|
@ -210,6 +210,7 @@ async function fetchCurrentQueue() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJobs() {
|
async function fetchJobs() {
|
||||||
|
if (tab.value === '-') return;
|
||||||
jobsFetching.value = true;
|
jobsFetching.value = true;
|
||||||
const state = jobState.value;
|
const state = jobState.value;
|
||||||
jobs.value = await misskeyApi('admin/queue/jobs', {
|
jobs.value = await misskeyApi('admin/queue/jobs', {
|
||||||
|
@ -307,6 +308,7 @@ async function removeJobs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshJob(jobId: string) {
|
async function refreshJob(jobId: string) {
|
||||||
|
if (tab.value === '-') return;
|
||||||
const newJob = await misskeyApi('admin/queue/show-job', { queue: tab.value, jobId });
|
const newJob = await misskeyApi('admin/queue/show-job', { queue: tab.value, jobId });
|
||||||
const index = jobs.value.findIndex((job) => job.id === jobId);
|
const index = jobs.value.findIndex((job) => job.id === jobId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
|
|
@ -25,18 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</SearchMarker>
|
</SearchMarker>
|
||||||
|
|
||||||
<SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
|
<SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
|
||||||
<MkSelect
|
<MkSelect v-model="ugcVisibilityForVisitor" :items="ugcVisibilityForVisitorDef" @update:modelValue="onChange_ugcVisibilityForVisitor">
|
||||||
v-model="ugcVisibilityForVisitor" :items="[{
|
|
||||||
value: 'all',
|
|
||||||
label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all,
|
|
||||||
}, {
|
|
||||||
value: 'local',
|
|
||||||
label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly + ' (' + i18n.ts.recommended + ')',
|
|
||||||
}, {
|
|
||||||
value: 'none',
|
|
||||||
label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none,
|
|
||||||
}] as const" @update:modelValue="onChange_ugcVisibilityForVisitor"
|
|
||||||
>
|
|
||||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
|
<template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
|
<div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
|
||||||
|
@ -176,6 +165,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { fetchInstance } from '@/instance.js';
|
import { fetchInstance } from '@/instance.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
@ -185,7 +175,17 @@ const meta = await misskeyApi('admin/meta');
|
||||||
|
|
||||||
const enableRegistration = ref(!meta.disableRegistration);
|
const enableRegistration = ref(!meta.disableRegistration);
|
||||||
const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
|
const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
|
||||||
const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor);
|
const {
|
||||||
|
model: ugcVisibilityForVisitor,
|
||||||
|
def: ugcVisibilityForVisitorDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all, value: 'all' },
|
||||||
|
{ label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly, value: 'local' },
|
||||||
|
{ label: i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none, value: 'none' },
|
||||||
|
],
|
||||||
|
initialValue: meta.ugcVisibilityForVisitor,
|
||||||
|
});
|
||||||
const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
|
const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
|
||||||
const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
|
const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
|
||||||
const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
|
const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
|
||||||
|
@ -221,7 +221,7 @@ function onChange_emailRequiredForSignup(value: boolean) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange_ugcVisibilityForVisitor(value: Misskey.entities.AdminUpdateMetaRequest['ugcVisibilityForVisitor']) {
|
function onChange_ugcVisibilityForVisitor(value: typeof ugcVisibilityForVisitor.value) {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
ugcVisibilityForVisitor: value,
|
ugcVisibilityForVisitor: value,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
|
@ -8,10 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkPaginationControl :paginator="paginator" canFilter>
|
<MkPaginationControl :paginator="paginator" canFilter>
|
||||||
<MkSelect v-model="type" style="margin: 0; flex: 1;">
|
<MkSelect v-model="type" :items="typeDef" style="margin: 0; flex: 1;">
|
||||||
<template #label>{{ i18n.ts.type }}</template>
|
<template #label>{{ i18n.ts.type }}</template>
|
||||||
<option :value="null">{{ i18n.ts.all }}</option>
|
|
||||||
<option v-for="t in Misskey.moderationLogTypes" :key="t" :value="t">{{ i18n.ts._moderationLogTypes[t] ?? t }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkInput v-model="moderatorId" style="margin: 0; flex: 1;">
|
<MkInput v-model="moderatorId" style="margin: 0; flex: 1;">
|
||||||
|
@ -54,12 +52,22 @@ import MkTl from '@/components/MkTl.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkPaginationControl from '@/components/MkPaginationControl.vue';
|
import MkPaginationControl from '@/components/MkPaginationControl.vue';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
const type = ref<string | null>(null);
|
const {
|
||||||
|
model: type,
|
||||||
|
def: typeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: null },
|
||||||
|
...Misskey.moderationLogTypes.map(t => ({ label: i18n.ts._moderationLogTypes[t] ?? t, value: t })),
|
||||||
|
],
|
||||||
|
initialValue: null,
|
||||||
|
});
|
||||||
const moderatorId = ref('');
|
const moderatorId = ref('');
|
||||||
|
|
||||||
const paginator = markRaw(new Paginator('admin/show-moderation-logs', {
|
const paginator = markRaw(new Paginator('admin/show-moderation-logs', {
|
||||||
|
|
|
@ -26,7 +26,7 @@ initChart();
|
||||||
|
|
||||||
const chartEl = useTemplateRef('chartEl');
|
const chartEl = useTemplateRef('chartEl');
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart = null;
|
let chartInstance: Chart | null = null;
|
||||||
const chartLimit = 7;
|
const chartLimit = 7;
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="item _panel sub">
|
<div class="item _panel sub">
|
||||||
<div class="icon"><i class="ti ti-world-download"></i></div>
|
<div class="icon"><i class="ti ti-world-download"></i></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="value">
|
<div v-if="federationSubActive != null" class="value">
|
||||||
{{ number(federationSubActive) }}
|
{{ number(federationSubActive) }}
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
|
<MkNumberDiff v-if="federationSubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">Sub</div>
|
<div class="label">Sub</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="item _panel pub">
|
<div class="item _panel pub">
|
||||||
<div class="icon"><i class="ti ti-world-upload"></i></div>
|
<div class="icon"><i class="ti ti-world-upload"></i></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="value">
|
<div v-if="federationPubActive != null" class="value">
|
||||||
{{ number(federationPubActive) }}
|
{{ number(federationPubActive) }}
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
|
<MkNumberDiff v-if="federationPubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">Pub</div>
|
<div class="label">Pub</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,23 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="_panel" :class="$style.root">
|
<div class="_panel" :class="$style.root">
|
||||||
<MkSelect v-model="src" style="margin: 0 0 12px 0;" small>
|
<MkSelect v-model="src" :items="srcDef" style="margin: 0 0 12px 0;" small>
|
||||||
<option value="active-users">Active users</option>
|
|
||||||
<option value="notes">Notes</option>
|
|
||||||
<option value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
|
|
||||||
<option value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
|
|
||||||
<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkHeatmap :src="src"/>
|
<MkHeatmap :src="src"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
|
||||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
|
|
||||||
const src = ref('active-users');
|
const {
|
||||||
|
model: src,
|
||||||
|
def: srcDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: 'Active users', value: 'active-users' },
|
||||||
|
{ label: 'Notes', value: 'notes' },
|
||||||
|
{ label: 'AP Requests: inboxReceived', value: 'ap-requests-inbox-received' },
|
||||||
|
{ label: 'AP Requests: deliverSucceeded', value: 'ap-requests-deliver-succeeded' },
|
||||||
|
{ label: 'AP Requests: deliverFailed', value: 'ap-requests-deliver-failed' },
|
||||||
|
],
|
||||||
|
initialValue: 'active-users',
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -32,15 +32,17 @@ const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
position: 'middle',
|
position: 'middle',
|
||||||
});
|
});
|
||||||
|
|
||||||
let chartInstance: Chart;
|
let chartInstance: Chart | null = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
labels: props.data.map(x => x.name),
|
labels: props.data.map(x => x.name),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
backgroundColor: props.data.map(x => x.color),
|
backgroundColor: props.data.map(x => x.color ?? '#000'),
|
||||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
hoverOffset: 0,
|
hoverOffset: 0,
|
||||||
|
@ -57,9 +59,10 @@ onMounted(() => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onClick: (ev) => {
|
onClick: (ev) => {
|
||||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
if (ev.native == null) return;
|
||||||
if (hit && props.data[hit.index].onClick) {
|
const hit = chartInstance!.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
|
||||||
props.data[hit.index].onClick();
|
if (hit && props.data[hit.index].onClick != null) {
|
||||||
|
props.data[hit.index].onClick!();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|
|
@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl');
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
let chartInstance: Chart;
|
let chartInstance: Chart | null = null;
|
||||||
|
|
||||||
function setData(values) {
|
function setData(values: number[]) {
|
||||||
if (chartInstance == null) return;
|
if (chartInstance == null || chartInstance.data.labels == null) return;
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
chartInstance.data.labels.push('');
|
chartInstance.data.labels.push('');
|
||||||
chartInstance.data.datasets[0].data.push(value);
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
|
@ -41,8 +41,8 @@ function setData(values) {
|
||||||
chartInstance.update();
|
chartInstance.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushData(value) {
|
function pushData(value: number) {
|
||||||
if (chartInstance == null) return;
|
if (chartInstance == null || chartInstance.data.labels == null) return;
|
||||||
chartInstance.data.labels.push('');
|
chartInstance.data.labels.push('');
|
||||||
chartInstance.data.datasets[0].data.push(value);
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
if (chartInstance.data.datasets[0].data.length > 100) {
|
if (chartInstance.data.datasets[0].data.length > 100) {
|
||||||
|
@ -67,6 +67,8 @@ const color =
|
||||||
'?' as never;
|
'?' as never;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XChart from './overview.queue.chart.vue';
|
import XChart from './overview.queue.chart.vue';
|
||||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
import type { ApQueueDomain } from '@/pages/admin/federation-job-queue.vue';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
@ -64,10 +64,10 @@ function onStats(stats: Misskey.entities.QueueStats) {
|
||||||
delayed.value = stats[props.domain].delayed;
|
delayed.value = stats[props.domain].delayed;
|
||||||
waiting.value = stats[props.domain].waiting;
|
waiting.value = stats[props.domain].waiting;
|
||||||
|
|
||||||
chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
|
chartProcess.value?.pushData(stats[props.domain].activeSincePrevTick);
|
||||||
chartActive.value.pushData(stats[props.domain].active);
|
chartActive.value?.pushData(stats[props.domain].active);
|
||||||
chartDelayed.value.pushData(stats[props.domain].delayed);
|
chartDelayed.value?.pushData(stats[props.domain].delayed);
|
||||||
chartWaiting.value.pushData(stats[props.domain].waiting);
|
chartWaiting.value?.pushData(stats[props.domain].waiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
|
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
|
||||||
|
@ -83,10 +83,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
|
||||||
dataWaiting.push(stats[props.domain].waiting);
|
dataWaiting.push(stats[props.domain].waiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
chartProcess.value.setData(dataProcess);
|
chartProcess.value?.setData(dataProcess);
|
||||||
chartActive.value.setData(dataActive);
|
chartActive.value?.setData(dataActive);
|
||||||
chartDelayed.value.setData(dataDelayed);
|
chartDelayed.value?.setData(dataDelayed);
|
||||||
chartWaiting.value.setData(dataWaiting);
|
chartWaiting.value?.setData(dataWaiting);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div>
|
<div>
|
||||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div v-else :class="$style.root">
|
<div v-else-if="stats != null" :class="$style.root">
|
||||||
<div class="item _panel users">
|
<div class="item _panel users">
|
||||||
<div class="icon"><i class="ti ti-users"></i></div>
|
<div class="icon"><i class="ti ti-users"></i></div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/>
|
<MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/>
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
|
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">Users</div>
|
<div class="label">Users</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/>
|
<MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/>
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
|
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">Notes</div>
|
<div class="label">Notes</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<MkError v-else/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -71,8 +72,8 @@ import { customEmojis } from '@/custom-emojis.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||||
const usersComparedToThePrevDay = ref<number>();
|
const usersComparedToThePrevDay = ref<number | null>(null);
|
||||||
const notesComparedToThePrevDay = ref<number>();
|
const notesComparedToThePrevDay = ref<number | null>(null);
|
||||||
const onlineUsersCount = ref(0);
|
const onlineUsersCount = ref(0);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
|
@ -85,11 +86,11 @@ onMounted(async () => {
|
||||||
onlineUsersCount.value = _onlineUsersCount;
|
onlineUsersCount.value = _onlineUsersCount;
|
||||||
|
|
||||||
misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
||||||
usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1];
|
usersComparedToThePrevDay.value = _stats.originalUsersCount - chart.local.total[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
||||||
notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1];
|
notesComparedToThePrevDay.value = _stats.originalNotesCount - chart.local.total[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
|
|
|
@ -95,7 +95,7 @@ const federationPubActiveDiff = ref<number | null>(null);
|
||||||
const federationSubActive = ref<number | null>(null);
|
const federationSubActive = ref<number | null>(null);
|
||||||
const federationSubActiveDiff = ref<number | null>(null);
|
const federationSubActiveDiff = ref<number | null>(null);
|
||||||
const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
|
const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
|
||||||
const activeInstances = shallowRef<Misskey.entities.FederationInstance | null>(null);
|
const activeInstances = shallowRef<Misskey.entities.FederationInstancesResponse | null>(null);
|
||||||
const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
|
const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const filesPagination = {
|
const filesPagination = {
|
||||||
|
|
|
@ -30,19 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts._role.descriptionOfDisplayOrder }}</template>
|
<template #caption>{{ i18n.ts._role.descriptionOfDisplayOrder }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkSelect v-model="rolePermission" :readonly="readonly">
|
<MkSelect v-model="rolePermission" :items="rolePermissionDef" :readonly="readonly">
|
||||||
<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
|
<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
|
||||||
<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
|
<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
|
||||||
<option value="normal">{{ i18n.ts.normalUser }}</option>
|
|
||||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
|
||||||
<option value="administrator">{{ i18n.ts.administrator }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkSelect v-model="role.target" :readonly="readonly">
|
<MkSelect v-model="role.target" :items="[{ label: i18n.ts._role.manual, value: 'manual' }, { label: i18n.ts._role.conditional, value: 'conditional' }]" :readonly="readonly">
|
||||||
<template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template>
|
<template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template>
|
||||||
<template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template>
|
<template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template>
|
||||||
<option value="manual">{{ i18n.ts._role.manual }}</option>
|
|
||||||
<option value="conditional">{{ i18n.ts._role.conditional }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
<MkFolder v-if="role.target === 'conditional'" defaultOpen>
|
<MkFolder v-if="role.target === 'conditional'" defaultOpen>
|
||||||
|
@ -176,11 +171,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="role.policies.chatAvailability.useDefault" :readonly="readonly">
|
<MkSwitch v-model="role.policies.chatAvailability.useDefault" :readonly="readonly">
|
||||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSelect v-model="role.policies.chatAvailability.value" :disabled="role.policies.chatAvailability.useDefault" :readonly="readonly">
|
<MkSelect
|
||||||
|
v-model="role.policies.chatAvailability.value"
|
||||||
|
:items="[
|
||||||
|
{ label: i18n.ts.enabled, value: 'available' },
|
||||||
|
{ label: i18n.ts.readonly, value: 'readonly' },
|
||||||
|
{ label: i18n.ts.disabled, value: 'unavailable' },
|
||||||
|
]"
|
||||||
|
:disabled="role.policies.chatAvailability.useDefault"
|
||||||
|
:readonly="readonly"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
<option value="available">{{ i18n.ts.enabled }}</option>
|
|
||||||
<option value="readonly">{{ i18n.ts.readonly }}</option>
|
|
||||||
<option value="unavailable">{{ i18n.ts.disabled }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkRange v-model="role.policies.chatAvailability.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
<MkRange v-model="role.policies.chatAvailability.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
@ -830,7 +831,6 @@ import { watch, ref, computed } from 'vue';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import RolesEditorFormula from './RolesEditorFormula.vue';
|
import RolesEditorFormula from './RolesEditorFormula.vue';
|
||||||
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkColorInput from '@/components/MkColorInput.vue';
|
import MkColorInput from '@/components/MkColorInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
@ -842,6 +842,7 @@ import FormSlot from '@/components/form/slot.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: any): void;
|
(ev: 'update:modelValue', v: any): void;
|
||||||
|
@ -871,11 +872,17 @@ function updateAvatarDecorationLimit(value: string | number) {
|
||||||
role.value.policies.avatarDecorationLimit.value = limited;
|
role.value.policies.avatarDecorationLimit.value = limited;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolePermission = computed({
|
const rolePermissionDef = [
|
||||||
|
{ label: i18n.ts.normalUser, value: 'normal' },
|
||||||
|
{ label: i18n.ts.moderator, value: 'moderator' },
|
||||||
|
{ label: i18n.ts.administrator, value: 'administrator' },
|
||||||
|
] as const satisfies MkSelectItem[];
|
||||||
|
|
||||||
|
const rolePermission = computed<GetMkSelectValueTypesFromDef<typeof rolePermissionDef>>({
|
||||||
get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal',
|
get: () => role.value.isAdministrator ? 'administrator' : role.value.isModerator ? 'moderator' : 'normal',
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
role.value.isAdministrator = val === 'administrator';
|
role.value.isAdministrator = (val === 'administrator');
|
||||||
role.value.isModerator = val === 'moderator';
|
role.value.isModerator = (val === 'moderator');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ import { Paginator } from '@/utility/paginator.js';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id?: string;
|
id: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const usersPaginator = markRaw(new Paginator('admin/roles/users', {
|
const usersPaginator = markRaw(new Paginator('admin/roles/users', {
|
||||||
|
@ -115,15 +115,15 @@ async function assign() {
|
||||||
const { canceled: canceled2, result: period } = await os.select({
|
const { canceled: canceled2, result: period } = await os.select({
|
||||||
title: i18n.ts.period + ': ' + role.name,
|
title: i18n.ts.period + ': ' + role.name,
|
||||||
items: [{
|
items: [{
|
||||||
value: 'indefinitely', text: i18n.ts.indefinitely,
|
value: 'indefinitely', label: i18n.ts.indefinitely,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneHour', text: i18n.ts.oneHour,
|
value: 'oneHour', label: i18n.ts.oneHour,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneDay', text: i18n.ts.oneDay,
|
value: 'oneDay', label: i18n.ts.oneDay,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneWeek', text: i18n.ts.oneWeek,
|
value: 'oneWeek', label: i18n.ts.oneWeek,
|
||||||
}, {
|
}, {
|
||||||
value: 'oneMonth', text: i18n.ts.oneMonth,
|
value: 'oneMonth', label: i18n.ts.oneMonth,
|
||||||
}],
|
}],
|
||||||
default: 'indefinitely',
|
default: 'indefinitely',
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,11 +52,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.chatAvailability, 'chatAvailability'])">
|
||||||
<template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
|
<template #label>{{ i18n.ts._role._options.chatAvailability }}</template>
|
||||||
<template #suffix>{{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }}</template>
|
||||||
<MkSelect v-model="policies.chatAvailability">
|
<MkSelect
|
||||||
|
v-model="policies.chatAvailability"
|
||||||
|
:items="[
|
||||||
|
{ label: i18n.ts.enabled, value: 'available' },
|
||||||
|
{ label: i18n.ts.readonly, value: 'readonly' },
|
||||||
|
{ label: i18n.ts.disabled, value: 'unavailable' },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
<option value="available">{{ i18n.ts.enabled }}</option>
|
|
||||||
<option value="readonly">{{ i18n.ts.readonly }}</option>
|
|
||||||
<option value="unavailable">{{ i18n.ts.disabled }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
@ -346,6 +350,7 @@ import { definePage } from '@/page.js';
|
||||||
import { instance, fetchInstance } from '@/instance.js';
|
import { instance, fetchInstance } from '@/instance.js';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -353,10 +358,7 @@ const baseRoleQ = ref('');
|
||||||
|
|
||||||
const roles = await misskeyApi('admin/roles/list');
|
const roles = await misskeyApi('admin/roles/list');
|
||||||
|
|
||||||
const policies = reactive<Record<typeof Misskey.rolePolicies[number], any>>({});
|
const policies = reactive(deepClone(instance.policies));
|
||||||
for (const ROLE_POLICY of Misskey.rolePolicies) {
|
|
||||||
policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatarDecorationLimit = computed({
|
const avatarDecorationLimit = computed({
|
||||||
get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)),
|
get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)),
|
||||||
|
@ -376,6 +378,7 @@ function matchQuery(keywords: string[]): boolean {
|
||||||
|
|
||||||
async function updateBaseRole() {
|
async function updateBaseRole() {
|
||||||
await os.apiWithDialog('admin/roles/update-default-policies', {
|
await os.apiWithDialog('admin/roles/update-default-policies', {
|
||||||
|
//@ts-expect-error misskey-js側の型定義が不十分
|
||||||
policies,
|
policies,
|
||||||
});
|
});
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
|
|
|
@ -11,26 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton style="margin-left: auto" @click="resetQuery">{{ i18n.ts.reset }}</MkButton>
|
<MkButton style="margin-left: auto" @click="resetQuery">{{ i18n.ts.reset }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.inputs">
|
<div :class="$style.inputs">
|
||||||
<MkSelect v-model="sort" style="flex: 1;">
|
<MkSelect v-model="sort" :items="sortDef" style="flex: 1;">
|
||||||
<template #label>{{ i18n.ts.sort }}</template>
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
<option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
<option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option>
|
|
||||||
<option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.descendingOrder }})</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="state" style="flex: 1;">
|
<MkSelect v-model="state" :items="stateDef" style="flex: 1;">
|
||||||
<template #label>{{ i18n.ts.state }}</template>
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
<option value="all">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="available">{{ i18n.ts.normal }}</option>
|
|
||||||
<option value="admin">{{ i18n.ts.administrator }}</option>
|
|
||||||
<option value="moderator">{{ i18n.ts.moderator }}</option>
|
|
||||||
<option value="suspended">{{ i18n.ts.suspend }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="origin" style="flex: 1;">
|
<MkSelect v-model="origin" :items="originDef" style="flex: 1;">
|
||||||
<template #label>{{ i18n.ts.instance }}</template>
|
<template #label>{{ i18n.ts.instance }}</template>
|
||||||
<option value="combined">{{ i18n.ts.all }}</option>
|
|
||||||
<option value="local">{{ i18n.ts.local }}</option>
|
|
||||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.inputs">
|
<div :class="$style.inputs">
|
||||||
|
@ -67,23 +55,57 @@ import * as os from '@/os.js';
|
||||||
import { lookupUser } from '@/utility/admin-lookup.js';
|
import { lookupUser } from '@/utility/admin-lookup.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import { Paginator } from '@/utility/paginator.js';
|
import { Paginator } from '@/utility/paginator.js';
|
||||||
|
|
||||||
type SearchQuery = {
|
type SearchQuery = {
|
||||||
sort?: string;
|
sort?: '-createdAt' | '+createdAt' | '-updatedAt' | '+updatedAt';
|
||||||
state?: string;
|
state?: 'all' | 'available' | 'admin' | 'moderator' | 'suspended';
|
||||||
origin?: string;
|
origin?: 'combined' | 'local' | 'remote';
|
||||||
username?: string;
|
username?: string;
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
|
const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
|
||||||
|
|
||||||
const sort = ref(storedQuery.sort ?? '+createdAt');
|
const {
|
||||||
const state = ref(storedQuery.state ?? 'all');
|
model: sort,
|
||||||
const origin = ref(storedQuery.origin ?? 'local');
|
def: sortDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: `${i18n.ts.registeredDate} (${i18n.ts.ascendingOrder})`, value: '-createdAt' },
|
||||||
|
{ label: `${i18n.ts.registeredDate} (${i18n.ts.descendingOrder})`, value: '+createdAt' },
|
||||||
|
{ label: `${i18n.ts.lastUsed} (${i18n.ts.ascendingOrder})`, value: '-updatedAt' },
|
||||||
|
{ label: `${i18n.ts.lastUsed} (${i18n.ts.descendingOrder})`, value: '+updatedAt' },
|
||||||
|
],
|
||||||
|
initialValue: storedQuery.sort ?? '+createdAt',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: state,
|
||||||
|
def: stateDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'all' },
|
||||||
|
{ label: i18n.ts.normal, value: 'available' },
|
||||||
|
{ label: i18n.ts.administrator, value: 'admin' },
|
||||||
|
{ label: i18n.ts.moderator, value: 'moderator' },
|
||||||
|
{ label: i18n.ts.suspend, value: 'suspended' },
|
||||||
|
],
|
||||||
|
initialValue: storedQuery.state ?? 'all',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: origin,
|
||||||
|
def: originDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: i18n.ts.all, value: 'combined' },
|
||||||
|
{ label: i18n.ts.local, value: 'local' },
|
||||||
|
{ label: i18n.ts.remote, value: 'remote' },
|
||||||
|
],
|
||||||
|
initialValue: storedQuery.origin ?? 'local',
|
||||||
|
});
|
||||||
const searchUsername = ref(storedQuery.username ?? '');
|
const searchUsername = ref(storedQuery.username ?? '');
|
||||||
const searchHost = ref(storedQuery.hostname ?? '');
|
const searchHost = ref(storedQuery.hostname ?? '');
|
||||||
const paginator = markRaw(new Paginator('admin/show-users', {
|
const paginator = markRaw(new Paginator('admin/show-users', {
|
||||||
|
|
|
@ -101,12 +101,12 @@ async function addRole() {
|
||||||
const roles = await misskeyApi('admin/roles/list');
|
const roles = await misskeyApi('admin/roles/list');
|
||||||
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
|
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
|
||||||
|
|
||||||
const { canceled, result: role } = await os.select({
|
const { canceled, result: roleId } = await os.select({
|
||||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ label: r.name, value: r.id })),
|
||||||
});
|
});
|
||||||
if (canceled || role == null) return;
|
if (canceled || roleId == null) return;
|
||||||
|
|
||||||
rolesThatCanBeUsedThisDecoration.value.push(role);
|
rolesThatCanBeUsedThisDecoration.value.push(roles.find(r => r.id === roleId)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRole(role, ev) {
|
async function removeRole(role, ev) {
|
||||||
|
|
|
@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
definePage(() => ({
|
definePage(() => ({
|
||||||
title: i18n.ts.chat + ' (beta)',
|
title: i18n.ts.directMessage,
|
||||||
icon: 'ti ti-messages',
|
icon: 'ti ti-messages',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -46,6 +46,6 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts.directMessage,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -421,7 +421,7 @@ const tab = ref('chat');
|
||||||
|
|
||||||
const headerTabs = computed(() => room.value ? [{
|
const headerTabs = computed(() => room.value ? [{
|
||||||
key: 'chat',
|
key: 'chat',
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts._chat.messages,
|
||||||
icon: 'ti ti-messages',
|
icon: 'ti ti-messages',
|
||||||
}, {
|
}, {
|
||||||
key: 'members',
|
key: 'members',
|
||||||
|
@ -437,7 +437,7 @@ const headerTabs = computed(() => room.value ? [{
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
}] : [{
|
}] : [{
|
||||||
key: 'chat',
|
key: 'chat',
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts._chat.messages,
|
||||||
icon: 'ti ti-messages',
|
icon: 'ti ti-messages',
|
||||||
}, {
|
}, {
|
||||||
key: 'search',
|
key: 'search',
|
||||||
|
@ -466,12 +466,12 @@ definePage(computed(() => {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts.directMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
title: i18n.ts.chat,
|
title: i18n.ts.directMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -11,11 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkResult v-if="resultType === 'notFound'" type="notFound"/>
|
<MkResult v-if="resultType === 'notFound'" type="notFound"/>
|
||||||
<MkResult v-if="resultType === 'error'" type="error"/>
|
<MkResult v-if="resultType === 'error'" type="error"/>
|
||||||
<MkSelect
|
<MkSelect
|
||||||
v-model="resultType" :items="[
|
v-model="resultType" :items="resultTypeDef"
|
||||||
{ label: 'empty', value: 'empty' },
|
|
||||||
{ label: 'notFound', value: 'notFound' },
|
|
||||||
{ label: 'error', value: 'error' },
|
|
||||||
]"
|
|
||||||
></MkSelect>
|
></MkSelect>
|
||||||
|
|
||||||
<MkSystemIcon v-if="iconType === 'info'" type="info" style="width: 150px;"/>
|
<MkSystemIcon v-if="iconType === 'info'" type="info" style="width: 150px;"/>
|
||||||
|
@ -25,14 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSystemIcon v-if="iconType === 'error'" type="error" style="width: 150px;"/>
|
<MkSystemIcon v-if="iconType === 'error'" type="error" style="width: 150px;"/>
|
||||||
<MkSystemIcon v-if="iconType === 'waiting'" type="waiting" style="width: 150px;"/>
|
<MkSystemIcon v-if="iconType === 'waiting'" type="waiting" style="width: 150px;"/>
|
||||||
<MkSelect
|
<MkSelect
|
||||||
v-model="iconType" :items="[
|
v-model="iconType" :items="iconTypeDef"
|
||||||
{ label: 'info', value: 'info' },
|
|
||||||
{ label: 'question', value: 'question' },
|
|
||||||
{ label: 'success', value: 'success' },
|
|
||||||
{ label: 'warn', value: 'warn' },
|
|
||||||
{ label: 'error', value: 'error' },
|
|
||||||
{ label: 'waiting', value: 'waiting' },
|
|
||||||
]"
|
|
||||||
></MkSelect>
|
></MkSelect>
|
||||||
|
|
||||||
<div class="_buttons">
|
<div class="_buttons">
|
||||||
|
@ -56,10 +45,34 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const resultType = ref('empty');
|
const {
|
||||||
const iconType = ref('info');
|
model: resultType,
|
||||||
|
def: resultTypeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: 'empty', value: 'empty' },
|
||||||
|
{ label: 'notFound', value: 'notFound' },
|
||||||
|
{ label: 'error', value: 'error' },
|
||||||
|
],
|
||||||
|
initialValue: 'empty',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
model: iconType,
|
||||||
|
def: iconTypeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: 'info', value: 'info' },
|
||||||
|
{ label: 'question', value: 'question' },
|
||||||
|
{ label: 'success', value: 'success' },
|
||||||
|
{ label: 'warn', value: 'warn' },
|
||||||
|
{ label: 'error', value: 'error' },
|
||||||
|
{ label: 'waiting', value: 'waiting' },
|
||||||
|
],
|
||||||
|
initialValue: 'info',
|
||||||
|
});
|
||||||
|
|
||||||
definePage(() => ({
|
definePage(() => ({
|
||||||
title: 'DEBUG ROOM',
|
title: 'DEBUG ROOM',
|
||||||
|
|
|
@ -23,12 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_woodenFrame" style="text-align: center;">
|
<div class="_woodenFrame" style="text-align: center;">
|
||||||
<div class="_woodenFrameInner">
|
<div class="_woodenFrameInner">
|
||||||
<div class="_gaps" style="padding: 16px;">
|
<div class="_gaps" style="padding: 16px;">
|
||||||
<MkSelect v-model="gameMode">
|
<MkSelect v-model="gameMode" :items="gameModeDef">
|
||||||
<option value="normal">NORMAL</option>
|
|
||||||
<option value="square">SQUARE</option>
|
|
||||||
<option value="yen">YEN</option>
|
|
||||||
<option value="sweets">SWEETS</option>
|
|
||||||
<!--<option value="space">SPACE</option>-->
|
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,11 +87,24 @@ import XGame from './drop-and-fusion.game.vue';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||||
|
|
||||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
|
const {
|
||||||
|
model: gameMode,
|
||||||
|
def: gameModeDef,
|
||||||
|
} = useMkSelect({
|
||||||
|
items: [
|
||||||
|
{ label: 'NORMAL', value: 'normal' },
|
||||||
|
{ label: 'SQUARE', value: 'square' },
|
||||||
|
{ label: 'YEN', value: 'yen' },
|
||||||
|
{ label: 'SWEETS', value: 'sweets' },
|
||||||
|
//{ label: 'SPACE', value: 'space' },
|
||||||
|
],
|
||||||
|
initialValue: 'normal',
|
||||||
|
});
|
||||||
const gameStarted = ref(false);
|
const gameStarted = ref(false);
|
||||||
const mute = ref(false);
|
const mute = ref(false);
|
||||||
const ranking = ref<Misskey.entities.BubbleGameRankingResponse | null>(null);
|
const ranking = ref<Misskey.entities.BubbleGameRankingResponse | null>(null);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue