Compare commits
96 Commits
68999e411a
...
c7ba767af6
Author | SHA1 | Date |
---|---|---|
|
c7ba767af6 | |
|
218070eb13 | |
|
0f8c068e84 | |
|
69d66b89f2 | |
|
211365de64 | |
|
966127c63e | |
|
54800971eb | |
|
13d5c6d2b2 | |
|
2cff00eedd | |
|
3fc2261041 | |
|
18d66c0233 | |
|
2f52c20150 | |
|
9d70c9ad78 | |
|
42b2aea533 | |
|
97adf6f2cc | |
|
93ff209c51 | |
|
5fe08d0bbb | |
|
8c413d01e6 | |
|
b231da7c7c | |
|
df3e44f62e | |
|
e504560477 | |
|
bcb2073715 | |
|
6a80c23a50 | |
|
2621f468ff | |
|
d4654dd7bd | |
|
b7da6cad87 | |
|
5b4115e21a | |
|
c174c5c144 | |
|
aebc3f781e | |
|
f60b6291d7 | |
|
7673874675 | |
|
6e3354f95d | |
|
b9df928097 | |
|
0754678144 | |
|
a8cc51dc77 | |
|
690edcef16 | |
|
2ea784f345 | |
|
20d257b562 | |
|
c215415613 | |
|
726c03d96a | |
|
e65ddb546c | |
|
85aea9077f | |
|
f3fffce6a9 | |
|
eb7db5a3aa | |
|
e33eb26863 | |
|
430310f306 | |
|
1e1eea521e | |
|
86ad771221 | |
|
057acf471e | |
|
2bfe257879 | |
|
6d75624aa8 | |
|
369f0ec88a | |
|
788c5660ba | |
|
6cf1f86636 | |
|
5b994b3e03 | |
|
7b2abb7577 | |
|
b681788315 | |
|
279af1d72f | |
|
9e188ca3fa | |
|
de1b2223ff | |
|
9b565728e7 | |
|
a92fd8856a | |
|
047773341d | |
|
842670e100 | |
|
ffc481a994 | |
|
2ccf4f94cb | |
|
3566bc207f | |
|
4a0e968662 | |
|
b1479ab1d8 | |
|
18a9ccf7af | |
|
959e72b2b3 | |
|
a3d78b2f08 | |
|
3c998e1f48 | |
|
782c9f9852 | |
|
d27c740ab0 | |
|
08ecf7ca79 | |
|
bdec4bf87a | |
|
7000095b44 | |
|
18e42cc83d | |
|
11204eeb43 | |
|
c95092903a | |
|
21b2b9e5f8 | |
|
665ec2c43c | |
|
34bd840525 | |
|
3d1cbcf094 | |
|
5f5d88036f | |
|
24739cd040 | |
|
b491432daa | |
|
ebe029458e | |
|
d127d82c5b | |
|
aabda5a956 | |
|
bd5b38c9d9 | |
|
647e03bf34 | |
|
d16db7f311 | |
|
ec4731dee4 | |
|
65a4d77a7f |
|
@ -105,6 +105,16 @@ port: 3000
|
|||
# socket: /path/to/misskey.sock
|
||||
# chmodSocket: '777'
|
||||
|
||||
# Proxy trust settings
|
||||
#
|
||||
# Changes how the server interpret the origin IP of the request.
|
||||
#
|
||||
# Any format supported by Fastify is accepted.
|
||||
# Default: trust all proxies (i.e. trustProxy: true)
|
||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
||||
#
|
||||
# trustProxy: 1
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.15.0
|
||||
22.20.0
|
||||
|
|
49
CHANGELOG.md
49
CHANGELOG.md
|
@ -1,3 +1,40 @@
|
|||
## 2025.9.1
|
||||
|
||||
### NOTE
|
||||
- pnpm 10.16.0 が必要です
|
||||
|
||||
### General
|
||||
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
|
||||
|
||||
### Client
|
||||
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
|
||||
- Feat: 動画を圧縮してアップロードできるようになりました
|
||||
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
|
||||
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加
|
||||
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
|
||||
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
|
||||
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
|
||||
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
|
||||
|
||||
## 2025.9.0
|
||||
|
||||
### Client
|
||||
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
|
||||
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
|
||||
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
|
||||
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
|
||||
- Enhance: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように
|
||||
- Fix: プッシュ通知を有効にできない問題を修正
|
||||
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
|
||||
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
|
||||
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
|
||||
|
||||
### Server
|
||||
- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
|
||||
|
||||
## 2025.8.0
|
||||
|
||||
### Note
|
||||
|
@ -6,14 +43,12 @@
|
|||
### General
|
||||
- ノートを削除した際、関連するノートが同時に削除されないようになりました
|
||||
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
|
||||
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
|
||||
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
|
||||
- 定期的に古いリモートの投稿を削除する機能が実装されました
|
||||
- コントロールパネル→パフォーマンス→Remote Notes Cleaning で有効化できます
|
||||
- データベースの肥大化を防止することが可能です
|
||||
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
|
||||
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
|
||||
- データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください
|
||||
- ハイパーリンクによる参照は検知できないためリンク切れとなります。
|
||||
- 現時点では、2023-10-01以前にクリップされたリモートのノートは検知しないため削除対象となります。
|
||||
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
|
||||
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
|
||||
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
|
||||
|
@ -46,9 +81,11 @@
|
|||
- Enhance: トルコ語 (tr-TR) に対応
|
||||
- Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました
|
||||
- Enhance: 画像エフェクトのパラメータ名の多言語対応
|
||||
- Enhance: 依存ソフトウェアの更新
|
||||
- Enhance: ノートを非表示にする相対期間を1ヶ月単位で自由に指定できるように
|
||||
- Enhance: メールアドレス確認画面のUIを改善
|
||||
- Enhance: アイコンのスクロール追従を無効化する際の適用範囲を強化
|
||||
- Enhance: レンダリングパフォーマンスの向上
|
||||
- Enhance: 依存ソフトウェアの更新
|
||||
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
|
||||
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
||||
|
@ -60,6 +97,7 @@
|
|||
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
|
||||
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
|
||||
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
|
||||
- Fix: タッチ操作時にマウスホバー時のユーザープレビューが開くことがある問題を修正
|
||||
- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
|
||||
- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
|
||||
|
||||
|
@ -76,6 +114,7 @@
|
|||
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
|
||||
- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正
|
||||
- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように
|
||||
- Fix: タイムラインAPIの withRenotes: false 時のレスポンスを修正
|
||||
|
||||
|
||||
## 2025.7.0
|
||||
|
|
|
@ -1644,7 +1644,7 @@ _serverSettings:
|
|||
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
|
||||
remoteNotesCleaning: "Neteja automàtica de notes remotes"
|
||||
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
|
||||
remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
|
||||
remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja"
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
|
||||
inquiryUrl: "URL de consulta "
|
||||
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
|
||||
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard."
|
||||
remoteContentsCleaning: "Neteja automàtica del contingut rebut"
|
||||
remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge."
|
||||
remoteContentsCleaning_description2: "Alguns mètodes de referència, com els enllaços, no poden ser detectats pel sistema."
|
||||
adminInfo: "Informació de l'administrador "
|
||||
adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes."
|
||||
adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada."
|
||||
|
|
|
@ -1668,6 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Some current settings will be reset."
|
||||
entrancePageStyle: "Entrance page style"
|
||||
showTimelineForVisitor: "Show timeline"
|
||||
showActivitiesForVisitor: "Show activities"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Everything is public"
|
||||
localOnly: "Only local content is published, remote content is kept private"
|
||||
|
@ -3193,6 +3194,7 @@ _imageEffector:
|
|||
mirror: "Mirror"
|
||||
invert: "Invert Colors"
|
||||
grayscale: "Grayscale"
|
||||
blur: "Blur"
|
||||
colorAdjust: "Color Correction"
|
||||
colorClamp: "Color Compression"
|
||||
colorClampAdvanced: "Color Compression (Advanced)"
|
||||
|
@ -3208,6 +3210,8 @@ _imageEffector:
|
|||
angle: "Angle"
|
||||
scale: "Size"
|
||||
size: "Size"
|
||||
radius: "Radius"
|
||||
samples: "Samples"
|
||||
color: "Color"
|
||||
opacity: "Opacity"
|
||||
normalize: "Normalize"
|
||||
|
|
|
@ -2137,7 +2137,7 @@ _aboutMisskey:
|
|||
_displayOfSensitiveMedia:
|
||||
respect: "Esconder medios marcados como sensibles"
|
||||
ignore: "Mostrar medios marcados como sensibles"
|
||||
force: "Esconder todala multimedia"
|
||||
force: "Esconder toda la multimedia"
|
||||
_instanceTicker:
|
||||
none: "No mostrar"
|
||||
remote: "Mostrar a usuarios remotos"
|
||||
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
|
||||
remoteContentsCleaning: "Limpieza automática de los contenidos recibidos"
|
||||
remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento."
|
||||
remoteContentsCleaning_description2: "Ciertos métodos de referencia, como los hipervínculos, no pueden ser detectados por el sistema."
|
||||
adminInfo: "Información del administrador"
|
||||
adminInfo_description: "Establece la información del administrador para recibir consultas."
|
||||
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."
|
||||
|
|
|
@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
|
|||
* 処理中
|
||||
*/
|
||||
"processing": string;
|
||||
/**
|
||||
* 準備中
|
||||
*/
|
||||
"preprocessing": string;
|
||||
/**
|
||||
* プレビュー
|
||||
*/
|
||||
|
@ -1227,7 +1231,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"noMoreHistory": string;
|
||||
/**
|
||||
* チャットを始める
|
||||
* メッセージを送る
|
||||
*/
|
||||
"startChat": string;
|
||||
/**
|
||||
|
@ -1927,7 +1931,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"markAsReadAllUnreadNotes": string;
|
||||
/**
|
||||
* すべてのチャットを既読にする
|
||||
* すべてのダイレクトメッセージを既読にする
|
||||
*/
|
||||
"markAsReadAllTalkMessages": string;
|
||||
/**
|
||||
|
@ -5390,6 +5394,14 @@ export interface Locale extends ILocale {
|
|||
* チャット
|
||||
*/
|
||||
"chat": string;
|
||||
/**
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"directMessage": string;
|
||||
/**
|
||||
* メッセージ
|
||||
*/
|
||||
"directMessage_short": string;
|
||||
/**
|
||||
* 旧設定情報を移行
|
||||
*/
|
||||
|
@ -5501,6 +5513,14 @@ export interface Locale extends ILocale {
|
|||
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
||||
*/
|
||||
"defaultImageCompressionLevel_description": string;
|
||||
/**
|
||||
* デフォルトの圧縮度
|
||||
*/
|
||||
"defaultCompressionLevel": string;
|
||||
/**
|
||||
* 低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。
|
||||
*/
|
||||
"defaultCompressionLevel_description": string;
|
||||
/**
|
||||
* 分
|
||||
*/
|
||||
|
@ -5529,6 +5549,40 @@ export interface Locale extends ILocale {
|
|||
* ベータ版の検証にご協力いただきありがとうございます!
|
||||
*/
|
||||
"thankYouForTestingBeta": string;
|
||||
/**
|
||||
* ユーザー指定ノートを作成
|
||||
*/
|
||||
"createUserSpecifiedNote": string;
|
||||
"_compression": {
|
||||
"_quality": {
|
||||
/**
|
||||
* 高品質
|
||||
*/
|
||||
"high": string;
|
||||
/**
|
||||
* 中品質
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* 低品質
|
||||
*/
|
||||
"low": string;
|
||||
};
|
||||
"_size": {
|
||||
/**
|
||||
* サイズ大
|
||||
*/
|
||||
"large": string;
|
||||
/**
|
||||
* サイズ中
|
||||
*/
|
||||
"medium": string;
|
||||
/**
|
||||
* サイズ小
|
||||
*/
|
||||
"small": string;
|
||||
};
|
||||
};
|
||||
"_order": {
|
||||
/**
|
||||
* 新しい順
|
||||
|
@ -5540,6 +5594,10 @@ export interface Locale extends ILocale {
|
|||
"oldest": string;
|
||||
};
|
||||
"_chat": {
|
||||
/**
|
||||
* メッセージ
|
||||
*/
|
||||
"messages": string;
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
*/
|
||||
|
@ -5549,36 +5607,36 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"newMessage": string;
|
||||
/**
|
||||
* 個人チャット
|
||||
* 個別
|
||||
*/
|
||||
"individualChat": string;
|
||||
/**
|
||||
* 特定ユーザーとの一対一のチャットができます。
|
||||
* 特定ユーザーと個別にメッセージのやりとりができます。
|
||||
*/
|
||||
"individualChat_description": string;
|
||||
/**
|
||||
* ルームチャット
|
||||
* グループ
|
||||
*/
|
||||
"roomChat": string;
|
||||
/**
|
||||
* 複数人でのチャットができます。
|
||||
* また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
|
||||
* 複数人でメッセージのやりとりができます。
|
||||
* また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。
|
||||
*/
|
||||
"roomChat_description": string;
|
||||
/**
|
||||
* ルームを作成
|
||||
* グループを作成
|
||||
*/
|
||||
"createRoom": string;
|
||||
/**
|
||||
* ユーザーを招待してチャットを始めましょう
|
||||
* ユーザーを招待してメッセージを送信しましょう
|
||||
*/
|
||||
"inviteUserToChat": string;
|
||||
/**
|
||||
* 作成したルーム
|
||||
* 作成したグループ
|
||||
*/
|
||||
"yourRooms": string;
|
||||
/**
|
||||
* 参加中のルーム
|
||||
* 参加中のグループ
|
||||
*/
|
||||
"joiningRooms": string;
|
||||
/**
|
||||
|
@ -5598,7 +5656,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"noHistory": string;
|
||||
/**
|
||||
* ルームはありません
|
||||
* グループはありません
|
||||
*/
|
||||
"noRooms": string;
|
||||
/**
|
||||
|
@ -5618,7 +5676,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"ignore": string;
|
||||
/**
|
||||
* ルームから退出
|
||||
* グループから退出
|
||||
*/
|
||||
"leave": string;
|
||||
/**
|
||||
|
@ -5642,35 +5700,35 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"newline": string;
|
||||
/**
|
||||
* このルームをミュート
|
||||
* このグループをミュート
|
||||
*/
|
||||
"muteThisRoom": string;
|
||||
/**
|
||||
* ルームを削除
|
||||
* グループを削除
|
||||
*/
|
||||
"deleteRoom": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは有効化されていません。
|
||||
* このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。
|
||||
*/
|
||||
"chatNotAvailableForThisAccountOrServer": string;
|
||||
/**
|
||||
* このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。
|
||||
* このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。
|
||||
*/
|
||||
"chatIsReadOnlyForThisAccountOrServer": string;
|
||||
/**
|
||||
* 相手のアカウントでチャット機能が使えない状態になっています。
|
||||
* 相手のアカウントでダイレクトメッセージが使えない状態になっています。
|
||||
*/
|
||||
"chatNotAvailableInOtherAccount": string;
|
||||
/**
|
||||
* このユーザーとのチャットを開始できません
|
||||
* このユーザーとのダイレクトメッセージを開始できません
|
||||
*/
|
||||
"cannotChatWithTheUser": string;
|
||||
/**
|
||||
* チャットが使えない状態になっているか、相手がチャットを開放していません。
|
||||
* ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。
|
||||
*/
|
||||
"cannotChatWithTheUser_description": string;
|
||||
/**
|
||||
* あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||
* あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。
|
||||
*/
|
||||
"youAreNotAMemberOfThisRoomButInvited": string;
|
||||
/**
|
||||
|
@ -5678,31 +5736,31 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"doYouAcceptInvitation": string;
|
||||
/**
|
||||
* チャットする
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chatWithThisUser": string;
|
||||
/**
|
||||
* このユーザーはフォロワーからのみチャットを受け付けています。
|
||||
* このユーザーはフォロワーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||
/**
|
||||
* このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。
|
||||
* このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||
/**
|
||||
* このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
|
||||
* このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromMutualFollowing": string;
|
||||
/**
|
||||
* このユーザーは誰からもチャットを受け付けていません。
|
||||
* このユーザーは誰からもメッセージを受け付けていません。
|
||||
*/
|
||||
"thisUserNotAllowedChatAnyone": string;
|
||||
/**
|
||||
* チャットを許可する相手
|
||||
* メッセージを許可する相手
|
||||
*/
|
||||
"chatAllowedUsers": string;
|
||||
/**
|
||||
* 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
|
||||
* 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。
|
||||
*/
|
||||
"chatAllowedUsers_note": string;
|
||||
"_chatAllowedUsers": {
|
||||
|
@ -6531,7 +6589,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"remoteNotesCleaning": string;
|
||||
/**
|
||||
* 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。
|
||||
* 有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。
|
||||
*/
|
||||
"remoteNotesCleaning_description": string;
|
||||
/**
|
||||
|
@ -7856,7 +7914,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"canImportUserLists": string;
|
||||
/**
|
||||
* チャットを許可
|
||||
* ダイレクトメッセージを許可
|
||||
*/
|
||||
"chatAvailability": string;
|
||||
/**
|
||||
|
@ -8706,7 +8764,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"badge": string;
|
||||
/**
|
||||
* チャットの背景
|
||||
* メッセージの背景
|
||||
*/
|
||||
"messageBg": string;
|
||||
/**
|
||||
|
@ -8733,7 +8791,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"reaction": string;
|
||||
/**
|
||||
* チャットのメッセージ
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chatMessage": string;
|
||||
};
|
||||
|
@ -9017,11 +9075,11 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"write:following": string;
|
||||
/**
|
||||
* チャットを見る
|
||||
* ダイレクトメッセージを見る
|
||||
*/
|
||||
"read:messaging": string;
|
||||
/**
|
||||
* チャットを操作する
|
||||
* ダイレクトメッセージを操作する
|
||||
*/
|
||||
"write:messaging": string;
|
||||
/**
|
||||
|
@ -9313,11 +9371,11 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"write:report-abuse": string;
|
||||
/**
|
||||
* チャットを操作する
|
||||
* ダイレクトメッセージを操作する
|
||||
*/
|
||||
"write:chat": string;
|
||||
/**
|
||||
* チャットを閲覧する
|
||||
* ダイレクトメッセージを閲覧する
|
||||
*/
|
||||
"read:chat": string;
|
||||
};
|
||||
|
@ -9543,7 +9601,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"birthdayFollowings": string;
|
||||
/**
|
||||
* チャット
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
|
@ -10283,7 +10341,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleAssigned": string;
|
||||
/**
|
||||
* チャットルームへ招待されました
|
||||
* ダイレクトメッセージのグループへ招待されました
|
||||
*/
|
||||
"chatRoomInvitationReceived": string;
|
||||
/**
|
||||
|
@ -10396,7 +10454,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleAssigned": string;
|
||||
/**
|
||||
* チャットルームへ招待された
|
||||
* ダイレクトメッセージのグループへ招待された
|
||||
*/
|
||||
"chatRoomInvitationReceived": string;
|
||||
/**
|
||||
|
@ -10578,7 +10636,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"roleTimeline": string;
|
||||
/**
|
||||
* チャット
|
||||
* ダイレクトメッセージ
|
||||
*/
|
||||
"chat": string;
|
||||
};
|
||||
|
@ -10945,7 +11003,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"deleteGalleryPost": string;
|
||||
/**
|
||||
* チャットルームを削除
|
||||
* ダイレクトメッセージのグループを削除
|
||||
*/
|
||||
"deleteChatRoom": string;
|
||||
/**
|
||||
|
@ -12036,13 +12094,9 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"remoteContentsCleaning": string;
|
||||
/**
|
||||
* 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。
|
||||
* 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。
|
||||
*/
|
||||
"remoteContentsCleaning_description": string;
|
||||
/**
|
||||
* ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。
|
||||
*/
|
||||
"remoteContentsCleaning_description2": string;
|
||||
/**
|
||||
* 管理者情報
|
||||
*/
|
||||
|
@ -12223,10 +12277,18 @@ export interface Locale extends ILocale {
|
|||
* テキスト
|
||||
*/
|
||||
"text": string;
|
||||
/**
|
||||
* 二次元コード
|
||||
*/
|
||||
"qr": string;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
"position": string;
|
||||
/**
|
||||
* マージン
|
||||
*/
|
||||
"margin": string;
|
||||
/**
|
||||
* タイプ
|
||||
*/
|
||||
|
@ -12283,6 +12345,10 @@ export interface Locale extends ILocale {
|
|||
* サブドットの数
|
||||
*/
|
||||
"polkadotSubDotDivisions": string;
|
||||
/**
|
||||
* 空欄にするとアカウントのURLになります
|
||||
*/
|
||||
"leaveBlankToAccountUrl": string;
|
||||
};
|
||||
"_imageEffector": {
|
||||
/**
|
||||
|
@ -12322,6 +12388,10 @@ export interface Locale extends ILocale {
|
|||
* 白黒
|
||||
*/
|
||||
"grayscale": string;
|
||||
/**
|
||||
* ぼかし
|
||||
*/
|
||||
"blur": string;
|
||||
/**
|
||||
* 色調補正
|
||||
*/
|
||||
|
@ -12366,6 +12436,10 @@ export interface Locale extends ILocale {
|
|||
* ティアリング
|
||||
*/
|
||||
"tearing": string;
|
||||
/**
|
||||
* 塗りつぶし
|
||||
*/
|
||||
"fill": string;
|
||||
};
|
||||
"_fxProps": {
|
||||
/**
|
||||
|
@ -12380,6 +12454,18 @@ export interface Locale extends ILocale {
|
|||
* サイズ
|
||||
*/
|
||||
"size": string;
|
||||
/**
|
||||
* 半径
|
||||
*/
|
||||
"radius": string;
|
||||
/**
|
||||
* サンプル数
|
||||
*/
|
||||
"samples": string;
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
"offset": string;
|
||||
/**
|
||||
* 色
|
||||
*/
|
||||
|
@ -12492,6 +12578,10 @@ export interface Locale extends ILocale {
|
|||
* 黒色にする
|
||||
*/
|
||||
"zoomLinesBlack": string;
|
||||
/**
|
||||
* 円形
|
||||
*/
|
||||
"circle": string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
|
@ -12552,6 +12642,68 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"listDrafts": string;
|
||||
};
|
||||
/**
|
||||
* 二次元コード
|
||||
*/
|
||||
"qr": string;
|
||||
"_qr": {
|
||||
/**
|
||||
* 表示
|
||||
*/
|
||||
"showTabTitle": string;
|
||||
/**
|
||||
* 読み取る
|
||||
*/
|
||||
"readTabTitle": string;
|
||||
/**
|
||||
* {name} {acct}
|
||||
*/
|
||||
"shareTitle": ParameterizedString<"name" | "acct">;
|
||||
/**
|
||||
* Fediverseで私をフォローしてください!
|
||||
*/
|
||||
"shareText": string;
|
||||
/**
|
||||
* カメラを選択
|
||||
*/
|
||||
"chooseCamera": string;
|
||||
/**
|
||||
* ライト選択不可
|
||||
*/
|
||||
"cannotToggleFlash": string;
|
||||
/**
|
||||
* ライトをオンにする
|
||||
*/
|
||||
"turnOnFlash": string;
|
||||
/**
|
||||
* ライトをオフにする
|
||||
*/
|
||||
"turnOffFlash": string;
|
||||
/**
|
||||
* コードリーダーを再開
|
||||
*/
|
||||
"startQr": string;
|
||||
/**
|
||||
* コードリーダーを停止
|
||||
*/
|
||||
"stopQr": string;
|
||||
/**
|
||||
* QRコードが見つかりません
|
||||
*/
|
||||
"noQrCodeFound": string;
|
||||
/**
|
||||
* 端末の画像をスキャン
|
||||
*/
|
||||
"scanFile": string;
|
||||
/**
|
||||
* テキスト
|
||||
*/
|
||||
"raw": string;
|
||||
/**
|
||||
* MFM
|
||||
*/
|
||||
"mfm": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -139,7 +139,7 @@ overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali"
|
|||
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
|
||||
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
|
||||
attachCancel: "Rimuovi allegato"
|
||||
deleteFile: "File da Drive eliminato"
|
||||
deleteFile: "Elimina un file dal Drive"
|
||||
markAsSensitive: "Segna come esplicito"
|
||||
unmarkAsSensitive: "Non segnare come esplicito "
|
||||
enterFileName: "Nome del file"
|
||||
|
@ -2222,7 +2222,7 @@ _theme:
|
|||
hashtag: "Hashtag"
|
||||
mention: "Menzioni"
|
||||
mentionMe: "Menzioni (di me)"
|
||||
renote: "Renota"
|
||||
renote: "Rinota"
|
||||
modalBg: "Sfondo modale."
|
||||
divider: "Interruzione di linea"
|
||||
scrollbarHandle: "Maniglie della barra di scorrimento"
|
||||
|
@ -2663,7 +2663,7 @@ _notification:
|
|||
createToken: "È stato creato un token di accesso"
|
||||
createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})."
|
||||
_types:
|
||||
all: "Tutto"
|
||||
all: "Tutte"
|
||||
note: "Nuove Note"
|
||||
follow: "Follower"
|
||||
mention: "Menzioni"
|
||||
|
@ -2671,7 +2671,7 @@ _notification:
|
|||
renote: "Rinota"
|
||||
quote: "Cita"
|
||||
reaction: "Reazioni"
|
||||
pollEnded: "Sondaggio chiuso."
|
||||
pollEnded: "Sondaggio terminato"
|
||||
receiveFollowRequest: "Richieste di follow in arrivo"
|
||||
followRequestAccepted: "Richieste di follow accettate"
|
||||
roleAssigned: "Ruolo concesso"
|
||||
|
@ -2679,7 +2679,7 @@ _notification:
|
|||
achievementEarned: "Risultato raggiunto"
|
||||
exportCompleted: "Esportazione completata"
|
||||
login: "Accessi"
|
||||
createToken: "Creare un token di accesso"
|
||||
createToken: "Aggiunto un token di accesso"
|
||||
test: "Notifiche di test"
|
||||
app: "Notifiche da applicazioni"
|
||||
_actions:
|
||||
|
@ -2771,56 +2771,56 @@ _abuseReport:
|
|||
notifiedWebhook: "Webhook da usare"
|
||||
deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?"
|
||||
_moderationLogTypes:
|
||||
createRole: "Ruolo creato"
|
||||
deleteRole: "Ruolo eliminato"
|
||||
updateRole: "Ruolo aggiornato"
|
||||
assignRole: "Ruolo assegnato"
|
||||
unassignRole: "Ruolo disassegnato"
|
||||
suspend: "Sospensione"
|
||||
unsuspend: "Sospensione rimossa"
|
||||
addCustomEmoji: "Emoji personalizzata aggiunta"
|
||||
updateCustomEmoji: "Emoji personalizzata aggiornata"
|
||||
deleteCustomEmoji: "Emoji personalizzata eliminata"
|
||||
updateServerSettings: "Impostazioni del server aggiornate"
|
||||
updateUserNote: "Promemoria di moderazione aggiornato"
|
||||
deleteDriveFile: "File da Drive eliminato"
|
||||
deleteNote: "Nota eliminata"
|
||||
createGlobalAnnouncement: "Annuncio globale creato"
|
||||
createUserAnnouncement: "Annuncio ai profili iscritti creato"
|
||||
updateGlobalAnnouncement: "Annuncio globale aggiornato"
|
||||
updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato"
|
||||
deleteGlobalAnnouncement: "Annuncio globale eliminato"
|
||||
deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato"
|
||||
resetPassword: "Password azzerata"
|
||||
suspendRemoteInstance: "Istanza remota sospesa"
|
||||
unsuspendRemoteInstance: "Istanza remota riattivata"
|
||||
updateRemoteInstanceNote: "Aggiornamento del promemoria di moderazione per il server remoto"
|
||||
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
|
||||
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
|
||||
resolveAbuseReport: "Segnalazione risolta"
|
||||
forwardAbuseReport: "Segnalazione inoltrata"
|
||||
updateAbuseReportNote: "Ha aggiornato la segnalazione"
|
||||
createInvitation: "Genera codice di invito"
|
||||
createAd: "Banner creato"
|
||||
deleteAd: "Banner eliminato"
|
||||
updateAd: "Banner aggiornato"
|
||||
createAvatarDecoration: "Creazione decorazione della foto profilo"
|
||||
updateAvatarDecoration: "Aggiornamento decorazione foto profilo"
|
||||
deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
|
||||
unsetUserAvatar: "Rimossa foto profilo"
|
||||
unsetUserBanner: "Rimossa intestazione profilo"
|
||||
createSystemWebhook: "Crea un SystemWebhook"
|
||||
updateSystemWebhook: "Modifica SystemWebhook"
|
||||
deleteSystemWebhook: "Elimina SystemWebhook"
|
||||
createRole: "Crea un Ruolo"
|
||||
deleteRole: "Elimina un Ruolo"
|
||||
updateRole: "Modifica un ruolo"
|
||||
assignRole: "Assegna un Ruolo"
|
||||
unassignRole: "Toglie un Ruolo al Profilo"
|
||||
suspend: "Sospende"
|
||||
unsuspend: "Solleva la sospensione"
|
||||
addCustomEmoji: "Aggiunge Emoji personalizzata"
|
||||
updateCustomEmoji: "Modifica Emoji personalizzata"
|
||||
deleteCustomEmoji: "Elimina Emoji personalizzata"
|
||||
updateServerSettings: "Modifica le impostazioni del server"
|
||||
updateUserNote: "Modifica un promemoria di moderazione"
|
||||
deleteDriveFile: "Elimina un file dal Drive"
|
||||
deleteNote: "Elimina una Nota"
|
||||
createGlobalAnnouncement: "Crea un annuncio globale"
|
||||
createUserAnnouncement: "Crea un annuncio ai profili già iscritti"
|
||||
updateGlobalAnnouncement: "Modifica un annuncio globale"
|
||||
updateUserAnnouncement: "Modifica un annuncio ai profili già iscritti"
|
||||
deleteGlobalAnnouncement: "Elimina un annuncio globale"
|
||||
deleteUserAnnouncement: "Elimina un annuncio ai profili già iscritti"
|
||||
resetPassword: "Azzera la password"
|
||||
suspendRemoteInstance: "Sospende una istanza remota"
|
||||
unsuspendRemoteInstance: "Riattiva una istanza remota"
|
||||
updateRemoteInstanceNote: "Modifica il promemoria di moderazione per il server remoto"
|
||||
markSensitiveDriveFile: "Aggiunge NSFW a un file nel Drive"
|
||||
unmarkSensitiveDriveFile: "Toglie NSFW da un file nel Drive"
|
||||
resolveAbuseReport: "Risolve una segnalazione"
|
||||
forwardAbuseReport: "Inoltra una segnalazione"
|
||||
updateAbuseReportNote: "Modifica una segnalazione"
|
||||
createInvitation: "Genera un codice di invito"
|
||||
createAd: "Aggiunge un Banner"
|
||||
deleteAd: "Elimina un Banner"
|
||||
updateAd: "Modifica un Banner"
|
||||
createAvatarDecoration: "Crea una decorazione della foto profilo"
|
||||
updateAvatarDecoration: "Modifica una decorazione della foto profilo"
|
||||
deleteAvatarDecoration: "Elimina una decorazione della foto profilo"
|
||||
unsetUserAvatar: "Toglie una foto profilo"
|
||||
unsetUserBanner: "Toglie una immagine di intestazione profilo"
|
||||
createSystemWebhook: "Aggiunge un System Webhook"
|
||||
updateSystemWebhook: "Modifica un System Webhook"
|
||||
deleteSystemWebhook: "Elimina un System Webhook"
|
||||
createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni"
|
||||
updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni"
|
||||
deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni"
|
||||
deleteAccount: "Quando viene eliminato un profilo"
|
||||
deletePage: "Pagina eliminata"
|
||||
deleteFlash: "Play eliminato"
|
||||
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
|
||||
deleteChatRoom: "Elimina chat"
|
||||
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
|
||||
updateAbuseReportNotificationRecipient: "Modifica un destinatario per le notifiche di segnalazioni"
|
||||
deleteAbuseReportNotificationRecipient: "Elimina un destinatario per le notifiche di segnalazioni"
|
||||
deleteAccount: "Elimina un profilo"
|
||||
deletePage: "Elimina una Pagina"
|
||||
deleteFlash: "Elimina un Play"
|
||||
deleteGalleryPost: "Elimina pubblicazione nella Galleria"
|
||||
deleteChatRoom: "Elimina una Chat"
|
||||
updateProxyAccountDescription: "Aggiorna la descrizione del profilo proxy"
|
||||
_fileViewer:
|
||||
title: "Dettagli del file"
|
||||
type: "Tipo di file"
|
||||
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
|
||||
remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo"
|
||||
remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione."
|
||||
remoteContentsCleaning_description2: "Alcuni metodi di riferimento, come i collegamenti ipertestuali, non possono essere rilevati sul sistema."
|
||||
adminInfo: "Informazioni sull'amministratore"
|
||||
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
|
||||
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."
|
||||
|
|
|
@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
|
|||
pinLimitExceeded: "これ以上ピン留めできません"
|
||||
done: "完了"
|
||||
processing: "処理中"
|
||||
preprocessing: "準備中"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
defaultValueIs: "デフォルト: {value}"
|
||||
|
@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
|
|||
explore: "みつける"
|
||||
messageRead: "既読"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
startChat: "チャットを始める"
|
||||
startChat: "メッセージを送る"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agree: "同意する"
|
||||
|
@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
|
|||
uploadFolder: "既定アップロード先"
|
||||
markAsReadAllNotifications: "すべての通知を既読にする"
|
||||
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
|
||||
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
|
||||
help: "ヘルプ"
|
||||
inputMessageHere: "ここにメッセージを入力"
|
||||
close: "閉じる"
|
||||
|
@ -1343,6 +1344,8 @@ postForm: "投稿フォーム"
|
|||
textCount: "文字数"
|
||||
information: "情報"
|
||||
chat: "チャット"
|
||||
directMessage: "ダイレクトメッセージ"
|
||||
directMessage_short: "メッセージ"
|
||||
migrateOldSettings: "旧設定情報を移行"
|
||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||
compress: "圧縮"
|
||||
|
@ -1370,6 +1373,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
|||
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
||||
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
||||
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
||||
defaultCompressionLevel: "デフォルトの圧縮度"
|
||||
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
|
||||
inMinutes: "分"
|
||||
inDays: "日"
|
||||
safeModeEnabled: "セーフモードが有効です"
|
||||
|
@ -1377,53 +1382,65 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
|
|||
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
|
||||
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
|
||||
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
|
||||
createUserSpecifiedNote: "ユーザー指定ノートを作成"
|
||||
|
||||
_compression:
|
||||
_quality:
|
||||
high: "高品質"
|
||||
medium: "中品質"
|
||||
low: "低品質"
|
||||
_size:
|
||||
large: "サイズ大"
|
||||
medium: "サイズ中"
|
||||
small: "サイズ小"
|
||||
|
||||
_order:
|
||||
newest: "新しい順"
|
||||
oldest: "古い順"
|
||||
|
||||
_chat:
|
||||
messages: "メッセージ"
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
newMessage: "新しいメッセージ"
|
||||
individualChat: "個人チャット"
|
||||
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
|
||||
roomChat: "ルームチャット"
|
||||
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
|
||||
createRoom: "ルームを作成"
|
||||
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
|
||||
yourRooms: "作成したルーム"
|
||||
joiningRooms: "参加中のルーム"
|
||||
individualChat: "個別"
|
||||
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
|
||||
roomChat: "グループ"
|
||||
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
|
||||
createRoom: "グループを作成"
|
||||
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
|
||||
yourRooms: "作成したグループ"
|
||||
joiningRooms: "参加中のグループ"
|
||||
invitations: "招待"
|
||||
noInvitations: "招待はありません"
|
||||
history: "履歴"
|
||||
noHistory: "履歴はありません"
|
||||
noRooms: "ルームはありません"
|
||||
noRooms: "グループはありません"
|
||||
inviteUser: "ユーザーを招待"
|
||||
sentInvitations: "送信した招待"
|
||||
join: "参加"
|
||||
ignore: "無視"
|
||||
leave: "ルームから退出"
|
||||
leave: "グループから退出"
|
||||
members: "メンバー"
|
||||
searchMessages: "メッセージを検索"
|
||||
home: "ホーム"
|
||||
send: "送信"
|
||||
newline: "改行"
|
||||
muteThisRoom: "このルームをミュート"
|
||||
deleteRoom: "ルームを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
|
||||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||
muteThisRoom: "このグループをミュート"
|
||||
deleteRoom: "グループを削除"
|
||||
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
|
||||
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
|
||||
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
|
||||
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
|
||||
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
|
||||
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
|
||||
doYouAcceptInvitation: "招待を承認しますか?"
|
||||
chatWithThisUser: "チャットする"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
||||
chatAllowedUsers: "チャットを許可する相手"
|
||||
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
|
||||
chatWithThisUser: "ダイレクトメッセージ"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
|
||||
chatAllowedUsers: "メッセージを許可する相手"
|
||||
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
|
||||
_chatAllowedUsers:
|
||||
everyone: "誰でも"
|
||||
followers: "自分のフォロワーのみ"
|
||||
|
@ -1660,7 +1677,7 @@ _serverSettings:
|
|||
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
|
||||
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
||||
remoteNotesCleaning: "リモート投稿の自動クリーニング"
|
||||
remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
|
||||
remoteNotesCleaning_description: "有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
|
||||
remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間"
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数"
|
||||
inquiryUrl: "問い合わせ先URL"
|
||||
|
@ -2034,7 +2051,7 @@ _role:
|
|||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
chatAvailability: "チャットを許可"
|
||||
chatAvailability: "ダイレクトメッセージを許可"
|
||||
uploadableFileTypes: "アップロード可能なファイル種別"
|
||||
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
|
||||
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
|
||||
|
@ -2281,7 +2298,7 @@ _theme:
|
|||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
badge: "バッジ"
|
||||
messageBg: "チャットの背景"
|
||||
messageBg: "メッセージの背景"
|
||||
fgHighlighted: "強調された文字"
|
||||
|
||||
_sfx:
|
||||
|
@ -2289,7 +2306,7 @@ _sfx:
|
|||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
reaction: "リアクション選択時"
|
||||
chatMessage: "チャットのメッセージ"
|
||||
chatMessage: "ダイレクトメッセージ"
|
||||
|
||||
_soundSettings:
|
||||
driveFile: "ドライブの音声を使用"
|
||||
|
@ -2369,8 +2386,8 @@ _permissions:
|
|||
"write:favorites": "お気に入りを操作する"
|
||||
"read:following": "フォローの情報を見る"
|
||||
"write:following": "フォロー・フォロー解除する"
|
||||
"read:messaging": "チャットを見る"
|
||||
"write:messaging": "チャットを操作する"
|
||||
"read:messaging": "ダイレクトメッセージを見る"
|
||||
"write:messaging": "ダイレクトメッセージを操作する"
|
||||
"read:mutes": "ミュートを見る"
|
||||
"write:mutes": "ミュートを操作する"
|
||||
"write:notes": "ノートを作成・削除する"
|
||||
|
@ -2443,8 +2460,8 @@ _permissions:
|
|||
"read:clip-favorite": "クリップのいいねを見る"
|
||||
"read:federation": "連合に関する情報を取得する"
|
||||
"write:report-abuse": "違反を報告する"
|
||||
"write:chat": "チャットを操作する"
|
||||
"read:chat": "チャットを閲覧する"
|
||||
"write:chat": "ダイレクトメッセージを操作する"
|
||||
"read:chat": "ダイレクトメッセージを閲覧する"
|
||||
|
||||
_auth:
|
||||
shareAccessTitle: "アプリへのアクセス許可"
|
||||
|
@ -2507,7 +2524,7 @@ _widgets:
|
|||
chooseList: "リストを選択"
|
||||
clicker: "クリッカー"
|
||||
birthdayFollowings: "今日誕生日のユーザー"
|
||||
chat: "チャット"
|
||||
chat: "ダイレクトメッセージ"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
@ -2714,7 +2731,7 @@ _notification:
|
|||
newNote: "新しい投稿"
|
||||
unreadAntennaNote: "アンテナ {name}"
|
||||
roleAssigned: "ロールが付与されました"
|
||||
chatRoomInvitationReceived: "チャットルームへ招待されました"
|
||||
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
|
||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||
achievementEarned: "実績を獲得"
|
||||
testNotification: "通知テスト"
|
||||
|
@ -2744,7 +2761,7 @@ _notification:
|
|||
receiveFollowRequest: "フォロー申請を受け取った"
|
||||
followRequestAccepted: "フォローが受理された"
|
||||
roleAssigned: "ロールが付与された"
|
||||
chatRoomInvitationReceived: "チャットルームへ招待された"
|
||||
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
|
||||
achievementEarned: "実績の獲得"
|
||||
exportCompleted: "エクスポートが完了した"
|
||||
login: "ログイン"
|
||||
|
@ -2794,7 +2811,7 @@ _deck:
|
|||
mentions: "メンション"
|
||||
direct: "指名"
|
||||
roleTimeline: "ロールタイムライン"
|
||||
chat: "チャット"
|
||||
chat: "ダイレクトメッセージ"
|
||||
|
||||
_dialog:
|
||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||
|
@ -2897,7 +2914,7 @@ _moderationLogTypes:
|
|||
deletePage: "ページを削除"
|
||||
deleteFlash: "Playを削除"
|
||||
deleteGalleryPost: "ギャラリーの投稿を削除"
|
||||
deleteChatRoom: "チャットルームを削除"
|
||||
deleteChatRoom: "ダイレクトメッセージのグループを削除"
|
||||
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
|
||||
|
||||
_fileViewer:
|
||||
|
@ -3217,8 +3234,7 @@ _serverSetupWizard:
|
|||
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
|
||||
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
|
||||
remoteContentsCleaning: "リモートコンテンツの自動クリーニング"
|
||||
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
|
||||
remoteContentsCleaning_description2: "ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。"
|
||||
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
|
||||
adminInfo: "管理者情報"
|
||||
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
|
||||
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"
|
||||
|
@ -3272,7 +3288,9 @@ _watermarkEditor:
|
|||
opacity: "不透明度"
|
||||
scale: "サイズ"
|
||||
text: "テキスト"
|
||||
qr: "二次元コード"
|
||||
position: "位置"
|
||||
margin: "マージン"
|
||||
type: "タイプ"
|
||||
image: "画像"
|
||||
advanced: "高度"
|
||||
|
@ -3287,6 +3305,7 @@ _watermarkEditor:
|
|||
polkadotSubDotOpacity: "サブドットの不透明度"
|
||||
polkadotSubDotRadius: "サブドットの大きさ"
|
||||
polkadotSubDotDivisions: "サブドットの数"
|
||||
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
|
||||
|
||||
_imageEffector:
|
||||
title: "エフェクト"
|
||||
|
@ -3300,6 +3319,7 @@ _imageEffector:
|
|||
mirror: "ミラー"
|
||||
invert: "色の反転"
|
||||
grayscale: "白黒"
|
||||
blur: "ぼかし"
|
||||
colorAdjust: "色調補正"
|
||||
colorClamp: "色の圧縮"
|
||||
colorClampAdvanced: "色の圧縮(高度)"
|
||||
|
@ -3311,11 +3331,15 @@ _imageEffector:
|
|||
checker: "チェッカー"
|
||||
blockNoise: "ブロックノイズ"
|
||||
tearing: "ティアリング"
|
||||
fill: "塗りつぶし"
|
||||
|
||||
_fxProps:
|
||||
angle: "角度"
|
||||
scale: "サイズ"
|
||||
size: "サイズ"
|
||||
radius: "半径"
|
||||
samples: "サンプル数"
|
||||
offset: "位置"
|
||||
color: "色"
|
||||
opacity: "不透明度"
|
||||
normalize: "正規化"
|
||||
|
@ -3344,6 +3368,7 @@ _imageEffector:
|
|||
zoomLinesThreshold: "集中線の幅"
|
||||
zoomLinesMaskSize: "中心径"
|
||||
zoomLinesBlack: "黒色にする"
|
||||
circle: "円形"
|
||||
|
||||
drafts: "下書き"
|
||||
_drafts:
|
||||
|
@ -3360,3 +3385,20 @@ _drafts:
|
|||
restoreFromDraft: "下書きから復元"
|
||||
restore: "復元"
|
||||
listDrafts: "下書き一覧"
|
||||
|
||||
qr: "二次元コード"
|
||||
_qr:
|
||||
showTabTitle: "表示"
|
||||
readTabTitle: "読み取る"
|
||||
shareTitle: "{name} {acct}"
|
||||
shareText: "Fediverseで私をフォローしてください!"
|
||||
chooseCamera: "カメラを選択"
|
||||
cannotToggleFlash: "ライト選択不可"
|
||||
turnOnFlash: "ライトをオンにする"
|
||||
turnOffFlash: "ライトをオフにする"
|
||||
startQr: "コードリーダーを再開"
|
||||
stopQr: "コードリーダーを停止"
|
||||
noQrCodeFound: "QRコードが見つかりません"
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
|
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
|
||||
remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
|
||||
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
|
||||
remoteContentsCleaning_description2: "로컬 내 원격 콘텐츠로의 하이퍼링크는 깨진 링크로 됩니다."
|
||||
adminInfo: "관리자 정보"
|
||||
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
|
||||
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."
|
||||
|
|
|
@ -1215,6 +1215,7 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
|
|||
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
|
||||
avatarDecorations: "Украшения для аватара"
|
||||
attach: "Прикрепить"
|
||||
detachAll: "Убрать всё"
|
||||
angle: "Угол"
|
||||
flip: "Переворот"
|
||||
showAvatarDecorations: "Показать украшения для аватара"
|
||||
|
@ -1253,7 +1254,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до
|
|||
performance: "Производительность"
|
||||
modified: "Изменено"
|
||||
signinWithPasskey: "Войдите в систему, используя свой пароль"
|
||||
unknownWebAuthnKey: "Не известный ключ "
|
||||
unknownWebAuthnKey: "Неизвестный ключ"
|
||||
passkeyVerificationFailed: "Ошибка проверка ключа доступа "
|
||||
messageToFollower: "Сообщение подписчикам"
|
||||
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. <strong>Не использовать это в рабочей среде</strong>"
|
||||
|
@ -1268,8 +1269,11 @@ availableRoles: "Доступные роли"
|
|||
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
|
||||
draft: "Черновик"
|
||||
markAsSensitiveConfirm: "Отметить контент как чувствительный?"
|
||||
preferences: "Основное"
|
||||
resetToDefaultValue: "Сбросить настройки до стандартных"
|
||||
syncBetweenDevices: "Синхронизировать между устройствами"
|
||||
postForm: "Форма отправки"
|
||||
textCount: "Количество символов"
|
||||
information: "Описание"
|
||||
inMinutes: "мин"
|
||||
inDays: "сут"
|
||||
|
@ -1281,6 +1285,11 @@ _chat:
|
|||
send: "Отправить"
|
||||
_settings:
|
||||
webhook: "Вебхук"
|
||||
preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям"
|
||||
timelineAndNote: "Лента и заметки"
|
||||
_chat:
|
||||
showSenderName: "Показывать имя отправителя"
|
||||
sendOnEnter: "Использовать Enter для отправки"
|
||||
_delivery:
|
||||
stop: "Заморожено"
|
||||
_type:
|
||||
|
@ -1529,7 +1538,7 @@ _achievements:
|
|||
description: "Нажато здесь"
|
||||
_justPlainLucky:
|
||||
title: "Чистая удача"
|
||||
description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
|
||||
description: "Может достаться с вероятностью 0,005% каждые 10 секунд."
|
||||
_setNameToSyuilo:
|
||||
title: "Комплекс бога"
|
||||
description: "Установлено «syuilo» в качестве имени"
|
||||
|
@ -1557,6 +1566,12 @@ _achievements:
|
|||
title: "Brain Diver"
|
||||
description: "Опубликована ссылка на песню «Brain Diver»"
|
||||
flavor: "Мисски-Мисски Ла-Ту-Ма"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "Самый большой объект в Bubble game"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "Двойной🤯"
|
||||
description: "Два самых больших объекта в Bubble game одновременно!"
|
||||
_role:
|
||||
new: "Новая роль"
|
||||
edit: "Изменить роль"
|
||||
|
|
|
@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
|
|||
disconnectedFromServer: "Sunucu bağlantısı kesildi"
|
||||
reload: "Yenile"
|
||||
doNothing: "Yoksay"
|
||||
reloadConfirm: "Zaman çizelgesini yenilemek ister misin?"
|
||||
reloadConfirm: "Panoyu yenilemek ister misin?"
|
||||
watch: "İzle"
|
||||
unwatch: "İzlemeyi bırak"
|
||||
accept: "Kabul et"
|
||||
|
@ -573,9 +573,9 @@ objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yap
|
|||
s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmen gerekebilir."
|
||||
serverLogs: "Sunucu log kayıtları"
|
||||
deleteAll: "Tümünü sil"
|
||||
showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle"
|
||||
showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)"
|
||||
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et"
|
||||
showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
|
||||
showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)"
|
||||
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
|
||||
newNoteRecived: "Yeni Not'lar var"
|
||||
newNote: "Yeni Not"
|
||||
sounds: "Sesler"
|
||||
|
@ -1059,7 +1059,7 @@ achievements: "Başarılar"
|
|||
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
|
||||
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
|
||||
thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
|
||||
thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder"
|
||||
thisPostMayBeAnnoyingHome: "Ana panoya gönder"
|
||||
thisPostMayBeAnnoyingCancel: "İptal"
|
||||
thisPostMayBeAnnoyingIgnore: "Yine de gönder"
|
||||
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster"
|
||||
|
@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları gö
|
|||
hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle"
|
||||
showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster"
|
||||
hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle"
|
||||
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını zaman çizelgende diğer kullanıcılara göstermek istiyor musun?"
|
||||
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını zaman tünelinde cidden göstermeyecek misin?"
|
||||
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?"
|
||||
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?"
|
||||
externalServices: "Dış Hizmetler"
|
||||
sourceCode: "Kaynak kodu"
|
||||
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin."
|
||||
|
@ -1570,9 +1570,9 @@ _initialTutorial:
|
|||
description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
|
||||
_note:
|
||||
title: "Not nedir?"
|
||||
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
|
||||
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
|
||||
reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder."
|
||||
renote: "Bu notu kendi zaman çizelgende paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsin."
|
||||
renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin."
|
||||
reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak."
|
||||
menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin."
|
||||
_reaction:
|
||||
|
@ -1640,7 +1640,7 @@ _serverSettings:
|
|||
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması."
|
||||
fanoutTimelineDescription: "Etkinleştirildiğinde Pano alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşün."
|
||||
fanoutTimelineDbFallback: "Veritabanına geri dön"
|
||||
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar."
|
||||
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek panoların aralığını sınırlar."
|
||||
reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacakt."
|
||||
remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi"
|
||||
remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek."
|
||||
|
@ -1668,6 +1668,7 @@ _serverSettings:
|
|||
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
|
||||
entrancePageStyle: "Giriş sayfası stili"
|
||||
showTimelineForVisitor: "Panoyu göster"
|
||||
showActivitiesForVisitor: "Aktiviteleri göster"
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Her şey halka açıktır."
|
||||
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
|
||||
|
@ -1876,7 +1877,7 @@ _achievements:
|
|||
title: "Öz Referans"
|
||||
description: "Kendi notunuzu alıntı yapın"
|
||||
_htl20npm:
|
||||
title: "Akış Zaman Çizelgesi"
|
||||
title: "Akış Panosu"
|
||||
description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
|
||||
_viewInstanceChart:
|
||||
title: "Analist"
|
||||
|
@ -1965,7 +1966,7 @@ _role:
|
|||
asBadge: "Rozet olarak göster"
|
||||
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
|
||||
isExplorable: "Rolü keşfedilebilir hale getir"
|
||||
descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
|
||||
descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
|
||||
displayOrder: "Pozisyon"
|
||||
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
|
||||
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
|
||||
|
@ -1979,7 +1980,7 @@ _role:
|
|||
high: "Yüksek"
|
||||
_options:
|
||||
gtlAvailable: "Global Pano'yu görüntüleyebilir"
|
||||
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
|
||||
ltlAvailable: "Yerel panoyu görüntüleyebilir"
|
||||
canPublicNote: "Halka açık notlar gönderebilir"
|
||||
mentionMax: "Bir notta maksimum bahsetme sayısı"
|
||||
canInvite: "Sunucu davet kodları oluşturabilir"
|
||||
|
@ -2484,7 +2485,7 @@ _visibility:
|
|||
public: "Halka açık"
|
||||
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
|
||||
home: "Pano"
|
||||
homeDescription: "Yalnızca ana zaman çizelgesine gönder"
|
||||
homeDescription: "Yalnızca ana panoya gönder"
|
||||
followers: "Takipçiler"
|
||||
followersDescription: "Sadece takipçilerine görünür hale getir"
|
||||
specified: "Doğrudan"
|
||||
|
@ -2531,7 +2532,7 @@ _exportOrImport:
|
|||
userLists: "Kullanıcı listeleri"
|
||||
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
|
||||
excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut"
|
||||
withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin"
|
||||
withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin"
|
||||
_charts:
|
||||
federation: "Federasyon"
|
||||
apRequest: "Talepler"
|
||||
|
@ -2925,7 +2926,7 @@ _reversi:
|
|||
freeMatch: "Ücretsiz Eşleştirme"
|
||||
lookingForPlayer: "Rakip aranıyor..."
|
||||
gameCanceled: "Oyun iptal edildi."
|
||||
shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş"
|
||||
shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş"
|
||||
iStartedAGame: "Oyun başladı! #MisskeyReversi"
|
||||
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
|
||||
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
|
||||
|
@ -3153,7 +3154,7 @@ _clientPerformanceIssueTip:
|
|||
_clip:
|
||||
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
|
||||
_userLists:
|
||||
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir."
|
||||
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano olarak görüntülenebilir."
|
||||
watermark: "Filigran"
|
||||
defaultPreset: "Varsayılan Ön Ayar"
|
||||
_watermarkEditor:
|
||||
|
|
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
|
||||
remoteContentsCleaning: "自动清理传入内容"
|
||||
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
|
||||
remoteContentsCleaning_description2: "如超链接之类的某些引用方法无法被系统检测到。"
|
||||
adminInfo: "管理员信息"
|
||||
adminInfo_description: "设置用于接受询问的管理员信息。"
|
||||
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"
|
||||
|
|
|
@ -3120,7 +3120,6 @@ _serverSetupWizard:
|
|||
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
|
||||
remoteContentsCleaning: "自動清理接收的內容"
|
||||
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
|
||||
remoteContentsCleaning_description2: "有些引用方式系統上無法檢測到,例如超連結。"
|
||||
adminInfo: "管理員資訊"
|
||||
adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
|
||||
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"
|
||||
|
|
19
package.json
19
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.8.0-beta.5",
|
||||
"version": "2025.9.1-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"packageManager": "pnpm@10.16.0",
|
||||
"workspaces": [
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend",
|
||||
|
@ -62,21 +62,22 @@
|
|||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.5.6",
|
||||
"tar": "7.4.3",
|
||||
"terser": "5.43.1",
|
||||
"terser": "5.44.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||
"@types/node": "22.17.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "14.5.4",
|
||||
"eslint": "9.34.0",
|
||||
"eslint": "9.35.0",
|
||||
"globals": "16.3.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.15.0",
|
||||
"start-server-and-test": "2.0.13"
|
||||
"pnpm": "10.16.0",
|
||||
"start-server-and-test": "2.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.22.0"
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -39,17 +39,17 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.13.4",
|
||||
"@swc/core-darwin-x64": "1.13.4",
|
||||
"@swc/core-darwin-arm64": "1.13.5",
|
||||
"@swc/core-darwin-x64": "1.13.5",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.13.4",
|
||||
"@swc/core-linux-arm64-gnu": "1.13.4",
|
||||
"@swc/core-linux-arm64-musl": "1.13.4",
|
||||
"@swc/core-linux-x64-gnu": "1.13.4",
|
||||
"@swc/core-linux-x64-musl": "1.13.4",
|
||||
"@swc/core-win32-arm64-msvc": "1.13.4",
|
||||
"@swc/core-win32-ia32-msvc": "1.13.4",
|
||||
"@swc/core-win32-x64-msvc": "1.13.4",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.13.5",
|
||||
"@swc/core-linux-arm64-gnu": "1.13.5",
|
||||
"@swc/core-linux-arm64-musl": "1.13.5",
|
||||
"@swc/core-linux-x64-gnu": "1.13.5",
|
||||
"@swc/core-linux-x64-musl": "1.13.5",
|
||||
"@swc/core-win32-arm64-msvc": "1.13.5",
|
||||
"@swc/core-win32-ia32-msvc": "1.13.5",
|
||||
"@swc/core-win32-x64-msvc": "1.13.5",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
|
@ -69,20 +69,20 @@
|
|||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.873.0",
|
||||
"@aws-sdk/lib-storage": "3.873.0",
|
||||
"@aws-sdk/client-s3": "3.883.0",
|
||||
"@aws-sdk/lib-storage": "3.883.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
"@fastify/cors": "10.1.0",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "10.0.2",
|
||||
"@fastify/multipart": "9.0.3",
|
||||
"@fastify/multipart": "9.2.1",
|
||||
"@fastify/static": "8.2.0",
|
||||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@napi-rs/canvas": "0.1.77",
|
||||
"@napi-rs/canvas": "0.1.79",
|
||||
"@nestjs/common": "11.1.6",
|
||||
"@nestjs/core": "11.1.6",
|
||||
"@nestjs/testing": "11.1.6",
|
||||
|
@ -93,7 +93,7 @@
|
|||
"@sinonjs/fake-timers": "11.3.1",
|
||||
"@smithy/node-http-handler": "2.5.0",
|
||||
"@swc/cli": "0.7.8",
|
||||
"@swc/core": "1.13.4",
|
||||
"@swc/core": "1.13.5",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@types/redis-info": "3.0.3",
|
||||
"accepts": "1.3.8",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.58.1",
|
||||
"bullmq": "5.58.5",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.2",
|
||||
"chalk": "5.6.0",
|
||||
|
@ -114,13 +114,13 @@
|
|||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.5.0",
|
||||
"fastify": "5.6.0",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.6.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.4",
|
||||
"got": "14.4.7",
|
||||
"got": "14.4.8",
|
||||
"happy-dom": "16.8.1",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
|
@ -141,7 +141,7 @@
|
|||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"ms": "3.0.0-canary.202508261828",
|
||||
"nanoid": "5.1.5",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
|
@ -175,7 +175,7 @@
|
|||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.27.7",
|
||||
"systeminformation": "5.27.8",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
|
@ -210,7 +210,7 @@
|
|||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "22.17.2",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/nodemailer": "6.4.19",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
|
@ -222,7 +222,7 @@
|
|||
"@types/ratelimiter": "3.4.6",
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/supertest": "6.0.3",
|
||||
|
@ -231,8 +231,8 @@
|
|||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as fs from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
@ -27,6 +28,7 @@ type Source = {
|
|||
url?: string;
|
||||
port?: number;
|
||||
socket?: string;
|
||||
trustProxy?: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket?: string;
|
||||
disableHsts?: boolean;
|
||||
db: {
|
||||
|
@ -118,6 +120,7 @@ export type Config = {
|
|||
url: string;
|
||||
port: number;
|
||||
socket: string | undefined;
|
||||
trustProxy: FastifyServerOptions['trustProxy'];
|
||||
chmodSocket: string | undefined;
|
||||
disableHsts: boolean | undefined;
|
||||
db: {
|
||||
|
@ -266,6 +269,7 @@ export function loadConfig(): Config {
|
|||
url: url.origin,
|
||||
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
|
||||
socket: config.socket,
|
||||
trustProxy: config.trustProxy,
|
||||
chmodSocket: config.chmodSocket,
|
||||
disableHsts: config.disableHsts,
|
||||
host,
|
||||
|
|
|
@ -29,7 +29,7 @@ export class AiService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> {
|
||||
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
|
||||
try {
|
||||
if (isSupportedCpu === undefined) {
|
||||
isSupportedCpu = await this.computeIsSupportedCpu();
|
||||
|
@ -51,7 +51,7 @@ export class AiService {
|
|||
});
|
||||
}
|
||||
|
||||
const buffer = await fs.promises.readFile(path);
|
||||
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
|
||||
const image = await tf.node.decodeImage(buffer, 3) as any;
|
||||
try {
|
||||
const predictions = await this.model.classify(image);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
|||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { PredictionType } from 'nsfwjs';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
|
||||
export type FileInfo = {
|
||||
size: number;
|
||||
|
@ -204,16 +205,7 @@ export class FileInfoService {
|
|||
return [sensitive, porn];
|
||||
}
|
||||
|
||||
if ([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
].includes(mime)) {
|
||||
const result = await this.aiService.detectSensitive(source);
|
||||
if (result) {
|
||||
[sensitive, porn] = judgePrediction(result);
|
||||
}
|
||||
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
|
||||
if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
|
||||
const [outDir, disposeOutDir] = await createTempDir();
|
||||
try {
|
||||
const command = FFmpeg()
|
||||
|
@ -281,6 +273,23 @@ export class FileInfoService {
|
|||
} finally {
|
||||
disposeOutDir();
|
||||
}
|
||||
} else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) {
|
||||
/*
|
||||
* tfjs-node は限られた画像形式しか受け付けないため、sharp で PNG に変換する
|
||||
* せっかくなので内部処理で使われる最大サイズの299x299に事前にリサイズする
|
||||
*/
|
||||
const png = await (await sharpBmp(source, mime))
|
||||
.resize(299, 299, {
|
||||
withoutEnlargement: false,
|
||||
})
|
||||
.rotate()
|
||||
.flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
|
||||
.png()
|
||||
.toBuffer();
|
||||
const result = await this.aiService.detectSensitive(png);
|
||||
if (result) {
|
||||
[sensitive, porn] = judgePrediction(result);
|
||||
}
|
||||
}
|
||||
|
||||
return [sensitive, porn];
|
||||
|
|
|
@ -756,8 +756,8 @@ export class QueueService {
|
|||
@bindThis
|
||||
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
const job = await queue.getJob(jobId);
|
||||
if (job != null) {
|
||||
if (job.finishedOn != null) {
|
||||
await job.retry();
|
||||
} else {
|
||||
|
@ -769,8 +769,8 @@ export class QueueService {
|
|||
@bindThis
|
||||
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
const job = await queue.getJob(jobId);
|
||||
if (job != null) {
|
||||
await job.remove();
|
||||
}
|
||||
}
|
||||
|
@ -803,8 +803,8 @@ export class QueueService {
|
|||
@bindThis
|
||||
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const job: Bull.Job | null = await queue.getJob(jobId);
|
||||
if (job) {
|
||||
const job = await queue.getJob(jobId);
|
||||
if (job != null) {
|
||||
return this.packJobData(job);
|
||||
} else {
|
||||
throw new Error(`Job not found: ${jobId}`);
|
||||
|
|
|
@ -31,6 +31,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
// misskey-js の rolePolicies と同期すべし
|
||||
export type RolePolicies = {
|
||||
gtlAvailable: boolean;
|
||||
ltlAvailable: boolean;
|
||||
|
@ -100,14 +101,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
canImportAntennas: true,
|
||||
canImportBlocking: true,
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canImportAntennas: false,
|
||||
canImportBlocking: false,
|
||||
canImportFollowing: false,
|
||||
canImportMuting: false,
|
||||
canImportUserLists: false,
|
||||
chatAvailability: 'available',
|
||||
uploadableFileTypes: [
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/json',
|
||||
'image/*',
|
||||
'video/*',
|
||||
|
|
|
@ -117,6 +117,7 @@ export class MetaEntityService {
|
|||
ratio: ad.ratio,
|
||||
imageUrl: ad.imageUrl,
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive ? true : undefined,
|
||||
})),
|
||||
notesPerOneAd: instance.notesPerOneAd,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
|
|
@ -54,10 +54,17 @@ export class MiAd {
|
|||
length: 8192, nullable: false,
|
||||
})
|
||||
public memo: string;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0, nullable: false,
|
||||
})
|
||||
public dayOfWeek: number;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
constructor(data: Partial<MiAd>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MiAccessToken } from './AccessToken.js';
|
|||
import { MiRole } from './Role.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
// misskey-js の notificationTypes と同期すべし
|
||||
export type MiNotification = {
|
||||
type: 'note';
|
||||
id: string;
|
||||
|
|
|
@ -60,5 +60,10 @@ export const packedAdSchema = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
|
|||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: true,
|
||||
trustProxy: this.config.trustProxy ?? true,
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
|
|
@ -176,6 +176,17 @@ export class ApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.all('/clear-browser-cache', (request, reply) => {
|
||||
if (['GET', 'POST'].includes(request.method)) {
|
||||
reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
|
||||
reply.code(204);
|
||||
reply.send();
|
||||
} else {
|
||||
reply.code(405);
|
||||
reply.send();
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure any unknown path under /api returns HTTP 404 Not Found,
|
||||
// because otherwise ClientServerService will return the base client HTML
|
||||
// page with HTTP 200.
|
||||
|
|
|
@ -34,13 +34,22 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'MeDetailed',
|
||||
properties: {
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'MeDetailed',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
|||
startsAt: { type: 'integer' },
|
||||
imageUrl: { type: 'string', minLength: 1 },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
isSensitive: { type: 'boolean' },
|
||||
},
|
||||
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
|
||||
} as const;
|
||||
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: new Date(ps.expiresAt),
|
||||
startsAt: new Date(ps.startsAt),
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
isSensitive: ps.isSensitive,
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
priority: ps.priority,
|
||||
|
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ad.expiresAt.toISOString(),
|
||||
startsAt: ad.startsAt.toISOString(),
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive,
|
||||
url: ad.url,
|
||||
imageUrl: ad.imageUrl,
|
||||
priority: ad.priority,
|
||||
|
|
|
@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ad.expiresAt.toISOString(),
|
||||
startsAt: ad.startsAt.toISOString(),
|
||||
dayOfWeek: ad.dayOfWeek,
|
||||
isSensitive: ad.isSensitive,
|
||||
url: ad.url,
|
||||
imageUrl: ad.imageUrl,
|
||||
memo: ad.memo,
|
||||
|
|
|
@ -39,6 +39,7 @@ export const paramDef = {
|
|||
expiresAt: { type: 'integer' },
|
||||
startsAt: { type: 'integer' },
|
||||
dayOfWeek: { type: 'integer' },
|
||||
isSensitive: { type: 'boolean' },
|
||||
},
|
||||
required: ['id'],
|
||||
} as const;
|
||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
|
||||
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
isSensitive: ps.isSensitive,
|
||||
});
|
||||
|
||||
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||
|
|
|
@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
|
|
@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
qb.orWhere(new Brackets(qb => {
|
||||
qb.where('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -29,10 +29,16 @@ export const meta = {
|
|||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||
},
|
||||
|
||||
signinRequired: {
|
||||
message: 'Signin required.',
|
||||
code: 'SIGNIN_REQUIRED',
|
||||
id: '8e75455b-738c-471d-9f80-62693f33372e',
|
||||
contentRestrictedByUser: {
|
||||
message: 'Content restricted by user. Please sign in to view.',
|
||||
code: 'CONTENT_RESTRICTED_BY_USER',
|
||||
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;
|
||||
|
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
|
||||
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) {
|
||||
throw new ApiError(meta.errors.signinRequired);
|
||||
throw new ApiError(meta.errors.contentRestrictedByServer);
|
||||
}
|
||||
|
||||
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, {
|
||||
|
|
|
@ -242,6 +242,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -223,6 +223,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -22,17 +22,26 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserList',
|
||||
properties: {
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
allOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserList',
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
{
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
likedCount: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
isLiked: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -201,6 +201,8 @@ export class ClientServerService {
|
|||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
const configUrl = new URL(this.config.url);
|
||||
|
||||
fastify.register(fastifyView, {
|
||||
root: _dirname + '/views',
|
||||
engine: {
|
||||
|
@ -239,7 +241,6 @@ export class ClientServerService {
|
|||
done();
|
||||
});
|
||||
} else {
|
||||
const configUrl = new URL(this.config.url);
|
||||
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
|
||||
|
||||
const port = (process.env.VITE_PORT ?? '5173');
|
||||
|
@ -887,6 +888,22 @@ export class ClientServerService {
|
|||
[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
|
||||
|
||||
fastify.get('/flush', async (request, reply) => {
|
||||
let sendHeader = true;
|
||||
|
||||
if (request.headers['origin']) {
|
||||
const originURL = new URL(request.headers['origin']);
|
||||
if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https
|
||||
sendHeader = false;
|
||||
}
|
||||
if (originURL.host !== configUrl.host) {
|
||||
sendHeader = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sendHeader) {
|
||||
reply.header('Clear-Site-Data', '"*"');
|
||||
}
|
||||
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
|
||||
return await reply.view('flush');
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
};
|
||||
window.onunhandledrejection = (e) => {
|
||||
console.error(e);
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
|
||||
};
|
||||
|
||||
let forceError = localStorage.getItem('forceError');
|
||||
|
|
|
@ -6,41 +6,45 @@ html
|
|||
const msg = document.getElementById('msg');
|
||||
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
|
||||
|
||||
message('Start flushing.');
|
||||
if (!document.cookie) {
|
||||
message('Your site data is fully cleared by your browser.');
|
||||
message(successText);
|
||||
} else {
|
||||
message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
|
||||
(async function() {
|
||||
try {
|
||||
localStorage.clear();
|
||||
message('localStorage cleared.');
|
||||
|
||||
(async function() {
|
||||
try {
|
||||
localStorage.clear();
|
||||
message('localStorage cleared.');
|
||||
const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
|
||||
const delidb = indexedDB.deleteDatabase(name);
|
||||
delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
|
||||
delidb.onerror = e => rej(e)
|
||||
}));
|
||||
|
||||
const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
|
||||
const delidb = indexedDB.deleteDatabase(name);
|
||||
delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
|
||||
delidb.onerror = e => rej(e)
|
||||
}));
|
||||
await Promise.all(idbPromises);
|
||||
|
||||
await Promise.all(idbPromises);
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
await navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
})
|
||||
.catch(e => { throw new Error(e) });
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
await navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
})
|
||||
.catch(e => { throw new Error(e) });
|
||||
message(successText);
|
||||
} catch (e) {
|
||||
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
|
||||
message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
|
||||
|
||||
console.error(e);
|
||||
setTimeout(() => {
|
||||
location = '/';
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
message(successText);
|
||||
} catch (e) {
|
||||
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
|
||||
message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
|
||||
|
||||
console.error(e);
|
||||
setTimeout(() => {
|
||||
location = '/';
|
||||
}, 10000)
|
||||
}
|
||||
})();
|
||||
})();
|
||||
}
|
||||
|
||||
function message(text) {
|
||||
msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
|
||||
|
|
|
@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
|
|||
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
|
||||
ADMIN_CACHE.set(host, {
|
||||
id: res.id,
|
||||
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
|
||||
i: res.token,
|
||||
});
|
||||
return res as Misskey.entities.SignupResponse;
|
||||
|
|
|
@ -20,6 +20,6 @@
|
|||
"dependencies": {
|
||||
"estree-walker": "3.0.3",
|
||||
"magic-string": "0.30.17",
|
||||
"vite": "7.0.6"
|
||||
"vite": "7.0.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,71 @@ export default [
|
|||
allowSingleExtends: true,
|
||||
}],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
'id-denylist': ['error', 'window', 'e'],
|
||||
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
// 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'],
|
||||
'vue/attributes-order': ['error', {
|
||||
alphabetical: false,
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
"@discordapp/twemoji": "16.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.2.0",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "3.5.19",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
"astring": "1.9.0",
|
||||
"buraha": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
|
@ -26,16 +26,16 @@
|
|||
"mfm-js": "0.25.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.48.0",
|
||||
"sass": "1.90.0",
|
||||
"shiki": "3.11.0",
|
||||
"rollup": "4.50.1",
|
||||
"sass": "1.92.1",
|
||||
"shiki": "3.12.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.2",
|
||||
"uuid": "11.1.0",
|
||||
"vite": "7.1.3",
|
||||
"vue": "3.5.19"
|
||||
"vite": "7.1.5",
|
||||
"vue": "3.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.3",
|
||||
|
@ -43,14 +43,14 @@
|
|||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.17.2",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vue/runtime-core": "3.5.19",
|
||||
"@vue/runtime-core": "3.5.21",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
|
@ -59,11 +59,11 @@
|
|||
"happy-dom": "18.0.1",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.10.5",
|
||||
"msw": "2.11.1",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.6.2",
|
||||
"start-server-and-test": "2.0.13",
|
||||
"tsx": "4.20.4",
|
||||
"start-server-and-test": "2.1.0",
|
||||
"tsx": "4.20.5",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.0.6",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
|
|
|
@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
|
|||
console.log('Misskey Embed');
|
||||
|
||||
//#region Embedパラメータの取得・パース
|
||||
const params = new URLSearchParams(location.search);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const embedParams = parseEmbedParams(params);
|
||||
if (_DEV_) console.log(embedParams);
|
||||
//#endregion
|
||||
|
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
|
|||
//#endregion
|
||||
|
||||
// サイズの制限
|
||||
document.documentElement.style.maxWidth = '500px';
|
||||
window.document.documentElement.style.maxWidth = '500px';
|
||||
|
||||
// iframeIdの設定
|
||||
function setIframeIdHandler(event: MessageEvent) {
|
||||
|
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
|
|||
const rootEl = ((): HTMLElement => {
|
||||
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) {
|
||||
console.warn('multiple import detected');
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
const root = document.createElement('div');
|
||||
const root = window.document.createElement('div');
|
||||
root.id = MISSKEY_MOUNT_DIV_ID;
|
||||
document.body.appendChild(root);
|
||||
window.document.body.appendChild(root);
|
||||
return root;
|
||||
})();
|
||||
|
||||
|
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
|
|||
//#endregion
|
||||
|
||||
function removeSplash() {
|
||||
const splash = document.getElementById('splash');
|
||||
const splash = window.document.getElementById('splash');
|
||||
if (splash) {
|
||||
splash.style.opacity = '0';
|
||||
splash.style.pointerEvents = 'none';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
|
|||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
|
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
|
|||
);
|
||||
resolve(workers);
|
||||
} else {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
|
|
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
|||
// if no instance data is given, this is for the local instance
|
||||
const instance = props.instance ?? {
|
||||
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');
|
||||
|
|
|
@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
|
|||
|
||||
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);
|
||||
const bgCss = bg.toRgbString();
|
||||
</script>
|
||||
|
|
|
@ -134,7 +134,7 @@ const isBackTop = ref(false);
|
|||
const empty = computed(() => items.value.size === 0);
|
||||
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();
|
||||
|
||||
|
@ -353,7 +353,7 @@ watch(visibility, () => {
|
|||
BACKGROUND_PAUSE_WAIT_SEC * 1000);
|
||||
} else { // 'visible'
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
} else {
|
||||
isPausingUpdate = false;
|
||||
|
@ -447,11 +447,11 @@ onBeforeMount(() => {
|
|||
init().then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
window.setTimeout(toBottom, 800);
|
||||
|
||||
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
|
||||
// more = trueを遅らせる
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
moreFetching.value = false;
|
||||
}, 2000);
|
||||
});
|
||||
|
@ -461,11 +461,11 @@ onBeforeMount(() => {
|
|||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerForSetPause) {
|
||||
clearTimeout(timerForSetPause);
|
||||
window.clearTimeout(timerForSetPause);
|
||||
timerForSetPause = null;
|
||||
}
|
||||
if (preventAppearFetchMoreTimer.value) {
|
||||
clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
window.clearTimeout(preventAppearFetchMoreTimer.value);
|
||||
preventAppearFetchMoreTimer.value = null;
|
||||
}
|
||||
scrollObserver.value?.disconnect();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
const providedContextEl = document.getElementById('misskey_embedCtx');
|
||||
const providedContextEl = window.document.getElementById('misskey_embedCtx');
|
||||
|
||||
export type ServerContext = {
|
||||
clip?: Misskey.entities.Clip;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import * as Misskey from 'misskey-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;
|
||||
|
||||
|
|
|
@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
|
|||
export function applyTheme(theme: Theme, persist = true) {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
|
||||
document.documentElement.classList.add('_themeChanging_');
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('_themeChanging_');
|
||||
window.document.documentElement.classList.remove('_themeChanging_');
|
||||
}, 1000);
|
||||
|
||||
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.dataset.colorScheme = colorScheme;
|
||||
window.document.documentElement.dataset.colorScheme = colorScheme;
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
|
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||
|
||||
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') {
|
||||
tag.setAttribute('content', props['htmlThemeColor']);
|
||||
break;
|
||||
|
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
|||
}
|
||||
|
||||
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参照
|
||||
|
|
|
@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
const page = location.pathname.split('/')[2];
|
||||
const contentId = safeURIDecode(location.pathname.split('/')[3]);
|
||||
const page = window.location.pathname.split('/')[2];
|
||||
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
|
||||
if (_DEV_) console.log(page, contentId);
|
||||
|
||||
const embedParams = inject(DI.embedParams, defaultEmbedParams);
|
||||
|
|
|
@ -64,6 +64,8 @@ function toBase62(n: number): string {
|
|||
}
|
||||
|
||||
export function getConfig(): UserConfig {
|
||||
const localesHash = toBase62(hash(JSON.stringify(locales)));
|
||||
|
||||
return {
|
||||
base: '/embed_vite/',
|
||||
|
||||
|
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
|
|||
// dependencies of i18n.ts
|
||||
'config': ['@@/js/config.js'],
|
||||
},
|
||||
entryFileNames: 'scripts/[hash:8].js',
|
||||
chunkFileNames: 'scripts/[hash:8].js',
|
||||
assetFileNames: 'assets/[hash:8][extname]',
|
||||
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
|
||||
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
|
||||
paths(id) {
|
||||
for (const p of externalPackages) {
|
||||
if (p.match.test(id)) {
|
||||
|
|
|
@ -51,9 +51,71 @@ export default [
|
|||
allowSingleExtends: true,
|
||||
}],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
'id-denylist': ['error', 'window', 'e'],
|
||||
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||
// 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'],
|
||||
'vue/attributes-order': ['error', {
|
||||
alphabetical: false,
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
|
||||
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||
|
||||
export const host = address.host;
|
||||
export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const port = address.port;
|
||||
export const apiUrl = location.origin + '/api';
|
||||
export const wsOrigin = location.origin;
|
||||
export const apiUrl = window.location.origin + '/api';
|
||||
export const wsOrigin = window.location.origin;
|
||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
|
||||
export const langs = _LANGS_;
|
||||
export const version = _VERSION_;
|
||||
|
|
|
@ -54,68 +54,6 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
|
|||
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||
*/
|
||||
|
||||
export const notificationTypes = [
|
||||
'note',
|
||||
'follow',
|
||||
'mention',
|
||||
'reply',
|
||||
'renote',
|
||||
'quote',
|
||||
'reaction',
|
||||
'pollEnded',
|
||||
'receiveFollowRequest',
|
||||
'followRequestAccepted',
|
||||
'roleAssigned',
|
||||
'chatRoomInvitationReceived',
|
||||
'achievementEarned',
|
||||
'exportCompleted',
|
||||
'login',
|
||||
'createToken',
|
||||
'test',
|
||||
'app',
|
||||
] as const;
|
||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||
|
||||
export const ROLE_POLICIES = [
|
||||
'gtlAvailable',
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'mentionLimit',
|
||||
'canInvite',
|
||||
'inviteLimit',
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canManageAvatarDecorations',
|
||||
'canSearchNotes',
|
||||
'canSearchUsers',
|
||||
'canUseTranslator',
|
||||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'maxFileSizeMb',
|
||||
'alwaysMarkNsfw',
|
||||
'canUpdateBioMedia',
|
||||
'pinLimit',
|
||||
'antennaLimit',
|
||||
'wordMuteLimit',
|
||||
'webhookLimit',
|
||||
'clipLimit',
|
||||
'noteEachClipsLimit',
|
||||
'userListLimit',
|
||||
'userEachUserListsLimit',
|
||||
'rateLimitFactor',
|
||||
'avatarDecorationLimit',
|
||||
'canImportAntennas',
|
||||
'canImportBlocking',
|
||||
'canImportFollowing',
|
||||
'canImportMuting',
|
||||
'canImportUserLists',
|
||||
'chatAvailability',
|
||||
'uploadableFileTypes',
|
||||
'noteDraftLimit',
|
||||
'watermarkAvailable',
|
||||
] as const;
|
||||
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
|
||||
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
||||
tada: ['speed=', 'delay='],
|
||||
|
|
|
@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
|
|||
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
|
||||
let prevTopVisible = firstTopVisible;
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (!window.document.body.contains(el)) return;
|
||||
|
||||
const topVisible = isHeadVisible(el, tolerance);
|
||||
if (topVisible !== prevTopVisible) {
|
||||
|
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
|||
|
||||
const containerOrWindow = container ?? window;
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (!window.document.body.contains(el)) return;
|
||||
if (isTailVisible(el, 1, container)) {
|
||||
cb();
|
||||
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
|
||||
export function getBodyScrollHeight() {
|
||||
return Math.max(
|
||||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||||
document.body.clientHeight, document.documentElement.clientHeight,
|
||||
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
|
||||
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
|
||||
window.document.body.clientHeight, window.document.documentElement.clientHeight,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
|
|||
import type { Ref } from 'vue';
|
||||
|
||||
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
|
||||
const visibility = ref(document.visibilityState);
|
||||
const visibility = ref(window.document.visibilityState);
|
||||
|
||||
const onChange = (): void => {
|
||||
visibility.value = document.visibilityState;
|
||||
visibility.value = window.document.visibilityState;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', onChange);
|
||||
window.document.addEventListener('visibilitychange', onChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', onChange);
|
||||
window.document.removeEventListener('visibilitychange', onChange);
|
||||
});
|
||||
|
||||
return visibility;
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.17.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@types/node": "22.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"esbuild": "0.25.9",
|
||||
"eslint-plugin-vue": "10.4.0",
|
||||
"nodemon": "3.1.10",
|
||||
|
@ -35,6 +35,6 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.19"
|
||||
"vue": "3.5.21"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ export function galleryPost(isSensitive = false) {
|
|||
}
|
||||
}
|
||||
|
||||
export function file(isSensitive = false) {
|
||||
export function file(isSensitive = false): entities.DriveFile {
|
||||
return {
|
||||
id: 'somefileid',
|
||||
createdAt: '2016-12-28T22:49:51.000Z',
|
||||
|
@ -207,6 +207,7 @@ export function federationInstance(): entities.FederationInstance {
|
|||
isSuspended: false,
|
||||
suspensionState: 'none',
|
||||
isBlocked: false,
|
||||
isMediaSilenced: false,
|
||||
softwareName: 'misskey',
|
||||
softwareVersion: '2024.5.0',
|
||||
openRegistrations: false,
|
||||
|
@ -311,6 +312,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti
|
|||
alsoKnownAs: null,
|
||||
notify: 'none',
|
||||
memo: null,
|
||||
canChat: true,
|
||||
chatScope: 'everyone',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -378,6 +381,7 @@ export function role(params: {
|
|||
asBadge: params.asBadge ?? true,
|
||||
canEditMembersByModerator: params.canEditMembersByModerator ?? false,
|
||||
usersCount: params.usersCount ?? 10,
|
||||
preserveAssignmentOnMoveAccount: false,
|
||||
condFormula: {
|
||||
id: '',
|
||||
type: 'or',
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.2.0",
|
||||
"@sentry/vue": "10.5.0",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@sentry/vue": "10.10.0",
|
||||
"@syuilo/aiscript": "1.1.0",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "3.5.19",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
|
||||
"analytics": "0.8.19",
|
||||
"astring": "1.9.0",
|
||||
|
@ -41,7 +41,7 @@
|
|||
"chartjs-chart-matrix": "3.0.0",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "13.1.3",
|
||||
"chromatic": "13.1.4",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.1",
|
||||
"date-fns": "4.1.0",
|
||||
|
@ -57,27 +57,30 @@
|
|||
"json5": "2.2.3",
|
||||
"magic-string": "0.30.18",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.15.1",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.48.0",
|
||||
"qr-code-styling": "1.9.2",
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.50.1",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.90.0",
|
||||
"shiki": "3.11.0",
|
||||
"sass": "1.92.1",
|
||||
"shiki": "3.12.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.179.1",
|
||||
"three": "0.180.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.9.2",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.1.3",
|
||||
"vue": "3.5.19",
|
||||
"vite": "7.1.5",
|
||||
"vue": "3.5.21",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
|
@ -85,7 +88,7 @@
|
|||
"@misskey-dev/summaly": "5.2.3",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "9.1.3",
|
||||
"@storybook/addon-links": "9.1.5",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
|
@ -93,31 +96,31 @@
|
|||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "9.1.3",
|
||||
"@storybook/react-vite": "9.1.3",
|
||||
"@storybook/react": "9.1.5",
|
||||
"@storybook/react-vite": "9.1.5",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "9.1.3",
|
||||
"@storybook/vue3-vite": "9.1.3",
|
||||
"@storybook/vue3": "9.1.5",
|
||||
"@storybook/vue3-vite": "9.1.5",
|
||||
"@tabler/icons-webfont": "3.34.1",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/matter-js": "0.20.0",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.17.2",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.40.0",
|
||||
"@typescript-eslint/parser": "8.40.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.42.0",
|
||||
"@typescript-eslint/parser": "8.42.0",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vue/compiler-core": "3.5.19",
|
||||
"@vue/runtime-core": "3.5.19",
|
||||
"@vue/compiler-core": "3.5.21",
|
||||
"@vue/runtime-core": "3.5.21",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.0.0",
|
||||
"cypress": "14.5.4",
|
||||
|
@ -128,17 +131,17 @@
|
|||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.0.3",
|
||||
"msw": "2.10.5",
|
||||
"msw": "2.11.1",
|
||||
"msw-storybook-addon": "2.0.5",
|
||||
"nodemon": "3.1.10",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.0.13",
|
||||
"storybook": "9.1.3",
|
||||
"start-server-and-test": "2.1.0",
|
||||
"storybook": "9.1.5",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.20.4",
|
||||
"tsx": "4.20.5",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "3.2.4",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
|
|
|
@ -251,13 +251,30 @@ export async function openAccountMenu(opts: {
|
|||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
} else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない
|
||||
return {
|
||||
type: 'button' as const,
|
||||
text: username,
|
||||
active: opts.active != null ? opts.active === id : false,
|
||||
action: async () => {
|
||||
// TODO
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
|
||||
initialUsername: username,
|
||||
}, {
|
||||
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
|
||||
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
|
||||
|
||||
if (callback) {
|
||||
fetchAccount(res.i, id).then(account => {
|
||||
callback(account);
|
||||
});
|
||||
} else {
|
||||
switchAccount(host, id);
|
||||
}
|
||||
},
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
|
|||
throw new errors.AiScriptRuntimeError('expected param');
|
||||
}
|
||||
utils.assertObject(param);
|
||||
return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
|
||||
return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => {
|
||||
return utils.jsToVal(res);
|
||||
}, err => {
|
||||
return values.ERROR('request_failed', utils.jsToVal(err));
|
||||
|
|
|
@ -151,7 +151,21 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
//#region Sync dark mode
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
|
||||
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
|
||||
// see: https://github.com/misskey-dev/misskey/issues/16562
|
||||
watch(store.r.darkMode, (darkMode) => {
|
||||
const theme = (() => {
|
||||
if (darkMode) {
|
||||
|
@ -183,18 +197,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
|||
});
|
||||
}
|
||||
|
||||
//#region Sync dark mode
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', isDeviceDarkmode());
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
|
||||
if (prefer.s.syncDeviceDarkMode) {
|
||||
store.set('darkMode', mql.matches);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
if (!isSafeMode) {
|
||||
if (prefer.s.darkTheme && store.s.darkMode) {
|
||||
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
|
||||
|
|
|
@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js';
|
|||
import { Cache } from '@/utility/cache.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
|
||||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
|
||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 }));
|
||||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 }));
|
||||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
|
||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
|
||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 }));
|
||||
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));
|
||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
|
||||
import isChromatic from 'chromatic/isChromatic';
|
||||
import { initShaderProgram } from '@/utility/webgl.js';
|
||||
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
|
||||
|
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
|
|||
focus: 1.0,
|
||||
});
|
||||
|
||||
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
|
||||
const shader = gl.createShader(type);
|
||||
if (shader == null) return null;
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
alert(
|
||||
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
|
||||
);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
||||
|
||||
const shaderProgram = gl.createProgram();
|
||||
if (vertexShader == null || fragmentShader == null) return null;
|
||||
|
||||
gl.attachShader(shaderProgram, vertexShader);
|
||||
gl.attachShader(shaderProgram, fragmentShader);
|
||||
gl.linkProgram(shaderProgram);
|
||||
|
||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||
alert(
|
||||
`failed to init shader: ${gl.getProgramInfoLog(
|
||||
shaderProgram,
|
||||
)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shaderProgram;
|
||||
}
|
||||
|
||||
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -71,7 +31,7 @@ onMounted(() => {
|
|||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
|
||||
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
|
||||
if (maybeGl == null) return;
|
||||
|
||||
const gl = maybeGl;
|
||||
|
@ -82,18 +42,16 @@ onMounted(() => {
|
|||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `
|
||||
attribute vec2 vertex;
|
||||
|
||||
const shaderProgram = initShaderProgram(gl, `#version 300 es
|
||||
in vec2 position;
|
||||
uniform vec2 u_scale;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec2 in_uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertex, 0.0, 1.0);
|
||||
v_pos = vertex / u_scale;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
in_uv = position / u_scale;
|
||||
}
|
||||
`, `
|
||||
`, `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
vec3 mod289(vec3 x) {
|
||||
|
@ -143,6 +101,7 @@ onMounted(() => {
|
|||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
in vec2 in_uv;
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_spread;
|
||||
|
@ -150,8 +109,7 @@ onMounted(() => {
|
|||
uniform float u_warp;
|
||||
uniform float u_focus;
|
||||
uniform float u_itensity;
|
||||
|
||||
varying vec2 v_pos;
|
||||
out vec4 out_color;
|
||||
|
||||
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
|
||||
float SPREAD = 0.7 * u_spread;
|
||||
|
@ -182,13 +140,13 @@ onMounted(() => {
|
|||
|
||||
float ratio = u_resolution.x / u_resolution.y;
|
||||
|
||||
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
|
||||
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
|
||||
|
||||
vec3 color = vec3( 0.0 );
|
||||
|
||||
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
|
||||
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
|
||||
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
|
||||
|
||||
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
|
||||
|
@ -198,10 +156,10 @@ onMounted(() => {
|
|||
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
|
||||
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
|
||||
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
|
||||
|
||||
vec3 inverted = vec3( 1.0 ) - color;
|
||||
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
|
||||
out_color = vec4(color, max(max(color.x, color.y), color.z));
|
||||
}
|
||||
`);
|
||||
if (shaderProgram == null) return;
|
||||
|
@ -223,7 +181,7 @@ onMounted(() => {
|
|||
gl.uniform1f(u_itensity, 0.5);
|
||||
gl.uniform2fv(u_scale, [props.scale, props.scale]);
|
||||
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
|
||||
const vertex = gl.getAttribLocation(shaderProgram, 'position');
|
||||
gl.enableVertexAttribArray(vertex);
|
||||
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
|
|
|
@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="src">
|
||||
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
|
||||
<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 v-if="src === 'list'" v-model="userListId">
|
||||
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
|
||||
<template #label>{{ i18n.ts.userList }}</template>
|
||||
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
|
||||
</MkSelect>
|
||||
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
|
||||
<template #label>{{ i18n.ts.users }}</template>
|
||||
|
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { watch, ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { DeepPartial } from '@/utility/merge.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
@ -64,6 +58,7 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
|
||||
id?: string;
|
||||
|
@ -99,9 +94,35 @@ const emit = defineEmits<{
|
|||
(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 src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
|
||||
const userListId = ref<string | null>(initialAntenna.userListId);
|
||||
const users = ref<string>(initialAntenna.users.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'));
|
||||
|
|
|
@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</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.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<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">
|
||||
|
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
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) {
|
||||
valueForSelect.value = v;
|
||||
|
|
|
@ -167,9 +167,13 @@ async function init() {
|
|||
for (const user of usersRes) {
|
||||
if (users.value.has(user.id)) continue;
|
||||
|
||||
const account = accounts.find(a => a.id === user.id);
|
||||
|
||||
if (!account || account.token == null) continue;
|
||||
|
||||
users.value.set(user.id, {
|
||||
...user,
|
||||
token: accounts.find(a => a.id === user.id)!.token,
|
||||
token: account.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
||||
<span v-if="q != null && typeof q === 'string'" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
||||
<span v-else v-text="emoji.name"></span>
|
||||
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
|
||||
</li>
|
||||
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</li>
|
||||
</ol>
|
||||
<ol v-else-if="type === 'mfmParam' && mfmParams.length > 0" ref="suggests" :class="$style.list">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
|
||||
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="completeMfmParam(param)" @keydown="onKeydown">
|
||||
<span>{{ param }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -194,6 +194,11 @@ const mfmParams = ref<string[]>([]);
|
|||
const select = ref(-1);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
function completeMfmParam(param: string) {
|
||||
if (props.type !== 'mfmParam') throw new Error('Invalid type');
|
||||
complete('mfmParam', props.q.params.toSpliced(-1, 1, param).join(','));
|
||||
}
|
||||
|
||||
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
|
||||
emit('done', { type, value });
|
||||
emit('closed');
|
||||
|
|
|
@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
|
|||
onMounted(async () => {
|
||||
users.value = await misskeyApi('users/show', {
|
||||
userIds: props.userIds,
|
||||
}) as unknown as Misskey.entities.UserLite[];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -25,12 +25,12 @@ defineProps<{
|
|||
showing: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
series?: {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
text: string;
|
||||
}[];
|
||||
}[] | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
@ -38,7 +38,7 @@ export const Default = {
|
|||
};
|
||||
},
|
||||
args: {
|
||||
file: file(),
|
||||
imageFile: file(),
|
||||
aspectRatio: NaN,
|
||||
},
|
||||
parameters: {
|
||||
|
|
|
@ -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' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
<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>
|
||||
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||
<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="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 MkInput from '@/components/MkInput.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';
|
||||
|
||||
type Input = {
|
||||
|
@ -67,17 +60,9 @@ type Input = {
|
|||
maxLength?: number;
|
||||
};
|
||||
|
||||
type SelectItem = {
|
||||
value: any;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type Select = {
|
||||
items: (SelectItem | {
|
||||
sectionTitle: string;
|
||||
items: SelectItem[];
|
||||
})[];
|
||||
default: string | null;
|
||||
items: MkSelectItem[];
|
||||
default: OptionValue | null;
|
||||
};
|
||||
|
||||
type Result = string | number | true | null;
|
||||
|
@ -115,7 +100,6 @@ const emit = defineEmits<{
|
|||
const modal = useTemplateRef('modal');
|
||||
|
||||
const inputValue = ref<string | number | null>(props.input?.default ?? null);
|
||||
const selectedValue = ref(props.select?.default ?? null);
|
||||
|
||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||
if (props.input) {
|
||||
|
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
|
|||
return null;
|
||||
});
|
||||
|
||||
const {
|
||||
def: selectDef,
|
||||
model: selectedValue,
|
||||
} = useMkSelect({
|
||||
items: computed(() => props.select?.items ?? []),
|
||||
initialValue: props.select?.default ?? null,
|
||||
});
|
||||
|
||||
// overload function を使いたいので lint エラーを無視する
|
||||
function done(canceled: true): void;
|
||||
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
|
||||
|
|
|
@ -699,7 +699,7 @@ useGlobalEvent('driveFoldersDeleted', (folders) => {
|
|||
}
|
||||
});
|
||||
|
||||
let connection: Misskey.ChannelConnection<Misskey.Channels['drive']> | null = null;
|
||||
let connection: Misskey.IChannelConnection<Misskey.Channels['drive']> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (store.s.realtimeMode) {
|
||||
|
|
|
@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #suffix>px</template>
|
||||
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="colorMode">
|
||||
<MkSelect v-model="colorMode" :items="colorModeDef">
|
||||
<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>
|
||||
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</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 { i18n } from '@/i18n.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
|
||||
|
||||
|
@ -160,9 +158,20 @@ const embedPreviewUrl = computed(() => {
|
|||
|
||||
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
|
||||
const header = ref(props.params?.header ?? true);
|
||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
|
||||
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
|
||||
|
||||
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 colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
|
||||
const rounded = ref(props.params?.rounded ?? true);
|
||||
const border = ref(props.params?.border ?? true);
|
||||
|
||||
|
|
|
@ -530,6 +530,14 @@ defineExpose({
|
|||
--eachSize: 50px;
|
||||
}
|
||||
|
||||
&.s4 {
|
||||
--eachSize: 55px;
|
||||
}
|
||||
|
||||
&.s5 {
|
||||
--eachSize: 60px;
|
||||
}
|
||||
|
||||
&.w1 {
|
||||
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
|
||||
--columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
|
|
|
@ -39,13 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-text="v.label || k"></span>
|
||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||
</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>
|
||||
<option v-for="option in v.enum" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||
</MkSelect>
|
||||
<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>
|
||||
<option v-for="option in v.options" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
|
||||
</MkRadios>
|
||||
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
|
||||
<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 MkRadios from './MkRadios.vue';
|
||||
import XFile from './MkFormDialog.file.vue';
|
||||
import type { Form } 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 { i18n } from '@/i18n.js';
|
||||
|
||||
|
@ -99,7 +99,11 @@ const dialog = useTemplateRef('dialog');
|
|||
const values = reactive({});
|
||||
|
||||
for (const item in props.form) {
|
||||
values[item] = props.form[item].default ?? null;
|
||||
if ('default' in props.form[item]) {
|
||||
values[item] = props.form[item].default ?? null;
|
||||
} else {
|
||||
values[item] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function ok() {
|
||||
|
@ -115,4 +119,18 @@ function cancel() {
|
|||
});
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||
return def.enum.map((v) => {
|
||||
if (typeof v === 'string') {
|
||||
return { value: v, label: v };
|
||||
} else {
|
||||
return { value: v.value, label: v.label };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getRadioKey(e: RadioFormItem['options'][number]) {
|
||||
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.root">
|
||||
<div :class="$style.container">
|
||||
<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="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||
<div class="_acrylic" :class="$style.editControls">
|
||||
<button class="_button" :class="[$style.previewControlsButton, penMode != null ? $style.active : null]" @click="showPenMenu"><i class="ti ti-pencil"></i></button>
|
||||
</div>
|
||||
<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 = true">After</button>
|
||||
|
@ -212,6 +215,129 @@ watch(enabled, () => {
|
|||
renderer.render();
|
||||
}
|
||||
});
|
||||
|
||||
const penMode = ref<'fill' | 'blur' | null>(null);
|
||||
|
||||
function showPenMenu(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts._imageEffector._fxs.fill,
|
||||
action: () => {
|
||||
penMode.value = 'fill';
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts._imageEffector._fxs.blur,
|
||||
action: () => {
|
||||
penMode.value = 'blur';
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function onImagePointerdown(ev: PointerEvent) {
|
||||
if (canvasEl.value == null || imageBitmap == null || penMode.value == null) 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();
|
||||
if (penMode.value === 'fill') {
|
||||
layers.push({
|
||||
id,
|
||||
fxId: 'fill',
|
||||
params: {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.1,
|
||||
angle: 0,
|
||||
opacity: 1,
|
||||
color: [1, 1, 1],
|
||||
},
|
||||
});
|
||||
} else if (penMode.value === 'blur') {
|
||||
layers.push({
|
||||
id,
|
||||
fxId: 'blur',
|
||||
params: {
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.1,
|
||||
angle: 0,
|
||||
radius: 3,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_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);
|
||||
|
||||
penMode.value = null;
|
||||
}
|
||||
|
||||
canvasEl.value.addEventListener('pointermove', move);
|
||||
canvasEl.value.addEventListener('pointerup', up);
|
||||
canvasEl.value.setPointerCapture(ev.pointerId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
|
@ -251,6 +377,18 @@ watch(enabled, () => {
|
|||
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 {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
@ -283,9 +421,13 @@ watch(enabled, () => {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
/* なんかiOSでレンダリングがおかしい
|
||||
width: stretch;
|
||||
height: stretch;
|
||||
*/
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 40px);
|
||||
margin: 20px;
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-for="v, k in paramDefs" :key="k">
|
||||
<MkSwitch
|
||||
v-if="v.type === 'boolean'"
|
||||
v-model="params[k]">
|
||||
v-model="params[k]"
|
||||
>
|
||||
<template #label>{{ v.label ?? k }}</template>
|
||||
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
||||
</MkSwitch>
|
||||
|
@ -53,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
|
||||
|
||||
defineProps<{
|
||||
paramDefs: ImageEffectorFxParamDefs;
|
||||
|
|
|
@ -43,7 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts">
|
||||
type SupportedTypes = 'text' | 'password' | 'email' | 'url' | 'tel' | 'number' | 'search' | 'date' | 'time' | 'datetime-local' | 'color';
|
||||
type ModelValueType<T extends SupportedTypes> =
|
||||
T extends 'number' ? number :
|
||||
T extends 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' | 'date' | 'time' | 'datetime-local' | 'color' ? string :
|
||||
never;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends SupportedTypes = 'text'">
|
||||
import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
@ -55,8 +63,8 @@ import { Autocomplete } from '@/utility/autocomplete.js';
|
|||
import { genId } from '@/utility/id.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number | null;
|
||||
type?: InputHTMLAttributes['type'];
|
||||
modelValue: ModelValueType<T> | null;
|
||||
type?: T;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -83,11 +91,11 @@ const emit = defineEmits<{
|
|||
(ev: 'change', _ev: KeyboardEvent): void;
|
||||
(ev: 'keydown', _ev: KeyboardEvent): void;
|
||||
(ev: 'enter', _ev: KeyboardEvent): void;
|
||||
(ev: 'update:modelValue', value: string | number): void;
|
||||
(ev: 'update:modelValue', value: ModelValueType<T>): void;
|
||||
}>();
|
||||
|
||||
const { modelValue, type, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const { modelValue } = toRefs(props);
|
||||
const v = ref<ModelValueType<T> | null>(modelValue.value);
|
||||
const id = genId();
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
|
@ -120,8 +128,8 @@ const onKeydown = (ev: KeyboardEvent) => {
|
|||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
if (type.value === 'number') {
|
||||
emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
|
||||
if (props.type === 'number') {
|
||||
emit('update:modelValue', typeof v.value === 'number' ? v.value as ModelValueType<T> : parseFloat(v.value ?? '0') as ModelValueType<T>);
|
||||
} else {
|
||||
emit('update:modelValue', v.value ?? '');
|
||||
}
|
||||
|
@ -167,7 +175,7 @@ useInterval(() => {
|
|||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (autofocus.value) {
|
||||
if (props.autofocus) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>Chart</template>
|
||||
<div :class="$style.chart">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
|
||||
<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>
|
||||
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
|
||||
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
|
||||
</div>
|
||||
<div class="chart _panel">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
|
||||
|
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkFoldableSection class="item">
|
||||
<template #header>Active users heatmap</template>
|
||||
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
|
||||
<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>
|
||||
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
|
||||
<div class="_panel" :class="$style.heatmap">
|
||||
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
|
||||
</div>
|
||||
|
@ -84,11 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, computed, useTemplateRef } from 'vue';
|
||||
import { onMounted, computed, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
import { $i } from '@/i.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -100,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
initChart();
|
||||
|
||||
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
|
||||
|
||||
const chartLimit = 500;
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('active-users');
|
||||
const heatmapSrc = ref<HeatmapSource>('active-users');
|
||||
const {
|
||||
model: chartSpan,
|
||||
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 pubDoughnutEl = useTemplateRef('pubDoughnutEl');
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||
tabindex="0"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote?.reply ?? null" :class="$style.replyTo"/>
|
||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
|
@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote?.renote ?? null" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div v-if="note" :class="$style.root">
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="note.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
|
@ -31,6 +31,7 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
|||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note | null;
|
||||
|
@ -54,9 +55,12 @@ const showContent = ref(false);
|
|||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
position: sticky !important;
|
||||
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||
left: 0;
|
||||
|
||||
&.useSticky {
|
||||
position: sticky !important;
|
||||
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
|
|
|
@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { notificationTypes } from '@@/js/const.js';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
|
|
|
@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkButton>
|
||||
<MkLoading v-else/>
|
||||
</div>
|
||||
<slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
|
||||
<slot :items="getValue(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
|
||||
<div v-if="direction === 'down' || direction === 'both'" v-show="downButtonVisible">
|
||||
<MkButton v-if="!downButtonLoading" :class="$style.more" primary rounded @click="downButtonClick">
|
||||
{{ i18n.ts.loadMore }}
|
||||
|
@ -90,6 +90,10 @@ function onContextmenu(ev: MouseEvent) {
|
|||
}], ev);
|
||||
}
|
||||
|
||||
function getValue(v: IPaginator['items']) {
|
||||
return unref(v) as UnwrapRef<T['items']>;
|
||||
}
|
||||
|
||||
if (props.autoLoad) {
|
||||
onMounted(() => {
|
||||
props.paginator.init();
|
||||
|
@ -134,7 +138,7 @@ function downButtonClick() {
|
|||
|
||||
defineSlots<{
|
||||
empty: () => void;
|
||||
default: (props: { items: UnwrapRef<T['items']> }) => void;
|
||||
default: (props: { items: UnwrapRef<T['items']>, fetching: boolean }) => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div :class="$style.root">
|
||||
<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>
|
||||
</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>
|
||||
|
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: T;
|
||||
|
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
|
|||
const searchOpened = ref(false);
|
||||
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 q = ref<string | null>(null);
|
||||
|
||||
|
|
|
@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, accented ? $style.accented : null]"></div>
|
||||
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
accented?: boolean;
|
||||
revered?: boolean;
|
||||
height?: number;
|
||||
}>(), {
|
||||
accented: false,
|
||||
revered: false,
|
||||
height: 200,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
|
|||
--dot-size: 2px;
|
||||
--gap-size: 40px;
|
||||
--offset: calc(var(--gap-size) / 2);
|
||||
--height: v-bind('props.height + "px"');
|
||||
|
||||
height: 200px;
|
||||
margin-bottom: -200px;
|
||||
|
||||
height: var(--height);
|
||||
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
|
||||
background-position: 0 0, 0 0, var(--offset) var(--offset);
|
||||
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
|
||||
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
|
||||
&.revered {
|
||||
mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
|
||||
import { sum } from '@/utility/array.js';
|
||||
import { pleaseLogin } from '@/utility/please-login.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useLowresTime } from '@/composables/use-lowres-time.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
|
@ -48,7 +48,21 @@ const props = defineProps<{
|
|||
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 closed = computed(() => remaining.value === 0);
|
||||
|
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
|||
url: `https://${host}/notes/${props.noteId}`,
|
||||
}));
|
||||
|
||||
// 期限付きアンケート
|
||||
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) => {
|
||||
const vote = async (id: number) => {
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
pleaseLogin({ openOnRemote: pleaseLoginContext.value });
|
||||
|
|
|
@ -22,11 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
||||
<section>
|
||||
<div>
|
||||
<MkSelect v-model="expiration" small>
|
||||
<MkSelect v-model="expiration" :items="expirationDef" small>
|
||||
<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>
|
||||
<section v-if="expiration === 'at'">
|
||||
<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">
|
||||
<template #label>{{ i18n.ts._poll.duration }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="unit" small>
|
||||
<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>
|
||||
<MkSelect v-model="unit" :items="unitDef" small></MkSelect>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -61,6 +53,7 @@ import MkButton from './MkButton.vue';
|
|||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
import { addTime } from '@/utility/time.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
export type PollEditorModelValue = {
|
||||
expiresAt: number | null;
|
||||
|
@ -78,11 +71,32 @@ const emit = defineEmits<{
|
|||
|
||||
const choices = ref(props.modelValue.choices);
|
||||
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 atTime = ref('00:00');
|
||||
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) {
|
||||
expiration.value = 'at';
|
||||
|
|
|
@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div :class="[$style.root]">
|
||||
<div :class="$style.items">
|
||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
|
||||
<button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-arrow-up-left"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-arrow-up"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-arrow-up-right"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-arrow-left"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-focus-2"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-arrow-right"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-arrow-down-left"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-arrow-down"></i></button>
|
||||
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-arrow-down-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
|
||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
|
@ -218,6 +218,10 @@ const uploader = useUploader({
|
|||
multiple: true,
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uploader.dispose();
|
||||
});
|
||||
|
||||
uploader.events.on('itemUploaded', ctx => {
|
||||
files.value.push(ctx.item.uploaded!);
|
||||
uploader.removeItem(ctx.item);
|
||||
|
@ -567,11 +571,11 @@ async function toggleReactionAcceptance() {
|
|||
const select = await os.select({
|
||||
title: i18n.ts.reactionAcceptance,
|
||||
items: [
|
||||
{ value: null, text: i18n.ts.all },
|
||||
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
|
||||
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
|
||||
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
||||
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
|
||||
{ value: null, label: i18n.ts.all },
|
||||
{ value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
|
||||
{ value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
|
||||
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
|
||||
{ value: 'likeOnly' as const, label: i18n.ts.likeOnly },
|
||||
],
|
||||
default: reactionAcceptance.value,
|
||||
});
|
||||
|
@ -823,17 +827,15 @@ async function saveServerDraft(clearLocal = false) {
|
|||
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
|
||||
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
|
||||
text: text.value,
|
||||
useCw: useCw.value,
|
||||
cw: cw.value,
|
||||
cw: useCw.value ? cw.value || null : null,
|
||||
visibility: visibility.value,
|
||||
localOnly: localOnly.value,
|
||||
hashtag: hashtags.value,
|
||||
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
|
||||
poll: poll.value,
|
||||
...(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,
|
||||
quoteId: quoteId.value,
|
||||
channelId: targetChannel.value ? targetChannel.value.id : undefined,
|
||||
reactionAcceptance: reactionAcceptance.value,
|
||||
}).then(() => {
|
||||
|
@ -1302,6 +1304,7 @@ async function canClose() {
|
|||
|
||||
defineExpose({
|
||||
clear,
|
||||
abortUploader: () => uploader.abortAll(),
|
||||
canClose,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -54,6 +54,7 @@ function onPosted() {
|
|||
async function _close() {
|
||||
const canClose = await form.value?.canClose();
|
||||
if (!canClose) return;
|
||||
form.value?.abortUploader();
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ function subscribe() {
|
|||
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||
return promiseDialog(registration.value.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToBase64(instance.swPublickey),
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
})
|
||||
.then(async subscription => {
|
||||
pushSubscription.value = subscription;
|
||||
|
@ -90,7 +90,7 @@ function subscribe() {
|
|||
publickey: encode(subscription.getKey('p256dh')),
|
||||
});
|
||||
}, async err => { // When subscribe failed
|
||||
// 通知が許可されていなかったとき
|
||||
// 通知が許可されていなかったとき
|
||||
if (err?.name === 'NotAllowedError') {
|
||||
console.info('User denied the notification permission request.');
|
||||
return;
|
||||
|
@ -114,14 +114,13 @@ async function unsubscribe() {
|
|||
|
||||
if ($i && accounts.length >= 2) {
|
||||
apiWithDialog('sw/unregister', {
|
||||
i: $i.token,
|
||||
endpoint,
|
||||
});
|
||||
}, $i.token);
|
||||
} else {
|
||||
pushSubscription.value.unsubscribe();
|
||||
apiWithDialog('sw/unregister', {
|
||||
endpoint,
|
||||
});
|
||||
}, null);
|
||||
pushSubscription.value = null;
|
||||
}
|
||||
}
|
||||
|
@ -131,16 +130,22 @@ function encode(buffer: ArrayBuffer | null) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert the URL safe base64 string to a base64 string
|
||||
* Convert the URL safe base64 string to a Uint8Array
|
||||
* @param base64String base64 string
|
||||
*/
|
||||
function urlBase64ToBase64(base64String: string): string {
|
||||
function urlBase64ToUint8Array(base64String: string): BufferSource {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
return base64;
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker == null) {
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</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-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
|
||||
</template>
|
||||
|
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
role: Misskey.entities.Role;
|
||||
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
|
||||
forModeration: boolean;
|
||||
detailed?: boolean;
|
||||
}>(), {
|
||||
|
|
|
@ -102,12 +102,12 @@ async function addRole() {
|
|||
const items = roles.value
|
||||
.filter(r => r.isPublic)
|
||||
.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 });
|
||||
if (canceled || role == null) return;
|
||||
const { canceled, result: roleId } = await os.select({ items });
|
||||
if (canceled || roleId == null) return;
|
||||
|
||||
selectedRoleIds.value.push(role.id);
|
||||
selectedRoleIds.value.push(roleId);
|
||||
}
|
||||
|
||||
async function removeRole(roleId: string) {
|
||||
|
|
|
@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
type ItemOption = {
|
||||
export type OptionValue = string | number | null;
|
||||
|
||||
export type ItemOption<T extends OptionValue = OptionValue> = {
|
||||
type?: 'option';
|
||||
value: string | number | null;
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ItemGroup = {
|
||||
export type ItemGroup<T extends OptionValue = OptionValue> = {
|
||||
type: 'group';
|
||||
label: string;
|
||||
items: ItemOption[];
|
||||
label?: string;
|
||||
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)[]
|
||||
? U extends { type: 'group'; items: infer V }
|
||||
? V extends (infer W)[]
|
||||
? W extends { value: infer X }
|
||||
? X
|
||||
: never
|
||||
: never
|
||||
: U extends { value: infer Y }
|
||||
? Y
|
||||
: never
|
||||
export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
|
||||
? T['items'][number]['value']
|
||||
: T extends ItemOption
|
||||
? T['value']
|
||||
: never;
|
||||
|
||||
export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
|
||||
? GetMkSelectValueType<T[number]>
|
||||
: never;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends MkSelectItem[]">
|
||||
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
|
||||
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
|
||||
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import type { VNode, VNodeChild } from 'vue';
|
||||
import type { MenuItem } from '@/types/menu.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<{
|
||||
modelValue: ValuesOfItems<T>;
|
||||
items: ITEMS;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -88,16 +83,17 @@ const props = defineProps<{
|
|||
inline?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
items?: T;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
|
||||
}>();
|
||||
type ModelTChecked = MODELT & (
|
||||
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 opening = ref(false);
|
||||
const currentValueText = ref<string | null>(null);
|
||||
|
@ -140,52 +136,26 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
watch([modelValue, () => props.items], () => {
|
||||
if (props.items) {
|
||||
let found: ItemOption | null = null;
|
||||
for (const item of props.items) {
|
||||
if (item.type === 'group') {
|
||||
for (const option of item.items) {
|
||||
if (option.value === modelValue.value) {
|
||||
found = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.value === modelValue.value) {
|
||||
found = item;
|
||||
watch([model, () => props.items], () => {
|
||||
let found: ItemOption | null = null;
|
||||
for (const item of props.items) {
|
||||
if (item.type === 'group') {
|
||||
for (const option of item.items) {
|
||||
if (option.value === model.value) {
|
||||
found = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.value === model.value) {
|
||||
found = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
currentValueText.value = found.label;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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!());
|
||||
if (found) {
|
||||
currentValueText.value = found.label;
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
function show() {
|
||||
|
@ -196,68 +166,32 @@ function show() {
|
|||
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
if (props.items) {
|
||||
for (const item of props.items) {
|
||||
if (item.type === 'group') {
|
||||
for (const item of props.items) {
|
||||
if (item.type === 'group') {
|
||||
if (item.label != null) {
|
||||
menu.push({
|
||||
type: 'label',
|
||||
text: item.label,
|
||||
});
|
||||
for (const option of item.items) {
|
||||
menu.push({
|
||||
text: option.label,
|
||||
active: computed(() => modelValue.value === option.value),
|
||||
action: () => {
|
||||
emit('update:modelValue', option.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
for (const option of item.items) {
|
||||
menu.push({
|
||||
text: item.label,
|
||||
active: computed(() => modelValue.value === item.value),
|
||||
text: option.label,
|
||||
active: computed(() => model.value === option.value),
|
||||
action: () => {
|
||||
emit('update:modelValue', item.value);
|
||||
model.value = option.value as ModelTChecked;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let options = slots.default!();
|
||||
|
||||
const pushOption = (option: VNode) => {
|
||||
} else {
|
||||
menu.push({
|
||||
text: option.children as string,
|
||||
active: computed(() => modelValue.value === option.props?.value),
|
||||
text: item.label,
|
||||
active: computed(() => model.value === item.value),
|
||||
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, {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue