Compare commits

...

39 Commits

Author SHA1 Message Date
tamaina 7ea86f1a45
Merge cb6e2be2a3 into 218070eb13 2025-09-25 20:33:22 +09:00
かっこかり 218070eb13
fix(frontend): ビルド成果物のファイル名にlocalesのhashを含めるように (#16580) 2025-09-24 17:01:48 +09:00
syuilo 0f8c068e84
feat(frontend): Video compression (#16574)
* wip

* Update CHANGELOG.md

* wip

* wip

* wip

* wip

* Update use-uploader.ts

* Update use-uploader.ts
2025-09-24 09:01:06 +09:00
github-actions[bot] 69d66b89f2 Bump version to 2025.9.1-alpha.1 2025-09-22 10:52:25 +00:00
饺子w (Yumechi) 211365de64
enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加 (#16567)
* enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* try harder

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-09-22 19:45:01 +09:00
syuilo 966127c63e enhance(frontend): 絵文字ピッカーのサイズをより大きくできるように 2025-09-22 19:43:31 +09:00
果物リン 54800971eb
プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正 (#16570)
* プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正

* fix lint
2025-09-22 19:21:01 +09:00
syuilo 13d5c6d2b2 refactor MkAnimBg 2025-09-22 19:00:47 +09:00
syuilo 2cff00eedd update pnpm 2025-09-22 18:27:39 +09:00
syuilo 3fc2261041 dev(pnpm): set minimumReleaseAge to 7days
to mitigate supply-chain attack
Resolve #16572
2025-09-22 18:23:43 +09:00
syuilo 18d66c0233 Update CHANGELOG.md 2025-09-20 14:21:51 +09:00
Copilot 2f52c20150
Implement professional-grade Gaussian-approximated blur effect with resolution independence and configurable quality for image effector system (#16571)
* Initial plan

* Implement blur effect for image effector system

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Improve blur quality with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert to simpler blur implementation with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Fix blur size independence from sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Make blur radius resolution-independent

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Update blur.ts

* Enhance blur quality with explicit diagonal sampling and fix parameter configuration

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Implement Gaussian-approximated blur with distance-based weighting for superior quality

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert "Implement Gaussian-approximated blur with distance-based weighting for superior quality"

This reverts commit c739e9f55b.

* Revert "Enhance blur quality with explicit diagonal sampling and fix parameter configuration"

This reverts commit ffbc6eeba7.

* wip

* tweak

* ellipse

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-20 14:19:35 +09:00
github-actions[bot] 9d70c9ad78 Bump version to 2025.9.1-alpha.0 2025-09-19 13:51:21 +00:00
tamaina 42b2aea533
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue)

* added to navbar

* qr.show.vue

* fix

* added to navbar

* fix size

* 🎨

* 🎨

* fix div warn

* fix

* use * 0.25

* fix??

* fix lint

* clean up

* ???

* ?

* fix

* 🎨

* 🎨

* refactor

* 🎨

* 🎨

* :ar:t

* 🎨

* iphone flip

* no lazy import

* 🎨

* 🎨

* 🎨

* ユーザー全部flipでいいや

* ✌️

* fix

* fix

* fix lint

* 🎨

* fix type

* fix: local user profile url cannot be resolved with ap/show

* fix: local user url with hostname could not be resolved

* chore: use common utility for checking self host

* wip

* 🎨

* 🎨

* fix imports

* fix

* fix

* fix

* 🎨

* fix...

* set spacer-w

* ✌️

* 全画面でQRを読むように

* fix

* 🎨

* modify navbar.ts

* start/stop on vue activation

* display raw content read from qr

* 端末のQRをスキャンするボタンを追加

* chore

* やっぱりmfmを先に表示する

* 🎨

* fix 18n

* QRの内容は/users/:userIdにする

* add spdx

* use cqh

* `defineProps` is a compiler macro and no longer needs to be imported.

* use MkUserName

* 🎨

* 🎨

* refactor

* clean up

* refactor

* 🎨

* Update qr.show.vue

* Misskeyロゴにdrop-shadowを追加

* clean up: do not use empty css

* fix os.select usage

* Update qr.vue

* Update qr.show.vue

* Update qr.show.vue

* Update get-user-menu.ts

* ✌️

* Update show.ts

* Update ja-JP.yml

* watermark

* Update CHANGELOG.md

* Update qr.read.vue

* Update qr.read.vue

* wip

* Update MkWatermarkEditorDialog.Layer.vue

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-19 21:02:30 +09:00
syuilo 97adf6f2cc 🎨 2025-09-19 14:23:34 +09:00
tamaina 93ff209c51
enhance(frontend): bootでonunhandledrejectionでrenderErrorする場合、PromiseRejectionEvent.reasonを渡すように (#16563) 2025-09-18 19:35:23 +09:00
syuilo 5fe08d0bbb fix(frontend): iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
Fix #16562
2025-09-18 19:18:31 +09:00
syuilo 8c413d01e6
enhance(frontend): マスクエフェクト (#16556)
* wip

* wip

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update fillSquare.ts

* Update CHANGELOG.md

* Update fillSquare.ts
2025-09-17 18:38:56 +09:00
syuilo b231da7c7c enhance(frontend): チャットの日本語名称をダイレクトメッセージに & ベータを外す 2025-09-16 16:24:10 +09:00
syuilo df3e44f62e enhance(backend): allow upload csv by default
Related #16541
2025-09-16 12:16:18 +09:00
かっこかり e504560477
fix: サーバー設定によりコンテンツの閲覧が制限されている場合のメッセージを区別するように (#16527) 2025-09-16 11:53:20 +09:00
syuilo bcb2073715 enhance(backend): 初期設定をスキップした場合の初期状態ポリシーでインポート系をオプトインに
Resolve #16540
2025-09-16 11:26:35 +09:00
syuilo 6a80c23a50
chore(frontend): enable enableFolderPageView by default 2025-09-15 14:33:32 +09:00
syuilo 2621f468ff enhance: 広告ごとにセンシティブフラグを設定できるように 2025-09-14 15:25:22 +09:00
tamaina cb6e2be2a3
Merge branch 'develop' into sw-file 2025-08-16 00:37:29 +09:00
tamaina 2464b552e2
Merge branch 'develop' into sw-file 2025-08-08 19:53:54 +09:00
tamaina 3a3e90cd12
Merge branch 'develop' into sw-file 2025-08-01 22:25:38 +09:00
tamaina 76de4ebea9 revert useUploader→FileUploader 2025-08-01 21:22:07 +09:00
tamaina 7de7ecb566 Merge branch 'develop' into sw-file 2025-08-01 21:12:37 +09:00
tamaina ba6effb57d
Merge branch 'develop' into sw-file 2025-07-31 18:45:24 +09:00
tamaina 8b36fde37e sw.tsで/sw/shareが叩かれたら必ずshare-url-tempを初期化する 2025-07-26 02:46:06 +09:00
tamaina 6c61ef8861 remove async 2025-07-26 02:32:47 +09:00
tamaina 02d6b258c4 SWが保存したファイルは投稿が完了するまでIndexedDBに保持 2025-07-26 02:29:59 +09:00
tamaina c4137efec1 update share.vue 2025-07-26 02:22:30 +09:00
tamaina f968a9a196 fix file → files 2025-07-26 02:14:57 +09:00
tamaina 135c8ee6a0 fix sw.ts 2025-07-26 02:07:59 +09:00
tamaina a672a757a2 modify manifest 2025-07-26 01:55:32 +09:00
tamaina 2315c09134 modify manifest 2025-07-26 01:48:46 +09:00
tamaina 539356023d ✌️ 2025-07-26 01:43:19 +09:00
69 changed files with 2326 additions and 302 deletions

View File

@ -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 └────────────────────────────────

View File

@ -1,14 +1,23 @@
## Unreleased
## 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

View File

@ -3194,6 +3194,7 @@ _imageEffector:
mirror: "Mirror"
invert: "Invert Colors"
grayscale: "Grayscale"
blur: "Blur"
colorAdjust: "Color Correction"
colorClamp: "Color Compression"
colorClampAdvanced: "Color Compression (Advanced)"
@ -3209,6 +3210,8 @@ _imageEffector:
angle: "Angle"
scale: "Size"
size: "Size"
radius: "Radius"
samples: "Samples"
color: "Color"
opacity: "Opacity"
normalize: "Normalize"

236
locales/index.d.ts vendored
View File

@ -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": {
@ -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;
/**
@ -12219,10 +12277,18 @@ export interface Locale extends ILocale {
*
*/
"text": string;
/**
*
*/
"qr": string;
/**
*
*/
"position": string;
/**
*
*/
"margin": string;
/**
*
*/
@ -12279,6 +12345,10 @@ export interface Locale extends ILocale {
*
*/
"polkadotSubDotDivisions": string;
/**
* URLになります
*/
"leaveBlankToAccountUrl": string;
};
"_imageEffector": {
/**
@ -12318,6 +12388,10 @@ export interface Locale extends ILocale {
*
*/
"grayscale": string;
/**
*
*/
"blur": string;
/**
* 調
*/
@ -12362,6 +12436,10 @@ export interface Locale extends ILocale {
*
*/
"tearing": string;
/**
*
*/
"fill": string;
};
"_fxProps": {
/**
@ -12376,6 +12454,18 @@ export interface Locale extends ILocale {
*
*/
"size": string;
/**
*
*/
"radius": string;
/**
*
*/
"samples": string;
/**
*
*/
"offset": string;
/**
*
*/
@ -12488,6 +12578,10 @@ export interface Locale extends ILocale {
*
*/
"zoomLinesBlack": string;
/**
*
*/
"circle": string;
};
};
/**
@ -12548,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;

View File

@ -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: "自分のフォロワーのみ"
@ -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:
@ -3271,7 +3288,9 @@ _watermarkEditor:
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
qr: "二次元コード"
position: "位置"
margin: "マージン"
type: "タイプ"
image: "画像"
advanced: "高度"
@ -3286,6 +3305,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "サブドットの不透明度"
polkadotSubDotRadius: "サブドットの大きさ"
polkadotSubDotDivisions: "サブドットの数"
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
_imageEffector:
title: "エフェクト"
@ -3299,6 +3319,7 @@ _imageEffector:
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
blur: "ぼかし"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
@ -3310,11 +3331,15 @@ _imageEffector:
checker: "チェッカー"
blockNoise: "ブロックノイズ"
tearing: "ティアリング"
fill: "塗りつぶし"
_fxProps:
angle: "角度"
scale: "サイズ"
size: "サイズ"
radius: "半径"
samples: "サンプル数"
offset: "位置"
color: "色"
opacity: "不透明度"
normalize: "正規化"
@ -3343,6 +3368,7 @@ _imageEffector:
zoomLinesThreshold: "集中線の幅"
zoomLinesMaskSize: "中心径"
zoomLinesBlack: "黒色にする"
circle: "円形"
drafts: "下書き"
_drafts:
@ -3359,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"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.9.0",
"version": "2025.9.1-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.15.1",
"packageManager": "pnpm@10.16.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -76,7 +76,7 @@
"eslint": "9.35.0",
"globals": "16.3.0",
"ncp": "2.0.0",
"pnpm": "10.15.1",
"pnpm": "10.16.0",
"start-server-and-test": "2.1.0"
},
"optionalDependencies": {

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SensitiveAd1757823175259 {
name = 'SensitiveAd1757823175259'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`);
}
}

View File

@ -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,

View File

@ -101,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/*',

View File

@ -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,

View File

@ -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;

View File

@ -60,5 +60,10 @@ export const packedAdSchema = {
optional: false,
nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false,
nullable: false,
},
},
} as const;

View File

@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
type: 'integer',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
},

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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 });

View File

@ -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'],

View File

@ -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, {

View File

@ -158,13 +158,19 @@ export class ClientServerService {
'purpose': 'any',
}],
'share_target': {
'action': '/share/',
'method': 'GET',
'enctype': 'application/x-www-form-urlencoded',
'action': '/sw/share',
'method': 'POST',
'enctype': 'multipart/form-data',
'params': {
'title': 'title',
'text': 'text',
'url': 'url',
'files': [
{
'name': 'files',
'accept': '*/*',
},
],
},
},
'shortcuts': [{

View File

@ -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');

View File

@ -26,13 +26,19 @@
}
],
"share_target": {
"action": "/share/",
"method": "GET",
"enctype": "application/x-www-form-urlencoded",
"action": "/sw/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
"url": "url",
"files": [
{
"name": "files",
"accept": "*/*"
}
]
}
},
"shortcuts": [

View File

@ -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)) {

View File

@ -57,12 +57,15 @@
"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",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.50.1",
"sanitize-html": "2.17.0",
"sass": "1.92.1",

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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,11 +218,19 @@ const uploader = useUploader({
multiple: true,
});
onUnmounted(() => {
uploader.dispose();
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
});
if (props.initialLocalFiles) {
uploader.addFiles(props.initialLocalFiles);
}
const draftKey = computed((): string => {
let key = targetChannel.value ? `channel:${targetChannel.value.id}` : '';
@ -1300,6 +1308,7 @@ async function canClose() {
defineExpose({
clear,
abortUploader: () => uploader.abortAll(),
canClose,
});
</script>

View File

@ -54,6 +54,7 @@ function onPosted() {
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
form.value?.abortUploader();
modal.value?.close();
}

View File

@ -10,7 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
:key="item.id"
v-panel
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
:style="{
'--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%',
'--pp': item.preprocessProgress != null ? `${item.preprocessProgress * 100}%` : '100%',
}"
@contextmenu.prevent.stop="onContextmenu(item, $event)"
>
<div :class="$style.itemInner">
@ -19,11 +22,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
<div :class="$style.itemBody">
<div><i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div>
<i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i>
<MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine>
</div>
<div :class="$style.itemInfo">
<span>{{ item.file.type }}</span>
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
<span v-else>{{ bytes(item.file.size) }}</span>
<span v-if="item.preprocessing">{{ i18n.ts.preprocessing }}<MkLoading inline em style="margin-left: 0.5em;"/></span>
</div>
<div>
</div>
@ -97,7 +104,7 @@ function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
position: absolute;
top: 0;
left: 0;
width: 100%;
width: var(--pp, 100%);
height: 100%;
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
background-size: 25px 25px;

View File

@ -18,6 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
></MkPositionSelector>
</FormSlot>
<MkRange
:modelValue="layer.align.margin ?? 0"
:min="0"
:max="0.25"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'text' }>).align.margin = v"
>
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
</MkRange>
<MkRange
v-model="layer.scale"
:min="0"
@ -66,6 +78,18 @@ SPDX-License-Identifier: AGPL-3.0-only
></MkPositionSelector>
</FormSlot>
<MkRange
:modelValue="layer.align.margin ?? 0"
:min="0"
:max="0.25"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'image' }>).align.margin = v"
>
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
</MkRange>
<MkRange
v-model="layer.scale"
:min="0"
@ -107,6 +131,55 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
<template v-else-if="layer.type === 'qr'">
<MkInput v-model="layer.data" debounce>
<template #label>{{ i18n.ts._watermarkEditor.text }}</template>
<template #caption>{{ i18n.ts._watermarkEditor.leaveBlankToAccountUrl }}</template>
</MkInput>
<FormSlot>
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
<MkPositionSelector
v-model:x="layer.align.x"
v-model:y="layer.align.y"
></MkPositionSelector>
</FormSlot>
<MkRange
:modelValue="layer.align.margin ?? 0"
:min="0"
:max="0.25"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
@update:modelValue="(v) => (layer as Extract<WatermarkPreset['layers'][number], { type: 'qr' }>).align.margin = v"
>
<template #label>{{ i18n.ts._watermarkEditor.margin }}</template>
</MkRange>
<MkRange
v-model="layer.scale"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
</MkRange>
<MkRange
v-model="layer.opacity"
:min="0"
:max="1"
:step="0.01"
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
continuousUpdate
>
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
</MkRange>
</template>
<template v-else-if="layer.type === 'stripe'">
<MkRange
v-model="layer.frequency"

View File

@ -30,22 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
<MkSelect v-model="type" :items="typeDef">
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
</MkSelect>
<div v-if="type === 'text' || type === 'image'">
<XLayer
v-for="(layer, i) in preset.layers"
:key="layer.id"
v-model:layer="preset.layers[i]"
></XLayer>
</div>
<div v-else-if="type === 'advanced'" class="_gaps_s">
<div class="_gaps_s">
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
<template #label>
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
<div v-if="layer.type === 'qr'">{{ i18n.ts._watermarkEditor.qr }}</div>
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
@ -95,7 +85,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
id: genId(),
type: 'text',
text: `(c) @${$i.username}`,
align: { x: 'right', y: 'bottom' },
align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
opacity: 0.75,
@ -109,7 +99,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
type: 'image',
imageId: null,
imageUrl: null,
align: { x: 'right', y: 'bottom' },
align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
angle: 0,
opacity: 0.75,
@ -118,6 +108,17 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
};
}
function createQrLayer(): WatermarkPreset['layers'][number] {
return {
id: genId(),
type: 'qr',
data: '',
align: { x: 'right', y: 'bottom', margin: 0 },
scale: 0.3,
opacity: 1,
};
}
function createStripeLayer(): WatermarkPreset['layers'][number] {
return {
id: genId(),
@ -165,7 +166,7 @@ const props = defineProps<{
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
id: genId(),
name: '',
layers: [createTextLayer()],
layers: [],
});
const emit = defineEmits<{
@ -187,28 +188,6 @@ async function cancel() {
dialog.value?.close();
}
const {
model: type,
def: typeDef,
} = useMkSelect({
items: [
{ label: i18n.ts._watermarkEditor.text, value: 'text' },
{ label: i18n.ts._watermarkEditor.image, value: 'image' },
{ label: i18n.ts._watermarkEditor.advanced, value: 'advanced' },
],
initialValue: preset.layers.length > 1 ? 'advanced' : preset.layers[0].type,
});
watch(type, () => {
if (type.value === 'text') {
preset.layers = [createTextLayer()];
} else if (type.value === 'image') {
preset.layers = [createImageLayer()];
} else if (type.value === 'advanced') {
// nop
}
});
watch(preset, async (newValue, oldValue) => {
if (renderer != null) {
renderer.setLayers(preset.layers);
@ -338,6 +317,11 @@ function addLayer(ev: MouseEvent) {
action: () => {
preset.layers.push(createImageLayer());
},
}, {
text: i18n.ts._watermarkEditor.qr,
action: () => {
preset.layers.push(createQrLayer());
},
}, {
text: i18n.ts._watermarkEditor.stripe,
action: () => {

View File

@ -75,6 +75,7 @@ const common = {
place: '',
imageUrl: '',
dayOfWeek: 7,
isSensitive: false,
},
},
parameters: {

View File

@ -43,6 +43,12 @@ const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/webp',
];
const VIDEO_COMPRESSION_SUPPORTED_TYPES = [ // TODO
'video/mp4',
'video/quicktime',
'video/x-matroska',
];
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const IMAGE_PREPROCESS_NEEDED_TYPES = [
@ -51,6 +57,10 @@ const IMAGE_PREPROCESS_NEEDED_TYPES = [
...IMAGE_EDITING_SUPPORTED_TYPES,
];
const VIDEO_PREPROCESS_NEEDED_TYPES = [
...VIDEO_COMPRESSION_SUPPORTED_TYPES,
];
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
@ -64,6 +74,7 @@ export type UploaderItem = {
progress: { max: number; value: number } | null;
thumbnail: string | null;
preprocessing: boolean;
preprocessProgress: number | null;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
@ -76,6 +87,7 @@ export type UploaderItem = {
isSensitive?: boolean;
caption?: string | null;
abort?: (() => void) | null;
abortPreprocess?: (() => void) | null;
};
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
@ -129,11 +141,12 @@ export function useUploader(options: {
progress: null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
preprocessing: false,
preprocessProgress: null,
uploading: false,
aborted: false,
uploaded: null,
uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel,
compressionLevel: IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultImageCompressionLevel : VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultVideoCompressionLevel : 0,
watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file),
});
@ -318,7 +331,7 @@ export function useUploader(options: {
}
if (
IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
(IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) || VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type)) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
@ -391,6 +404,19 @@ export function useUploader(options: {
removeItem(item);
},
});
} else if (item.preprocessing && item.abortPreprocess != null) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-player-stop',
text: i18n.ts.abort,
danger: true,
action: () => {
if (item.abortPreprocess != null) {
item.abortPreprocess();
}
},
});
} else if (item.uploading) {
menu.push({
type: 'divider',
@ -474,6 +500,10 @@ export function useUploader(options: {
continue;
}
if (item.abortPreprocess != null) {
item.abortPreprocess();
}
if (item.abort != null) {
item.abort();
}
@ -484,18 +514,30 @@ export function useUploader(options: {
async function preprocess(item: UploaderItem): Promise<void> {
item.preprocessing = true;
item.preprocessProgress = null;
try {
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
try {
await preprocessForImage(item);
}
} catch (err) {
console.error('Failed to preprocess image', err);
// nop
}
}
if (VIDEO_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
try {
await preprocessForVideo(item);
} catch (err) {
console.error('Failed to preprocess video', err);
// nop
}
}
item.preprocessing = false;
item.preprocessProgress = null;
}
async function preprocessForImage(item: UploaderItem): Promise<void> {
@ -564,10 +606,74 @@ export function useUploader(options: {
item.preprocessedFile = markRaw(preprocessedFile);
}
onUnmounted(() => {
async function preprocessForVideo(item: UploaderItem): Promise<void> {
let preprocessedFile: Blob | File = item.file;
const needsCompress = item.compressionLevel !== 0 && VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type);
if (needsCompress) {
const mediabunny = await import('mediabunny');
const source = new mediabunny.BlobSource(preprocessedFile);
const input = new mediabunny.Input({
source,
formats: mediabunny.ALL_FORMATS,
});
const output = new mediabunny.Output({
target: new mediabunny.BufferTarget(),
format: new mediabunny.Mp4OutputFormat(),
});
const currentConversion = await mediabunny.Conversion.init({
input,
output,
video: {
//width: 320, // Height will be deduced automatically to retain aspect ratio
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
},
audio: {
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
},
});
currentConversion.onProgress = newProgress => item.preprocessProgress = newProgress;
item.abortPreprocess = () => {
item.abortPreprocess = null;
currentConversion.cancel();
item.preprocessing = false;
item.preprocessProgress = null;
};
await currentConversion.execute();
item.abortPreprocess = null;
preprocessedFile = new Blob([output.target.buffer!], { type: output.format.mimeType });
item.compressedSize = output.target.buffer!.byteLength;
item.uploadName = `${item.name}.mp4`;
} else {
item.compressedSize = null;
item.uploadName = item.name;
}
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
item.preprocessedFile = markRaw(preprocessedFile);
}
function dispose() {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
abortAll();
}
onUnmounted(() => {
dispose();
});
return {
@ -575,6 +681,7 @@ export function useUploader(options: {
addFiles,
removeItem,
abortAll,
dispose,
upload,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),

View File

@ -66,6 +66,12 @@ export const navbarItemDef = reactive({
lookup();
},
},
qr: {
title: i18n.ts.qr,
icon: 'ti ti-qrcode',
show: computed(() => $i != null),
to: '/qr',
},
lists: {
title: i18n.ts.lists,
icon: 'ti ti-list',
@ -111,7 +117,7 @@ export const navbarItemDef = reactive({
to: '/channels',
},
chat: {
title: i18n.ts.chat,
title: i18n.ts.directMessage_short,
icon: 'ti ti-messages',
to: '/chat',
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),

View File

@ -9,21 +9,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="filterType" :items="filterTypeDef" :class="$style.input" @update:modelValue="filterItems">
<template #label>{{ i18n.ts.state }}</template>
</MkSelect>
<div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
<MkInput v-model="ad.url" type="url">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" type="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkRadios v-model="ad.place">
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</MkRadios>
<!--
<div style="margin: 32px 0;">
{{ i18n.ts.priority }}
@ -32,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
@ -43,6 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkSwitch v-model="ad.isSensitive">
<template #label>{{ i18n.ts.sensitive }}</template>
</MkSwitch>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
<span>
@ -56,9 +67,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</span>
</MkFolder>
<MkTextarea v-model="ad.memo">
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="_buttons">
<MkButton inline primary style="margin-right: 12px;" @click="save(ad)">
<i
@ -70,6 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
</div>
</div>
<MkButton @click="more()">
<i class="ti ti-reload"></i>{{ i18n.ts.more }}
</MkButton>
@ -88,6 +102,7 @@ import MkRadios from '@/components/MkRadios.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
@ -158,6 +173,7 @@ function add() {
expiresAt: new Date().toISOString(),
startsAt: new Date().toISOString(),
dayOfWeek: 0,
isSensitive: false,
});
}

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
<MkPolkadots v-if="tab === 'home'" accented/>
<MkPolkadots v-if="tab === 'home'" accented :height="200" style="margin-bottom: -200px;"/>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<XHome v-if="tab === 'home'"/>
<XInvitations v-else-if="tab === 'invitations'"/>
@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
}]);
definePage(() => ({
title: i18n.ts.chat + ' (beta)',
title: i18n.ts.directMessage,
icon: 'ti ti-messages',
}));
</script>

View File

@ -46,6 +46,6 @@ onMounted(() => {
});
definePage({
title: i18n.ts.chat,
title: i18n.ts.directMessage,
});
</script>

View File

@ -421,7 +421,7 @@ const tab = ref('chat');
const headerTabs = computed(() => room.value ? [{
key: 'chat',
title: i18n.ts.chat,
title: i18n.ts._chat.messages,
icon: 'ti ti-messages',
}, {
key: 'members',
@ -437,7 +437,7 @@ const headerTabs = computed(() => room.value ? [{
icon: 'ti ti-info-circle',
}] : [{
key: 'chat',
title: i18n.ts.chat,
title: i18n.ts._chat.messages,
icon: 'ti ti-messages',
}, {
key: 'search',
@ -466,12 +466,12 @@ definePage(computed(() => {
};
} else {
return {
title: i18n.ts.chat,
title: i18n.ts.directMessage,
};
}
} else {
return {
title: i18n.ts.chat,
title: i18n.ts.directMessage,
};
}
}));

View File

@ -136,10 +136,10 @@ function fetchNote() {
});
}
}).catch(err => {
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
if (['fbcc002d-37d9-4944-a6b0-d9e29f2d33ab', '145f88d2-b03d-4087-8143-a78928883c4b'].includes(err.id)) {
pleaseLogin({
path: '/',
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
message: err.id === 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab' ? i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor : i18n.ts.signinOrContinueOnRemote,
openOnRemote: {
type: 'lookup',
url: `https://${host}/notes/${props.noteId}`,

View File

@ -0,0 +1,54 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder defaultOpen :withSpacer="false">
<template #label>{{ data.split('\n')[0] }}</template>
<template #header>
<MkTabs
v-model:tab="tab"
:tabs="[
{
key: 'mfm',
title: i18n.ts._qr.mfm,
icon: 'ti ti-align-left',
},
{
key: 'raw',
title: i18n.ts._qr.raw,
icon: 'ti ti-code',
},
]"
/>
</template>
<div v-show="tab === 'mfm'" class="_spacer _gaps">
<Mfm :text="data" :nyaize="false"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false"/>
</div>
<div v-show="tab === 'raw'" class="_spacer" style="--MI_SPACER-min: 10px; --MI_SPACER-max: 16px;">
<MkCode :code="data" lang="text"/>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as mfm from 'mfm-js';
import MkFolder from '@/components/MkFolder.vue';
import MkTabs from '@/components/MkTabs.vue';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm';
import MkCode from '@/components/MkCode.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
data: string;
}>();
const parsed = computed(() => mfm.parse(props.data));
const urls = computed(() => extractUrlFromMfm(parsed.value));
const tab = ref<'mfm' | 'raw'>('mfm');
</script>

View File

@ -0,0 +1,397 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
ref="rootEl"
:class="$style.root"
:style="{
'--MI-QrReadViewHeight': 'calc(100cqh - var(--MI-stickyTop, 0px) - var(--MI-stickyBottom, 0px))',
'--MI-QrReadVideoHeight': 'min(calc(var(--MI-QrReadViewHeight) * 0.3), 512px)',
}"
>
<MkStickyContainer>
<template #header>
<div :class="$style.view">
<video ref="videoEl" :class="$style.video" autoplay muted playsinline></video>
<div ref="overlayEl" :class="$style.overlay"></div>
<div :class="$style.controls">
<MkButton v-tooltip="i18n.ts._qr.scanFile" iconOnly @click="upload"><i class="ti ti-photo-plus"></i></MkButton>
<MkButton v-if="qrStarted" v-tooltip="i18n.ts._qr.stopQr" iconOnly @click="stopQr"><i class="ti ti-player-play"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts._qr.startQr" iconOnly danger @click="startQr"><i class="ti ti-player-pause"></i></MkButton>
<MkButton v-tooltip="i18n.ts._qr.chooseCamera" iconOnly @click="chooseCamera"><i class="ti ti-camera-rotate"></i></MkButton>
<MkButton v-if="!flashCanToggle" v-tooltip="i18n.ts._qr.cannotToggleFlash" iconOnly disabled><i class="ti ti-bolt"></i></MkButton>
<MkButton v-else-if="!flash" v-tooltip="i18n.ts._qr.turnOnFlash" iconOnly @click="toggleFlash(true)"><i class="ti ti-bolt-off"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts._qr.turnOffFlash" iconOnly @click="toggleFlash(false)"><i class="ti ti-bolt-filled"></i></MkButton>
</div>
</div>
</template>
<div
:class="['_spacer', $style.contents]"
:style="{
'--MI_SPACER-w': '800px'
}"
>
<MkStickyContainer>
<template #header>
<MkTab v-model="tab" :class="$style.tab">
<option value="users">{{ i18n.ts.users }}</option>
<option value="notes">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
</MkTab>
</template>
<div v-if="tab === 'users'" :class="[$style.users, '_margin']" style="padding-bottom: var(--MI-margin);">
<MkUserInfo v-for="user in users" :key="user.id" :user="user"/>
</div>
<div v-else-if="tab === 'notes'" class="_margin _gaps" style="padding-bottom: var(--MI-margin);">
<MkNote v-for="note in notes" :key="note.id" :note="note" :class="$style.note"/>
</div>
<div v-else-if="tab === 'all'" class="_margin _gaps" style="padding-bottom: var(--MI-margin);">
<MkQrReadRawViewer v-for="result in Array.from(results).reverse()" :key="result" :data="result"/>
</div>
</MkStickyContainer>
</div>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import QrScanner from 'qr-scanner';
import { onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getScrollContainer } from '@@/js/scroll.js';
import type { ApShowResponse } from 'misskey-js/entities.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import MkUserInfo from '@/components/MkUserInfo.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkNote from '@/components/MkNote.vue';
import MkTab from '@/components/MkTab.vue';
import MkButton from '@/components/MkButton.vue';
import MkQrReadRawViewer from '@/pages/qr.read.raw-viewer.vue';
const LIST_RERENDER_INTERVAL = 1500;
const rootEl = useTemplateRef('rootEl');
const videoEl = useTemplateRef('videoEl');
const overlayEl = useTemplateRef('overlayEl');
const scannerInstance = shallowRef<QrScanner | null>(null);
const tab = ref<'users' | 'notes' | 'all'>('users');
// higher is recent
const results = ref(new Set<string>());
// lower is recent
const uris = ref<string[]>([]);
const sources = new Map<string, ApShowResponse | null>();
const users = ref<(misskey.entities.UserDetailed)[]>([]);
const usersCount = ref(0);
const notes = ref<misskey.entities.Note[]>([]);
const notesCount = ref(0);
const timer = ref<number | null>(null);
function updateLists() {
const responses = uris.value.map(uri => sources.get(uri)).filter((r): r is ApShowResponse => !!r);
users.value = responses.filter(r => r.type === 'User').map(r => r.object).filter((u): u is misskey.entities.UserDetailed => !!u);
usersCount.value = users.value.length;
notes.value = responses.filter(r => r.type === 'Note').map(r => r.object).filter((n): n is misskey.entities.Note => !!n);
notesCount.value = notes.value.length;
updateRequired.value = false;
}
const updateRequired = ref(false);
watch(uris, () => {
if (timer.value) {
updateRequired.value = true;
return;
}
updateLists();
timer.value = window.setTimeout(() => {
timer.value = null;
if (updateRequired.value) {
updateLists();
}
}, LIST_RERENDER_INTERVAL) as number;
});
watch(tab, () => {
if (timer.value) {
window.clearTimeout(timer.value);
timer.value = null;
}
updateLists();
});
async function processResult(result: QrScanner.ScanResult) {
if (!result) return;
const trimmed = result.data.trim();
if (!trimmed) return;
const haveExisted = results.value.has(trimmed);
results.value.add(trimmed);
try {
new URL(trimmed);
} catch {
if (!haveExisted) {
tab.value = 'all';
}
return;
}
if (uris.value[0] !== trimmed) {
//
uris.value = [trimmed, ...uris.value.slice(0, 29).filter(u => u !== trimmed)];
}
if (sources.has(trimmed)) return;
// Start fetching user info
sources.set(trimmed, null);
await misskeyApi('ap/show', { uri: trimmed })
.then(data => {
if (data.type === 'User') {
sources.set(trimmed, data);
tab.value = 'users';
} else if (data.type === 'Note') {
sources.set(trimmed, data);
tab.value = 'notes';
}
updateLists();
})
.catch(err => {
tab.value = 'all';
throw err;
});
}
const qrStarted = ref(true);
const flashCanToggle = ref(false);
const flash = ref(false);
async function upload() {
os.chooseFileFromPc({ multiple: true }).then(files => {
if (files.length === 0) return;
for (const file of files) {
QrScanner.scanImage(file, { returnDetailedScanResult: true })
.then(result => {
processResult(result);
})
.catch(err => {
if (err.toString().includes('No QR code found')) {
os.alert({
type: 'info',
text: i18n.ts._qr.noQrCodeFound,
});
} else {
os.alert({
type: 'error',
text: err.toString(),
});
console.error(err);
}
});
}
});
}
async function chooseCamera() {
if (!scannerInstance.value) return;
const cameras = await QrScanner.listCameras(true);
if (cameras.length === 0) {
os.alert({
type: 'error',
});
return;
}
const select = await os.select({
title: i18n.ts._qr.chooseCamera,
items: cameras.map(camera => ({
label: camera.label,
value: camera.id,
})),
});
if (select.canceled) return;
if (select.result == null) return;
await scannerInstance.value.setCamera(select.result);
flashCanToggle.value = await scannerInstance.value.hasFlash();
flash.value = scannerInstance.value.isFlashOn();
}
async function toggleFlash(to = false) {
if (!scannerInstance.value) return;
flash.value = to;
if (flash.value) {
await scannerInstance.value.turnFlashOn();
} else {
await scannerInstance.value.turnFlashOff();
}
}
async function startQr() {
if (!scannerInstance.value) return;
await scannerInstance.value.start();
qrStarted.value = true;
}
function stopQr() {
if (!scannerInstance.value) return;
scannerInstance.value.stop();
qrStarted.value = false;
}
onActivated(() => {
startQr;
});
onDeactivated(() => {
stopQr;
});
const alertLock = ref(false);
onMounted(() => {
if (!videoEl.value || !overlayEl.value) {
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
return;
}
scannerInstance.value = new QrScanner(
videoEl.value,
processResult,
{
highlightScanRegion: true,
highlightCodeOutline: true,
overlay: overlayEl.value,
calculateScanRegion(video: HTMLVideoElement): QrScanner.ScanRegion {
const aspectRatio = video.videoWidth / video.videoHeight;
const SHORT_SIDE_SIZE_DOWNSCALED = 360;
return {
x: 0,
y: 0,
width: video.videoWidth,
height: video.videoHeight,
downScaledWidth: aspectRatio > 1 ? Math.round(SHORT_SIDE_SIZE_DOWNSCALED * aspectRatio) : SHORT_SIDE_SIZE_DOWNSCALED,
downScaledHeight: aspectRatio > 1 ? SHORT_SIDE_SIZE_DOWNSCALED : Math.round(SHORT_SIDE_SIZE_DOWNSCALED / aspectRatio),
};
},
onDecodeError(err) {
if (err.toString().includes('No QR code found')) return;
if (alertLock.value) return;
alertLock.value = true;
os.alert({
type: 'error',
text: err.toString(),
}).finally(() => {
alertLock.value = false;
});
},
},
);
scannerInstance.value.start()
.then(async () => {
qrStarted.value = true;
if (!scannerInstance.value) return;
flashCanToggle.value = await scannerInstance.value.hasFlash();
flash.value = scannerInstance.value.isFlashOn();
})
.catch(err => {
qrStarted.value = false;
os.alert({
type: 'error',
text: err.toString(),
});
console.error(err);
});
});
onUnmounted(() => {
if (timer.value) {
window.clearTimeout(timer.value);
timer.value = null;
}
scannerInstance.value?.destroy();
});
</script>
<style lang="scss" module>
.root {
position: relative;
}
.view {
position: sticky;
top: var(--MI-stickyTop, 0);
z-index: 1;
background: var(--MI_THEME-bg);
background-size: 16px 16px;
width: 100%;
height: var(--MI-QrReadVideoHeight);
}
.video {
width: 100%;
height: 100%;
object-fit: contain;
}
.controls {
width: 100%;
position: absolute;
right: 10px;
bottom: 10px;
display: flex;
justify-content: end;
align-items: center;
gap: 10px;
}
html[data-color-scheme=dark] .view {
--c: rgb(255 255 255 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
html[data-color-scheme=light] .view {
--c: rgb(0 0 0 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
.contents {
padding-top: calc(var(--MI-margin) / 2);
}
.tab {
padding: calc(var(--MI-margin) / 2) 0;
background: var(--MI_THEME-bg);
}
.users {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: var(--MI-margin);
}
.note {
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
</style>

View File

@ -0,0 +1,234 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="[$style.content]">
<div
ref="qrCodeEl" v-flip :style="{
'cursor': canShare ? 'pointer' : 'default',
}"
:class="$style.qr" @click="share"
></div>
<div v-flip :class="$style.user">
<MkAvatar :class="$style.avatar" :user="$i" :indicator="false"/>
<div>
<div :class="$style.name"><MkCondensedLine :minScale="2 / 3"><MkUserName :user="$i" :nowrap="true"/></MkCondensedLine></div>
<div><MkCondensedLine :minScale="2 / 3">{{ acct }}</MkCondensedLine></div>
</div>
</div>
<img v-if="deviceMotionPermissionNeeded" v-flip :class="$style.logo" :src="misskeysvg" alt="Misskey Logo" @click="requestDeviceMotion"/>
<img v-else v-flip :class="$style.logo" :src="misskeysvg" alt="Misskey Logo"/>
</div>
</div>
</template>
<script lang="ts" setup>
import tinycolor from 'tinycolor2';
import QRCodeStyling from 'qr-code-styling';
import { computed, ref, shallowRef, watch, onMounted, onUnmounted, useTemplateRef } from 'vue';
import { url, host } from '@@/js/config.js';
import type { Directive } from 'vue';
import { instance } from '@/instance.js';
import { ensureSignin } from '@/i.js';
import { userPage, userName } from '@/filters/user.js';
import misskeysvg from '/client-assets/misskey.svg';
import { getStaticImageUrl } from '@/utility/media-proxy.js';
import { i18n } from '@/i18n.js';
const $i = ensureSignin();
const acct = computed(() => `@${$i.username}@${host}`);
const userProfileUrl = computed(() => userPage($i, undefined, true));
const shareData = computed(() => ({
title: i18n.tsx._qr.shareTitle({ name: userName($i), acct: acct.value }),
text: i18n.ts._qr.shareText,
url: userProfileUrl.value,
}));
const canShare = computed(() => navigator.canShare && navigator.canShare(shareData.value));
const qrCodeEl = useTemplateRef('qrCodeEl');
const qrColor = computed(() => tinycolor(instance.themeColor ?? '#86b300'));
const qrHsl = computed(() => qrColor.value.toHsl());
function share() {
if (!canShare.value) return;
return navigator.share(shareData.value);
}
const qrCodeInstance = new QRCodeStyling({
width: 600,
height: 600,
margin: 42,
type: 'canvas',
data: `${url}/users/${$i.id}`,
image: instance.iconUrl ? getStaticImageUrl(instance.iconUrl) : '/favicon.ico',
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'H',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.3,
margin: 16,
crossOrigin: 'anonymous',
},
dotsOptions: {
type: 'dots',
color: tinycolor(`hsl(${qrHsl.value.h}, 100, 18)`).toRgbString(),
},
cornersDotOptions: {
type: 'dot',
},
cornersSquareOptions: {
type: 'extra-rounded',
},
backgroundOptions: {
color: tinycolor(`hsl(${qrHsl.value.h}, 100, 97)`).toRgbString(),
},
});
onMounted(() => {
if (qrCodeEl.value != null) {
qrCodeInstance.append(qrCodeEl.value);
}
});
//#region flip
const THRESHOLD = -3;
// @ts-expect-error TS(2339)
const deviceMotionPermissionNeeded = window.DeviceMotionEvent && typeof window.DeviceMotionEvent.requestPermission === 'function';
const flipEls: Set<Element> = new Set();
const flip = ref(false);
function handleOrientationChange(event: DeviceOrientationEvent) {
const isUpsideDown = event.beta ? event.beta < THRESHOLD : false;
flip.value = isUpsideDown;
}
watch(flip, (newState) => {
flipEls.forEach(el => {
el.classList.toggle('_qrShowFlipFliped', newState);
});
});
function requestDeviceMotion() {
if (!deviceMotionPermissionNeeded) return;
// @ts-expect-error TS(2339)
window.DeviceMotionEvent.requestPermission()
.then((response: string) => {
if (response === 'granted') {
window.addEventListener('deviceorientation', handleOrientationChange);
}
})
.catch(console.error);
}
onMounted(() => {
window.addEventListener('deviceorientation', handleOrientationChange);
});
onUnmounted(() => {
window.removeEventListener('deviceorientation', handleOrientationChange);
});
const vFlip = {
mounted(el: Element) {
flipEls.add(el);
el.classList.add('_qrShowFlip');
},
unmounted(el: Element) {
el.classList.remove('_qrShowFlip');
flipEls.delete(el);
},
} as Directive;
//#endregion
</script>
<style lang="scss" module>
$s1: 14px;
$s2: 21px;
$s3: 28px;
$avatarSize: 58px;
.root {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.qr {
position: relative;
margin: 0 auto;
width: 100%;
max-width: 230px;
border-radius: 12px;
overflow: clip;
aspect-ratio: 1;
> svg,
> canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
.user {
display: flex;
flex-direction: column;
margin: $s3 auto;
justify-content: center;
align-items: center;
text-align: center;
overflow: visible;
width: fit-content;
max-width: 100%;
}
.avatar {
width: $avatarSize;
height: $avatarSize;
margin-bottom: 16px;
}
.name {
font-weight: bold;
font-size: 110%;
}
.logo {
width: 100px;
margin: $s3 auto 0;
filter: drop-shadow(0 0 6px #0007);
}
</style>
<style lang="scss">
/*
* useCssModuleで$styleを読み込みたかったがrollupでのunwindが壊れてしまうらしく失敗
* グローバルにクラスを定義することでお茶を濁す
*/
._qrShowFlip {
transition: rotate .3s linear, scale .3s .15s step-start;
}
._qrShowFlipFliped {
scale: -1 1;
rotate: x 180deg;
}
</style>

View File

@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" class="_pageScrollable">
<div class="_spacer" :class="$style.main">
<MkButton v-if="read" :class="$style.button" rounded @click="read = false"><i class="ti ti-qrcode"></i> {{ i18n.ts._qr.showTabTitle }}</MkButton>
<MkButton v-else :class="$style.button" rounded @click="read = true"><i class="ti ti-scan"></i> {{ i18n.ts._qr.readTabTitle }}</MkButton>
<MkQrRead v-if="read"/>
<MkQrShow v-else/>
</div>
<MkPolkadots v-if="!read" accented revered :height="200" style="position: sticky; bottom: 0; margin-top: -200px;"/>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref, shallowRef } from 'vue';
import MkQrShow from './qr.show.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i';
import MkButton from '@/components/MkButton.vue';
import MkPolkadots from '@/components/MkPolkadots.vue';
// router definitionloginRequired
const $i = ensureSignin();
const read = ref(false);
const MkQrRead = defineAsyncComponent(() => import('./qr.read.vue'));
definePage(() => ({
title: i18n.ts.qr,
icon: 'ti ti-qrcode',
}));
</script>
<style lang="scss" module>
.root {
height: 100%;
}
.main {
min-height: 100%;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.button {
margin: 0 auto 16px auto;
}
</style>

View File

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormLink @click="chooseUploadFolder()">
<SearchLabel>{{ i18n.ts.uploadFolder }}</SearchLabel>
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
<template #suffixIcon><i class="ti ti-folder"></i></template>
<template #icon><i class="ti ti-folder"></i></template>
</FormLink>
</SearchMarker>
@ -129,13 +129,37 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect
v-model="defaultImageCompressionLevel" :items="[
{ label: i18n.ts.none, value: 0 },
{ label: i18n.ts.low, value: 1 },
{ label: i18n.ts.medium, value: 2 },
{ label: i18n.ts.high, value: 3 },
{ label: `${i18n.ts.low} (${i18n.ts._compression._quality.high}; ${i18n.ts._compression._size.large})`, value: 1 },
{ label: `${i18n.ts.medium} (${i18n.ts._compression._quality.medium}; ${i18n.ts._compression._size.medium})`, value: 2 },
{ label: `${i18n.ts.high} (${i18n.ts._compression._quality.low}; ${i18n.ts._compression._size.small})`, value: 3 },
]"
>
<template #label><SearchLabel>{{ i18n.ts.defaultImageCompressionLevel }}</SearchLabel></template>
<template #caption><div v-html="i18n.ts.defaultImageCompressionLevel_description"></div></template>
<template #label><SearchLabel>{{ i18n.ts.defaultCompressionLevel }}</SearchLabel></template>
<template #caption><div v-html="i18n.ts.defaultCompressionLevel_description"></div></template>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
</div>
</FormSection>
</SearchMarker>
<SearchMarker :keywords="['video']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.video }}</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker :keywords="['default', 'video', 'compression']">
<MkPreferenceContainer k="defaultVideoCompressionLevel">
<MkSelect
v-model="defaultVideoCompressionLevel" :items="[
{ label: i18n.ts.none, value: 0 },
{ label: `${i18n.ts.low} (${i18n.ts._compression._quality.high}; ${i18n.ts._compression._size.large})`, value: 1 },
{ label: `${i18n.ts.medium} (${i18n.ts._compression._quality.medium}; ${i18n.ts._compression._size.medium})`, value: 2 },
{ label: `${i18n.ts.high} (${i18n.ts._compression._quality.low}; ${i18n.ts._compression._size.small})`, value: 3 },
]"
>
<template #label><SearchLabel>{{ i18n.ts.defaultCompressionLevel }}</SearchLabel></template>
<template #caption><div v-html="i18n.ts.defaultCompressionLevel_description"></div></template>
</MkSelect>
</MkPreferenceContainer>
</SearchMarker>
@ -196,6 +220,7 @@ const meterStyle = computed(() => {
const keepOriginalFilename = prefer.model('keepOriginalFilename');
const defaultWatermarkPresetId = prefer.model('defaultWatermarkPresetId');
const defaultImageCompressionLevel = prefer.model('defaultImageCompressionLevel');
const defaultVideoCompressionLevel = prefer.model('defaultVideoCompressionLevel');
const watermarkPresetsSyncEnabled = ref(prefer.isSyncEnabled('watermarkPresets'));

View File

@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<option :value="1">{{ i18n.ts.small }}</option>
<option :value="2">{{ i18n.ts.medium }}</option>
<option :value="3">{{ i18n.ts.large }}</option>
<option :value="4">{{ i18n.ts.large }}+</option>
<option :value="5">{{ i18n.ts.large }}++</option>
</MkRadios>
</MkPreferenceContainer>
</SearchMarker>
@ -95,11 +97,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'picker', 'style']">
<MkPreferenceContainer k="emojiPickerStyle">
<MkSelect v-model="emojiPickerStyle" :items="[
<MkSelect
v-model="emojiPickerStyle" :items="[
{ label: i18n.ts.auto, value: 'auto' },
{ label: i18n.ts.popup, value: 'popup' },
{ label: i18n.ts.drawer, value: 'drawer' },
]">
]"
>
<template #label><SearchLabel>{{ i18n.ts.style }}</SearchLabel></template>
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
</MkSelect>
@ -116,13 +120,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { genId } from '@/utility/id.js';
import XPalette from './emoji-palette.palette.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import { genId } from '@/utility/id.js';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';

View File

@ -414,7 +414,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="$i.policies.chatAvailability !== 'unavailable'">
<SearchMarker v-slot="slotProps" :keywords="['chat', 'messaging']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
<template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></template>
<div class="_gaps_s">

View File

@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['chat']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
<div class="_gaps_m">
<MkInfo v-if="$i.policies.chatAvailability === 'unavailable'">{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>

View File

@ -151,6 +151,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
</SearchMarker>
<hr>
<SearchMarker :keywords="['qrcode']">
<FormLink to="/qr">
<template #icon><i class="ti ti-qrcode"></i></template>
<SearchLabel>{{ i18n.ts.qr }}</SearchLabel>
</FormLink>
</SearchMarker>
</div>
</SearchMarker>
</template>
@ -164,6 +173,7 @@ import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSlot from '@/components/form/slot.vue';
import FormLink from '@/components/form/link.vue';
import { chooseDriveFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';

View File

@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:initialText="initialText"
:initialVisibility="visibility"
:initialFiles="files"
:initialLocalFiles="tempFiles"
:initialLocalOnly="localOnly"
:reply="reply"
:renote="renote"
@ -33,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { get, del } from 'idb-keyval';
import MkButton from '@/components/MkButton.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import * as os from '@/os.js';
@ -41,7 +43,19 @@ import { definePage } from '@/page.js';
import { postMessageToParentWindow } from '@/utility/post-message.js';
import { i18n } from '@/i18n.js';
//#region parameters
const urlParams = new URLSearchParams(window.location.search);
// merge hash parameters
try {
const hashParams = new URLSearchParams(window.location.hash.slice(1));
for (const [key, value] of hashParams.entries()) {
urlParams.set(key, value);
}
} catch (e) {
console.error('Failed to parse hash parameters:', e);
}
//#endregion
const localOnlyQuery = urlParams.get('localOnly');
const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number];
@ -56,6 +70,7 @@ const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visi
const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
const files = ref([] as Misskey.entities.DriveFile[]);
const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
const tempFiles = ref([] as File[]);
async function init() {
let noteText = '';
@ -181,6 +196,29 @@ async function init() {
});
}
//#region Local files
// If the browser supports IndexedDB, try to get the temporary files from temp.
if (typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true) {
const filesFromIdb = await get<File[]>('share-files-temp');
if (Array.isArray(filesFromIdb) && filesFromIdb.length > 0 && filesFromIdb.every(file => file instanceof Blob)) {
tempFiles.value = filesFromIdb;
}
}
if (urlParams.has('file') && urlParams.get('file').startsWith('data:')) {
try {
const file = await window.fetch(urlParams.get('file')).then(res => res.blob());
if (file instanceof Blob) {
tempFiles.value.push(file as File);
} else {
console.error('Fetched file is not a Blob:', file);
}
} catch (e) {
console.error('Failed to fetch file:', e);
}
}
//#endregion
state.value = 'writing';
}
@ -201,10 +239,31 @@ function goToMisskey(): void {
function onPosted(): void {
state.value = 'posted';
// SW稿IndexedDB
del('share-files-temp');
postMessageToParentWindow('misskey:shareForm:shareCompleted');
}
const headerActions = computed(() => []);
const headerActions = computed(() => [
{
icon: 'ti ti-dots',
text: i18n.ts.menu,
handler: (ev: MouseEvent) => {
os.popupMenu([
{
icon: 'ti ti-home',
text: i18n.ts.goToMisskey,
action: () => goToMisskey(),
},
{
icon: 'ti ti-x',
text: i18n.ts.close,
action: () => close(),
},
], ev.currentTarget ?? ev.target);
},
},
]);
const headerTabs = computed(() => []);

View File

@ -439,6 +439,9 @@ export const PREF_DEF = definePreferences({
defaultImageCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
defaultVideoCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
'sound.masterVolume': {
default: 0.5,
@ -508,7 +511,7 @@ export const PREF_DEF = definePreferences({
default: false,
},
'experimental.enableFolderPageView': {
default: false,
default: true,
},
'experimental.enableHapticFeedback': {
default: false,

View File

@ -590,6 +590,10 @@ export const ROUTE_DEF = [{
path: '/reversi/g/:gameId',
component: page(() => import('@/pages/reversi/game.vue')),
loginRequired: false,
}, {
path: '/qr',
component: page(() => import('@/pages/qr.vue')),
loginRequired: true,
}, {
path: '/debug',
component: page(() => import('@/pages/debug.vue')),

View File

@ -22,6 +22,7 @@ export interface PostFormProps {
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalFiles?: File[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;

View File

@ -215,16 +215,31 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
});
}
if ($i && meId === user.id) {
menuItems.push({
icon: 'ti ti-qrcode',
text: i18n.ts.qr,
action: () => {
router.push('/qr');
},
});
}
if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) {
menuItems.push({
icon: 'ti ti-search',
text: i18n.ts.searchThisUsersNotes,
action: () => {
router.push('/search', {
query: {
const query = {
username: user.username,
host: user.host ?? undefined,
},
} as { username: string, host?: string };
if (user.host !== null) {
query.host = user.host;
}
router.push('/search', {
query
});
},
});
@ -366,8 +381,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
//}
menuItems.push({ type: 'divider' }, {
icon: 'ti ti-mail',
text: i18n.ts.sendMessage,
icon: 'ti ti-pencil-heart',
text: i18n.ts.createUserSpecifiedNote,
action: () => {
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
os.post({ specified: user, initialText: `${canonical} ` });

View File

@ -3,8 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import QRCodeStyling from 'qr-code-styling';
import { url, host } from '@@/js/config.js';
import { getProxiedImageUrl } from '../media-proxy.js';
import { initShaderProgram } from '../webgl.js';
import { ensureSignin } from '@/i.js';
export type ImageEffectorRGB = [r: number, g: number, b: number];
@ -48,6 +51,7 @@ interface AlignParamDef extends CommonParamDef {
default: {
x: 'left' | 'center' | 'right';
y: 'top' | 'center' | 'bottom';
margin?: number;
};
};
@ -58,7 +62,13 @@ interface SeedParamDef extends CommonParamDef {
interface TextureParamDef extends CommonParamDef {
type: 'texture';
default: { type: 'text'; text: string | null; } | { type: 'url'; url: string | null; } | null;
default: {
type: 'text'; text: string | null;
} | {
type: 'url'; url: string | null;
} | {
type: 'qr'; data: string | null;
} | null;
};
interface ColorParamDef extends CommonParamDef {
@ -324,7 +334,11 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
if (_DEV_) console.log(`Baking texture of <${textureKey}>...`);
const texture = v.type === 'text' ? await createTextureFromText(this.gl, v.text) : v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) : null;
const texture =
v.type === 'text' ? await createTextureFromText(this.gl, v.text) :
v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) :
v.type === 'qr' ? await createTextureFromQr(this.gl, { data: v.data }) :
null;
if (texture == null) continue;
this.paramTextures.set(textureKey, texture);
@ -352,7 +366,12 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
private getTextureKeyForParam(v: ParamTypeToPrimitive['texture']) {
if (v == null) return '';
return v.type === 'text' ? `text:${v.text}` : v.type === 'url' ? `url:${v.url}` : '';
return (
v.type === 'text' ? `text:${v.text}` :
v.type === 'url' ? `url:${v.url}` :
v.type === 'qr' ? `qr:${v.data}` :
''
);
}
/*
@ -467,3 +486,53 @@ async function createTextureFromText(gl: WebGL2RenderingContext, text: string |
return info;
}
async function createTextureFromQr(gl: WebGL2RenderingContext, options: { data: string | null }, resolution = 512): Promise<{ texture: WebGLTexture, width: number, height: number } | null> {
const $i = ensureSignin();
const qrCodeInstance = new QRCodeStyling({
width: resolution,
height: resolution,
margin: 42,
type: 'canvas',
data: options.data == null || options.data === '' ? `${url}/users/${$i.id}` : options.data,
image: $i.avatarUrl,
qrOptions: {
typeNumber: 0,
mode: 'Byte',
errorCorrectionLevel: 'H',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.3,
margin: 16,
crossOrigin: 'anonymous',
},
dotsOptions: {
type: 'dots',
},
cornersDotOptions: {
type: 'dot',
},
cornersSquareOptions: {
type: 'extra-rounded',
},
});
const blob = await qrCodeInstance.getRawData('png') as Blob | null;
if (blob == null) return null;
const image = await window.createImageBitmap(blob);
const texture = createTexture(gl);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, resolution, resolution, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.bindTexture(gl.TEXTURE_2D, null);
return {
texture,
width: resolution,
height: resolution,
};
}

View File

@ -18,6 +18,8 @@ import { FX_stripe } from './fxs/stripe.js';
import { FX_threshold } from './fxs/threshold.js';
import { FX_zoomLines } from './fxs/zoomLines.js';
import { FX_blockNoise } from './fxs/blockNoise.js';
import { FX_fill } from './fxs/fill.js';
import { FX_blur } from './fxs/blur.js';
import type { ImageEffectorFx } from './ImageEffector.js';
export const FXS = [
@ -36,4 +38,6 @@ export const FXS = [
FX_chromaticAberration,
FX_tearing,
FX_blockNoise,
FX_fill,
FX_blur,
] as const satisfies ImageEffectorFx<string, any>[];

View File

@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_offset;
uniform vec2 u_scale;
uniform bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
void main() {
float angle = -(u_angle * PI);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + u_offset;
bool isInside = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
// This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
// Calculate how many samples to take in each direction
// This determines the grid density, not the blur extent
int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
// Sample in a grid pattern within the specified radius
for (int x = -sampleRadius; x <= sampleRadius; x++) {
for (int y = -sampleRadius; y <= sampleRadius; y++) {
// Normalize the grid position to [-1, 1] range
float normalizedX = float(x) / float(sampleRadius);
float normalizedY = float(y) / float(sampleRadius);
// Scale by radius to get the actual sampling offset
vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
vec2 sampleUV = in_uv + offset;
// Only sample if within texture bounds
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
result += texture(in_texture, sampleUV);
totalSamples += 1.0;
}
}
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
`;
export const FX_blur = defineImageEffectorFx({
id: 'blur',
name: i18n.ts._imageEffector._fxs.blur,
shader,
uniforms: ['offset', 'scale', 'ellipse', 'angle', 'radius', 'samples'] as const,
params: {
offsetX: {
label: i18n.ts._imageEffector._fxProps.offset + ' X',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
offsetY: {
label: i18n.ts._imageEffector._fxProps.offset + ' Y',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleX: {
label: i18n.ts._imageEffector._fxProps.scale + ' W',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleY: {
label: i18n.ts._imageEffector._fxProps.scale + ' H',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
ellipse: {
label: i18n.ts._imageEffector._fxProps.circle,
type: 'boolean',
default: false,
},
angle: {
label: i18n.ts._imageEffector._fxProps.angle,
type: 'number',
default: 0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 90) + '°',
},
radius: {
label: i18n.ts._imageEffector._fxProps.strength,
type: 'number',
default: 3.0,
min: 0.0,
max: 10.0,
step: 0.5,
},
},
main: ({ gl, u, params }) => {
gl.uniform2f(u.offset, params.offsetX / 2, params.offsetY / 2);
gl.uniform2f(u.scale, params.scaleX / 2, params.scaleY / 2);
gl.uniform1i(u.ellipse, params.ellipse ? 1 : 0);
gl.uniform1f(u.angle, params.angle / 2);
gl.uniform1f(u.radius, params.radius);
gl.uniform1i(u.samples, 256);
},
});

View File

@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_offset;
uniform vec2 u_scale;
uniform bool u_ellipse;
uniform float u_angle;
uniform vec3 u_color;
uniform float u_opacity;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
//float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
//float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + u_offset;
bool isInside = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
}
out_color = isInside ? vec4(
mix(in_color.r, u_color.r, u_opacity),
mix(in_color.g, u_color.g, u_opacity),
mix(in_color.b, u_color.b, u_opacity),
in_color.a
) : in_color;
}
`;
export const FX_fill = defineImageEffectorFx({
id: 'fill',
name: i18n.ts._imageEffector._fxs.fill,
shader,
uniforms: ['offset', 'scale', 'ellipse', 'angle', 'color', 'opacity'] as const,
params: {
offsetX: {
label: i18n.ts._imageEffector._fxProps.offset + ' X',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
offsetY: {
label: i18n.ts._imageEffector._fxProps.offset + ' Y',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleX: {
label: i18n.ts._imageEffector._fxProps.scale + ' W',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleY: {
label: i18n.ts._imageEffector._fxProps.scale + ' H',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
ellipse: {
label: i18n.ts._imageEffector._fxProps.circle,
type: 'boolean',
default: false,
},
angle: {
label: i18n.ts._imageEffector._fxProps.angle,
type: 'number',
default: 0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 90) + '°',
},
color: {
label: i18n.ts._imageEffector._fxProps.color,
type: 'color',
default: [1, 1, 1],
},
opacity: {
label: i18n.ts._imageEffector._fxProps.opacity,
type: 'number',
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
},
main: ({ gl, u, params }) => {
gl.uniform2f(u.offset, params.offsetX / 2, params.offsetY / 2);
gl.uniform2f(u.scale, params.scaleX / 2, params.scaleY / 2);
gl.uniform1i(u.ellipse, params.ellipse ? 1 : 0);
gl.uniform1f(u.angle, params.angle / 2);
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
gl.uniform1f(u.opacity, params.opacity);
},
});

View File

@ -23,6 +23,7 @@ uniform float u_opacity;
uniform bool u_repeat;
uniform int u_alignX; // 0: left, 1: center, 2: right
uniform int u_alignY; // 0: top, 1: center, 2: bottom
uniform float u_alignMargin;
uniform int u_fitMode; // 0: contain, 1: cover
out vec4 out_color;
@ -51,6 +52,9 @@ void main() {
float x_offset = u_alignX == 0 ? x_scale / 2.0 : u_alignX == 2 ? 1.0 - (x_scale / 2.0) : 0.5;
float y_offset = u_alignY == 0 ? y_scale / 2.0 : u_alignY == 2 ? 1.0 - (y_scale / 2.0) : 0.5;
x_offset += (u_alignX == 0 ? 1.0 : u_alignX == 2 ? -1.0 : 0.0) * u_alignMargin;
y_offset += (u_alignY == 0 ? 1.0 : u_alignY == 2 ? -1.0 : 0.0) * u_alignMargin;
float angle = -(u_angle * PI);
vec2 center = vec2(x_offset, y_offset);
//vec2 centeredUv = (in_uv - center) * vec2(in_x_ratio, in_y_ratio);
@ -86,7 +90,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
id: 'watermarkPlacement',
name: '(internal)',
shader,
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'fitMode'] as const,
uniforms: ['texture_watermark', 'resolution_watermark', 'scale', 'angle', 'opacity', 'repeat', 'alignX', 'alignY', 'alignMargin', 'fitMode'] as const,
params: {
cover: {
type: 'boolean',
@ -112,7 +116,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
},
align: {
type: 'align',
default: { x: 'right', y: 'bottom' },
default: { x: 'right', y: 'bottom', margin: 0 },
},
opacity: {
type: 'number',
@ -143,6 +147,7 @@ export const FX_watermarkPlacement = defineImageEffectorFx({
gl.uniform1i(u.repeat, params.repeat ? 1 : 0);
gl.uniform1i(u.alignX, params.align.x === 'left' ? 0 : params.align.x === 'right' ? 2 : 1);
gl.uniform1i(u.alignY, params.align.y === 'top' ? 0 : params.align.y === 'bottom' ? 2 : 1);
gl.uniform1f(u.alignMargin, params.align.margin ?? 0);
gl.uniform1i(u.fitMode, params.cover ? 1 : 0);
},
});

View File

@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
const WATERMARK_FXS = [
@ -17,6 +17,8 @@ const WATERMARK_FXS = [
FX_checker,
] as const satisfies ImageEffectorFx<string, any>[];
type Align = { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom'; margin?: number; };
export type WatermarkPreset = {
id: string;
name: string;
@ -27,7 +29,7 @@ export type WatermarkPreset = {
repeat: boolean;
scale: number;
angle: number;
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
align: Align;
opacity: number;
} | {
id: string;
@ -38,7 +40,14 @@ export type WatermarkPreset = {
repeat: boolean;
scale: number;
angle: number;
align: { x: 'left' | 'center' | 'right'; y: 'top' | 'center' | 'bottom' };
align: Align;
opacity: number;
} | {
id: string;
type: 'qr';
data: string;
scale: number;
align: Align;
opacity: number;
} | {
id: string;
@ -125,6 +134,23 @@ export class WatermarkRenderer {
},
},
};
} else if (layer.type === 'qr') {
return {
fxId: 'watermarkPlacement',
id: layer.id,
params: {
repeat: false,
scale: layer.scale,
align: layer.align,
angle: 0,
opacity: layer.opacity,
cover: false,
watermark: {
type: 'qr',
data: layer.data,
},
},
};
} else if (layer.type === 'stripe') {
return {
fxId: 'stripe',
@ -164,7 +190,7 @@ export class WatermarkRenderer {
},
};
} else {
throw new Error(`Unknown layer type`);
throw new Error(`Unrecognized layer type: ${(layer as any).type}`);
}
});
}

View File

@ -85,6 +85,8 @@ export function toBase62(n: number): string {
}
export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return {
base: '/vite/',
@ -188,9 +190,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)) {

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.9.0",
"version": "2025.9.1-alpha.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -4285,6 +4285,7 @@ export type components = {
imageUrl: string;
memo: string;
dayOfWeek: number;
isSensitive: boolean;
};
Announcement: {
/**
@ -5381,6 +5382,7 @@ export type components = {
/** Format: url */
imageUrl: string;
dayOfWeek: number;
isSensitive?: boolean;
}[];
/** @default 0 */
notesPerOneAd: number;
@ -6242,6 +6244,7 @@ export interface operations {
startsAt: number;
imageUrl: string;
dayOfWeek: number;
isSensitive?: boolean;
};
};
};
@ -6454,6 +6457,7 @@ export interface operations {
expiresAt?: number;
startsAt?: number;
dayOfWeek?: number;
isSensitive?: boolean;
};
};
};

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { get } from 'idb-keyval';
import { get, set } from 'idb-keyval';
import * as Misskey from 'misskey-js';
import type { PushNotificationDataMap } from '@/types.js';
import type { I18n } from '@@/js/i18n.js';
@ -40,6 +40,34 @@ async function offlineContentHTML() {
}
globalThis.addEventListener('fetch', ev => {
//#region /sw/share
const url = new URL(ev.request.url);
if (url.pathname === '/sw/share') {
ev.respondWith((async () => {
const responseUrl = new URL(ev.request.url);
responseUrl.pathname = '/share';
const formData = await ev.request.formData();
// とりあえず初期化 (IndexedDBの削除は時間がかかる可能性があるため空の配列をセット)
await set('share-url-temp', []);
if (formData.has('files')) {
const files = formData.getAll('files');
if (files.length > 0 && files.every(file => file instanceof Blob)) {
set('share-files-temp', files);
}
}
formData.delete('files');
for (const [key, value] of formData.entries()) {
responseUrl.searchParams.set(key, value.toString());
}
return Response.redirect(responseUrl, 303);
})());
return;
}
//#region others
let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
isHTMLRequest = true;
@ -62,6 +90,7 @@ globalThis.addEventListener('fetch', ev => {
});
}),
);
//#endregion
});
globalThis.addEventListener('push', ev => {

View File

@ -83,8 +83,8 @@ importers:
specifier: 2.0.0
version: 2.0.0
pnpm:
specifier: 10.15.1
version: 10.15.1
specifier: 10.16.0
version: 10.16.0
start-server-and-test:
specifier: 2.1.0
version: 2.1.0
@ -829,6 +829,9 @@ importers:
matter-js:
specifier: 0.20.0
version: 0.20.0
mediabunny:
specifier: 1.15.1
version: 1.15.1
mfm-js:
specifier: 0.25.0
version: 0.25.0
@ -847,6 +850,12 @@ importers:
punycode.js:
specifier: 2.3.1
version: 2.3.1
qr-code-styling:
specifier: 1.9.2
version: 1.9.2
qr-scanner:
specifier: 1.4.2
version: 1.4.2
rollup:
specifier: 4.50.1
version: 4.50.1
@ -2434,138 +2443,163 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm64@1.1.0':
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.1.0':
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.1.0':
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.1.0':
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.1.0':
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm64@0.34.2':
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.2':
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.2':
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.2':
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-arm64@0.34.2':
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.2':
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@ -2887,30 +2921,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.79':
resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==}
@ -3265,36 +3304,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.0':
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.0':
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.0':
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.0':
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@ -3464,111 +3509,133 @@ packages:
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-gnueabihf@4.50.1':
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.46.2':
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-gnu@4.50.1':
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.46.2':
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-musl@4.50.1':
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.46.2':
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-musl@4.50.1':
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.46.2':
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.50.1':
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.46.2':
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.50.1':
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.46.2':
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-x64-musl@4.50.1':
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.50.1':
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
@ -4302,24 +4369,28 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@swc/core-linux-arm64-musl@1.13.5':
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@swc/core-linux-x64-gnu@1.13.5':
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@swc/core-linux-x64-musl@1.13.5':
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@swc/core-win32-arm64-msvc@1.13.5':
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
@ -4543,6 +4614,12 @@ packages:
'@types/doctrine@0.0.9':
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
'@types/dom-mediacapture-transform@0.1.11':
resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==}
'@types/dom-webcodecs@0.1.13':
resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==}
'@types/eslint@7.29.0':
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
@ -8134,6 +8211,9 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
mediabunny@1.15.1:
resolution: {integrity: sha512-+eRTVzd3E4LuGYZzPSQcPzuGdAIljohSlzYTX358XsfLM2qH1lQIBYa+erx7wzVcGQLRNjdV7x7ZS0EpK04DfA==}
meilisearch@0.52.0:
resolution: {integrity: sha512-RqPsB4a78sXf/ATB7PIVvKCG7yf0y1M+uCj8Z9Wku44WmCy3iz0C1PHjVV5xphQolo09CdhdyFoRxHQSJkOdpg==}
@ -9004,8 +9084,8 @@ packages:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
pnpm@10.15.1:
resolution: {integrity: sha512-NOU4wym1VTAUyo6PRTWZf5YYCh0PYUM5NXRJk1NQ2STiL4YUaCGRJk7DPRRirCFWGv+X9rsYBlNRwWLH6PbeZw==}
pnpm@10.16.0:
resolution: {integrity: sha512-gGbnsDQhe3AKmk27OgBQYdZBuhMKiZFSE6ELPKSRnBnAN77IBmr9xVm4ljX9uAaxbqZz8kaPuyiqv6E8U+P3aQ==}
engines: {node: '>=18.12'}
hasBin: true
@ -9374,6 +9454,16 @@ packages:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
qr-code-styling@1.9.2:
resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==}
engines: {node: '>=18.18.0'}
qr-scanner@1.4.2:
resolution: {integrity: sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==}
qrcode-generator@1.5.2:
resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
@ -9898,24 +9988,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
slacc-linux-arm64-musl@0.0.10:
resolution: {integrity: sha512-3lUX7752f6Okn54aONioaA+9M5TvifqXBAart+u2lNXEdWmmh003cVSU2Vcwg7nJ9lLHtju2DkDmKKfJjFuShA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
slacc-linux-x64-gnu@0.0.10:
resolution: {integrity: sha512-BxxvylF9zlOLRLCpiyMvKTIUpdLlpetNBJ+DSMDh5+Ggq+AmQz2NUGawmcBJw58F8nMCj9TpWLlGNWc2AuY+JQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
slacc-linux-x64-musl@0.0.10:
resolution: {integrity: sha512-TYJi8LOtJiTFcZvka4du7bMjF9Bz1RHRwyLnScr5E5yjjgoLRrsvgSu7bxp87xH+rgJ3CdEwE3w3Ux8EiewHpA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
slacc-win32-arm64-msvc@0.0.10:
resolution: {integrity: sha512-1CHPLiDB4exzFyT5ndtJDsRRhBxNg8mGz6I6eJEMjelGkJR2KZPT9LZuby/1bS/bcVOr7zuJvGNfbEGBeHRwPQ==}
@ -10943,6 +11037,9 @@ packages:
vue-component-type-helpers@3.0.6:
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
vue-component-type-helpers@3.1.0-alpha.0:
resolution: {integrity: sha512-K1guwS1Oy0gNfBdIdIn8JMkUV+S38sriR1zf5dP+KkPS7/r5nHnPZUL74meY2CYlxYBH4qSQ+k7bpHfwiRvaMg==}
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
@ -14853,7 +14950,7 @@ snapshots:
storybook: 9.1.5(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(msw@2.11.1(@types/node@22.18.1)(typescript@5.9.2))(prettier@3.6.2)(utf-8-validate@6.0.5)(vite@7.1.5(@types/node@22.18.1)(sass@1.92.1)(terser@5.44.0)(tsx@4.20.5))
type-fest: 2.19.0
vue: 3.5.21(typescript@5.9.2)
vue-component-type-helpers: 3.0.6
vue-component-type-helpers: 3.1.0-alpha.0
'@stylistic/eslint-plugin@2.13.0(eslint@9.35.0)(typescript@5.9.2)':
dependencies:
@ -15205,6 +15302,12 @@ snapshots:
'@types/doctrine@0.0.9': {}
'@types/dom-mediacapture-transform@0.1.11':
dependencies:
'@types/dom-webcodecs': 0.1.13
'@types/dom-webcodecs@0.1.13': {}
'@types/eslint@7.29.0':
dependencies:
'@types/estree': 1.0.8
@ -19652,6 +19755,11 @@ snapshots:
media-typer@0.3.0: {}
mediabunny@1.15.1:
dependencies:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
meilisearch@0.52.0: {}
memoizerific@1.11.3:
@ -20621,7 +20729,7 @@ snapshots:
pngjs@5.0.0: {}
pnpm@10.15.1: {}
pnpm@10.16.0: {}
polished@4.2.2:
dependencies:
@ -20999,6 +21107,16 @@ snapshots:
pvutils@1.1.3: {}
qr-code-styling@1.9.2:
dependencies:
qrcode-generator: 1.5.2
qr-scanner@1.4.2:
dependencies:
'@types/offscreencanvas': 2019.7.0
qrcode-generator@1.5.2: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.2
@ -22719,6 +22837,8 @@ snapshots:
vue-component-type-helpers@3.0.6: {}
vue-component-type-helpers@3.1.0-alpha.0: {}
vue-demi@0.14.7(vue@3.5.21(typescript@5.9.2)):
dependencies:
vue: 3.5.21(typescript@5.9.2)

View File

@ -30,3 +30,4 @@ onlyBuiltDependencies:
- v-code-diff
- vue-demi
ignorePatchFailures: false
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack