Compare commits

...

41 Commits

Author SHA1 Message Date
かっこかり 04dddece96
Merge 685274c592 into d2154214ba 2026-01-30 00:11:43 +00:00
kakkokari-gtyih 685274c592 ✌️ 2026-01-30 09:07:03 +09:00
あわわわとーにゅ 71b5314394 enhance: 起動からlistenまでかかる時間を減らす (MisskeyIO#1410) 2026-01-30 09:04:07 +09:00
kakkokari-gtyih 2e0ed4f672 fix 2026-01-30 09:00:42 +09:00
kakkokari-gtyih 8d3c2b7ff7 Merge branch 'develop' into bundle-backend-alt 2026-01-30 09:00:38 +09:00
renovate[bot] d2154214ba
fix(deps): update dependency tar to v7.5.7 [security] (#17104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:18:34 +09:00
renovate[bot] 97312b97f8
chore(deps): update pnpm to v10.28.2 [security] (#17133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 10:53:08 +09:00
poppingmoon 5e2b041f84
fix(frontend): remove ensureSignin from reversi game page (#17130) 2026-01-28 20:44:58 +09:00
syuilo ec97f49919 Create copilot-instructions.md 2026-01-28 19:42:43 +09:00
かっこかり 4910fff7fb
fix(frontend): mCaptchaが動作しない問題を修正 (#17127)
* fix(frontend): mCaptchaが動作しない問題を修正

* Update Changelog

* remove mcaptcha vanila glue
2026-01-28 11:55:35 +09:00
かっこかり fc7655c808
deps: update dependencies [ci skip] (#17126)
* deps: update dependencies

* update vunerable packages as well
2026-01-24 02:41:10 +09:00
syuilo ae2ac9d50f fix(frontend): 投稿フォームのアップロードファイルを右クリックしたときの挙動がおかしいのを修正 2026-01-22 20:31:42 +09:00
syuilo 8932492fd3 enhance(frontend): 添付画像のメニューを右クリックでも呼び出せるように 2026-01-22 20:29:11 +09:00
syuilo a168e7b648
enhance(dev): Improve mem report (#17119)
* wip

* Update report-backend-memory.yml

* Update report-backend-memory.yml

* Update measure-memory.mjs

* Update report-backend-memory.yml
2026-01-22 18:53:53 +09:00
syuilo 1adcb03b93 Update report-backend-memory.yml 2026-01-22 15:01:38 +09:00
syuilo b6e737dc76 Update report-backend-memory.yml 2026-01-22 14:47:05 +09:00
syuilo 2fa6ecc7ef
enhance(dev): improve mem report (#17118)
* wip

* wip

* Update report-backend-memory.yml

* Update report-backend-memory.yml

* Update .github/workflows/report-backend-memory.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 14:32:57 +09:00
syuilo f744b5711f
enhance(dev): improve mem report (#17117)
* wip

* Update report-backend-memory.yml
2026-01-22 13:14:05 +09:00
syuilo 2b3d72bb73
Update measure-memory.mjs (#17116) 2026-01-22 12:16:38 +09:00
syuilo 3205eb6925 lint 2026-01-22 12:14:51 +09:00
syuilo d4fcc694a6 enhance(backend): remove redis-info dep 2026-01-22 10:28:03 +09:00
かっこかり 389861f1da
refactor(frontend): fix invalid self-closing tags (#17103)
* fix(frontend): fix invalid self-closing tags

* apply eslint rules

* fix

* fix
2026-01-16 22:42:08 +09:00
syuilo ec683f04d1 enhance(dev): メモリ使用量比較のActionsでGCしてから取得する
Resolve #16961
2026-01-14 16:12:44 +09:00
かっこかり bd81a6c8ad
refactor(frontend): anyを除去2 (#17092)
* wip

* fix types

* fix
2026-01-14 14:45:45 +09:00
syuilo d8318c02a1 refactor(frontend): MkRadioをMkRadiosに統合 2026-01-14 14:21:51 +09:00
かっこかり b941c896aa
refactor(frontend): MkRadiosの指定をpropsから行うように (#16597)
* refactor(frontend): MkRadiosの指定をpropsから行うように

* spdx

* fix lint

* fix: mkradiosを動的slotsに対応させる

* fix: remove comment [ci skip]

* fix lint

* fix lint

* migrate

* rename

* fix

* fix

* fix types

* remove unused imports

* fix

* wip

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2026-01-14 14:02:50 +09:00
github-actions[bot] 153ebd4392 Bump version to 2026.1.0-beta.0 2026-01-13 06:30:05 +00:00
かっこかり bc5aef846b
enhance(frontend): 画面幅が広いときはメディアを横並びで表示できるようにするオプションを追加 (#16946)
* enhance(frontend): 画面幅が広いときはメディアを横並びで表示できるようにするオプションを追加

* Update Changelog

* Update CHANGELOG.md
2026-01-13 15:28:54 +09:00
poppingmoon 4a0edf348a
fix(frontend): use logical OR for fallback of file comment (#17089) 2026-01-13 15:04:23 +09:00
かっこかり f3aa5081ed
fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように (#17096)
* fix(frontend): MkFormで入力に不備がある場合は完了ボタンを押して続行できないように

* fix lint
2026-01-13 15:02:50 +09:00
poppingmoon c0d5c0df69
fix(frontend): add "px" suffix to borderWidth of Ui:C:container (#17088)
* fix: add "px" suffix to borderWidth of Ui:C:container

* docs: update changelog
2026-01-13 15:00:46 +09:00
おさむのひと 0b383efa5a
Refactor FileServerService: Extract file handling logic into dedicated handlers (#17090)
- Introduced FileServerDriveHandler to manage drive file requests.
- Created FileServerFileResolver for resolving file access keys and downloading files.
- Added FileServerProxyHandler to handle proxy requests and image processing.
- Moved utility functions to FileServerUtils for better organization.
- Removed redundant methods from FileServerService, improving readability and maintainability.
2026-01-13 15:00:11 +09:00
かっこかり abe745ec87
fix(frontend/widget): AiscriptAppのスクリプト適用を手動に変更 (#17095) 2026-01-11 21:56:58 +09:00
syuilo 2168395b71
New Crowdin updates (#16982)
* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
2026-01-11 19:37:00 +09:00
renovate[bot] 7a9c4591c2
chore(deps): update [docker] update dependencies (#16082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 19:33:48 +09:00
かっこかり 4bc0026900
deps(frontend): update vue-tsc to v3.2.1 (#17091) 2026-01-11 15:00:00 +09:00
syuilo faf2399e31 enhance(frontend): 連合なしが指定されているときに公開範囲を指名にできるように
Resolve #14760
2026-01-11 13:58:58 +09:00
kakkokari-gtyih 9e05bdeedf remove unused build script 2026-01-06 14:01:34 +09:00
kakkokari-gtyih 91a808dfed fix [ci skip] 2026-01-06 10:31:02 +09:00
kakkokari-gtyih ca8e502dff fix 2026-01-06 10:23:02 +09:00
kakkokari-gtyih 4b01c4971d enhance(backend): bundle backend using rolldown 2026-01-06 10:20:58 +09:00
166 changed files with 5250 additions and 4407 deletions

3
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,3 @@
# Copilot Instructions for Misskey
- en-US.yml を編集しないでください。

View File

@ -54,55 +54,110 @@ jobs:
BASE_MEMORY=$(cat ./artifacts/memory-base.json) BASE_MEMORY=$(cat ./artifacts/memory-base.json)
HEAD_MEMORY=$(cat ./artifacts/memory-head.json) HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0') variation() {
HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0') calc() {
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
# Calculate difference DIFF=$((HEAD - BASE))
if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then if [ "$BASE" -gt 0 ]; then
DIFF=$((HEAD_RSS - BASE_RSS)) DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
# Convert to MB for readability
BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
echo "has_data=true" >> "$GITHUB_OUTPUT"
# Determine if this is a significant change (more than 5% increase)
if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
echo "significant_increase=true" >> "$GITHUB_OUTPUT"
else else
echo "significant_increase=false" >> "$GITHUB_OUTPUT" DIFF_PERCENT=0
fi
else
echo "has_data=false" >> "$GITHUB_OUTPUT"
fi fi
# Convert KB to MB for readability
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
JSON=$(jq -c -n \
--argjson base "$BASE_MB" \
--argjson head "$HEAD_MB" \
--argjson diff "$DIFF_MB" \
--argjson diff_percent "$DIFF_PERCENT" \
'{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
echo "$JSON"
}
JSON=$(jq -c -n \
--argjson VmRSS "$(calc $1 VmRSS)" \
--argjson VmHWM "$(calc $1 VmHWM)" \
--argjson VmSize "$(calc $1 VmSize)" \
--argjson VmData "$(calc $1 VmData)" \
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
echo "$JSON"
}
JSON=$(jq -c -n \
--argjson beforeGc "$(variation beforeGc)" \
--argjson afterGc "$(variation afterGc)" \
--argjson afterRequest "$(variation afterRequest)" \
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
echo "res=$JSON" >> "$GITHUB_OUTPUT"
- id: build-comment - id: build-comment
name: Build memory comment name: Build memory comment
env:
RES: ${{ steps.compare.outputs.res }}
run: | run: |
HEADER="## Backend Memory Usage Comparison" HEADER="## Backend memory usage comparison"
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
echo "$HEADER" > ./output.md echo "$HEADER" > ./output.md
echo >> ./output.md echo >> ./output.md
if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then table() {
echo "| Metric | base | head | Diff |" >> ./output.md echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
echo "|--------|------|------|------|" >> ./output.md echo "|--------|------:|------:|------:|------:|" >> ./output.md
echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
line() {
METRIC=$2
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
DIFF="+$DIFF"
DIFF_PERCENT="+$DIFF_PERCENT"
fi
# highlight VmRSS
if [ "$2" = "VmRSS" ]; then
METRIC="**${METRIC}**"
BASE="**${BASE}**"
HEAD="**${HEAD}**"
DIFF="**${DIFF}**"
DIFF_PERCENT="**${DIFF_PERCENT}**"
fi
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
}
line $1 VmRSS
line $1 VmHWM
line $1 VmSize
line $1 VmData
}
echo "### Before GC" >> ./output.md
table beforeGc
echo >> ./output.md echo >> ./output.md
if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then echo "### After GC" >> ./output.md
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md table afterGc
echo >> ./output.md echo >> ./output.md
fi
else echo "### After Request" >> ./output.md
echo "Could not retrieve memory usage data." >> ./output.md table afterRequest
echo >> ./output.md
# Determine if this is a significant change (more than 5% increase)
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
echo >> ./output.md echo >> ./output.md
fi fi

View File

@ -13,6 +13,7 @@
- Enhance: ドライブのファイル一覧で自動でもっと見るを利用可能に - Enhance: ドライブのファイル一覧で自動でもっと見るを利用可能に
- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように - Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
- Enhance: ウィジェットの設定項目のラベルの多言語対応 - Enhance: ウィジェットの設定項目のラベルの多言語対応
- Enhance: 画面幅が広いときにメディアを横並びで表示できるようにするオプションを追加
- Enhance: パフォーマンスの向上 - Enhance: パフォーマンスの向上
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061 - Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正 - Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
@ -21,6 +22,9 @@
- Fix: 管理画面でアーカイブ済のお知らせを表示した際にアクティブなお知らせが多い旨の警告が出る問題を修正 - Fix: 管理画面でアーカイブ済のお知らせを表示した際にアクティブなお知らせが多い旨の警告が出る問題を修正
- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正 - Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
- Fix: 2月29日を誕生日に設定している場合、閏年以外は3月1日を誕生日として扱うように修正 - Fix: 2月29日を誕生日に設定している場合、閏年以外は3月1日を誕生日として扱うように修正
- Fix: `Mk:C:container``borderWidth` が正しく反映されない問題を修正
- Fix: mCaptchaが正しく動作しない問題を修正
- Fix: 非ログイン時にリバーシの対局が表示されない問題を修正
### Server ### Server
- Enhance: OAuthのクライアント情報取得Client Information Discoveryにおいて、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました - Enhance: OAuthのクライアント情報取得Client Information Discoveryにおいて、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4 # syntax = docker/dockerfile:1.20
ARG NODE_VERSION=22.15.0-bookworm ARG NODE_VERSION=22.21.1-bookworm
# build assets & compile TypeScript # build assets & compile TypeScript

View File

@ -1365,6 +1365,14 @@ _widgets:
userList: "قائمة المستخدمين" userList: "قائمة المستخدمين"
_userList: _userList:
chooseList: "اختر قائمة" chooseList: "اختر قائمة"
_widgetOptions:
height: "الإرتفاع"
_button:
colored: "ملوّن"
_clock:
size: "الحجم"
_birthdayFollowings:
period: "المدة"
_cw: _cw:
hide: "إخفاء" hide: "إخفاء"
show: "عرض المزيد" show: "عرض المزيد"

View File

@ -1137,6 +1137,14 @@ _widgets:
aichan: "আই চান" aichan: "আই চান"
_userList: _userList:
chooseList: "লিস্ট নির্বাচন করুন" chooseList: "লিস্ট নির্বাচন করুন"
_widgetOptions:
height: "উচ্চতা"
_button:
colored: "রঙ্গিন"
_clock:
size: "আকার"
_birthdayFollowings:
period: "ব্যাপ্তিকাল"
_cw: _cw:
hide: "লুকান" hide: "লুকান"
show: "আরও দেখুন" show: "আরও দেখুন"

View File

@ -1406,6 +1406,7 @@ youAreAdmin: "Ets l'administrador "
frame: "Marc" frame: "Marc"
presets: "Predefinit" presets: "Predefinit"
zeroPadding: "Sense omplir" zeroPadding: "Sense omplir"
nothingToConfigure: "No hi ha res a configurar"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "Títol de l'arxiu" caption: "Títol de l'arxiu"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "Mostrar les pestanyes de les línies de temps a la part inferior" showPageTabBarBottom: "Mostrar les pestanyes de les línies de temps a la part inferior"
emojiPaletteBanner: "Pots registrar ajustos preestablerts com paletes perquè es mostrin permanentment al selector d'emojis, o personalitzar la configuració de visió del selector." emojiPaletteBanner: "Pots registrar ajustos preestablerts com paletes perquè es mostrin permanentment al selector d'emojis, o personalitzar la configuració de visió del selector."
enableAnimatedImages: "Activar imatges animades" enableAnimatedImages: "Activar imatges animades"
settingsPersistence_title: "Persistència de la configuració "
settingsPersistence_description1: "Habilitar la persistència de la configuració permet que no es perdi la informació de la configuració "
settingsPersistence_description2: "Depenent de l'entorn pot ser que no puguis habilitar aquesta opció."
_chat: _chat:
showSenderName: "Mostrar el nom del remitent" showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar" sendOnEnter: "Introdueix per enviar"
@ -1609,8 +1613,8 @@ _bubbleGame:
highScore: "Millor puntuació " highScore: "Millor puntuació "
maxChain: "Nombre màxim de combos" maxChain: "Nombre màxim de combos"
yen: "{yen}Ien" yen: "{yen}Ien"
estimatedQty: "{qty}peces" estimatedQty: "{qty} Peces"
scoreSweets: "{onigiriQtyWithUnit}ongiris" scoreSweets: "{onigiriQtyWithUnit} Boles d'arròs "
_howToPlay: _howToPlay:
section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa." section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa."
section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts." section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts."
@ -2541,6 +2545,44 @@ _widgets:
clicker: "Clicker" clicker: "Clicker"
birthdayFollowings: "Usuaris que fan l'aniversari avui" birthdayFollowings: "Usuaris que fan l'aniversari avui"
chat: "Xateja amb aquest usuari" chat: "Xateja amb aquest usuari"
_widgetOptions:
showHeader: "Mostrar la capçalera"
transparent: "Fons transparent"
height: "Alçada "
_button:
colored: "Colorit"
_clock:
size: "Mida"
thickness: "Amplada de l'agulla "
thicknessThin: "Esvelt "
thicknessMedium: "Normal"
thicknessThick: "Gruixut "
graduations: "Marques de l'esfera "
graduationDots: "Punt"
graduationArabic: "Nombres àrabs "
fadeGraduations: "Efecte gradient "
sAnimation: "Animació de la maneta dels segons"
sAnimationElastic: "Real"
sAnimationEaseOut: "Suau"
twentyFour: "Format 24 hores"
labelTime: "Temps"
labelTz: "Fus horari"
labelTimeAndTz: "Hora i fus horari"
timezone: "Fus horari"
showMs: "Mostrar mil·lisegons"
showLabel: "Mostrar etiqueta"
_jobQueue:
sound: "Reprodueix so"
_rss:
url: "URL del canal RSS"
refreshIntervalSec: "Interval d'actualitzacions (segons)"
maxEntries: "Nombre màxim d'entrades a mostrar"
_rssTicker:
shuffle: "Visualització aleatòria "
duration: "Velocitat desplaçament bàner informatiu "
reverse: "Desplaçament contrari"
_birthdayFollowings:
period: "Període"
_cw: _cw:
hide: "Amagar" hide: "Amagar"
show: "Carregar més" show: "Carregar més"
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "L'amplada mínima es farà servir quan \"Ajust automàtic de l'amplada\" estigui activat" usedAsMinWidthWhenFlexible: "L'amplada mínima es farà servir quan \"Ajust automàtic de l'amplada\" estigui activat"
flexible: "Ajust automàtic de l'amplada" flexible: "Ajust automàtic de l'amplada"
enableSyncBetweenDevicesForProfiles: "Activar la sincronització de la informació de perfils de dispositiu a dispositiu" enableSyncBetweenDevicesForProfiles: "Activar la sincronització de la informació de perfils de dispositiu a dispositiu"
showHowToUse: "Veure la descripció de la interfície d'usuari "
_howToUse:
addColumn_title: "Afegir columna"
addColumn_description: "Pots seleccionar i afegir tipus de columnes."
settings_title: "Configuració de la interfície d'usuari "
settings_description: "Pots configurar la interfície d'usuari amb detall."
switchProfile_title: "Canviar perfil"
switchProfile_description: "Pots desar el disseny de la interfície d'usuari com un perfil i anar canviant entre ells quan vulguis."
_columns: _columns:
main: "Principal" main: "Principal"
widgets: "Ginys" widgets: "Ginys"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "Efecte" title: "Efecte"
addEffect: "Afegeix un efecte" addEffect: "Afegeix un efecte"
discardChangesConfirm: "Vols descartar els canvis i sortir?" discardChangesConfirm: "Vols descartar els canvis i sortir?"
nothingToConfigure: "No hi ha opcions de configuració disponibles"
failedToLoadImage: "Error en carregar la imatge" failedToLoadImage: "Error en carregar la imatge"
_fxs: _fxs:
chromaticAberration: "Aberració cromàtica" chromaticAberration: "Aberració cromàtica"

View File

@ -1789,6 +1789,14 @@ _widgets:
_userList: _userList:
chooseList: "Vybrat seznam" chooseList: "Vybrat seznam"
clicker: "Clicker" clicker: "Clicker"
_widgetOptions:
height: "Výška"
_button:
colored: "Barevné"
_clock:
size: "Velikost"
_birthdayFollowings:
period: "Trvání"
_cw: _cw:
hide: "Skrýt" hide: "Skrýt"
show: "Zobrazit více" show: "Zobrazit více"

View File

@ -83,6 +83,7 @@ files: "Dateien"
download: "Herunterladen" download: "Herunterladen"
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden." driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden."
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?" unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
cancelFollowRequestConfirm: "Möchten Sie die Voll-Anfrage an {name} zurückziehen?"
rejectFollowRequestConfirm: "Möchtest du die Follow-Anfrage von {name} ablehnen?" rejectFollowRequestConfirm: "Möchtest du die Follow-Anfrage von {name} ablehnen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
@ -254,6 +255,7 @@ noteDeleteConfirm: "Möchtest du diese Notiz wirklich löschen?"
pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften." pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften."
done: "Fertig" done: "Fertig"
processing: "In Bearbeitung …" processing: "In Bearbeitung …"
preprocessing: "In Vorbereitung"
preview: "Vorschau" preview: "Vorschau"
default: "Standard" default: "Standard"
defaultValueIs: "Standardwert: {value}" defaultValueIs: "Standardwert: {value}"
@ -302,6 +304,7 @@ uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschl
uploadNFiles: "Lade {n} Dateien hoch" uploadNFiles: "Lade {n} Dateien hoch"
explore: "Erkunden" explore: "Erkunden"
messageRead: "Gelesen" messageRead: "Gelesen"
readAllChatMessages: "Alle Nachrichten als gelesen markieren"
noMoreHistory: "Kein weiterer Verlauf vorhanden" noMoreHistory: "Kein weiterer Verlauf vorhanden"
startChat: "Chat starten" startChat: "Chat starten"
nUsersRead: "Von {n} Benutzern gelesen" nUsersRead: "Von {n} Benutzern gelesen"
@ -334,6 +337,7 @@ fileName: "Dateiname"
selectFile: "Datei auswählen" selectFile: "Datei auswählen"
selectFiles: "Dateien auswählen" selectFiles: "Dateien auswählen"
selectFolder: "Ordner auswählen" selectFolder: "Ordner auswählen"
unselectFolder: "Ordnerauswahl aufheben"
selectFolders: "Ordner auswählen" selectFolders: "Ordner auswählen"
fileNotSelected: "Keine Datei ausgewählt" fileNotSelected: "Keine Datei ausgewählt"
renameFile: "Datei umbenennen" renameFile: "Datei umbenennen"
@ -346,6 +350,7 @@ addFile: "Datei hinzufügen"
showFile: "Datei anzeigen" showFile: "Datei anzeigen"
emptyDrive: "Deine Drive ist leer" emptyDrive: "Deine Drive ist leer"
emptyFolder: "Dieser Ordner ist leer" emptyFolder: "Dieser Ordner ist leer"
dropHereToUpload: "Dateien hier ablegen, um sie hochzuladen."
unableToDelete: "Nicht löschbar" unableToDelete: "Nicht löschbar"
inputNewFileName: "Gib einen neuen Dateinamen ein" inputNewFileName: "Gib einen neuen Dateinamen ein"
inputNewDescription: "Gib eine neue Beschreibung ein" inputNewDescription: "Gib eine neue Beschreibung ein"
@ -773,6 +778,7 @@ lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt
alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren" alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen" loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen" disableShowingAnimatedImages: "Animierte Bilder nicht abspielen"
disableShowingAnimatedImages_caption: "Unabhängig von dieser Einstellung kann es vorkommen, dass animierte Bilder nicht abgespielt werden, wenn z. B. die Barrierefreiheits- oder Energiespareinstellungen des Browsers oder des Betriebssystems eingreifen."
highlightSensitiveMedia: "Sensitive Medien markieren" highlightSensitiveMedia: "Sensitive Medien markieren"
verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen." verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
notSet: "Nicht konfiguriert" notSet: "Nicht konfiguriert"
@ -1020,6 +1026,8 @@ pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterst
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald sie gelesen wurden" sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald sie gelesen wurden"
sendPushNotificationReadMessageCaption: "Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen." sendPushNotificationReadMessageCaption: "Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
pleaseAllowPushNotification: "Bitte erlauben Sie Benachrichtigungen in Ihrem Browser." pleaseAllowPushNotification: "Bitte erlauben Sie Benachrichtigungen in Ihrem Browser."
browserPushNotificationDisabled: "Das Abrufen der Berechtigung zum Senden von Benachrichtigungen ist fehlgeschlagen."
browserPushNotificationDisabledDescription: "Sie haben keine Berechtigung, Benachrichtigungen von {serverName} zu senden. Bitte erlauben Sie Benachrichtigungen in den Browser-Einstellungen und versuchen Sie es erneut."
windowMaximize: "Maximieren" windowMaximize: "Maximieren"
windowMinimize: "Minimieren" windowMinimize: "Minimieren"
windowRestore: "Wiederherstellen" windowRestore: "Wiederherstellen"
@ -1095,6 +1103,7 @@ prohibitedWordsDescription2: "Durch die Verwendung von Leerzeichen können AND-V
hiddenTags: "Ausgeblendete Hashtags" hiddenTags: "Ausgeblendete Hashtags"
hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden." hiddenTagsDescription: "Die hier eingestellten Tags werden nicht mehr in den Trends angezeigt. Mit der Umschalttaste können mehrere ausgewählt werden."
notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar."
usersSearchNotAvailable: "Die Benutzersuche ist nicht verfügbar."
license: "Lizenz" license: "Lizenz"
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
myClips: "Meine Clips" myClips: "Meine Clips"
@ -1169,6 +1178,7 @@ installed: "Installiert"
branding: "Branding" branding: "Branding"
enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen" enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen"
enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren" enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren"
showRoleBadgesOfRemoteUsers: "Rollensymbole anzeigen, die Remote-Benutzern zugewiesen wurden."
turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen." turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen."
createInviteCode: "Einladung erstellen" createInviteCode: "Einladung erstellen"
createWithOptions: "Einladung mit Optionen erstellen" createWithOptions: "Einladung mit Optionen erstellen"
@ -1317,6 +1327,7 @@ acknowledgeNotesAndEnable: "Schalten Sie dies erst ein, wenn Sie die Vorsichtsma
federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren." federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren." federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
draft: "Entwurf" draft: "Entwurf"
draftsAndScheduledNotes: "Entwürfe und geplante Beiträge"
confirmOnReact: "Reagieren bestätigen" confirmOnReact: "Reagieren bestätigen"
reactAreYouSure: "Willst du eine \"{emoji}\"-Reaktion hinzufügen?" reactAreYouSure: "Willst du eine \"{emoji}\"-Reaktion hinzufügen?"
markAsSensitiveConfirm: "Möchtest du dieses Medium als sensibel kennzeichnen?" markAsSensitiveConfirm: "Möchtest du dieses Medium als sensibel kennzeichnen?"
@ -1345,6 +1356,7 @@ textCount: "Zeichenanzahl"
information: "Über" information: "Über"
chat: "Chat" chat: "Chat"
directMessage: "Mit dem Benutzer chatten" directMessage: "Mit dem Benutzer chatten"
directMessage_short: "Nachrichten"
migrateOldSettings: "Alte Client-Einstellungen migrieren" migrateOldSettings: "Alte Client-Einstellungen migrieren"
migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben." migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben."
compress: "Komprimieren" compress: "Komprimieren"
@ -1372,18 +1384,43 @@ redisplayAllTips: "Alle „Tipps und Tricks“ wieder anzeigen"
hideAllTips: "Alle „Tipps und Tricks“ ausblenden" hideAllTips: "Alle „Tipps und Tricks“ ausblenden"
defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe" defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität." defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
defaultCompressionLevel: "Standard-Kompressionsgrad"
defaultCompressionLevel_description: "Bei einem niedrigeren Wert bleibt die Qualität erhalten, aber die Dateigröße nimmt zu.<br> Bei einem höheren Wert lässt sich die Dateigröße verringern, aber die Qualität nimmt ab."
inMinutes: "Minute(n)" inMinutes: "Minute(n)"
inDays: "Tag(en)" inDays: "Tag(en)"
safeModeEnabled: "Der abgesicherte Modus ist aktiviert." safeModeEnabled: "Der abgesicherte Modus ist aktiviert."
pluginsAreDisabledBecauseSafeMode: "Da der abgesicherte Modus aktiviert ist, sind alle Plugins deaktiviert."
customCssIsDisabledBecauseSafeMode: "Da der abgesicherte Modus aktiviert ist, wird benutzerdefiniertes CSS nicht angewendet."
themeIsDefaultBecauseSafeMode: "Solange der abgesicherte Modus aktiviert ist, wird das Standard-Theme verwendet. Wenn Sie den abgesicherten Modus deaktivieren, wird es wieder zurückgesetzt."
thankYouForTestingBeta: "Vielen Dank für Ihre Unterstützung beim Testen der Beta-Version!"
createUserSpecifiedNote: "Benutzerdefinierte Notiz erstellen"
schedulePost: "Beitrag planen"
scheduleToPostOnX: "Der Beitrag wird für {x} geplant.x"
scheduledToPostOnX: "Der Beitrag ist für {x} geplant."
schedule: "Planen" schedule: "Planen"
scheduled: "Geplant" scheduled: "Geplant"
widgets: "Widgets" widgets: "Widgets"
deviceInfo: "Geräteinformation" deviceInfo: "Geräteinformation"
deviceInfoDescription: "Bei technischen Anfragen kann es hilfreich sein, die folgenden Informationen anzugeben, da dies zur Lösung des Problems beitragen kann."
youAreAdmin: "Sie sind ein Administrator" youAreAdmin: "Sie sind ein Administrator"
frame: "Rahmen"
presets: "Vorlage" presets: "Vorlage"
zeroPadding: "Nullauffüllung"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "Dateibeschriftung"
filename: "Dateiname" filename: "Dateiname"
filename_without_ext: "Dateiname ohne Erweiterung"
year: "Jahr der Aufnahme"
month: "Monat der Aufnahme"
day: "Tag der Aufnahme"
hour: "Stunde der Aufnahmezeit"
minute: "Minute der Aufnahmezeit"
second: "Sekunde der Aufnahmezeit"
camera_model: "Kameraname"
camera_lens_model: "Objektivname"
camera_mm: "Brennweite"
camera_mm_35: "Brennweite (35-mm-Äquivalent)"
_imageFrameEditor: _imageFrameEditor:
header: "Kopfzeile" header: "Kopfzeile"
font: "Schriftart" font: "Schriftart"
@ -1394,6 +1431,7 @@ _order:
newest: "Neueste zuerst" newest: "Neueste zuerst"
oldest: "Älteste zuerst" oldest: "Älteste zuerst"
_chat: _chat:
messages: "Nachrichten"
noMessagesYet: "Noch keine Nachrichten" noMessagesYet: "Noch keine Nachrichten"
newMessage: "Neue Nachricht" newMessage: "Neue Nachricht"
individualChat: "Privater Chat" individualChat: "Privater Chat"
@ -2455,6 +2493,15 @@ _widgets:
clicker: "Klickzähler" clicker: "Klickzähler"
birthdayFollowings: "Nutzer, die heute Geburtstag haben" birthdayFollowings: "Nutzer, die heute Geburtstag haben"
chat: "Mit dem Benutzer chatten" chat: "Mit dem Benutzer chatten"
_widgetOptions:
showHeader: "Kopfzeile anzeigen"
height: "Höhe"
_button:
colored: "Farbig"
_clock:
size: "Größe"
_birthdayFollowings:
period: "Dauer"
_cw: _cw:
hide: "Inhalt verbergen" hide: "Inhalt verbergen"
show: "Inhalt anzeigen" show: "Inhalt anzeigen"

View File

@ -1406,6 +1406,7 @@ youAreAdmin: "You are admin"
frame: "Frame" frame: "Frame"
presets: "Preset" presets: "Preset"
zeroPadding: "Zero padding" zeroPadding: "Zero padding"
nothingToConfigure: "No configurable options available"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "File caption" caption: "File caption"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "Show page tab bar at the bottom" showPageTabBarBottom: "Show page tab bar at the bottom"
emojiPaletteBanner: "You can register presets as palettes to display prominently in the emoji picker or customize the appearance of the picker." emojiPaletteBanner: "You can register presets as palettes to display prominently in the emoji picker or customize the appearance of the picker."
enableAnimatedImages: "Enable animated images" enableAnimatedImages: "Enable animated images"
settingsPersistence_title: "Persistence of Settings"
settingsPersistence_description1: "Enabling setting persistence prevents configuration information from being lost."
settingsPersistence_description2: "It may not be possible to enable this depending on the environment."
_chat: _chat:
showSenderName: "Show sender's name" showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send" sendOnEnter: "Press Enter to send"
@ -2541,6 +2545,44 @@ _widgets:
clicker: "Clicker" clicker: "Clicker"
birthdayFollowings: "Today's Birthdays" birthdayFollowings: "Today's Birthdays"
chat: "Chat with user" chat: "Chat with user"
_widgetOptions:
showHeader: "Show header"
transparent: "Make background transparent"
height: "Height"
_button:
colored: "Colored"
_clock:
size: "Size"
thickness: "Needle thickness"
thicknessThin: "Thin"
thicknessMedium: "Normal"
thicknessThick: "Thick"
graduations: "Dial markings"
graduationDots: "Dot"
graduationArabic: "Arabic numbers"
fadeGraduations: "Fade the scale"
sAnimation: "Second hand animation"
sAnimationElastic: "Real"
sAnimationEaseOut: "Smooth"
twentyFour: "24 Hour Format"
labelTime: "Time"
labelTz: "Timezone"
labelTimeAndTz: "Time and time zone"
timezone: "Timezone"
showMs: "Show Miliseconds"
showLabel: "Show Label"
_jobQueue:
sound: "Play Sounds"
_rss:
url: "RSS Feed Url"
refreshIntervalSec: "Update interval (in seconds)"
maxEntries: "Maximum number of items to display"
_rssTicker:
shuffle: "Random display order"
duration: "Banner scroll speed (in seconds)"
reverse: "Scroll in the opposite direction"
_birthdayFollowings:
period: "Duration"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Show content" show: "Show content"
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled" usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
flexible: "Auto-adjust width" flexible: "Auto-adjust width"
enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices" enableSyncBetweenDevicesForProfiles: "Enable profile information sync between devices"
showHowToUse: ""
_howToUse:
addColumn_title: "Add column"
addColumn_description: "You can select and add column types."
settings_title: "UI Settings"
settings_description: "You can configure detailed settings for the deck UI."
switchProfile_title: "Profile Switching"
switchProfile_description: "You can save UI layouts as profiles and switch between them at any time."
_columns: _columns:
main: "Main" main: "Main"
widgets: "Widgets" widgets: "Widgets"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "Effects" title: "Effects"
addEffect: "Add Effects" addEffect: "Add Effects"
discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes." discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes."
nothingToConfigure: "No configurable options available"
failedToLoadImage: "Failed to load image" failedToLoadImage: "Failed to load image"
_fxs: _fxs:
chromaticAberration: "Chromatic Aberration" chromaticAberration: "Chromatic Aberration"

View File

@ -1406,6 +1406,7 @@ youAreAdmin: "Eres administrador."
frame: "Marco" frame: "Marco"
presets: "Predefinido" presets: "Predefinido"
zeroPadding: "Relleno cero" zeroPadding: "Relleno cero"
nothingToConfigure: "No hay nada que configurar"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "Título del archivo" caption: "Título del archivo"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "Mostrar la barra de pestañas de la página en la parte inferior." showPageTabBarBottom: "Mostrar la barra de pestañas de la página en la parte inferior."
emojiPaletteBanner: "Puedes registrar ajustes preestablecidos como paletas para que se muestren permanentemente en el selector de emojis, o personalizar el método de visualización del selector." emojiPaletteBanner: "Puedes registrar ajustes preestablecidos como paletas para que se muestren permanentemente en el selector de emojis, o personalizar el método de visualización del selector."
enableAnimatedImages: "Habilitar imágenes animadas" enableAnimatedImages: "Habilitar imágenes animadas"
settingsPersistence_title: "Persistencia de la configuración"
settingsPersistence_description1: "Habilitar la persistencia de la configuración evita que se pierda la información de configuración."
settingsPersistence_description2: "Es posible que no se pueda habilitar esta función dependiendo del entorno."
_chat: _chat:
showSenderName: "Mostrar el nombre del remitente" showSenderName: "Mostrar el nombre del remitente"
sendOnEnter: "Intro para enviar" sendOnEnter: "Intro para enviar"
@ -1670,9 +1674,9 @@ _initialTutorial:
title: "El concepto de Línea de tiempo" title: "El concepto de Línea de tiempo"
description1: "Misskey proporciona múltiples líneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las políticas de la instancia)." description1: "Misskey proporciona múltiples líneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las políticas de la instancia)."
home: "Puedes ver los posts de las cuentas que sigues." home: "Puedes ver los posts de las cuentas que sigues."
local: "Puedes ver los posts de todos los usuarios de este servidor." local: "Puedes ver los posts de todos los usuarios de este servidor (también llamado instancia)."
social: "Se ven los posts de la línea de tiempo de inicio junto con los de la línea de tiempo local." social: "Se ven los posts de la línea de tiempo de inicio junto con los de la línea de tiempo local."
global: "Puedes ver notas de todos los servidores conectados." global: "Puedes ver notas de todos los servidores (instancias) conectados."
description2: "Puedes cambiar la línea de tiempo en la parte superior de la pantalla cuando quieras." description2: "Puedes cambiar la línea de tiempo en la parte superior de la pantalla cuando quieras."
description3: "Además, hay listas de líneas de tiempo y listas de canales. Para más detalle, por favor visita este enlace: {link}" description3: "Además, hay listas de líneas de tiempo y listas de canales. Para más detalle, por favor visita este enlace: {link}"
_postNote: _postNote:
@ -1682,14 +1686,14 @@ _initialTutorial:
description: "Puedes limitar quién puede ver tu nota." description: "Puedes limitar quién puede ver tu nota."
public: "Tu nota será visible para todos los usuarios." public: "Tu nota será visible para todos los usuarios."
home: "Publicar solo en la línea de tiempo de Inicio. La nota se verá en tu perfil, la verán tus seguidores y también cuando sea renotada." home: "Publicar solo en la línea de tiempo de Inicio. La nota se verá en tu perfil, la verán tus seguidores y también cuando sea renotada."
followers: "Visible solo para seguidores. Sólo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas." followers: "Visible solo para seguidores. Solo tus seguidores podrán ver la nota, y no podrá ser renotada por otras personas."
direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa." direct: "Visible sólo para usuarios específicos, y el destinatario será notificado. Puede usarse como alternativa a la mensajería directa."
doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!" doNotSendConfidencialOnDirect1: "¡Ten cuidado cuando vayas a enviar información sensible!"
doNotSendConfidencialOnDirect2: "Los administradores del servidor, también llamado instancia, pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores o instancias no confiables." doNotSendConfidencialOnDirect2: "Los administradores del servidor, también llamado instancia, pueden leer lo que escribes. Ten cuidado cuando envíes información sensible en notas directas en servidores o instancias no confiables."
localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba." localOnly: "Publicando con esta opción seleccionada, la nota no se federará hacia otros servidores. Los usuarios de otros servidores no podrán ver estas notas directamente, sin importar los ajustes seleccionados más arriba."
_cw: _cw:
title: "Alerta de contenido (CW)" title: "Alerta de contenido (CW)"
description: "En lugar de mostrarse el contenido de la nota, se mostrará lo que escribas en el campo \"comentarios\". Pulsando en \"leer más\" desplegará el contenido de la nota." description: "En lugar de mostrarse el contenido de la nota, se mostrará lo que escribas en el campo \"comentarios\". Pulsando en \"Ver más\" desplegará el contenido de la nota."
_exampleNote: _exampleNote:
cw: "¡Esto te hará tener hambre!" cw: "¡Esto te hará tener hambre!"
note: "Acabo de comerme un donut de chocolate glaseado 🍩😋" note: "Acabo de comerme un donut de chocolate glaseado 🍩😋"
@ -2207,7 +2211,7 @@ _registry:
key: "Clave" key: "Clave"
keys: "Clave" keys: "Clave"
domain: "Dominio" domain: "Dominio"
createKey: "Crear una llave" createKey: "Crear una clave"
_aboutMisskey: _aboutMisskey:
about: "Misskey es un software de código abierto, desarrollado por syuilo desde el 2014" about: "Misskey es un software de código abierto, desarrollado por syuilo desde el 2014"
contributors: "Principales colaboradores" contributors: "Principales colaboradores"
@ -2541,6 +2545,44 @@ _widgets:
clicker: "Cliqueador" clicker: "Cliqueador"
birthdayFollowings: "Hoy cumplen años" birthdayFollowings: "Hoy cumplen años"
chat: "Chatear" chat: "Chatear"
_widgetOptions:
showHeader: "Mostrar encabezados"
transparent: "Hacer fondo transparente"
height: "Altura"
_button:
colored: "Color"
_clock:
size: "Tamaño"
thickness: "Grosor de la aguja"
thicknessThin: "Delgada"
thicknessMedium: "Normal"
thicknessThick: "Gruesa"
graduations: "Marcas del dial"
graduationDots: "Puntos"
graduationArabic: "Números decimales"
fadeGraduations: "Desvanecer la escala"
sAnimation: "Animación de la manecilla de los segundos"
sAnimationElastic: "Real"
sAnimationEaseOut: "Suave"
twentyFour: "Formato 24 horas"
labelTime: "Hora"
labelTz: "Zona horaria"
labelTimeAndTz: "Hora y zona horaria"
timezone: "Zona horaria"
showMs: "Mostrar milisegundos"
showLabel: "Mostrar etiqueta"
_jobQueue:
sound: "Reproducir sonido"
_rss:
url: "URL del canal RSS"
refreshIntervalSec: "Intervalo de actualización (En segundos)"
maxEntries: "Número máximo de elementos a mostrar"
_rssTicker:
shuffle: "Orden de visualización aleatorio"
duration: "Velocidad de desplazamiento del baner (En segundos)"
reverse: "Desplázate en la dirección opuesta."
_birthdayFollowings:
period: "Duración"
_cw: _cw:
hide: "Ocultar" hide: "Ocultar"
show: "Ver más" show: "Ver más"
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada" usedAsMinWidthWhenFlexible: "Se usará el ancho mínimo cuando la opción \"Autoajustar ancho\" esté habilitada"
flexible: "Autoajustar ancho" flexible: "Autoajustar ancho"
enableSyncBetweenDevicesForProfiles: "Activar la sincronización de la información de perfiles entre dispositivos." enableSyncBetweenDevicesForProfiles: "Activar la sincronización de la información de perfiles entre dispositivos."
showHowToUse: "Ver la descripción de la interfaz de usuario"
_howToUse:
addColumn_title: "Añadir columna"
addColumn_description: "Puede seleccionar y añadir tipos de columnas."
settings_title: "Configuración de la interfaz de usuario"
settings_description: "Puedes configurar la interfaz de usuario en detalle."
switchProfile_title: "Cambiar de perfil"
switchProfile_description: "Puedes guardar diseños de interfaz de usuario como perfiles y cambiar entre ellos en cualquier momento."
_columns: _columns:
main: "Principal" main: "Principal"
widgets: "Widgets" widgets: "Widgets"
@ -3145,8 +3195,8 @@ _selfXssPrevention:
description2: "Si no entiendes que estás pegando exactamente, %cdetente ahora mismo y cierra esta ventana" description2: "Si no entiendes que estás pegando exactamente, %cdetente ahora mismo y cierra esta ventana"
description3: "Para más información visita esto {link}" description3: "Para más información visita esto {link}"
_followRequest: _followRequest:
recieved: "Petición de seguimiento recibida" recieved: "Solicitud de seguimiento recibida"
sent: "Petición de seguimiento enviada" sent: "Solicitud de seguimiento enviada"
_remoteLookupErrors: _remoteLookupErrors:
_federationNotAllowed: _federationNotAllowed:
title: "Incapaz de comunicarse con este servidor." title: "Incapaz de comunicarse con este servidor."
@ -3299,7 +3349,6 @@ _imageEffector:
title: "Efecto" title: "Efecto"
addEffect: "Añadir Efecto" addEffect: "Añadir Efecto"
discardChangesConfirm: "¿Ignorar cambios y salir?" discardChangesConfirm: "¿Ignorar cambios y salir?"
nothingToConfigure: "No hay opciones configurables disponibles."
failedToLoadImage: "Error al cargar la imagen" failedToLoadImage: "Error al cargar la imagen"
_fxs: _fxs:
chromaticAberration: "Aberración Cromática" chromaticAberration: "Aberración Cromática"

View File

@ -5,11 +5,12 @@ introMisskey: "Bienvenue ! Misskey est un service de microblogage décentralis
poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte <b>Misskey</b> (appelée \"instance Misskey\")." poweredByMisskeyDescription: "{name} est l'un des services propulsés par la plateforme ouverte <b>Misskey</b> (appelée \"instance Misskey\")."
monthAndDay: "{day}/{month}" monthAndDay: "{day}/{month}"
search: "Rechercher" search: "Rechercher"
reset: "Réinitialiser"
notifications: "Notifications" notifications: "Notifications"
username: "Nom dutilisateur·rice" username: "Nom dutilisateur·rice"
password: "Mot de passe" password: "Mot de passe"
initialPasswordForSetup: "Mot de passe initial pour la configuration" initialPasswordForSetup: "Mot de passe initial pour la configuration"
initialPasswordIsIncorrect: "Mot de passe initial pour la configuration est incorrecte" initialPasswordIsIncorrect: "Le mot de passe initial pour la configuration est incorrect"
initialPasswordForSetupDescription: "Utilisez le mot de passe que vous avez entré pour le fichier de configuration si vous avez installé Misskey vous-même.\nSi vous utilisez un service d'hébergement Misskey, utilisez le mot de passe fourni.\nSi vous n'avez pas défini de mot de passe, laissez le champ vide pour continuer." initialPasswordForSetupDescription: "Utilisez le mot de passe que vous avez entré pour le fichier de configuration si vous avez installé Misskey vous-même.\nSi vous utilisez un service d'hébergement Misskey, utilisez le mot de passe fourni.\nSi vous n'avez pas défini de mot de passe, laissez le champ vide pour continuer."
forgotPassword: "Mot de passe oublié" forgotPassword: "Mot de passe oublié"
fetchingAsApObject: "Récupération depuis le fédiverse …" fetchingAsApObject: "Récupération depuis le fédiverse …"
@ -48,6 +49,7 @@ pin: "Épingler sur le profil"
unpin: "Désépingler" unpin: "Désépingler"
copyContent: "Copier le contenu" copyContent: "Copier le contenu"
copyLink: "Copier le lien" copyLink: "Copier le lien"
copyRemoteLink: "Copier le lien de la note"
copyLinkRenote: "Copier le lien de la renote" copyLinkRenote: "Copier le lien de la renote"
delete: "Supprimer" delete: "Supprimer"
deleteAndEdit: "Supprimer et réécrire" deleteAndEdit: "Supprimer et réécrire"
@ -62,8 +64,8 @@ copyNoteId: "Copier l'identifiant de la note"
copyFileId: "Copier l'identifiant du fichier" copyFileId: "Copier l'identifiant du fichier"
copyFolderId: "Copier l'identifiant du dossier" copyFolderId: "Copier l'identifiant du dossier"
copyProfileUrl: "Copier l'URL du profil" copyProfileUrl: "Copier l'URL du profil"
searchUser: "Chercher un·e utilisateur·rice" searchUser: "Chercher un utilisateur"
searchThisUsersNotes: "Cherchez les notes de cet·te utilisateur·rice" searchThisUsersNotes: "Cherchez les notes de cet utilisateur"
reply: "Répondre" reply: "Répondre"
loadMore: "Afficher plus …" loadMore: "Afficher plus …"
showMore: "Voir plus" showMore: "Voir plus"
@ -81,6 +83,8 @@ files: "Fichiers"
download: "Télécharger" download: "Télécharger"
driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier « {name} » ? Les notes avec ce fichier joint seront aussi supprimées." driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier « {name} » ? Les notes avec ce fichier joint seront aussi supprimées."
unfollowConfirm: "Désirez-vous vous désabonner de {name} ?" unfollowConfirm: "Désirez-vous vous désabonner de {name} ?"
cancelFollowRequestConfirm: "Est-te vous sur de vouloir annuler la demande de suivi de {name} ?"
rejectFollowRequestConfirm: "Refuser la demande de suivi de {name} ?"
exportRequested: "Vous avez demandé une exportation. Lopération pourrait prendre un peu de temps. Une fois terminée, le fichier sera ajouté au Drive." exportRequested: "Vous avez demandé une exportation. Lopération pourrait prendre un peu de temps. Une fois terminée, le fichier sera ajouté au Drive."
importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps." importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps."
lists: "Listes" lists: "Listes"
@ -118,6 +122,8 @@ cantReRenote: "Impossible de renoter une Renote."
quote: "Citer" quote: "Citer"
inChannelRenote: "Renoter dans le canal" inChannelRenote: "Renoter dans le canal"
inChannelQuote: "Citer dans le canal" inChannelQuote: "Citer dans le canal"
renoteToChannel: "Renoter sur le canal"
renoteToOtherChannel: "Renoter sur un autre canal"
pinnedNote: "Note épinglée" pinnedNote: "Note épinglée"
pinned: "Épingler sur le profil" pinned: "Épingler sur le profil"
you: "Vous" you: "Vous"
@ -212,6 +218,7 @@ blockThisInstance: "Bloquer cette instance"
silenceThisInstance: "Mettre cette instance en sourdine" silenceThisInstance: "Mettre cette instance en sourdine"
operations: "Opérations" operations: "Opérations"
software: "Logiciel" software: "Logiciel"
softwareName: "Nom du logiciel"
version: "Version" version: "Version"
metadata: "Métadonnées" metadata: "Métadonnées"
withNFiles: "{n} fichier(s)" withNFiles: "{n} fichier(s)"
@ -231,6 +238,9 @@ blockedInstances: "Instances bloquées"
blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance." blockedInstancesDescription: "Listez les instances que vous désirez bloquer, une par ligne. Ces instances ne seront plus en capacité d'interagir avec votre instance."
silencedInstances: "Instances mises en sourdine" silencedInstances: "Instances mises en sourdine"
silencedInstancesDescription: "Énumérer les noms d'hôte des instances à mettre en sourdine. Tous les comptes des instances énumérées seront traités comme mis en sourdine, ne peuvent faire que des demandes de suivi et ne peuvent pas mentionner les comptes locaux s'ils ne sont pas suivis. Cela n'affectera pas les instances bloquées." silencedInstancesDescription: "Énumérer les noms d'hôte des instances à mettre en sourdine. Tous les comptes des instances énumérées seront traités comme mis en sourdine, ne peuvent faire que des demandes de suivi et ne peuvent pas mentionner les comptes locaux s'ils ne sont pas suivis. Cela n'affectera pas les instances bloquées."
mediaSilencedInstances: "Médias silencieux sur ces instances"
mediaSilencedInstancesDescription: "Liste des noms de serveurs où vous voulez que les médias soient silencieux, séparés par un retour à la ligne.\nTous les comptes des instances listées seront considérés comme sensibles, et ne peuvent pas utilisés d'émojis personnalisés. Ceci n'affectera pas les serveurs bloquées."
federationAllowedHosts: "Serveurs qui autorisent la fédération"
muteAndBlock: "Masqué·e·s / Bloqué·e·s" muteAndBlock: "Masqué·e·s / Bloqué·e·s"
mutedUsers: "Utilisateur·rice·s en sourdine" mutedUsers: "Utilisateur·rice·s en sourdine"
blockedUsers: "Utilisateur·rice·s bloqué·e·s" blockedUsers: "Utilisateur·rice·s bloqué·e·s"
@ -2005,6 +2015,14 @@ _widgets:
_userList: _userList:
chooseList: "Sélectionner une liste" chooseList: "Sélectionner une liste"
birthdayFollowings: "Utilisateurs qui fêtent l'anniversaire aujourd'hui" birthdayFollowings: "Utilisateurs qui fêtent l'anniversaire aujourd'hui"
_widgetOptions:
height: "Hauteur"
_button:
colored: "Coloré"
_clock:
size: "Taille"
_birthdayFollowings:
period: "Durée"
_cw: _cw:
hide: "Masquer" hide: "Masquer"
show: "Afficher le contenu" show: "Afficher le contenu"

View File

@ -2209,6 +2209,14 @@ _widgets:
clicker: "Pengeklik" clicker: "Pengeklik"
birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini" birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini"
chat: "Obrolan pengguna" chat: "Obrolan pengguna"
_widgetOptions:
height: "Tinggi"
_button:
colored: "Diwarnai"
_clock:
size: "Ukuran"
_birthdayFollowings:
period: "Durasi"
_cw: _cw:
hide: "Sembunyikan" hide: "Sembunyikan"
show: "Lihat konten" show: "Lihat konten"

View File

@ -613,7 +613,7 @@ descendingOrder: "Diminuisce"
scratchpad: "ScratchPad" scratchpad: "ScratchPad"
scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey." scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey."
uiInspector: "UI Inspector" uiInspector: "UI Inspector"
uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:." uiInspectorDescription: "Puoi visualizzare un elenco di elementi grafici presenti in memoria. I componenti dell'interfaccia grafica vengono generati dalle funzioni Ui:C:."
output: "Output" output: "Output"
script: "Script" script: "Script"
disablePagesScript: "Disabilitare AiScript nelle pagine" disablePagesScript: "Disabilitare AiScript nelle pagine"
@ -1406,6 +1406,7 @@ youAreAdmin: "Sei un amministratore"
frame: "Cornice" frame: "Cornice"
presets: "Preimpostato" presets: "Preimpostato"
zeroPadding: "Al vivo" zeroPadding: "Al vivo"
nothingToConfigure: "Niente da configurare"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "Didascalia dell'immagine" caption: "Didascalia dell'immagine"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "Visualizza le schede della pagina nella parte inferiore" showPageTabBarBottom: "Visualizza le schede della pagina nella parte inferiore"
emojiPaletteBanner: "Puoi salvare i le emoji predefinite da appuntare in alto nel raccoglitore emoji come tavolozza e personalizzare in che modo visualizzare il raccoglitore." emojiPaletteBanner: "Puoi salvare i le emoji predefinite da appuntare in alto nel raccoglitore emoji come tavolozza e personalizzare in che modo visualizzare il raccoglitore."
enableAnimatedImages: "Attivare le immagini animate" enableAnimatedImages: "Attivare le immagini animate"
settingsPersistence_title: "Configurazione persistente"
settingsPersistence_description1: "Attivando le impostazioni persistenti si può evitare di riconfigurare il client successivamente."
settingsPersistence_description2: "Potrebbe non essere possibile attivare, dipende dall'ambiente."
_chat: _chat:
showSenderName: "Mostra il nome del mittente" showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce" sendOnEnter: "Invio spedisce"
@ -1737,7 +1741,7 @@ _serverSettings:
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo."
deliverSuspendedSoftware: "Software fuori produzione" deliverSuspendedSoftware: "Software fuori produzione"
deliverSuspendedSoftwareDescription: "A causa di vulnerabilità o altri motivi, puoi interrompere la distribuzione di un software da un server specificandone il nome e la versione. Le informazioni sono fornite dall'altro server e l'autenticità non è garantita. Puoi indicare un intervallo di versione semantica, ma specificando >= 2024.3.1 non verranno incluse le versioni personalizzate come ad esempio 2024.3.1-custom.0, pertanto ti consigliamo di specificare una versione come >= 2024.3.1-0." deliverSuspendedSoftwareDescription: "A causa di vulnerabilità o altri motivi, puoi interrompere la distribuzione di un software da un server specificandone il nome e la versione. Le informazioni sono fornite dall'altro server e l'autenticità non è garantita. Puoi indicare un intervallo di versione semantica, ma specificando >= 2024.3.1 non verranno incluse le versioni personalizzate come ad esempio 2024.3.1-custom.0, pertanto ti consigliamo di specificare una versione come >= 2024.3.1-0."
singleUserMode: "Modalità utente singolo" singleUserMode: "Modalità utenza singola"
singleUserMode_description: "Se sei l'unica persona a utilizzare questo server, l'abilitazione di questa modalità ottimizzerà le prestazioni." singleUserMode_description: "Se sei l'unica persona a utilizzare questo server, l'abilitazione di questa modalità ottimizzerà le prestazioni."
signToActivityPubGet: "Firma delle richieste GET" signToActivityPubGet: "Firma delle richieste GET"
signToActivityPubGet_description: "Normalmente questa opzione dovrebbe essere abilitata. Se si verificano problemi con la comunicazione federata, disabilitarla potrebbe migliorare la situazione, ma d'altro canto potrebbe rendere impossibile la comunicazione, a seconda del server." signToActivityPubGet_description: "Normalmente questa opzione dovrebbe essere abilitata. Se si verificano problemi con la comunicazione federata, disabilitarla potrebbe migliorare la situazione, ma d'altro canto potrebbe rendere impossibile la comunicazione, a seconda del server."
@ -2541,6 +2545,44 @@ _widgets:
clicker: "Cliccheria" clicker: "Cliccheria"
birthdayFollowings: "Compleanni del giorno" birthdayFollowings: "Compleanni del giorno"
chat: "Chatta con questa persona" chat: "Chatta con questa persona"
_widgetOptions:
showHeader: "Mostra la testata"
transparent: "Sfondo trasparente"
height: "Altezza"
_button:
colored: "Colorato"
_clock:
size: "Dimensioni"
thickness: "Spessore lancette"
thicknessThin: "Sottili"
thicknessMedium: "Medie"
thicknessThick: "Larghe"
graduations: "Quadrante"
graduationDots: "Punti"
graduationArabic: "Numeri"
fadeGraduations: "Sfumatura"
sAnimation: "Animazione dei secondi"
sAnimationElastic: "Realistica"
sAnimationEaseOut: "Morbida"
twentyFour: "Formato 24 ore"
labelTime: "Orario"
labelTz: "Fuso orario"
labelTimeAndTz: "Orario e fuso orario"
timezone: "Fuso orario"
showMs: "Millisecondi visibili"
showLabel: "Etichetta visibile"
_jobQueue:
sound: "Emetti un suono"
_rss:
url: "URL del Feed RSS"
refreshIntervalSec: "Intervallo di aggiornamento (in secondi)"
maxEntries: "Quantità massima visualizzabile"
_rssTicker:
shuffle: "Ordine casuale"
duration: "Velocità di scorrimento del ticker (in secondi)"
reverse: "Direzione inversa"
_birthdayFollowings:
period: "Durata"
_cw: _cw:
hide: "Nascondere" hide: "Nascondere"
show: "Continua la lettura..." show: "Continua la lettura..."
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima"
flexible: "Larghezza flessibile" flexible: "Larghezza flessibile"
enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi" enableSyncBetweenDevicesForProfiles: "Abilita la sincronizzazione delle informazioni profilo tra dispositivi"
showHowToUse: "Guarda la spiegazione dell'interfaccia grafica"
_howToUse:
addColumn_title: "Aggiungere colonne"
addColumn_description: "Puoi selezionare un tipo di colonna e aggiungerlo."
settings_title: "Configurazione interfaccia grafica"
settings_description: "Puoi personalizzare i dettagli dell'interfaccia grafica."
switchProfile_title: "Selettore profilo"
switchProfile_description: "Puoi salvare la disposizione dell'interfaccia grafica nel tuo profilo, affinché cambi con comodità."
_columns: _columns:
main: "Principale" main: "Principale"
widgets: "Riquadri" widgets: "Riquadri"
@ -3067,7 +3117,7 @@ _contextMenu:
title: "Menu contestuale" title: "Menu contestuale"
app: "Applicazione" app: "Applicazione"
appWithShift: "Applicazione Shift+Tasto" appWithShift: "Applicazione Shift+Tasto"
native: "Interfaccia utente del browser" native: "Interfaccia grafica del browser"
_gridComponent: _gridComponent:
_error: _error:
requiredValue: "Campo obbligatorio" requiredValue: "Campo obbligatorio"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "Effetto" title: "Effetto"
addEffect: "Aggiungi effetto" addEffect: "Aggiungi effetto"
discardChangesConfirm: "Scarta le modifiche ed esci?" discardChangesConfirm: "Scarta le modifiche ed esci?"
nothingToConfigure: "Nessuna impostazione configurabile."
failedToLoadImage: "Impossibile caricare l'immagine" failedToLoadImage: "Impossibile caricare l'immagine"
_fxs: _fxs:
chromaticAberration: "Aberrazione cromatica" chromaticAberration: "Aberrazione cromatica"

View File

@ -543,6 +543,7 @@ regenerate: "再生成"
fontSize: "フォントサイズ" fontSize: "フォントサイズ"
mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ" mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ"
limitTo: "{x}を上限に" limitTo: "{x}を上限に"
showMediaListByGridInWideArea: "画面幅が広いときはメディアリストを横並びで表示する"
noFollowRequests: "フォロー申請はありません" noFollowRequests: "フォロー申請はありません"
openImageInNewTab: "画像を新しいタブで開く" openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード" dashboard: "ダッシュボード"

View File

@ -2378,6 +2378,15 @@ _widgets:
clicker: "クリッカー" clicker: "クリッカー"
birthdayFollowings: "今日誕生日のツレ" birthdayFollowings: "今日誕生日のツレ"
chat: "チャットしよか" chat: "チャットしよか"
_widgetOptions:
showHeader: "ヘッダー出す"
height: "高さ"
_button:
colored: "色付き"
_clock:
size: "大きさ"
_birthdayFollowings:
period: "期間"
_cw: _cw:
hide: "隠す" hide: "隠す"
show: "続き見して!" show: "続き見して!"

View File

@ -1406,6 +1406,7 @@ youAreAdmin: "당신은 관리자입니다."
frame: "프레임" frame: "프레임"
presets: "프리셋" presets: "프리셋"
zeroPadding: "0으로 채우기" zeroPadding: "0으로 채우기"
nothingToConfigure: "설정 항목이 없습니다."
_imageEditing: _imageEditing:
_vars: _vars:
caption: "파일 설명" caption: "파일 설명"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "페이지의 탭 바를 아래쪽에 표시" showPageTabBarBottom: "페이지의 탭 바를 아래쪽에 표시"
emojiPaletteBanner: "이모티콘 선택기에 고정 표시되는 프리셋을 팔레트로 등록하거나 선택기의 표시 방법을 커스터마이징할 수 있습니다." emojiPaletteBanner: "이모티콘 선택기에 고정 표시되는 프리셋을 팔레트로 등록하거나 선택기의 표시 방법을 커스터마이징할 수 있습니다."
enableAnimatedImages: "애니메이션 이미지 활성화" enableAnimatedImages: "애니메이션 이미지 활성화"
settingsPersistence_title: "설정 영구화"
settingsPersistence_description1: "설정 영구화를 활성화하면 설정 정보를 잃어버리는 것을 방지할 수 있습니다."
settingsPersistence_description2: "환경에 따라 활성화되지 않을 수 있습니다."
_chat: _chat:
showSenderName: "발신자 이름 표시" showSenderName: "발신자 이름 표시"
sendOnEnter: "엔터로 보내기" sendOnEnter: "엔터로 보내기"
@ -2541,6 +2545,44 @@ _widgets:
clicker: "클리커" clicker: "클리커"
birthdayFollowings: "오늘이 생일인 유저" birthdayFollowings: "오늘이 생일인 유저"
chat: "채팅하기" chat: "채팅하기"
_widgetOptions:
showHeader: "해더를 표시"
transparent: "배경을 투명하게 설정"
height: "높이"
_button:
colored: "색 입히기"
_clock:
size: "크기"
thickness: "시곗바늘의 두께"
thicknessThin: "얇게"
thicknessMedium: "보통"
thicknessThick: "굵게"
graduations: "문자반의 눈금"
graduationDots: "도트"
graduationArabic: "아라비아 숫자"
fadeGraduations: "눈금 페이드"
sAnimation: "초침 애니메이션"
sAnimationElastic: "사실적으로"
sAnimationEaseOut: "매끄럽게"
twentyFour: "24시간 표시"
labelTime: "시각"
labelTz: "시간대"
labelTimeAndTz: "시각과 시간대"
timezone: "시간대"
showMs: "밀리초 표시"
showLabel: "레이블 표시"
_jobQueue:
sound: "소리 재생"
_rss:
url: "RSS 필드의 URL"
refreshIntervalSec: "갱신 간격(초)"
maxEntries: "최대 표시 건수"
_rssTicker:
shuffle: "표시 순서 셔플"
duration: "티커 스크롤 속도(초)"
reverse: "역방향으로 스크롤"
_birthdayFollowings:
period: "기간"
_cw: _cw:
hide: "숨기기" hide: "숨기기"
show: "더 보기" show: "더 보기"
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다" usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다"
flexible: "폭 자동 조정" flexible: "폭 자동 조정"
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화" enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화"
showHowToUse: "UI 설명 보기"
_howToUse:
addColumn_title: "칼럼 추가"
addColumn_description: "칼럼의 종류를 선택해 추가할 수 있습니다."
settings_title: "UI 설정"
settings_description: "덱 UI의 상세 설정을 할 수 있습니다."
switchProfile_title: "프로필 전환"
switchProfile_description: "UI의 레이아웃을 프로필로 저장하고, 언제나 전환할 수 있습니다."
_columns: _columns:
main: "메인" main: "메인"
widgets: "위젯" widgets: "위젯"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "이펙트" title: "이펙트"
addEffect: "이펙트를 추가" addEffect: "이펙트를 추가"
discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?" discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?"
nothingToConfigure: "설정 항목이 없습니다."
failedToLoadImage: "이미지 로딩에 실패했습니다." failedToLoadImage: "이미지 로딩에 실패했습니다."
_fxs: _fxs:
chromaticAberration: "색수차" chromaticAberration: "색수차"

View File

@ -431,6 +431,8 @@ _widgets:
jobQueue: "ຄິວວຽກ" jobQueue: "ຄິວວຽກ"
_userList: _userList:
chooseList: "ເລືອກບັນຊີລາຍການ" chooseList: "ເລືອກບັນຊີລາຍການ"
_widgetOptions:
height: "ຄວາມສູງ"
_cw: _cw:
show: "ໂຫຼດເພີ່ມເຕີມ" show: "ໂຫຼດເພີ່ມເຕີມ"
_visibility: _visibility:

View File

@ -1017,6 +1017,8 @@ _widgets:
jobQueue: "Job Queue" jobQueue: "Job Queue"
_userList: _userList:
chooseList: "Kies een lijst." chooseList: "Kies een lijst."
_widgetOptions:
height: "Hoogte"
_cw: _cw:
show: "Laad meer" show: "Laad meer"
_visibility: _visibility:

View File

@ -639,6 +639,10 @@ _widgets:
userList: "Brukerliste" userList: "Brukerliste"
_userList: _userList:
chooseList: "Velg liste" chooseList: "Velg liste"
_widgetOptions:
height: "Høyde"
_clock:
size: "Størrelse"
_cw: _cw:
hide: "Skjul" hide: "Skjul"
show: "Vis mer" show: "Vis mer"

View File

@ -1361,6 +1361,14 @@ _widgets:
_userList: _userList:
chooseList: "Wybierz listę" chooseList: "Wybierz listę"
clicker: "Clicker" clicker: "Clicker"
_widgetOptions:
height: "Wysokość"
_button:
colored: "Kolorowe"
_clock:
size: "Rozmiar"
_birthdayFollowings:
period: "Czas trwania"
_cw: _cw:
hide: "Ukryj" hide: "Ukryj"
show: "Załaduj więcej" show: "Załaduj więcej"

View File

@ -2489,6 +2489,15 @@ _widgets:
clicker: "Clicker" clicker: "Clicker"
birthdayFollowings: "Usuários de aniversário hoje" birthdayFollowings: "Usuários de aniversário hoje"
chat: "Conversar com usuário" chat: "Conversar com usuário"
_widgetOptions:
showHeader: "Exibir cabeçalho"
height: "Altura"
_button:
colored: "Colorido"
_clock:
size: "Tamanho"
_birthdayFollowings:
period: "Duração"
_cw: _cw:
hide: "Esconder" hide: "Esconder"
show: "Carregar mais" show: "Carregar mais"
@ -3230,7 +3239,6 @@ _imageEffector:
title: "Efeitos" title: "Efeitos"
addEffect: "Adicionar efeitos" addEffect: "Adicionar efeitos"
discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas." discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas."
nothingToConfigure: "Não há nada para configurar"
_fxs: _fxs:
chromaticAberration: "Aberração cromática" chromaticAberration: "Aberração cromática"
glitch: "Glitch" glitch: "Glitch"

View File

@ -1301,6 +1301,12 @@ _widgets:
jobQueue: "coada de job-uri" jobQueue: "coada de job-uri"
_userList: _userList:
chooseList: "Selectează o listă" chooseList: "Selectează o listă"
_widgetOptions:
height: "Înălţime"
_button:
colored: "Colorat"
_clock:
size: "Dimensiune"
_cw: _cw:
show: "Incarcă mai mult" show: "Incarcă mai mult"
_visibility: _visibility:

View File

@ -83,6 +83,8 @@ files: "Файлы"
download: "Скачать" download: "Скачать"
driveFileDeleteConfirm: "Удалить файл «{name}»? Заметки с ним также будут удалены." driveFileDeleteConfirm: "Удалить файл «{name}»? Заметки с ним также будут удалены."
unfollowConfirm: "Отписаться от {name} ?" unfollowConfirm: "Отписаться от {name} ?"
cancelFollowRequestConfirm: "Вы уверены, что хотите отменить запрос на подписку пользователю {name}?"
rejectFollowRequestConfirm: "Отклонить запрос на подписку от {name}?"
exportRequested: "Вы запросили экспорт. Это может занять некоторое время. Результат будет добавлен на «Диск»." exportRequested: "Вы запросили экспорт. Это может занять некоторое время. Результат будет добавлен на «Диск»."
importRequested: "Вы запросили импорт. Это может занять некоторое время." importRequested: "Вы запросили импорт. Это может занять некоторое время."
lists: "Списки" lists: "Списки"
@ -199,7 +201,7 @@ searchWith: "Найденное «{q}»"
youHaveNoLists: "У вас нет ни одного списка" youHaveNoLists: "У вас нет ни одного списка"
followConfirm: "Подписаться на {name}?" followConfirm: "Подписаться на {name}?"
proxyAccount: "Учётная запись прокси" proxyAccount: "Учётная запись прокси"
proxyAccountDescription: "Учетная запись прокси предназначена служить подписчиком на пользователей с других сайтов. Например, если пользователь добавит кого-то с другого сайта а список, деятельность того не отобразится, пока никто с этого же сайта не подписан на него. Чтобы это стало возможным, на него подписывается прокси." proxyAccountDescription: "Учетная запись прокси предназначена служить подписчиком на пользователей с других сайтов. Например: если пользователь добавит кого-то с другого сайта в список, то деятельность того не отобразится, пока никто с этого же сайта не подписан на него. Чтобы это стало возможным, на него подписывается прокси."
host: "Хост" host: "Хост"
selectSelf: "Выбрать себя" selectSelf: "Выбрать себя"
selectUser: "Выберите пользователя" selectUser: "Выберите пользователя"
@ -302,6 +304,7 @@ uploadFromUrlMayTakeTime: "Загрузка может занять некото
uploadNFiles: "Загрузить {n} файл" uploadNFiles: "Загрузить {n} файл"
explore: "Обзор" explore: "Обзор"
messageRead: "Прочитали" messageRead: "Прочитали"
readAllChatMessages: "Отметить прочитанным"
noMoreHistory: "История закончилась" noMoreHistory: "История закончилась"
startChat: "Начать чат" startChat: "Начать чат"
nUsersRead: "Прочитали {n}" nUsersRead: "Прочитали {n}"
@ -328,11 +331,13 @@ dark: "Тёмный"
lightThemes: "Светлые темы" lightThemes: "Светлые темы"
darkThemes: "Тёмные темы" darkThemes: "Тёмные темы"
syncDeviceDarkMode: "Синхронизировать с тёмной темой системы" syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
switchDarkModeManuallyWhenSyncEnabledConfirm: "Включена функция \"{x}\". Отключить синхронизацию, чтобы переключать режим вручную?"
drive: "Диск" drive: "Диск"
fileName: "Имя файла" fileName: "Имя файла"
selectFile: "Выберите файл" selectFile: "Выберите файл"
selectFiles: "Выберите файлы" selectFiles: "Выберите файлы"
selectFolder: "Выберите папку" selectFolder: "Выберите папку"
unselectFolder: "Снять выбор"
selectFolders: "Выберите папки" selectFolders: "Выберите папки"
fileNotSelected: "Файл не выбран" fileNotSelected: "Файл не выбран"
renameFile: "Переименовать файл" renameFile: "Переименовать файл"
@ -345,6 +350,7 @@ addFile: "Добавить файл"
showFile: "Посмотреть файл" showFile: "Посмотреть файл"
emptyDrive: "Диск пуст" emptyDrive: "Диск пуст"
emptyFolder: "Папка пуста" emptyFolder: "Папка пуста"
dropHereToUpload: "Переместите файл сюда"
unableToDelete: "Удаление невозможно" unableToDelete: "Удаление невозможно"
inputNewFileName: "Введите имя нового файла" inputNewFileName: "Введите имя нового файла"
inputNewDescription: "Введите новую подпись" inputNewDescription: "Введите новую подпись"
@ -458,7 +464,7 @@ moderator: "Модератор"
moderation: "Модерация" moderation: "Модерация"
moderationNote: "Примечания модератора" moderationNote: "Примечания модератора"
moderationNoteDescription: "Вы можете заполнять заметки, которые будут доступны только модераторам." moderationNoteDescription: "Вы можете заполнять заметки, которые будут доступны только модераторам."
addModerationNote: "" addModerationNote: "Оставить заметку"
moderationLogs: "Журнал модерации" moderationLogs: "Журнал модерации"
nUsersMentioned: "Упомянуло пользователей: {n}" nUsersMentioned: "Упомянуло пользователей: {n}"
securityKeyAndPasskey: "Ключ безопасности и парольная фраза" securityKeyAndPasskey: "Ключ безопасности и парольная фраза"
@ -602,7 +608,7 @@ installedDate: "Дата установки"
lastUsedDate: "Дата использования" lastUsedDate: "Дата использования"
state: "Состояние" state: "Состояние"
sort: "Сортировать" sort: "Сортировать"
ascendingOrder: "по возрастанию" ascendingOrder: "По возрастанию"
descendingOrder: "По убыванию" descendingOrder: "По убыванию"
scratchpad: "Когтеточка" scratchpad: "Когтеточка"
scratchpadDescription: "«Когтеточка» — это место для опытов с AiScript. Здесь можно писать программы, взаимодействующие с Misskey, запускать и смотреть что из этого получается." scratchpadDescription: "«Когтеточка» — это место для опытов с AiScript. Здесь можно писать программы, взаимодействующие с Misskey, запускать и смотреть что из этого получается."
@ -623,9 +629,9 @@ removeAllFollowingDescription: "Отменить все подписки с до
userSuspended: "Эта учётная запись заморожена" userSuspended: "Эта учётная запись заморожена"
userSilenced: "Этот пользователь был заглушен" userSilenced: "Этот пользователь был заглушен"
yourAccountSuspendedTitle: "Эта учетная запись заблокирована" yourAccountSuspendedTitle: "Эта учетная запись заблокирована"
yourAccountSuspendedDescription: "Эта учетная запись была заблокирована из-за нарушения условий предоставления услуг сервера. Свяжитесь с администратором, если вы хотите узнать более подробную причину. Пожалуйста, не создавайте новую учетную запись." yourAccountSuspendedDescription: "Этот аккаунт нарушил ToS сервера, поэтому был заморожен. Свяжитесь с администратором, чтобы узнать подробности. Не пытайтесь создать новый аккаунт."
tokenRevoked: "Токен недействителен" tokenRevoked: "Токен недействителен"
tokenRevokedDescription: "Срок действия вашего токена входа истек. Пожалуйста, войдите снова." tokenRevokedDescription: "Токен входа устарел. Пожалуйста, войдите снова."
accountDeleted: "Учетная запись удалена" accountDeleted: "Учетная запись удалена"
accountDeletedDescription: "Эта учетная запись удалена" accountDeletedDescription: "Эта учетная запись удалена"
menu: "Меню" menu: "Меню"
@ -684,9 +690,9 @@ smtpPort: "Порт"
smtpUser: "Имя пользователя" smtpUser: "Имя пользователя"
smtpPass: "Пароль" smtpPass: "Пароль"
emptyToDisableSmtpAuth: "Не заполняйте имя пользователя и пароль, чтобы отключить аутентификацию в SMTP." emptyToDisableSmtpAuth: "Не заполняйте имя пользователя и пароль, чтобы отключить аутентификацию в SMTP."
smtpSecure: "Использовать SSL/TLS для SMTP-соединений" smtpSecure: "Использовать SSL/TLS"
smtpSecureInfo: "Выключите при использовании STARTTLS." smtpSecureInfo: "Выключите при использовании STARTTLS."
testEmail: "Проверка доставки электронной почты" testEmail: "Отправить тестовое письмо"
wordMute: "Скрытие слов" wordMute: "Скрытие слов"
wordMuteDescription: "Сведите к минимуму записи, содержащие указанное утверждение. Нажмите на свернутую запись, чтобы отобразить ее." wordMuteDescription: "Сведите к минимуму записи, содержащие указанное утверждение. Нажмите на свернутую запись, чтобы отобразить ее."
hardWordMute: "Строгое скрытие слов" hardWordMute: "Строгое скрытие слов"
@ -772,6 +778,7 @@ lockedAccountInfo: "Даже если вы вручную подтверждае
alwaysMarkSensitive: "Отмечать файлы как «содержимое не для всех» по умолчанию" alwaysMarkSensitive: "Отмечать файлы как «содержимое не для всех» по умолчанию"
loadRawImages: "Сразу показывать изображения в полном размере" loadRawImages: "Сразу показывать изображения в полном размере"
disableShowingAnimatedImages: "Не проигрывать анимацию" disableShowingAnimatedImages: "Не проигрывать анимацию"
disableShowingAnimatedImages_caption: "Если анимации всё равно не работают, проверьте настройки специальных возможностей и режимы экономии заряда в браузере или системе"
highlightSensitiveMedia: "Выделять содержимое не для всех" highlightSensitiveMedia: "Выделять содержимое не для всех"
verificationEmailSent: "Вам отправлено письмо для подтверждения. Пройдите, пожалуйста, по ссылке из письма, чтобы завершить проверку." verificationEmailSent: "Вам отправлено письмо для подтверждения. Пройдите, пожалуйста, по ссылке из письма, чтобы завершить проверку."
notSet: "Не настроено" notSet: "Не настроено"
@ -779,7 +786,7 @@ emailVerified: "Адрес электронной почты подтвержд
noteFavoritesCount: "Количество добавленного в избранное" noteFavoritesCount: "Количество добавленного в избранное"
pageLikesCount: "Количество понравившихся страниц" pageLikesCount: "Количество понравившихся страниц"
pageLikedCount: "Количество страниц, понравившихся другим" pageLikedCount: "Количество страниц, понравившихся другим"
contact: "Как связаться" contact: "Почта для связи"
useSystemFont: "Использовать шрифт, предлагаемый системой" useSystemFont: "Использовать шрифт, предлагаемый системой"
clips: "Подборки" clips: "Подборки"
experimentalFeatures: "Экспериментальные функции" experimentalFeatures: "Экспериментальные функции"
@ -838,7 +845,7 @@ showingPastTimeline: "Отображается старая лента"
clear: "Очистить" clear: "Очистить"
markAllAsRead: "Отметить всё как прочитанное" markAllAsRead: "Отметить всё как прочитанное"
goBack: "Выход" goBack: "Выход"
unlikeConfirm: "В самом деле отменить «нравится»?" unlikeConfirm: "В самом деле убрать «нравится»?"
fullView: "Полный вид" fullView: "Полный вид"
quitFullView: "Закрыть полный вид" quitFullView: "Закрыть полный вид"
addDescription: "Добавить описание" addDescription: "Добавить описание"
@ -883,7 +890,7 @@ priority: "Приоритет"
high: "Высокий" high: "Высокий"
middle: "Средне" middle: "Средне"
low: "Низкий" low: "Низкий"
emailNotConfiguredWarning: "Не указан адрес электронной почты" emailNotConfiguredWarning: "Адрес почты пустует"
ratio: "Соотношение" ratio: "Соотношение"
previewNoteText: "Предварительный просмотр" previewNoteText: "Предварительный просмотр"
customCss: "Индивидуальный CSS" customCss: "Индивидуальный CSS"
@ -963,13 +970,13 @@ reflectMayTakeTime: "Изменения могут занять время дл
failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте" failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте"
rateLimitExceeded: "Ограничение скорости превышено" rateLimitExceeded: "Ограничение скорости превышено"
cropImage: "Кадрирование" cropImage: "Кадрирование"
cropImageAsk: "Нужно ли кадрировать изображение?" cropImageAsk: "Обрезать изображение?"
cropYes: "Обрезать" cropYes: "Обрезать"
cropNo: "Не обрезать" cropNo: "Не обрезать"
file: "Файлы" file: "Файлы"
recentNHours: "Последние {n} ч" recentNHours: "Последние {n} ч"
recentNDays: "Последние {n} сут" recentNDays: "Последние {n} сут"
noEmailServerWarning: "Почтовый сервер не установлен " noEmailServerWarning: "Отправка писем выключена"
thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жалобы" thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жалобы"
recommended: "Рекомендуем" recommended: "Рекомендуем"
check: "Проверить" check: "Проверить"
@ -983,7 +990,7 @@ document: "Документ"
numberOfPageCache: "Количество сохранённых страниц в кэше" numberOfPageCache: "Количество сохранённых страниц в кэше"
numberOfPageCacheDescription: "Описание количества страниц в кэше" numberOfPageCacheDescription: "Описание количества страниц в кэше"
logoutConfirm: "Вы хотите выйти из аккаунта?" logoutConfirm: "Вы хотите выйти из аккаунта?"
logoutWillClearClientData: "Когда вы выйдете из системы, информация о конфигурации клиента будет удалена из браузера.Чтобы иметь возможность восстановить информацию о вашей конфигурации при повторном входе в систему, пожалуйста, включите опцию автоматического резервного копирования в настройках." logoutWillClearClientData: "Выход из аккаунта удалит настройки клиента из этого браузера. Включите автоматическое резервное копирование, чтобы иметь возможность восстановить настройки при повторном входе."
lastActiveDate: "Последняя дата использования" lastActiveDate: "Последняя дата использования"
statusbar: "Статусбар" statusbar: "Статусбар"
pleaseSelect: "Пожалуйста, выберите" pleaseSelect: "Пожалуйста, выберите"
@ -1002,6 +1009,7 @@ failedToUpload: "Сбой выгрузки"
cannotUploadBecauseInappropriate: "Файл не может быть загружен, так как было установлено, что он может содержать неприемлемое содержимое." cannotUploadBecauseInappropriate: "Файл не может быть загружен, так как было установлено, что он может содержать неприемлемое содержимое."
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске" cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла." cannotUploadBecauseExceedsFileSizeLimit: "Файл не может быть загружен, так как он превышает лимит размера файла."
cannotUploadBecauseUnallowedFileType: "Формат файла не подходит"
beta: "Бета" beta: "Бета"
enableAutoSensitive: "Автоматическое определение содержимого не для всех" enableAutoSensitive: "Автоматическое определение содержимого не для всех"
enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс." enableAutoSensitiveDescription: "Позволяет определять наличие содержимого не для всех при помощи искусственного интеллекта там, где это возможно. Даже если эту опцию отключить, она всё равно может быть включена на весь инстанс."
@ -1017,6 +1025,9 @@ pushNotificationAlreadySubscribed: "Push-уведомления уже вклю
pushNotificationNotSupported: "Push-уведмления не поддерживаются инстансом или браузером" pushNotificationNotSupported: "Push-уведмления не поддерживаются инстансом или браузером"
sendPushNotificationReadMessage: "Удалять push-уведомления когда сообщение или прочитано" sendPushNotificationReadMessage: "Удалять push-уведомления когда сообщение или прочитано"
sendPushNotificationReadMessageCaption: "На мгновение появится уведомление \"{emptyPushNotificationMessage}\". Расход заряда батареи может увеличиться " sendPushNotificationReadMessageCaption: "На мгновение появится уведомление \"{emptyPushNotificationMessage}\". Расход заряда батареи может увеличиться "
pleaseAllowPushNotification: "Пожалуйста, разрешите уведомление в браузере от сайта"
browserPushNotificationDisabled: "Вы не дали разрешение на уведомления сайту"
browserPushNotificationDisabledDescription: "Разрешите уведомления в настройках браузера от {serverName}, чтобы включить PUSH уведомления"
windowMaximize: "Развернуть" windowMaximize: "Развернуть"
windowMinimize: "Свернуть" windowMinimize: "Свернуть"
windowRestore: "Восстановить" windowRestore: "Восстановить"
@ -1038,7 +1049,7 @@ roles: "Роли"
role: "Роль" role: "Роль"
noRole: "Нет роли" noRole: "Нет роли"
normalUser: "Обычный пользователь" normalUser: "Обычный пользователь"
undefined: "неопределён" undefined: "неопределённо"
assign: "Назначить" assign: "Назначить"
unassign: "Отменить назначение" unassign: "Отменить назначение"
color: "Цвет" color: "Цвет"
@ -1053,6 +1064,7 @@ permissionDeniedError: "Операция запрещена"
permissionDeniedErrorDescription: "У этой учетной записи нет разрешения на выполнение этой операции." permissionDeniedErrorDescription: "У этой учетной записи нет разрешения на выполнение этой операции."
preset: "Шаблоны" preset: "Шаблоны"
selectFromPresets: "Выбрать из шаблонов" selectFromPresets: "Выбрать из шаблонов"
custom: "Пользовательские"
achievements: "Достижения" achievements: "Достижения"
gotInvalidResponseError: "Сервер ответил ошибкой" gotInvalidResponseError: "Сервер ответил ошибкой"
gotInvalidResponseErrorDescription: "Сервер временно не доступен. Возможно проводятся технические работы, или сервер отключен." gotInvalidResponseErrorDescription: "Сервер временно не доступен. Возможно проводятся технические работы, или сервер отключен."
@ -1091,6 +1103,7 @@ prohibitedWordsDescription2: "Разделение пробелом создаё
hiddenTags: "Скрытые хештеги" hiddenTags: "Скрытые хештеги"
hiddenTagsDescription: "Установленные теги не будут отображаться в тренде, можно установить несколько тегов." hiddenTagsDescription: "Установленные теги не будут отображаться в тренде, можно установить несколько тегов."
notesSearchNotAvailable: "Поиск заметок недоступен" notesSearchNotAvailable: "Поиск заметок недоступен"
usersSearchNotAvailable: "Функция \"поиска пользователей\" отключена"
license: "Лицензия" license: "Лицензия"
unfavoriteConfirm: "Удалить избранное?" unfavoriteConfirm: "Удалить избранное?"
myClips: "Мои подборки" myClips: "Мои подборки"
@ -1129,7 +1142,7 @@ vertical: "Вертикально"
horizontal: "Горизонтально" horizontal: "Горизонтально"
position: "Позиция" position: "Позиция"
serverRules: "Правила сервера" serverRules: "Правила сервера"
pleaseConfirmBelowBeforeSignup: "Для регистрации на данном сервере, необходимо согласится с нижеследующими положениями." pleaseConfirmBelowBeforeSignup: "Прочитайте и согласитесь с информацией ниже, чтобы продолжить"
pleaseAgreeAllToContinue: "Чтобы продолжить, необходимо поставить отметки во всех полях \"согласен\"." pleaseAgreeAllToContinue: "Чтобы продолжить, необходимо поставить отметки во всех полях \"согласен\"."
continue: "Продолжить" continue: "Продолжить"
preservedUsernames: "Зарезервированные имена пользователей" preservedUsernames: "Зарезервированные имена пользователей"
@ -1178,6 +1191,9 @@ expirationDate: "Дата истечения"
noExpirationDate: "Бессрочно" noExpirationDate: "Бессрочно"
inviteCodeUsedAt: "Дата и время, когда был использован пригласительный код" inviteCodeUsedAt: "Дата и время, когда был использован пригласительный код"
registeredUserUsingInviteCode: "Пользователи, которые использовали пригласительный код" registeredUserUsingInviteCode: "Пользователи, которые использовали пригласительный код"
waitingForMailAuth: "Подтвердите вашу электронную почту"
inviteCodeCreator: "Создатель приглашения"
usedAt: "Использовано"
unused: "Неиспользованное" unused: "Неиспользованное"
used: "Использован" used: "Использован"
expired: "Срок действия приглашения истёк" expired: "Срок действия приглашения истёк"
@ -1186,43 +1202,58 @@ beSureToReadThisAsItIsImportant: "Это важно, поэтому, пожал
iHaveReadXCarefullyAndAgree: "Я прочитал(а) и согласен(сна) с условиями \"{x}" iHaveReadXCarefullyAndAgree: "Я прочитал(а) и согласен(сна) с условиями \"{x}"
dialog: "Диалог" dialog: "Диалог"
icon: "Аватар" icon: "Аватар"
forYou: "Для вас"
currentAnnouncements: "Текущие новости" currentAnnouncements: "Текущие новости"
pastAnnouncements: "Предыдущие новости" pastAnnouncements: "Предыдущие новости"
youHaveUnreadAnnouncements: "У вас есть непрочитанные уведомления" youHaveUnreadAnnouncements: "У вас есть непрочитанные уведомления"
useSecurityKey: "Используйте ключ безопасности или Passkey, следуя подсказкам браузера"
replies: "Ответы" replies: "Ответы"
renotes: "Репост" renotes: "Репост"
loadReplies: "Показать ответы" loadReplies: "Показать ответы"
loadConversation: "Загрузить беседу" loadConversation: "Загрузить беседу"
pinnedList: "Закреплённый список" pinnedList: "Закреплённый список"
keepScreenOn: "Держать экран включённым" keepScreenOn: "Держать экран включённым"
verifiedLink: "Эта ссылка принадлежит пользователю"
notifyNotes: "Оповещать о публикациях"
unnotifyNotes: "Отписаться от сообщений" unnotifyNotes: "Отписаться от сообщений"
authentication: "Аутентификация" authentication: "Аутентификация"
authenticationRequiredToContinue: "Пожалуйста, пройдите аутентификацию, чтобы продолжить" authenticationRequiredToContinue: "Пожалуйста, пройдите аутентификацию, чтобы продолжить"
dateAndTime: "Дата и время" dateAndTime: "Дата и время"
showRenotes: "Показывать репосты" showRenotes: "Показывать репосты"
edited: "Изменено" edited: "Изменено"
notificationRecieveConfig: "Настроить оповещения"
mutualFollow: "Взаимные подписки" mutualFollow: "Взаимные подписки"
followingOrFollower: "Подписки или подписчики" followingOrFollower: "Подписки или подписчики"
fileAttachedOnly: "Только заметки с файлами" fileAttachedOnly: "Только заметки с файлами"
showRepliesToOthersInTimeline: "Показывать ответы в ленте" showRepliesToOthersInTimeline: "Показывать ответы в ленте"
hideRepliesToOthersInTimeline: "Скрыть чужие ответы в ленте"
showRepliesToOthersInTimelineAll: "Показывать в ленте ответы пользователей, на которых вы подписаны" showRepliesToOthersInTimelineAll: "Показывать в ленте ответы пользователей, на которых вы подписаны"
hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны" hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны"
confirmShowRepliesAll: "Это нельзя будет отменить. Показать ответы от всех, на кого вы подписаны?"
confirmHideRepliesAll: "Это нельзя будет отменить. Скрыть ответы от всех, на кого вы подписаны?"
sourceCode: "Исходный код" sourceCode: "Исходный код"
sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему." sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему."
repositoryUrl: "Ссылка на репозиторий" repositoryUrl: "Ссылка на репозиторий"
repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey" repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey"
repositoryUrlOrTarballRequired: "Если репозиторий закрыт, необходимо предоставить ссылку на tarball. Подробности см. в файле \".config/example.yml\""
feedback: "Обратная связь" feedback: "Обратная связь"
feedbackUrl: "Ссылка для обратной связи"
impressum: "О владельце"
privacyPolicy: "Политика Конфиденциальности" privacyPolicy: "Политика Конфиденциальности"
privacyPolicyUrl: "Ссылка на Политику Конфиденциальности" privacyPolicyUrl: "Ссылка на Политику Конфиденциальности"
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности" tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара" avatarDecorations: "Украшения для аватара"
attach: "Прикрепить" attach: "Прикрепить"
detach: "Открепить"
detachAll: "Убрать всё" detachAll: "Убрать всё"
angle: "Угол" angle: "Угол"
flip: "Переворот" flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара" showAvatarDecorations: "Показать украшения для аватара"
releaseToRefresh: "Отпустите, чтобы обновить"
refreshing: "Обновление..."
pullDownToRefresh: "Опустите что бы обновить" pullDownToRefresh: "Опустите что бы обновить"
useGroupedNotifications: "Отображать уведомления сгруппировано" useGroupedNotifications: "Отображать уведомления сгруппировано"
emailVerificationFailedError: "Не смогли подтвердить почту. Вероятно, истек срок письма"
cwNotationRequired: "Если включена опция «Скрыть содержимое», необходимо написать аннотацию." cwNotationRequired: "Если включена опция «Скрыть содержимое», необходимо написать аннотацию."
doReaction: "Добавить реакцию" doReaction: "Добавить реакцию"
code: "Код" code: "Код"
@ -1232,34 +1263,49 @@ overwriteContentConfirm: "Текущее содержимое будет пер
seasonalScreenEffect: "Эффект времени года на экране" seasonalScreenEffect: "Эффект времени года на экране"
decorate: "Украсить" decorate: "Украсить"
addMfmFunction: "Добавить MFM" addMfmFunction: "Добавить MFM"
enableQuickAddMfmFunction: "Показывать расширенный выбор MFM"
bubbleGame: "BubbleGame" bubbleGame: "BubbleGame"
sfx: "Звуковые эффекты" sfx: "Звуковые эффекты"
soundWillBePlayed: "Будет воспроизведен звук" soundWillBePlayed: "Будет воспроизведен звук"
showReplay: "Показать повтор" showReplay: "Показать повтор"
replay: "Ответить"
endReplay: "Конец повтора" endReplay: "Конец повтора"
lastNDays: "Последние {n} сут" lastNDays: "Последние {n} сут"
hemisphere: "Место проживания" hemisphere: "Место проживания"
userSaysSomethingSensitive: "Сообщение, содержит конфиденциальные файлы от {name}" userSaysSomethingSensitive: "Сообщение, содержит конфиденциальные файлы от {name}"
enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки" enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки"
loading: "Загрузка"
surrender: "Этот пост не может быть отменен." surrender: "Этот пост не может быть отменен."
gameRetry: "Повторить попытку" gameRetry: "Повторить попытку"
notUsePleaseLeaveBlank: "Если не используется, оставьте пустым" notUsePleaseLeaveBlank: "Если не используется, оставьте пустым"
useTotp: "Включить двухэтапную проверку"
useBackupCode: "Использовать резервные коды"
launchApp: "Запустить приложение"
useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука" useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука"
keepOriginalFilename: "Сохранять исходное имя файла" keepOriginalFilename: "Сохранять исходное имя файла"
keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке." keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке."
noDescription: "Нет описания"
alwaysConfirmFollow: "Всегда подтверждать подписку" alwaysConfirmFollow: "Всегда подтверждать подписку"
inquiry: "Связаться" inquiry: "Связаться"
tryAgain: "Попробуйте еще раз позже"
confirmWhenRevealingSensitiveMedia: "Спрашивать перед открытием NSFW контента"
sensitiveMediaRevealConfirm: "Возможно, это NSFW контент. Показать?"
createdLists: "Созданные списки"
createdAntennas: "Созданные антенны"
fromX: "Из {x}" fromX: "Из {x}"
genEmbedCode: "Сгенерировать код для " genEmbedCode: "Сгенерировать код для "
noteOfThisUser: "Список заметок этого пользователя" noteOfThisUser: "Список заметок этого пользователя"
clipNoteLimitExceeded: "К этому клипу больше нельзя добавить заметки" clipNoteLimitExceeded: "К этому клипу больше нельзя добавить заметки"
performance: "Производительность" performance: "Производительность"
modified: "Изменено" modified: "Изменено"
discard: "Отменить"
thereAreNChanges: "Изменено: {n}"
signinWithPasskey: "Войдите в систему, используя свой пароль" signinWithPasskey: "Войдите в систему, используя свой пароль"
unknownWebAuthnKey: "Неизвестный ключ" unknownWebAuthnKey: "Неизвестный ключ"
passkeyVerificationFailed: "Ошибка проверка ключа доступа " passkeyVerificationFailed: "Ошибка проверка ключа доступа "
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Проверка Passkey выполнена, но вход без пароля отключен"
messageToFollower: "Сообщение подписчикам" messageToFollower: "Сообщение подписчикам"
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. <strong>Не использовать это в рабочей среде</strong>" testCaptchaWarning: "Эта тестовая CAPTCHA. <strong>Не используйте её!</strong>"
prohibitedWordsForNameOfUser: "Запрещенные слова (имя пользователя)" prohibitedWordsForNameOfUser: "Запрещенные слова (имя пользователя)"
prohibitedWordsForNameOfUserDescription: "Если имя пользователя содержит строку из этого списка, изменение имени пользователя будет запрещено. На пользователей с правами модератора это ограничение не распространяется. Имена пользователей также проверяются путём замены всех букв в нижнем регистре" prohibitedWordsForNameOfUserDescription: "Если имя пользователя содержит строку из этого списка, изменение имени пользователя будет запрещено. На пользователей с правами модератора это ограничение не распространяется. Имена пользователей также проверяются путём замены всех букв в нижнем регистре"
yourNameContainsProhibitedWords: "Имя, которое вы пытаетесь изменить, содержит запрещенную строку символов" yourNameContainsProhibitedWords: "Имя, которое вы пытаетесь изменить, содержит запрещенную строку символов"
@ -1268,24 +1314,65 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "Автор сообщения у
lockdown: "Доступ ограничен" lockdown: "Доступ ограничен"
pleaseSelectAccount: "Выберите свой аккаунт" pleaseSelectAccount: "Выберите свой аккаунт"
availableRoles: "Доступные роли" availableRoles: "Доступные роли"
federationSpecified: "Сервер работает через белый список федерации. Связь с другими серверами ограничена"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах." federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик" draft: "Черновик"
draftsAndScheduledNotes: "Черновики и отложенные публикации"
confirmOnReact: "Подтверждать добавление реакции"
reactAreYouSure: "Добавить {emoji}?"
markAsSensitiveConfirm: "Отметить контент как чувствительный?" markAsSensitiveConfirm: "Отметить контент как чувствительный?"
unmarkAsSensitiveConfirm: "Снять пометку о NSFW контенте?"
preferences: "Основное" preferences: "Основное"
accessibility: "Специальные возможности"
preferencesProfile: "Настройки профиля"
copyPreferenceId: "Копировать ID настройки"
resetToDefaultValue: "Сбросить настройки до стандартных" resetToDefaultValue: "Сбросить настройки до стандартных"
overrideByAccount: "Переопределить этим аккаунтом"
untitled: "Без названия"
noName: "Имя не указано"
skip: "Пропустить"
syncBetweenDevices: "Синхронизировать между устройствами" syncBetweenDevices: "Синхронизировать между устройствами"
postForm: "Форма отправки" postForm: "Форма отправки"
textCount: "Количество символов" textCount: "Количество символов"
information: "Описание" information: "Описание"
inMinutes: "мин" inMinutes: "мин"
inDays: "сут" inDays: "сут"
schedule: "Отложить"
scheduled: "Отложено"
widgets: "Виджеты" widgets: "Виджеты"
deviceInfo: "Об устройстве"
deviceInfoDescription: "Эта информация может быть полезна при обращении в поддержку"
youAreAdmin: "Вы администратор"
frame: "Рамки"
presets: "Шаблоны" presets: "Шаблоны"
zeroPadding: "Без отступов"
nothingToConfigure: "Нечего менять"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "Описание файла"
filename: "Имя файла" filename: "Имя файла"
filename_without_ext: "Имя файла без расширения"
year: "Год создания"
month: "Месяц создания"
day: "День создания"
hour: "Час создания"
minute: "Минуты создания"
second: "Секунды создания"
camera_model: "Модель камеры"
camera_lens_model: "Модель линзы"
camera_mm: "Фокусное расстояние"
camera_mm_35: "Фокусное расстояние (экв. 35 мм)"
camera_f: "Диафрагма"
camera_s: "Выдержка"
camera_iso: "ISO"
gps_lat: "Широта"
gps_long: "Долгота"
_imageFrameEditor: _imageFrameEditor:
title: "Редактировать рамку"
header: "Заголовок" header: "Заголовок"
footer: "Нижняя часть"
borderThickness: "Толщина рамки"
labelThickness: "Толщина границ"
font: "Шрифт" font: "Шрифт"
fontSerif: "Антиква (с засечками)" fontSerif: "Антиква (с засечками)"
fontSansSerif: "Гротеск (без засечек)" fontSansSerif: "Гротеск (без засечек)"
@ -1661,6 +1748,7 @@ _emailUnavailable:
disposable: "Временный адрес электронной почты не принимается" disposable: "Временный адрес электронной почты не принимается"
mx: "Неверный почтовый сервер" mx: "Неверный почтовый сервер"
smtp: "Почтовый сервер не отвечает" smtp: "Почтовый сервер не отвечает"
banned: "Этот адрес почты недоступен"
_ffVisibility: _ffVisibility:
public: "Общедоступны" public: "Общедоступны"
followers: "Показываются только подписчикам" followers: "Показываются только подписчикам"
@ -1921,6 +2009,7 @@ _permissions:
"read:gallery-likes": "Просмотр списка понравившегося в галерее" "read:gallery-likes": "Просмотр списка понравившегося в галерее"
"write:gallery-likes": "Изменение списка понравившегося в галерее" "write:gallery-likes": "Изменение списка понравившегося в галерее"
"write:admin:reset-password": "Сбросить пароль пользователю" "write:admin:reset-password": "Сбросить пароль пользователю"
"write:admin:send-email": "Отправить письмо"
"write:chat": "Писать и удалять сообщения" "write:chat": "Писать и удалять сообщения"
_auth: _auth:
shareAccessTitle: "Разрешения для приложений" shareAccessTitle: "Разрешения для приложений"
@ -1976,6 +2065,14 @@ _widgets:
chooseList: "Выберите список" chooseList: "Выберите список"
clicker: "Счётчик щелчков" clicker: "Счётчик щелчков"
birthdayFollowings: "Пользователи, у которых сегодня день рождения" birthdayFollowings: "Пользователи, у которых сегодня день рождения"
_widgetOptions:
height: "Высота"
_button:
colored: "Выделена цветом"
_clock:
size: "Размер"
_birthdayFollowings:
period: "Длительность"
_cw: _cw:
hide: "Спрятать" hide: "Спрятать"
show: "Показать" show: "Показать"
@ -2245,6 +2342,7 @@ _abuseReport:
mail: "Электронная почта" mail: "Электронная почта"
webhook: "Вебхук" webhook: "Вебхук"
_captions: _captions:
mail: "Уведомлять модераторов по почте (только при поступлении жалоб)"
webhook: "Отправить уведомление Системному Вебхуку при получении или разрешении жалоб." webhook: "Отправить уведомление Системному Вебхуку при получении или разрешении жалоб."
notifiedWebhook: "Используемый Вебхук" notifiedWebhook: "Используемый Вебхук"
_moderationLogTypes: _moderationLogTypes:

View File

@ -1232,6 +1232,14 @@ _widgets:
aichan: "Ai" aichan: "Ai"
_userList: _userList:
chooseList: "Vyberte zoznam" chooseList: "Vyberte zoznam"
_widgetOptions:
height: "Výška"
_button:
colored: "Farebné"
_clock:
size: "Veľkosť"
_birthdayFollowings:
period: "Trvanie"
_cw: _cw:
hide: "Skryť" hide: "Skryť"
show: "Zobraziť viac" show: "Zobraziť viac"

View File

@ -639,6 +639,9 @@ _widgets:
jobQueue: "Jobbkö" jobQueue: "Jobbkö"
_userList: _userList:
chooseList: "Välj lista" chooseList: "Välj lista"
_widgetOptions:
_clock:
size: "Storlek"
_cw: _cw:
hide: "Dölj" hide: "Dölj"
show: "Ladda mer" show: "Ladda mer"

View File

@ -2489,6 +2489,15 @@ _widgets:
clicker: "คลิกเกอร์" clicker: "คลิกเกอร์"
birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้"
chat: "แชตเลย" chat: "แชตเลย"
_widgetOptions:
showHeader: "แสดงส่วนหัว"
height: "ความสูง"
_button:
colored: "สี"
_clock:
size: "ขนาด"
_birthdayFollowings:
period: "ระยะเวลา"
_cw: _cw:
hide: "ซ่อน" hide: "ซ่อน"
show: "โหลดเพิ่มเติม" show: "โหลดเพิ่มเติม"
@ -3230,7 +3239,6 @@ _imageEffector:
title: "เอฟเฟกต์" title: "เอฟเฟกต์"
addEffect: "เพิ่มเอฟเฟกต์" addEffect: "เพิ่มเอฟเฟกต์"
discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?" discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?"
nothingToConfigure: "ไม่มีอะไรให้ตั้งค่า"
_fxs: _fxs:
chromaticAberration: "ความคลาดสี" chromaticAberration: "ความคลาดสี"
glitch: "กลิตช์" glitch: "กลิตช์"

View File

@ -2466,6 +2466,15 @@ _widgets:
clicker: "Tıklayıcı" clicker: "Tıklayıcı"
birthdayFollowings: "Bugünün Doğum Günleri" birthdayFollowings: "Bugünün Doğum Günleri"
chat: "Sohbet" chat: "Sohbet"
_widgetOptions:
showHeader: "Başlığı göster"
height: "Yükseklik"
_button:
colored: "Renkli"
_clock:
size: "Boyut"
_birthdayFollowings:
period: "Süre"
_cw: _cw:
hide: "Gizle" hide: "Gizle"
show: "İçeriği göster" show: "İçeriği göster"
@ -3202,7 +3211,6 @@ _imageEffector:
title: "Effektler" title: "Effektler"
addEffect: "Efektler Ekle" addEffect: "Efektler Ekle"
discardChangesConfirm: "Cidden çıkmak istiyor musun? Kaydedilmemiş değişikliklerin var." discardChangesConfirm: "Cidden çıkmak istiyor musun? Kaydedilmemiş değişikliklerin var."
nothingToConfigure: "Yapılandırılabilir seçenekler mevcut değildir."
_fxs: _fxs:
chromaticAberration: "Renk Sapması" chromaticAberration: "Renk Sapması"
glitch: "Bozulma" glitch: "Bozulma"

View File

@ -1430,6 +1430,14 @@ _widgets:
userList: "Список користувачів" userList: "Список користувачів"
_userList: _userList:
chooseList: "Виберіть список" chooseList: "Виберіть список"
_widgetOptions:
height: "Висота"
_button:
colored: "Кольоровий"
_clock:
size: "Розмір"
_birthdayFollowings:
period: "Тривалість"
_cw: _cw:
hide: "Сховати" hide: "Сховати"
show: "Показати більше" show: "Показати більше"

View File

@ -945,6 +945,12 @@ _widgets:
jobQueue: "Vazifalar navbati" jobQueue: "Vazifalar navbati"
_userList: _userList:
chooseList: "Ro'yxat tanlash" chooseList: "Ro'yxat tanlash"
_widgetOptions:
height: "balandligi"
_button:
colored: "rangli"
_birthdayFollowings:
period: "Davomiylik"
_cw: _cw:
show: "Koproq korish" show: "Koproq korish"
chars: "{count} ta belgi(lar)" chars: "{count} ta belgi(lar)"

View File

@ -1826,6 +1826,14 @@ _widgets:
_userList: _userList:
chooseList: "Chọn danh sách" chooseList: "Chọn danh sách"
clicker: "clicker" clicker: "clicker"
_widgetOptions:
height: "Chiều cao"
_button:
colored: "Với màu"
_clock:
size: "Kích thước"
_birthdayFollowings:
period: "Thời hạn"
_cw: _cw:
hide: "Ẩn" hide: "Ẩn"
show: "Tải thêm" show: "Tải thêm"

View File

@ -10,7 +10,7 @@ notifications: "通知"
username: "用户名" username: "用户名"
password: "密码" password: "密码"
initialPasswordForSetup: "初始化密码" initialPasswordForSetup: "初始化密码"
initialPasswordIsIncorrect: "初始化密码不正确" initialPasswordIsIncorrect: "初始化密码不正确"
initialPasswordForSetupDescription: "如果是自己安装的 Misskey请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码请留空并继续。" initialPasswordForSetupDescription: "如果是自己安装的 Misskey请输入配置文件里设好的密码。\n如果使用的是 Misskey 的托管服务等,请输入服务商提供的密码。\n如果没有设置密码请留空并继续。"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..." fetchingAsApObject: "在联邦宇宙查询中..."
@ -146,11 +146,11 @@ markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名" enterFileName: "输入文件名"
mute: "屏蔽" mute: "屏蔽"
unmute: "取消屏蔽" unmute: "取消隐藏"
renoteMute: "屏蔽转帖" renoteMute: "隐藏转帖"
renoteUnmute: "取消屏蔽转帖" renoteUnmute: "取消隐藏转帖"
block: "拉黑" block: "屏蔽"
unblock: "取消拉黑" unblock: "取消屏蔽"
suspend: "冻结" suspend: "冻结"
unsuspend: "解除冻结" unsuspend: "解除冻结"
blockConfirm: "确定要屏蔽吗?" blockConfirm: "确定要屏蔽吗?"
@ -241,13 +241,13 @@ clearCachedFilesConfirm: "确定要清除所有缓存的远程文件吗?"
blockedInstances: "被屏蔽的服务器" blockedInstances: "被屏蔽的服务器"
blockedInstancesDescription: "设定要屏蔽的服务器,以换行分隔。被屏蔽的服务器将无法与本服务器进行交换通讯。子域名也同样会被屏蔽。" blockedInstancesDescription: "设定要屏蔽的服务器,以换行分隔。被屏蔽的服务器将无法与本服务器进行交换通讯。子域名也同样会被屏蔽。"
silencedInstances: "被静音的服务器" silencedInstances: "被静音的服务器"
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。被阻止的实例不受影响。" silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。已被屏蔽的实例不受影响。"
mediaSilencedInstances: "已隐藏媒体文件的服务器" mediaSilencedInstances: "已隐藏媒体文件的服务器"
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。已被屏蔽的实例不受影响。"
federationAllowedHosts: "允许联合的服务器" federationAllowedHosts: "允许联合的服务器"
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "隐藏/屏蔽"
mutedUsers: "已静音的用户" mutedUsers: "已隐藏的用户"
blockedUsers: "已屏蔽的用户" blockedUsers: "已屏蔽的用户"
noUsers: "无用户" noUsers: "无用户"
editProfile: "编辑资料" editProfile: "编辑资料"
@ -262,7 +262,7 @@ defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号" noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务" noJobs: "没有任务"
federating: "联合中" federating: "联合中"
blocked: "已拉黑" blocked: "已屏蔽"
suspended: "停止投递" suspended: "停止投递"
all: "全部" all: "全部"
subscribing: "已订阅" subscribing: "已订阅"
@ -693,13 +693,13 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用 STARTTLS 时关闭。" smtpSecureInfo: "使用 STARTTLS 时关闭。"
testEmail: "邮件发送测试" testEmail: "邮件发送测试"
wordMute: "屏蔽关键词" wordMute: "折叠关键词"
wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。" wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。"
hardWordMute: "屏蔽关键词" hardWordMute: "屏蔽关键词"
showMutedWord: "显示屏蔽关键词" showMutedWord: "显示已折叠的关键词"
hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。" hardWordMuteDescription: "屏蔽包含指定关键词的帖子。与折叠关键词不同,帖子将完全不会显示。"
regexpError: "正则表达式错误" regexpError: "正则表达式错误"
regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:" regexpErrorDescription: "{tab} 折叠关键词的第 {line} 行的正则表达式有错误:"
instanceMute: "已隐藏的服务器" instanceMute: "已隐藏的服务器"
userSaysSomething: "{name} 说了什么,但是被屏蔽词过滤了" userSaysSomething: "{name} 说了什么,但是被屏蔽词过滤了"
userSaysSomethingAbout: "{name} 说了关于「{word}」的什么" userSaysSomethingAbout: "{name} 说了关于「{word}」的什么"
@ -912,7 +912,7 @@ accountDeletionInProgress: "正在删除账户"
usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
aiChanMode: "小蓝模式" aiChanMode: "小蓝模式"
devMode: "开发者模式" devMode: "开发者模式"
keepCw: "回复时维持隐藏内容" keepCw: "始终开启内容警告"
pubSub: "Pub/Sub 账户" pubSub: "Pub/Sub 账户"
lastCommunication: "最近通信" lastCommunication: "最近通信"
resolved: "已解决" resolved: "已解决"
@ -931,8 +931,8 @@ manageAccounts: "管理账户"
makeReactionsPublic: "将回应设置为公开" makeReactionsPublic: "将回应设置为公开"
makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。"
classic: "经典" classic: "经典"
muteThread: "屏蔽帖文串" muteThread: "静音帖文串"
unmuteThread: "取消屏蔽帖文串" unmuteThread: "取消帖文串静音"
followingVisibility: "关注的人的公开范围" followingVisibility: "关注的人的公开范围"
followersVisibility: "关注者的公开范围" followersVisibility: "关注者的公开范围"
continueThread: "查看更多帖子" continueThread: "查看更多帖子"
@ -955,7 +955,7 @@ searchByGoogle: "Google"
instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultLightTheme: "服务器默认浅色主题"
instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultDarkTheme: "服务器默认深色主题"
instanceDefaultThemeDescription: "以对象格式输入主题代码" instanceDefaultThemeDescription: "以对象格式输入主题代码"
mutePeriod: "屏蔽期限" mutePeriod: "隐藏时长"
period: "截止时间" period: "截止时间"
indefinitely: "永久" indefinitely: "永久"
tenMinutes: "10分钟" tenMinutes: "10分钟"
@ -1293,7 +1293,7 @@ useNativeUIForVideoAudioPlayer: "使用浏览器的 UI 播放动画及音频"
keepOriginalFilename: "保持原文件名" keepOriginalFilename: "保持原文件名"
keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名将被替换为随机字符。" keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名将被替换为随机字符。"
noDescription: "没有描述" noDescription: "没有描述"
alwaysConfirmFollow: "总是确认关注" alwaysConfirmFollow: "在关注时始终确认"
inquiry: "联系我们" inquiry: "联系我们"
tryAgain: "请再试一次" tryAgain: "请再试一次"
confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认" confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
@ -1334,7 +1334,7 @@ markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?" unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
preferences: "偏好设置" preferences: "偏好设置"
accessibility: "辅助功能" accessibility: "辅助功能"
preferencesProfile: "设置的配置" preferencesProfile: "设置的配置文件"
copyPreferenceId: "复制设置 ID" copyPreferenceId: "复制设置 ID"
resetToDefaultValue: "重置为默认值" resetToDefaultValue: "重置为默认值"
overrideByAccount: "覆盖账号" overrideByAccount: "覆盖账号"
@ -1351,7 +1351,7 @@ preferenceSyncConflictChoiceDevice: "设备上的设定值"
preferenceSyncConflictChoiceCancel: "取消同步" preferenceSyncConflictChoiceCancel: "取消同步"
paste: "粘贴" paste: "粘贴"
emojiPalette: "表情符号调色板" emojiPalette: "表情符号调色板"
postForm: "投稿窗口" postForm: "发帖窗口"
textCount: "字数" textCount: "字数"
information: "关于" information: "关于"
chat: "聊天" chat: "聊天"
@ -1374,10 +1374,10 @@ advice: "建议"
realtimeMode: "实时模式" realtimeMode: "实时模式"
turnItOn: "开启" turnItOn: "开启"
turnItOff: "关闭" turnItOff: "关闭"
emojiMute: "屏蔽表情符号" emojiMute: "打码表情符号"
emojiUnmute: "取消屏蔽表情符号" emojiUnmute: "取消表情符号打码"
muteX: "屏蔽{x}" muteX: "隐藏{x}"
unmuteX: "取消屏蔽{x}" unmuteX: "取消对{x}的隐藏"
abort: "中止" abort: "中止"
tip: "提示和技巧" tip: "提示和技巧"
redisplayAllTips: "重新显示所有的提示和技巧" redisplayAllTips: "重新显示所有的提示和技巧"
@ -1406,6 +1406,7 @@ youAreAdmin: "你是管理员"
frame: "边框" frame: "边框"
presets: "预设值" presets: "预设值"
zeroPadding: "填充 0" zeroPadding: "填充 0"
nothingToConfigure: "没有项目"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "文件标题" caption: "文件标题"
@ -1485,7 +1486,7 @@ _chat:
home: "首页" home: "首页"
send: "发送" send: "发送"
newline: "换行" newline: "换行"
muteThisRoom: "屏蔽该群聊" muteThisRoom: "消息免打扰"
deleteRoom: "删除群聊" deleteRoom: "删除群聊"
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。"
@ -1548,13 +1549,16 @@ _settings:
showUrlPreview: "显示 URL 预览" showUrlPreview: "显示 URL 预览"
showAvailableReactionsFirstInNote: "在顶部显示可用的回应" showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
showPageTabBarBottom: "在下方显示页面标签栏" showPageTabBarBottom: "在下方显示页面标签栏"
emojiPaletteBanner: "可以将固定显示表情符号选择器的预设注册至调色板,也可以自定义表情符号选择器的显示方式。" emojiPaletteBanner: "可以将固定显示在表情符号选择器中的预设注册为调色板,也可以自定义表情符号选择器的显示方式。"
enableAnimatedImages: "启用动画图像" enableAnimatedImages: "启用动画图像"
settingsPersistence_title: "设置持久化"
settingsPersistence_description1: "启用设置持久化可防止设置信息丢失。"
settingsPersistence_description2: "根据环境不同,有可能无法开启。"
_chat: _chat:
showSenderName: "显示发送者的名字" showSenderName: "显示发送者的名字"
sendOnEnter: "回车键发送" sendOnEnter: "回车键发送"
_preferencesProfile: _preferencesProfile:
profileName: "配置名" profileName: "配置文件名"
profileNameDescription: "请指定用于识别此设备的名称" profileNameDescription: "请指定用于识别此设备的名称"
profileNameDescription2: "如「PC」、「手机」等" profileNameDescription2: "如「PC」、「手机」等"
manageProfiles: "管理配置文件" manageProfiles: "管理配置文件"
@ -2080,7 +2084,7 @@ _role:
canUpdateBioMedia: "可以更新头像和横幅" canUpdateBioMedia: "可以更新头像和横幅"
pinMax: "帖子置顶数量限制" pinMax: "帖子置顶数量限制"
antennaMax: "可创建的最大天线数量" antennaMax: "可创建的最大天线数量"
wordMuteMax: "屏蔽词的字数限制" wordMuteMax: "折叠词的字数限制"
webhookMax: "Webhook 创建数量限制" webhookMax: "Webhook 创建数量限制"
clipMax: "便签创建数量限制" clipMax: "便签创建数量限制"
noteEachClipsMax: "便签内贴文的最大数量" noteEachClipsMax: "便签内贴文的最大数量"
@ -2251,14 +2255,14 @@ _menuDisplay:
top: "顶部" top: "顶部"
hide: "隐藏" hide: "隐藏"
_wordMute: _wordMute:
muteWords: "要隐藏的词" muteWords: "要屏蔽的词"
muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。" muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "正则表达式用斜线包裹" muteWordsDescription2: "正则表达式用斜线包裹"
_instanceMute: _instanceMute:
instanceMuteDescription: "屏蔽服务器中所有的帖子和转帖,包括该服务器内用户的回复。" instanceMuteDescription: "隐藏来自这些服务器的所有帖子和转贴,包括这些服务器上用户的回复。"
instanceMuteDescription2: "通过换行符分隔进行设置" instanceMuteDescription2: "通过换行符分隔进行设置"
title: "下面实例中的帖子将被隐藏。" title: "下面实例中的帖子将被隐藏。"
heading: "已屏蔽的服务器" heading: "已隐藏的服务器"
_theme: _theme:
explore: "寻找主题" explore: "寻找主题"
install: "安装主题" install: "安装主题"
@ -2397,8 +2401,8 @@ _2fa:
_permissions: _permissions:
"read:account": "查看账户信息" "read:account": "查看账户信息"
"write:account": "更改帐户信息" "write:account": "更改帐户信息"
"read:blocks": "查看黑名单" "read:blocks": "查看屏蔽列表"
"write:blocks": "编辑黑名单" "write:blocks": "编辑屏蔽列表"
"read:drive": "查看网盘" "read:drive": "查看网盘"
"write:drive": "管理网盘文件" "write:drive": "管理网盘文件"
"read:favorites": "查看收藏夹" "read:favorites": "查看收藏夹"
@ -2407,8 +2411,8 @@ _permissions:
"write:following": "关注/取消关注" "write:following": "关注/取消关注"
"read:messaging": "查看私信" "read:messaging": "查看私信"
"write:messaging": "撰写或删除消息" "write:messaging": "撰写或删除消息"
"read:mutes": "查看屏蔽列表" "read:mutes": "查看已隐藏用户列表"
"write:mutes": "编辑屏蔽列表" "write:mutes": "编辑已隐藏用户列表"
"write:notes": "撰写或删除帖子" "write:notes": "撰写或删除帖子"
"read:notifications": "查看通知" "read:notifications": "查看通知"
"write:notifications": "管理通知" "write:notifications": "管理通知"
@ -2512,7 +2516,7 @@ _weekday:
_widgets: _widgets:
profile: "个人资料" profile: "个人资料"
instanceInfo: "服务器信息" instanceInfo: "服务器信息"
memo: "便" memo: "便利贴"
notifications: "通知" notifications: "通知"
timeline: "时间线" timeline: "时间线"
calendar: "日历" calendar: "日历"
@ -2525,11 +2529,11 @@ _widgets:
digitalClock: "数字时钟" digitalClock: "数字时钟"
unixClock: "UNIX 时钟" unixClock: "UNIX 时钟"
federation: "联合" federation: "联合"
instanceCloud: "服务器" instanceCloud: "服务器球状列表"
postForm: "投稿窗口" postForm: "发帖窗口"
slideshow: "幻灯片展示" slideshow: "幻灯片展示"
button: "按钮" button: "按钮"
onlineUsers: "在线用户" onlineUsers: "在线用户"
jobQueue: "作业队列" jobQueue: "作业队列"
serverMetric: "服务器指标" serverMetric: "服务器指标"
aiscript: "AiScript 控制台" aiscript: "AiScript 控制台"
@ -2541,6 +2545,44 @@ _widgets:
clicker: "点击器" clicker: "点击器"
birthdayFollowings: "今天是他们的生日" birthdayFollowings: "今天是他们的生日"
chat: "私信" chat: "私信"
_widgetOptions:
showHeader: "显示标题"
transparent: "使背景透明"
height: "高度"
_button:
colored: "彩色"
_clock:
size: "大小"
thickness: "指针宽度"
thicknessThin: "细"
thicknessMedium: "普通"
thicknessThick: "粗"
graduations: "表盘刻度"
graduationDots: "点"
graduationArabic: "阿拉伯数字"
fadeGraduations: "淡化表盘"
sAnimation: "秒针动画"
sAnimationElastic: "跳动"
sAnimationEaseOut: "平滑"
twentyFour: "24 小时制"
labelTime: "时间"
labelTz: "时区"
labelTimeAndTz: "时间和时区"
timezone: "时区"
showMs: "显示毫秒"
showLabel: "显示标签"
_jobQueue:
sound: "播放音效"
_rss:
url: "RSS feed 的 URL"
refreshIntervalSec: "更新间隔(秒)"
maxEntries: "最大显示个数"
_rssTicker:
shuffle: "随机顺序"
duration: "滚动速度(秒)"
reverse: "反方向滚动"
_birthdayFollowings:
period: "期限"
_cw: _cw:
hide: "隐藏" hide: "隐藏"
show: "查看更多" show: "查看更多"
@ -2628,10 +2670,10 @@ _exportOrImport:
favoritedNotes: "收藏的帖子" favoritedNotes: "收藏的帖子"
clips: "便签" clips: "便签"
followingList: "关注中" followingList: "关注中"
muteList: "屏蔽" muteList: "隐藏"
blockingList: "拉黑" blockingList: "屏蔽"
userLists: "列表" userLists: "列表"
excludeMutingUsers: "排除屏蔽用户" excludeMutingUsers: "排除已隐藏用户"
excludeInactiveUsers: "排除不活跃用户" excludeInactiveUsers: "排除不活跃用户"
withReplies: "在时间线中包含导入用户的回复" withReplies: "在时间线中包含导入用户的回复"
_charts: _charts:
@ -2812,10 +2854,18 @@ _deck:
introduction: "将各列进行组合以创建您自己的界面!" introduction: "将各列进行组合以创建您自己的界面!"
introduction2: "可以随时通过屏幕右侧的 + 来添加列" introduction2: "可以随时通过屏幕右侧的 + 来添加列"
widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具" widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具"
useSimpleUiForNonRootPages: "用简易UI表示非根页面" useSimpleUiForNonRootPages: "使用简易UI显示导航页面"
usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度" usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度"
flexible: "自适应宽度" flexible: "自适应宽度"
enableSyncBetweenDevicesForProfiles: "启用个人资料信息跨设备同步" enableSyncBetweenDevicesForProfiles: "启用配置文件跨设备同步"
showHowToUse: "查看用户界面说明"
_howToUse:
addColumn_title: "添加列"
addColumn_description: "可以选择要添加的列的类型。"
settings_title: "用户界面设置"
settings_description: "可以配置 Deck UI 的详细设置,"
switchProfile_title: "切换配置文件"
switchProfile_description: "将用户界面布局保存为配置文件,以便随时切换。"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"
@ -3064,10 +3114,10 @@ _mediaControls:
playbackRate: "播放速度" playbackRate: "播放速度"
loop: "循环播放" loop: "循环播放"
_contextMenu: _contextMenu:
title: "上下文菜单" title: "右键菜单"
app: "用" app: "使用"
appWithShift: "Shift 键应用" appWithShift: "按住 Shift 键使用"
native: "浏览器的用户界面" native: "浏览器的原生界面"
_gridComponent: _gridComponent:
_error: _error:
requiredValue: "此值为必填项" requiredValue: "此值为必填项"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "效果" title: "效果"
addEffect: "添加效果" addEffect: "添加效果"
discardChangesConfirm: "丢弃当前设置并退出?" discardChangesConfirm: "丢弃当前设置并退出?"
nothingToConfigure: "还没有设置"
failedToLoadImage: "图片加载失败" failedToLoadImage: "图片加载失败"
_fxs: _fxs:
chromaticAberration: "色差" chromaticAberration: "色差"

View File

@ -1089,9 +1089,9 @@ postToTheChannel: "發佈到頻道"
cannotBeChangedLater: "之後不能變更。" cannotBeChangedLater: "之後不能變更。"
reactionAcceptance: "接受表情反應" reactionAcceptance: "接受表情反應"
likeOnly: "僅限讚" likeOnly: "僅限讚"
likeOnlyForRemote: "遠端僅限讚" likeOnlyForRemote: "全部(遠端僅限讚"
nonSensitiveOnly: "僅限非敏感" nonSensitiveOnly: "僅限非敏感"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "僅限非敏感(遠端僅限讚)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "僅限非敏感(遠端僅限讚)"
rolesAssignedToMe: "指派給自己的角色" rolesAssignedToMe: "指派給自己的角色"
resetPasswordConfirm: "重設密碼?" resetPasswordConfirm: "重設密碼?"
sensitiveWords: "敏感詞" sensitiveWords: "敏感詞"
@ -1366,7 +1366,7 @@ top: "上"
embed: "嵌入" embed: "嵌入"
settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)" settingsMigrating: "正在移轉設定。請稍候……(之後也可以到「設定 → 其他 → 舊設定資訊移轉」中手動進行移轉)"
readonly: "唯讀" readonly: "唯讀"
goToDeck: "回去甲板" goToDeck: "回到多欄模式"
federationJobs: "聯邦通訊作業" federationJobs: "聯邦通訊作業"
driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。" driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。"
scrollToClose: "用滾輪關閉" scrollToClose: "用滾輪關閉"
@ -1393,7 +1393,7 @@ pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛
customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。" customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。"
themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。" themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。"
thankYouForTestingBeta: "感謝您協助驗證 beta 版!" thankYouForTestingBeta: "感謝您協助驗證 beta 版!"
createUserSpecifiedNote: "建立使用者指定的筆記" createUserSpecifiedNote: "建立指定使用者的貼文"
schedulePost: "排定發布" schedulePost: "排定發布"
scheduleToPostOnX: "排定在 {x} 發布" scheduleToPostOnX: "排定在 {x} 發布"
scheduledToPostOnX: "已排定在 {x} 發布貼文" scheduledToPostOnX: "已排定在 {x} 發布貼文"
@ -1406,6 +1406,7 @@ youAreAdmin: "您是管理員"
frame: "邊框" frame: "邊框"
presets: "預設值" presets: "預設值"
zeroPadding: "補零" zeroPadding: "補零"
nothingToConfigure: "無可設定的項目"
_imageEditing: _imageEditing:
_vars: _vars:
caption: "檔案標題" caption: "檔案標題"
@ -1550,6 +1551,9 @@ _settings:
showPageTabBarBottom: "在底部顯示頁面的標籤列" showPageTabBarBottom: "在底部顯示頁面的標籤列"
emojiPaletteBanner: "可以將固定顯示在表情符號選擇器的預設項目註冊為調色盤,或者自訂選擇器的顯示方式。" emojiPaletteBanner: "可以將固定顯示在表情符號選擇器的預設項目註冊為調色盤,或者自訂選擇器的顯示方式。"
enableAnimatedImages: "啟用動畫圖片" enableAnimatedImages: "啟用動畫圖片"
settingsPersistence_title: "設定的持久化"
settingsPersistence_description1: "啟用「設定的持久化」後,可以防止設定資訊遺失。"
settingsPersistence_description2: "依環境不同,可能無法啟用。"
_chat: _chat:
showSenderName: "顯示發送者的名稱" showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息" sendOnEnter: "按下 Enter 發送訊息"
@ -2541,6 +2545,44 @@ _widgets:
clicker: "點擊器" clicker: "點擊器"
birthdayFollowings: "今天生日的使用者" birthdayFollowings: "今天生日的使用者"
chat: "聊天" chat: "聊天"
_widgetOptions:
showHeader: "檢視標頭 "
transparent: "使背景透明"
height: "高度"
_button:
colored: "彩色"
_clock:
size: "尺寸"
thickness: "指針粗細"
thicknessThin: "細"
thicknessMedium: "普通"
thicknessThick: "粗"
graduations: "刻度盤"
graduationDots: "圓點"
graduationArabic: "阿拉伯數字"
fadeGraduations: "刻度淡出"
sAnimation: "秒針的動畫效果"
sAnimationElastic: "真實的"
sAnimationEaseOut: "滑順"
twentyFour: "24 小時制"
labelTime: "時間"
labelTz: "時區"
labelTimeAndTz: "時間與時區"
timezone: "時區"
showMs: "顯示毫秒"
showLabel: "顯示標記"
_jobQueue:
sound: "播放音效"
_rss:
url: "RSS 訂閱網址"
refreshIntervalSec: "更新間隔(秒)"
maxEntries: "最大顯示數量"
_rssTicker:
shuffle: "顯示順序隨機排列"
duration: "RSS 跑馬燈的捲動速度(秒)"
reverse: "反方向滾動"
_birthdayFollowings:
period: "時長"
_cw: _cw:
hide: "隱藏" hide: "隱藏"
show: "顯示內容" show: "顯示內容"
@ -2816,6 +2858,14 @@ _deck:
usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度" usedAsMinWidthWhenFlexible: "如果啟用「自動調整寬度」,此為最小寬度"
flexible: "自動調整寬度" flexible: "自動調整寬度"
enableSyncBetweenDevicesForProfiles: "啟用裝置與裝置之間的設定檔資料同步化" enableSyncBetweenDevicesForProfiles: "啟用裝置與裝置之間的設定檔資料同步化"
showHowToUse: "檢視使用者界面說明"
_howToUse:
addColumn_title: "新增欄位"
addColumn_description: "您可以選擇要新增的欄位類型。"
settings_title: "使用者界面設定"
settings_description: "您可以對多欄模式使用者界面做詳細設定。"
switchProfile_title: "切換設定檔"
switchProfile_description: "將使用者界面佈局儲存為設定檔,就可以隨時切換使用。"
_columns: _columns:
main: "主列" main: "主列"
widgets: "小工具" widgets: "小工具"
@ -3299,7 +3349,6 @@ _imageEffector:
title: "特效" title: "特效"
addEffect: "新增特效" addEffect: "新增特效"
discardChangesConfirm: "捨棄更改並退出嗎?" discardChangesConfirm: "捨棄更改並退出嗎?"
nothingToConfigure: "無可設定的項目"
failedToLoadImage: "圖片載入失敗" failedToLoadImage: "圖片載入失敗"
_fxs: _fxs:
chromaticAberration: "色差" chromaticAberration: "色差"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2026.1.0-alpha.4", "version": "2026.1.0-beta.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@10.27.0", "packageManager": "pnpm@10.28.2",
"workspaces": [ "workspaces": [
"packages/misskey-js", "packages/misskey-js",
"packages/i18n", "packages/i18n",
@ -28,9 +28,9 @@
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js", "start": "cd packages/backend && pnpm compile-config && node ./built/entry.js",
"start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js", "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/entry.js",
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js",
"cli": "cd packages/backend && pnpm cli", "cli": "cd packages/backend && pnpm cli",
"init": "pnpm migrate", "init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm migrate", "migrate": "cd packages/backend && pnpm migrate",
@ -52,10 +52,6 @@
"clean-all": "node scripts/clean-all.mjs", "clean-all": "node scripts/clean-all.mjs",
"cleanall": "pnpm clean-all" "cleanall": "pnpm clean-all"
}, },
"resolutions": {
"chokidar": "5.0.0",
"lodash": "4.17.21"
},
"dependencies": { "dependencies": {
"cssnano": "7.1.2", "cssnano": "7.1.2",
"esbuild": "0.27.2", "esbuild": "0.27.2",
@ -63,23 +59,23 @@
"ignore-walk": "8.0.0", "ignore-walk": "8.0.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"postcss": "8.5.6", "postcss": "8.5.6",
"tar": "7.5.2", "tar": "7.5.7",
"terser": "5.44.1" "terser": "5.46.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.2", "@eslint/js": "9.39.2",
"@misskey-dev/eslint-plugin": "2.2.0", "@misskey-dev/eslint-plugin": "2.2.0",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"@typescript/native-preview": "7.0.0-dev.20251226.1", "@typescript/native-preview": "7.0.0-dev.20260116.1",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"cypress": "15.8.1", "cypress": "15.9.0",
"eslint": "9.39.2", "eslint": "9.39.2",
"globals": "16.5.0", "globals": "16.5.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"pnpm": "10.27.0", "pnpm": "10.28.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"start-server-and-test": "2.1.3" "start-server-and-test": "2.1.3"
}, },
@ -88,7 +84,9 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"@aiscript-dev/aiscript-languageserver": "-" "@aiscript-dev/aiscript-languageserver": "-",
"chokidar": "5.0.0",
"lodash": "4.17.23"
}, },
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@sentry-internal/node-cpu-profiler", "@sentry-internal/node-cpu-profiler",

View File

@ -1,121 +0,0 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { build } from 'esbuild';
import { swcPlugin } from 'esbuild-plugin-swc';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
const resolveTsPathsPlugin = {
name: 'resolve-ts-paths',
setup(build) {
build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => {
if (args.importer) {
const absPath = join(args.resolveDir, args.path);
const tsPath = absPath.slice(0, -3) + '.ts';
if (fs.existsSync(tsPath)) return { path: tsPath };
const tsxPath = absPath.slice(0, -3) + '.tsx';
if (fs.existsSync(tsxPath)) return { path: tsxPath };
}
});
},
};
const externalIpaddrPlugin = {
name: 'external-ipaddr',
setup(build) {
build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => {
return { path: args.path, external: true };
});
},
};
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints: ['./src/boot/entry.ts'],
minify: true,
keepNames: true,
bundle: true,
outdir: './built/boot',
target: 'node22',
platform: 'node',
format: 'esm',
sourcemap: 'linked',
packages: 'external',
banner: {
js: 'import { createRequire as topLevelCreateRequire } from "module";' +
'import ___url___ from "url";' +
'const require = topLevelCreateRequire(import.meta.url);' +
'const __filename = ___url___.fileURLToPath(import.meta.url);' +
'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));',
},
plugins: [
externalIpaddrPlugin,
resolveTsPathsPlugin,
swcPlugin({
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
dynamicImport: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
},
experimental: {
keepImportAssertions: true,
},
baseUrl: join(_dirname, 'src'),
paths: {
'@/*': ['*'],
},
target: 'esnext',
keepClassNames: true,
},
}),
externalIpaddrPlugin,
],
// external: [
// 'slacc-*',
// 'class-transformer',
// 'class-validator',
// '@sentry/*',
// '@nestjs/websockets/socket-module',
// '@nestjs/microservices/microservices-module',
// '@nestjs/microservices',
// '@napi-rs/canvas-win32-x64-msvc',
// 'mock-aws-s3',
// 'aws-sdk',
// 'nock',
// 'sharp',
// 'jsdom',
// 're2',
// '@napi-rs/canvas',
// ],
};
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
if (!args.includes('--no-clean')) {
fs.rmSync('./built', { recursive: true, force: true });
}
await buildSrc();
async function buildSrc() {
console.log(`[${_package.name}] start building...`);
await build(options)
.then(() => {
console.log(`[${_package.name}] build succeeded.`);
})
.catch((err) => {
process.stderr.write(err.stderr || err.message || err);
process.exit(1);
});
console.log(`[${_package.name}] finish building.`);
}

View File

@ -1,6 +1,6 @@
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { loadConfig } from './src-js/config.js'; import { loadConfig } from './built/config.js';
import { entities } from './src-js/postgres.js'; import { entities } from './built/postgres.js';
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';

View File

@ -7,16 +7,16 @@
"node": "^22.15.0 || ^24.10.0" "node": "^22.15.0 || ^24.10.0"
}, },
"scripts": { "scripts": {
"start": "pnpm compile-config && node ./built/boot/entry.js", "start": "pnpm compile-config && node ./built/entry.js",
"start:inspect": "pnpm compile-config && node --inspect ./built/boot/entry.js", "start:inspect": "pnpm compile-config && node --inspect ./built/entry.js",
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js",
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js", "migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js", "revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
"cli": "pnpm compile-config && node ./src-js/boot/cli.js", "cli": "pnpm compile-config && node ./built/cli.js",
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js", "check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
"compile-config": "node ./scripts/compile_config.js", "compile-config": "node ./scripts/compile_config.js",
"build": "swc src -d src-js -D --strip-leading-paths && node ./build.js", "build": "rolldown -c",
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", "build:test": "swc src -d src-js -D --strip-leading-paths && swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
"watch:swc": "swc src -d built -D -w --strip-leading-paths", "watch:swc": "swc src -d built -D -w --strip-leading-paths",
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json", "build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "pnpm compile-config && node ./scripts/watch.mjs", "watch": "pnpm compile-config && node ./scripts/watch.mjs",
@ -32,26 +32,26 @@
"jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs", "jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache", "jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache",
"test": "pnpm jest", "test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test:e2e": "pnpm build:test && pnpm jest:e2e",
"test:fed": "pnpm jest:fed", "test:fed": "pnpm jest:fed",
"test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", "test-and-coverage:e2e": "pnpm build:test && pnpm jest-and-coverage:e2e",
"check-migrations": "node scripts/check_migrations_clean.js", "check-migrations": "node scripts/check_migrations_clean.js",
"generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js" "generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.15.7", "@swc/core-darwin-arm64": "1.15.8",
"@swc/core-darwin-x64": "1.15.7", "@swc/core-darwin-x64": "1.15.8",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.15.7", "@swc/core-linux-arm-gnueabihf": "1.15.8",
"@swc/core-linux-arm64-gnu": "1.15.7", "@swc/core-linux-arm64-gnu": "1.15.8",
"@swc/core-linux-arm64-musl": "1.15.7", "@swc/core-linux-arm64-musl": "1.15.8",
"@swc/core-linux-x64-gnu": "1.15.7", "@swc/core-linux-x64-gnu": "1.15.8",
"@swc/core-linux-x64-musl": "1.15.7", "@swc/core-linux-x64-musl": "1.15.8",
"@swc/core-win32-arm64-msvc": "1.15.7", "@swc/core-win32-arm64-msvc": "1.15.8",
"@swc/core-win32-ia32-msvc": "1.15.7", "@swc/core-win32-ia32-msvc": "1.15.8",
"@swc/core-win32-x64-msvc": "1.15.7", "@swc/core-win32-x64-msvc": "1.15.8",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.1.0", "bufferutil": "4.1.0",
@ -71,40 +71,39 @@
"utf-8-validate": "6.0.6" "utf-8-validate": "6.0.6"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.958.0", "@aws-sdk/client-s3": "3.970.0",
"@aws-sdk/lib-storage": "3.958.0", "@aws-sdk/lib-storage": "3.970.0",
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4", "@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0", "@fastify/cors": "11.2.0",
"@fastify/express": "4.0.2", "@fastify/express": "4.0.4",
"@fastify/http-proxy": "11.4.1", "@fastify/http-proxy": "11.4.1",
"@fastify/multipart": "9.3.0", "@fastify/multipart": "9.3.0",
"@fastify/static": "8.3.0", "@fastify/static": "8.3.0",
"@kitajs/html": "4.2.11", "@kitajs/html": "4.2.11",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5", "@misskey-dev/summaly": "5.2.5",
"@napi-rs/canvas": "0.1.87", "@napi-rs/canvas": "0.1.88",
"@nestjs/common": "11.1.10", "@nestjs/common": "11.1.12",
"@nestjs/core": "11.1.10", "@nestjs/core": "11.1.12",
"@nestjs/testing": "11.1.10", "@nestjs/testing": "11.1.12",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "10.32.1", "@sentry/node": "10.34.0",
"@sentry/profiling-node": "10.32.1", "@sentry/profiling-node": "10.34.0",
"@simplewebauthn/server": "13.2.2", "@simplewebauthn/server": "13.2.2",
"@sinonjs/fake-timers": "15.1.0", "@sinonjs/fake-timers": "15.1.0",
"@smithy/node-http-handler": "4.4.7", "@smithy/node-http-handler": "4.4.8",
"@swc/cli": "0.7.9", "@swc/cli": "0.7.10",
"@swc/core": "1.15.7", "@swc/core": "1.15.8",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.17.1", "ajv": "8.17.1",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"bcryptjs": "3.0.3", "bcryptjs": "3.0.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "2.2.1", "body-parser": "2.2.2",
"bullmq": "5.66.3", "bullmq": "5.66.5",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"chalk": "5.6.2", "chalk": "5.6.2",
"chalk-template": "1.1.2", "chalk-template": "1.1.2",
@ -113,24 +112,24 @@
"content-disposition": "1.0.1", "content-disposition": "1.0.1",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "5.6.2", "fastify": "5.7.1",
"fastify-raw-body": "5.0.0", "fastify-raw-body": "5.0.0",
"feed": "5.1.0", "feed": "5.2.0",
"file-type": "21.2.0", "file-type": "21.3.0",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5", "form-data": "4.0.5",
"got": "14.6.5", "got": "14.6.6",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"i18n": "workspace:*", "i18n": "workspace:*",
"ioredis": "5.8.2", "ioredis": "5.9.2",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0", "ipaddr.js": "2.3.0",
"is-svg": "6.1.0", "is-svg": "6.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "9.0.0", "jsonld": "9.0.0",
"juice": "11.0.3", "juice": "11.1.0",
"meilisearch": "0.54.0", "meilisearch": "0.55.0",
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"mime-types": "3.0.2", "mime-types": "3.0.2",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
@ -139,14 +138,14 @@
"nanoid": "5.1.6", "nanoid": "5.1.6",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"node-html-parser": "7.0.1", "node-html-parser": "7.0.2",
"nodemailer": "7.0.12", "nodemailer": "7.0.12",
"nsfwjs": "4.2.0", "nsfwjs": "4.2.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.4.1", "otpauth": "9.4.1",
"pg": "8.16.3", "pg": "8.17.1",
"pkce-challenge": "5.0.1", "pkce-challenge": "5.0.1",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
@ -154,7 +153,6 @@
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.23.0", "re2": "1.23.0",
"redis-info": "3.1.0",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
"rss-parser": "3.13.0", "rss-parser": "3.13.0",
@ -166,7 +164,7 @@
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.28.1", "systeminformation": "5.30.5",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.5", "tmp": "0.2.5",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
@ -174,14 +172,15 @@
"ulid": "3.0.2", "ulid": "3.0.2",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
"ws": "8.18.3", "ws": "8.19.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.3", "@kitajs/ts-html-plugin": "4.1.3",
"@nestjs/platform-express": "11.1.10", "@nestjs/platform-express": "11.1.12",
"@sentry/vue": "10.32.1", "@rollup/plugin-esm-shim": "0.1.8",
"@sentry/vue": "10.34.0",
"@simplewebauthn/types": "12.0.0", "@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39", "@swc/jest": "0.2.39",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -195,8 +194,8 @@
"@types/jsonld": "1.5.15", "@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1", "@types/mime-types": "3.0.1",
"@types/ms": "2.1.0", "@types/ms": "2.1.0",
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@types/nodemailer": "7.0.4", "@types/nodemailer": "7.0.5",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.16.0", "@types/pg": "8.16.0",
@ -214,22 +213,22 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.11", "cbor": "10.0.11",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"esbuild-plugin-swc": "1.0.1",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"execa": "9.6.1", "execa": "9.6.1",
"fkill": "10.0.1", "fkill": "10.0.3",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"nodemon": "3.1.11", "nodemon": "3.1.11",
"pid-port": "2.0.0", "pid-port": "2.0.1",
"rolldown": "1.0.0-rc.2",
"simple-oauth2": "5.1.0", "simple-oauth2": "5.1.0",
"supertest": "7.1.4", "supertest": "7.2.2",
"vite": "7.3.0" "vite": "7.3.1"
} }
} }

View File

@ -0,0 +1,44 @@
import { defineConfig } from 'rolldown';
import esmShim from '@rollup/plugin-esm-shim';
export default defineConfig([{
input: [
'./src/boot/entry.ts',
'./src/boot/cli.ts',
'./src/config.ts',
'./src/postgres.ts',
'./src/server/api/openapi/gen-spec.ts',
],
platform: 'node',
tsconfig: true,
plugins: [
esmShim(),
],
output: {
keepNames: true,
minify: true,
sourcemap: true,
dir: './built',
cleanDir: true,
format: 'esm',
},
external: [
/^slacc-.*/,
'class-transformer',
'class-validator',
/^@sentry\/.*/,
/^@sentry-internal\/.*/,
'@nestjs/websockets/socket-module',
'@nestjs/microservices/microservices-module',
'@nestjs/microservices',
/^@napi-rs\/.*/,
'mock-aws-s3',
'aws-sdk',
'nock',
'sharp',
'jsdom',
're2',
'ipaddr.js',
'oauth2orize',
],
}]);

View File

@ -4,8 +4,8 @@
*/ */
import Redis from 'ioredis'; import Redis from 'ioredis';
import { loadConfig } from '../src-js/config.js'; import { loadConfig } from '../built/config.js';
import { createPostgresDataSource } from '../src-js/postgres.js'; import { createPostgresDataSource } from '../built/postgres.js';
const config = loadConfig(); const config = loadConfig();

View File

@ -19,7 +19,7 @@ async function execBuildAssets() {
function execStart() { function execStart() {
// pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
// 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
backendProcess = execaNode('./built/boot/entry.js', [], { backendProcess = execaNode('./built/entry.js', [], {
stdout: process.stdout, stdout: process.stdout,
stderr: process.stderr, stderr: process.stderr,
env: { env: {

View File

@ -19,10 +19,10 @@ async function main() {
} }
/** @type {import('../src/config.js')} */ /** @type {import('../src/config.js')} */
const { loadConfig } = await import('../src-js/config.js'); const { loadConfig } = await import('../built/config.js');
/** @type {import('../src/server/api/openapi/gen-spec.js')} */ /** @type {import('../src/server/api/openapi/gen-spec.js')} */
const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js'); const { genOpenapiSpec } = await import('../built/gen-spec.js');
const config = loadConfig(); const config = loadConfig();
const spec = genOpenapiSpec(config, true); const spec = genOpenapiSpec(config, true);

View File

@ -14,24 +14,56 @@ import { fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import * as http from 'node:http';
import * as fs from 'node:fs/promises';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const SAMPLE_COUNT = 3; // Number of samples to measure
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
async function measureMemory() { const keys = {
const startTime = Date.now(); VmPeak: 0,
VmSize: 0,
VmHWM: 0,
VmRSS: 0,
VmData: 0,
VmStk: 0,
VmExe: 0,
VmLib: 0,
VmPTE: 0,
VmSwap: 0,
};
async function getMemoryUsage(pid) {
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
const result = {};
for (const key of Object.keys(keys)) {
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
if (match) {
result[key] = parseInt(match[1], 10);
} else {
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
}
}
return result;
}
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC // Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], { const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], {
cwd: join(__dirname, '..'), cwd: join(__dirname, '..'),
env: { env: {
...process.env, ...process.env,
NODE_ENV: 'test', NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
}, },
stdio: ['pipe', 'pipe', 'pipe', 'ipc'], stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
execArgv: [...process.execArgv, '--expose-gc'],
}); });
let serverReady = false; let serverReady = false;
@ -57,6 +89,40 @@ async function measureMemory() {
process.stderr.write(`[server error] ${err}\n`); process.stderr.write(`[server error] ${err}\n`);
}); });
async function triggerGc() {
const ok = new Promise((resolve) => {
serverProcess.once('message', (message) => {
if (message === 'gc ok') resolve();
});
});
serverProcess.send('gc');
await ok;
await setTimeout(1000);
}
function createRequest() {
return new Promise((resolve, reject) => {
const req = http.request({
host: 'localhost',
port: 61812,
path: '/api/meta',
method: 'POST',
}, (res) => {
res.on('data', () => { });
res.on('end', () => {
resolve();
});
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
// Wait for server to be ready or timeout // Wait for server to be ready or timeout
const startupStartTime = Date.now(); const startupStartTime = Date.now();
while (!serverReady) { while (!serverReady) {
@ -73,46 +139,23 @@ async function measureMemory() {
// Wait for memory to settle // Wait for memory to settle
await setTimeout(MEMORY_SETTLE_TIME); await setTimeout(MEMORY_SETTLE_TIME);
// Get memory usage from the server process via /proc
const pid = serverProcess.pid; const pid = serverProcess.pid;
let memoryInfo;
try { const beforeGc = await getMemoryUsage(pid);
const fs = await import('node:fs/promises');
// Read /proc/[pid]/status for detailed memory info await triggerGc();
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
memoryInfo = { const afterGc = await getMemoryUsage(pid);
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
};
} catch (err) {
// Fallback: use ps command
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
const { execSync } = await import('node:child_process'); // create some http requests to simulate load
try { const REQUEST_COUNT = 10;
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' }); await Promise.all(
const rssKb = parseInt(ps.trim(), 10); Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
memoryInfo = { );
rss: rssKb * 1024,
heapUsed: null, await triggerGc();
vmSize: null,
}; const afterRequest = await getMemoryUsage(pid);
} catch {
memoryInfo = {
rss: null,
heapUsed: null,
vmSize: null,
error: 'Could not measure memory',
};
}
}
// Stop the server // Stop the server
serverProcess.kill('SIGTERM'); serverProcess.kill('SIGTERM');
@ -135,15 +178,51 @@ async function measureMemory() {
const result = { const result = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
startupTimeMs: startupTime, beforeGc,
memory: memoryInfo, afterGc,
afterRequest,
};
return result;
}
async function main() {
// 直列の方が時間的に分散されて正確そうだから直列でやる
const results = [];
for (let i = 0; i < SAMPLE_COUNT; i++) {
const res = await measureMemory();
results.push(res);
}
// Calculate averages
const beforeGc = structuredClone(keys);
const afterGc = structuredClone(keys);
const afterRequest = structuredClone(keys);
for (const res of results) {
for (const key of Object.keys(keys)) {
beforeGc[key] += res.beforeGc[key];
afterGc[key] += res.afterGc[key];
afterRequest[key] += res.afterRequest[key];
}
}
for (const key of Object.keys(keys)) {
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
}
const result = {
timestamp: new Date().toISOString(),
beforeGc,
afterGc,
afterRequest,
}; };
// Output as JSON to stdout // Output as JSON to stdout
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }
measureMemory().catch((err) => { main().catch((err) => {
console.error(JSON.stringify({ console.error(JSON.stringify({
error: err.message, error: err.message,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),

View File

@ -4,16 +4,12 @@
*/ */
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
import { NestLogger } from '@/NestLogger.js'; import { NestLogger } from '@/NestLogger.js';
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
import { QueueStatsService } from '@/daemons/QueueStatsService.js';
import { ServerStatsService } from '@/daemons/ServerStatsService.js';
import { ServerService } from '@/server/ServerService.js';
import { MainModule } from '@/MainModule.js';
export async function server() { export async function server() {
const { MainModule } = await import('../MainModule.js');
const { ServerService } = await import('../server/ServerService.js');
const app = await NestFactory.createApplicationContext(MainModule, { const app = await NestFactory.createApplicationContext(MainModule, {
logger: new NestLogger(), logger: new NestLogger(),
}); });
@ -22,6 +18,10 @@ export async function server() {
await serverService.launch(); await serverService.launch();
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
const { QueueStatsService } = await import('../daemons/QueueStatsService.js');
const { ServerStatsService } = await import('../daemons/ServerStatsService.js');
app.get(ChartManagementService).start(); app.get(ChartManagementService).start();
app.get(QueueStatsService).start(); app.get(QueueStatsService).start();
app.get(ServerStatsService).start(); app.get(ServerStatsService).start();
@ -31,6 +31,10 @@ export async function server() {
} }
export async function jobQueue() { export async function jobQueue() {
const { QueueProcessorModule } = await import('../queue/QueueProcessorModule.js');
const { QueueProcessorService } = await import('../queue/QueueProcessorService.js');
const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
logger: new NestLogger(), logger: new NestLogger(),
}); });

View File

@ -13,8 +13,6 @@ import chalk from 'chalk';
import Xev from 'xev'; import Xev from 'xev';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
import { masterMain } from './master.js';
import { workerMain } from './worker.js';
import { readyRef } from './ready.js'; import { readyRef } from './ready.js';
import 'reflect-metadata'; import 'reflect-metadata';
@ -71,10 +69,12 @@ process.on('exit', code => {
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
if (cluster.isPrimary) { if (cluster.isPrimary) {
logger.info(`Start main process... pid: ${process.pid}`); logger.info(`Start main process... pid: ${process.pid}`);
const { masterMain } = await import('./master.js');
await masterMain(); await masterMain();
ev.mount(); ev.mount();
} else if (cluster.isWorker) { } else if (cluster.isWorker) {
logger.info(`Start worker process... pid: ${process.pid}`); logger.info(`Start worker process... pid: ${process.pid}`);
const { workerMain } = await import('./worker.js');
await workerMain(); await workerMain();
} else { } else {
throw new Error('Unknown process type'); throw new Error('Unknown process type');
@ -82,10 +82,23 @@ if (!envOption.disableClustering) {
} else { } else {
// 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない) // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
logger.info(`Start main process... pid: ${process.pid}`); logger.info(`Start main process... pid: ${process.pid}`);
const { masterMain } = await import('./master.js');
await masterMain(); await masterMain();
ev.mount(); ev.mount();
} }
process.on('message', msg => {
if (msg === 'gc') {
if (global.gc != null) {
logger.info('Manual GC triggered');
global.gc();
if (process.send != null) process.send('gc ok');
} else {
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
}
}
});
readyRef.value = true; readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため // ユニットテスト時にMisskeyが子プロセスで起動された時のため

View File

@ -6,7 +6,6 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { MetricsTime, type JobType } from 'bullmq'; import { MetricsTime, type JobType } from 'bullmq';
import { parse as parseRedisInfo } from 'redis-info';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js'; import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
@ -86,6 +85,19 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
pattern: '0 4 * * *', pattern: '0 4 * * *',
}]; }];
function parseRedisInfo(infoText: string): Record<string, string> {
const fields = infoText
.split('\n')
.filter(line => line.length > 0 && !line.startsWith('#'))
.map(line => line.trim().split(':'));
const result: Record<string, string> = {};
for (const [key, value] of fields) {
result[key] = value;
}
return result;
}
@Injectable() @Injectable()
export class QueueService { export class QueueService {
constructor( constructor(
@ -890,7 +902,7 @@ export class QueueService {
}, },
db: { db: {
version: db.redis_version, version: db.redis_version,
mode: db.redis_mode, mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
runId: db.run_id, runId: db.run_id,
processId: db.process_id, processId: db.process_id,
port: parseInt(db.tcp_port), port: parseInt(db.tcp_port),

View File

@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
return ['random', '1', '2'].includes(bw);
}
@Injectable() @Injectable()
export class ReversiGameEntityService { export class ReversiGameEntityService {
constructor( constructor(
@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
bw: game.bw, bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo, isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,
@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
bw: game.bw, bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo, isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere, canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard, loopedBoard: game.loopedBoard,

View File

@ -64,6 +64,7 @@ import {
packedMetaDetailedOnlySchema, packedMetaDetailedOnlySchema,
packedMetaDetailedSchema, packedMetaDetailedSchema,
packedMetaLiteSchema, packedMetaLiteSchema,
packedMetaClientOptionsSchema,
} from '@/models/json-schema/meta.js'; } from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js'; import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
@ -135,6 +136,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema, MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema, MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema, MetaDetailed: packedMetaDetailedSchema,
MetaClientOptions: packedMetaClientOptionsSchema,
UserWebhook: packedUserWebhookSchema, UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema, SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,

View File

@ -725,7 +725,11 @@ export class MiMeta {
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })
public clientOptions: Record<string, any>; public clientOptions: {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};
} }
export type SoftwareSuspension = { export type SoftwareSuspension = {

View File

@ -72,8 +72,7 @@ export const packedMetaLiteSchema = {
optional: false, nullable: true, optional: false, nullable: true,
}, },
clientOptions: { clientOptions: {
type: 'object', ref: 'MetaClientOptions',
optional: false, nullable: false,
}, },
disableRegistration: { disableRegistration: {
type: 'boolean', type: 'boolean',
@ -397,3 +396,23 @@ export const packedMetaDetailedSchema = {
}, },
], ],
} as const; } as const;
export const packedMetaClientOptionsSchema = {
type: 'object',
optional: false, nullable: false,
properties: {
entrancePageStyle: {
type: 'string',
enum: ['classic', 'simple'],
optional: false, nullable: false,
},
showTimelineForVisitor: {
type: 'boolean',
optional: false, nullable: false,
},
showActivitiesForVisitor: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -81,6 +81,7 @@ export const packedReversiGameLiteSchema = {
bw: { bw: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['random', '1', '2'],
}, },
noIrregularRules: { noIrregularRules: {
type: 'boolean', type: 'boolean',
@ -199,6 +200,7 @@ export const packedReversiGameDetailedSchema = {
bw: { bw: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['random', '1', '2'],
}, },
noIrregularRules: { noIrregularRules: {
type: 'boolean', type: 'boolean',

View File

@ -7,27 +7,22 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js'; import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { InternalStorageService } from '@/core/InternalStorageService.js'; import { InternalStorageService } from '@/core/InternalStorageService.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import { FileInfoService } from '@/core/FileInfoService.js'; import { FileInfoService } from '@/core/FileInfoService.js';
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { FileServerDriveHandler } from './file/FileServerDriveHandler.js';
import { FileServerFileResolver } from './file/FileServerFileResolver.js';
import { FileServerProxyHandler } from './file/FileServerProxyHandler.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
@ -38,6 +33,9 @@ const assets = `${_dirname}/../../server/file/assets/`;
@Injectable() @Injectable()
export class FileServerService { export class FileServerService {
private logger: Logger; private logger: Logger;
private driveHandler: FileServerDriveHandler;
private proxyHandler: FileServerProxyHandler;
private fileResolver: FileServerFileResolver;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -54,6 +52,24 @@ export class FileServerService {
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('server', 'gray'); this.logger = this.loggerService.getLogger('server', 'gray');
this.fileResolver = new FileServerFileResolver(
this.driveFilesRepository,
this.fileInfoService,
this.downloadService,
this.internalStorageService,
);
this.driveHandler = new FileServerDriveHandler(
this.config,
this.fileResolver,
assets,
this.videoProcessingService,
);
this.proxyHandler = new FileServerProxyHandler(
this.config,
this.fileResolver,
assets,
this.imageProcessingService,
);
//this.createServer = this.createServer.bind(this); //this.createServer = this.createServer.bind(this);
} }
@ -78,7 +94,7 @@ export class FileServerService {
}); });
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => { fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
return await this.sendDriveFile(request, reply) return await this.driveHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
@ -91,7 +107,7 @@ export class FileServerService {
Params: { url: string; }; Params: { url: string; };
Querystring: { url?: string; }; Querystring: { url?: string; };
}>('/proxy/:url*', async (request, reply) => { }>('/proxy/:url*', async (request, reply) => {
return await this.proxyHandler(request, reply) return await this.proxyHandler.handle(request, reply)
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
@ -116,462 +132,4 @@ export class FileServerService {
reply.code(500); reply.code(500);
return; return;
} }
@bindThis
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
const key = request.params.key;
const file = await this.getFileFromKey(key).then();
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
if (file.state === 'remote') {
let image: IImageStreamable | null = null;
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(url.toString(), 301);
}
}
if (!image) {
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
image = {
data: fs.createReadStream(file.path, {
start,
end,
}),
ext: file.ext,
type: file.mime,
};
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
}
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
);
return image.data;
}
if (file.fileRole !== 'original') {
const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString();
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', filename));
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
}
return fs.createReadStream(file.path);
} else {
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
if (request.headers.range && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
const fileStream = fs.createReadStream(file.path, {
start,
end,
});
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return fileStream;
}
return fs.createReadStream(file.path);
}
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
// アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query;
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
for (const [key, value] of Object.entries(request.query)) {
url.searchParams.append(key, value);
}
return await reply.redirect(
url.toString(),
301,
);
}
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
} else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file === '404') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', assets);
}
if (file === '204') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if (
'emoji' in request.query ||
'avatar' in request.query ||
'static' in request.query ||
'preview' in request.query ||
'badge' in request.query
) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
}
let image: IImageStreamable | null = null;
if ('emoji' in request.query || 'avatar' in request.query) {
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({
height: 'emoji' in request.query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
} else if ('static' in request.query) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
} else if ('preview' in request.query) {
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
} else if ('badge' in request.query) {
const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, {
fit: 'contain',
position: 'centre',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (file.mime === 'image/svg+xml') {
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
if (!image) {
if (request.headers.range && file.file && file.file.size > 0) {
const range = request.headers.range as string;
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
if (end > file.file.size) {
end = file.file.size - 1;
}
const chunksize = end - start + 1;
image = {
data: fs.createReadStream(file.path, {
start,
end,
}),
ext: file.ext,
type: file.mime,
};
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
} else {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
}
if ('cleanup' in file) {
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
// image.dataがstreamなら、stream終了後にcleanup
image.data.on('end', file.cleanup);
image.data.on('close', file.cleanup);
} else {
// image.dataがstreamでないなら直ちにcleanup
file.cleanup();
}
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
);
return image.data;
} catch (e) {
if ('cleanup' in file) file.cleanup();
throw e;
}
}
@bindThis
private async getStreamAndTypeFromUrl(url: string): Promise<
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: MiDriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
if (url.startsWith(`${this.config.url}/files/`)) {
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
return await this.getFileFromKey(key);
}
return await this.downloadAndDetectTypeFromUrl(url);
}
@bindThis
private async downloadAndDetectTypeFromUrl(url: string): Promise<
{ state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
> {
const [path, cleanup] = await createTemp();
try {
const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'remote',
mime, ext,
path, cleanup,
filename,
};
} catch (e) {
cleanup();
throw e;
}
}
@bindThis
private async getFileFromKey(key: string): Promise<
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: MiDriveFile; filename: string; mime: string; ext: string | null; path: string; }
| '404'
| '204'
> {
// Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
if (file == null) return '404';
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return '204';
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
return {
...result,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
filename: file.name,
};
}
const path = this.internalStorageService.resolvePath(key);
if (isThumbnail || isWebpublic) {
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
state: 'stored_internal',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file,
filename: file.name,
mime, ext,
path,
};
}
return {
state: 'stored_internal',
fileRole: 'original',
file,
filename: file.name,
// 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
mime: this.fileInfoService.fixMime(file.type),
ext: null,
path,
};
}
} }

View File

@ -428,8 +428,7 @@ export const meta = {
optional: false, nullable: true, optional: false, nullable: true,
}, },
clientOptions: { clientOptions: {
type: 'object', ref: 'MetaClientOptions',
optional: false, nullable: false,
}, },
description: { description: {
type: 'string', type: 'string',

View File

@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js'; import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -67,7 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true }, description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true },
clientOptions: { type: 'object', nullable: false }, clientOptions: {
type: 'object', nullable: false,
properties: {
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
showTimelineForVisitor: { type: 'boolean', nullable: false },
showActivitiesForVisitor: { type: 'boolean', nullable: false },
},
},
cacheRemoteFiles: { type: 'boolean' }, cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' },
@ -217,6 +225,9 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private metaService: MetaService, private metaService: MetaService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
) { ) {
@ -329,7 +340,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
if (ps.clientOptions !== undefined) { if (ps.clientOptions !== undefined) {
set.clientOptions = ps.clientOptions; set.clientOptions = {
...serverSettings.clientOptions,
...ps.clientOptions,
};
} }
if (ps.cacheRemoteFiles !== undefined) { if (ps.cacheRemoteFiles !== undefined) {

View File

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import rename from 'rename';
import type { Config } from '@/config.js';
import type { IImageStreamable } from '@/core/ImageProcessingService.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
import { attachStreamCleanup, handleRangeRequest, setFileResponseHeaders, getSafeContentType, needsCleanup } from './FileServerUtils.js';
import type { FileServerFileResolver } from './FileServerFileResolver.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
export class FileServerDriveHandler {
constructor(
private config: Config,
private fileResolver: FileServerFileResolver,
private assetsPath: string,
private videoProcessingService: VideoProcessingService,
) {}
public async handle(request: FastifyRequest<{ Params: { key: string } }>, reply: FastifyReply) {
const key = request.params.key;
const file = await this.fileResolver.resolveFileByAccessKey(key);
if (file.kind === 'not-found') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', this.assetsPath);
}
if (file.kind === 'unavailable') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
if (file.kind === 'remote') {
let image: IImageStreamable | null = null;
if (file.fileRole === 'thumbnail') {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
}
}
if (file.fileRole === 'webpublic') {
if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(url.toString(), 301);
}
}
image ??= {
data: handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path),
ext: file.ext,
type: file.mime,
};
attachStreamCleanup(image.data, file.cleanup);
reply.header('Content-Type', getSafeContentType(image.type));
reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
);
return image.data;
}
if (file.fileRole !== 'original') {
const filename = rename(file.filename, {
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
extname: file.ext ? `.${file.ext}` : '.unknown',
}).toString();
setFileResponseHeaders(reply, { mime: file.mime, filename });
return handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path);
} else {
setFileResponseHeaders(reply, { mime: file.file.type, filename: file.filename, size: file.file.size });
return handleRangeRequest(reply, request.headers.range as string | undefined, file.file.size, file.path);
}
} catch (e) {
if (file.kind === 'remote') file.cleanup();
throw e;
}
}
}

View File

@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import type { DriveFilesRepository, MiDriveFile } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import type { DownloadService } from '@/core/DownloadService.js';
import type { FileInfoService } from '@/core/FileInfoService.js';
import type { InternalStorageService } from '@/core/InternalStorageService.js';
export type DownloadedFileResult = {
kind: 'downloaded';
mime: string;
ext: string | null;
path: string;
cleanup: () => void;
filename: string;
};
export type FileResolveResult =
| { kind: 'not-found' }
| { kind: 'unavailable' }
| {
kind: 'stored';
fileRole: 'thumbnail' | 'webpublic' | 'original';
file: MiDriveFile;
filename: string;
mime: string;
ext: string | null;
path: string;
}
| {
kind: 'remote';
fileRole: 'thumbnail' | 'webpublic' | 'original';
file: MiDriveFile;
filename: string;
url: string;
mime: string;
ext: string | null;
path: string;
cleanup: () => void;
};
export class FileServerFileResolver {
constructor(
private driveFilesRepository: DriveFilesRepository,
private fileInfoService: FileInfoService,
private downloadService: DownloadService,
private internalStorageService: InternalStorageService,
) {}
public async downloadAndDetectTypeFromUrl(url: string): Promise<DownloadedFileResult> {
const [path, cleanup] = await createTemp();
try {
const { filename } = await this.downloadService.downloadUrl(url, path);
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
kind: 'downloaded',
mime, ext,
path, cleanup,
filename,
};
} catch (e) {
cleanup();
throw e;
}
}
public async resolveFileByAccessKey(key: string): Promise<FileResolveResult> {
// Fetch drive file
const file = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.accessKey = :accessKey', { accessKey: key })
.orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key })
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
.getOne();
if (file == null) return { kind: 'not-found' };
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return { kind: 'unavailable' };
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
const { kind: _kind, ...downloaded } = result;
file.size = (await fs.promises.stat(downloaded.path)).size; // DB file.sizeは正確とは限らないので
return {
kind: 'remote',
...downloaded,
url: file.uri,
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
file,
filename: file.name,
};
}
const path = this.internalStorageService.resolvePath(key);
if (isThumbnail || isWebpublic) {
const { mime, ext } = await this.fileInfoService.detectType(path);
return {
kind: 'stored',
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
file,
filename: file.name,
mime, ext,
path,
};
}
return {
kind: 'stored',
fileRole: 'original',
file,
filename: file.name,
// 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
mime: this.fileInfoService.fixMime(file.type),
ext: null,
path,
};
}
}

View File

@ -0,0 +1,272 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { StatusError } from '@/misc/status-error.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
import { createRangeStream, attachStreamCleanup, needsCleanup } from './FileServerUtils.js';
import type { DownloadedFileResult, FileResolveResult, FileServerFileResolver } from './FileServerFileResolver.js';
import type { FastifyReply, FastifyRequest } from 'fastify';
type ProxySource = DownloadedFileResult | FileResolveResult;
type CleanupableFile = ProxySource & { cleanup: () => void };
type AvailableFile = Exclude<ProxySource, { kind: 'not-found' | 'unavailable' }>;
type ProxyQuery = {
emoji?: string;
avatar?: string;
static?: string;
preview?: string;
badge?: string;
origin?: string;
url?: string;
};
export class FileServerProxyHandler {
constructor(
private config: Config,
private fileResolver: FileServerFileResolver,
private assetsPath: string,
private imageProcessingService: ImageProcessingService,
) {}
public async handle(request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
if (typeof url !== 'string') {
reply.code(400);
return;
}
// アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query;
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
return await this.redirectToExternalProxy(request, reply);
}
this.validateUserAgent(request);
// Create temp file
const file = await this.getStreamAndTypeFromUrl(url);
if (file.kind === 'not-found') {
reply.code(404);
reply.header('Cache-Control', 'max-age=86400');
return reply.sendFile('/dummy.png', this.assetsPath);
}
if (file.kind === 'unavailable') {
reply.code(204);
reply.header('Cache-Control', 'max-age=86400');
return;
}
try {
const image = await this.processImage(file, request, reply);
if (needsCleanup(file)) {
attachStreamCleanup(image.data, file.cleanup);
}
reply.header('Content-Type', image.type);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
correctFilename(file.filename, image.ext),
),
);
return image.data;
} catch (e) {
if (needsCleanup(file)) file.cleanup();
throw e;
}
}
/**
*
*/
private async redirectToExternalProxy(
request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>,
reply: FastifyReply,
) {
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
for (const [key, value] of Object.entries(request.query)) {
url.searchParams.append(key, value);
}
return reply.redirect(url.toString(), 301);
}
/**
* User-Agent
*/
private validateUserAgent(request: FastifyRequest): void {
if (!request.headers['user-agent']) {
throw new StatusError('User-Agent is required', 400, 'User-Agent is required');
}
if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) {
throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive');
}
}
/**
*
*/
private async processImage(
file: AvailableFile,
request: FastifyRequest<{ Params: { url: string }; Querystring: ProxyQuery }>,
reply: FastifyReply,
): Promise<IImageStreamable> {
const query = request.query;
const requiresImageConversion = 'emoji' in query || 'avatar' in query || 'static' in query || 'preview' in query || 'badge' in query;
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image-with-bmp');
if (requiresImageConversion && !isConvertibleImage) {
throw new StatusError('Unexpected mime', 404);
}
if ('emoji' in query || 'avatar' in query) {
return this.processEmojiOrAvatar(file, query);
}
if ('static' in query) {
return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
}
if ('preview' in query) {
return this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
}
if ('badge' in query) {
return this.processBadge(file);
}
if (file.mime === 'image/svg+xml') {
return this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
}
if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
}
return this.createDefaultStream(file, request, reply);
}
/**
*
*/
private async processEmojiOrAvatar(
file: AvailableFile,
query: Pick<ProxyQuery, 'emoji' | 'avatar' | 'static'>,
): Promise<IImageStreamable> {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if (!isAnimationConvertibleImage && !('static' in query)) {
return {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in query) }))
.resize({
height: 'emoji' in query ? 128 : 320,
withoutEnlargement: true,
})
.webp(webpDefault);
return {
data,
ext: 'webp',
type: 'image/webp',
};
}
/**
*
*/
private async processBadge(file: AvailableFile): Promise<IImageStreamable> {
const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, {
fit: 'contain',
position: 'centre',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
return {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
}
/**
* Range
*/
private createDefaultStream(
file: AvailableFile,
request: FastifyRequest,
reply: FastifyReply,
): IImageStreamable {
if (request.headers.range && 'file' in file && file.file.size > 0) {
const { stream, start, end, chunksize } = createRangeStream(request.headers.range as string, file.file.size, file.path);
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return {
data: stream,
ext: file.ext,
type: file.mime,
};
}
return {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
}
private async getStreamAndTypeFromUrl(url: string): Promise<ProxySource> {
if (url.startsWith(`${this.config.url}/files/`)) {
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
return await this.fileResolver.resolveFileByAccessKey(key);
}
return await this.fileResolver.downloadAndDetectTypeFromUrl(url);
}
}

View File

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { contentDisposition } from '@/misc/content-disposition.js';
import type { IImageStreamable } from '@/core/ImageProcessingService.js';
import type { FastifyReply } from 'fastify';
export type RangeStream = {
stream: fs.ReadStream;
start: number;
end: number;
chunksize: number;
};
/**
* Range
*/
export function createRangeStream(rangeHeader: string, size: number, path: string): RangeStream {
const parts = rangeHeader.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : size - 1;
if (end > size) {
end = size - 1;
}
const chunksize = end - start + 1;
return {
stream: fs.createReadStream(path, { start, end }),
start,
end,
chunksize,
};
}
/**
* cleanupハンドラを設定する
* cleanupを実行する
*/
export function attachStreamCleanup(data: IImageStreamable['data'], cleanup: () => void): void {
if ('pipe' in data && typeof data.pipe === 'function') {
data.on('end', cleanup);
data.on('close', cleanup);
} else {
cleanup();
}
}
/**
* MIME Content-Type
*/
export function getSafeContentType(mime: string): string {
return FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream';
}
/**
* Range
* Range
*/
export function handleRangeRequest(
reply: FastifyReply,
rangeHeader: string | undefined,
size: number,
path: string,
): fs.ReadStream {
if (rangeHeader && size > 0) {
const { stream, start, end, chunksize } = createRangeStream(rangeHeader, size, path);
reply.header('Content-Range', `bytes ${start}-${end}/${size}`);
reply.header('Accept-Ranges', 'bytes');
reply.header('Content-Length', chunksize);
reply.code(206);
return stream;
}
return fs.createReadStream(path);
}
export type FileResponseOptions = {
mime: string;
filename: string;
size?: number;
cacheControl?: string;
};
/**
*
*/
export function setFileResponseHeaders(
reply: FastifyReply,
options: FileResponseOptions,
): void {
reply.header('Content-Type', getSafeContentType(options.mime));
reply.header('Cache-Control', options.cacheControl ?? 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', options.filename));
if (options.size !== undefined) {
reply.header('Content-Length', options.size);
}
}
/**
* cleanup
*/
export function needsCleanup<T extends { kind?: string; cleanup?: () => void }>(file: T): file is T & { cleanup: () => void } {
return 'cleanup' in file && typeof file.cleanup === 'function';
}

View File

@ -11,15 +11,15 @@
}, },
"devDependencies": { "devDependencies": {
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"rollup": "4.54.0" "rollup": "4.55.1"
}, },
"dependencies": { "dependencies": {
"i18n": "workspace:*", "i18n": "workspace:*",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"magic-string": "0.30.21", "magic-string": "0.30.21",
"vite": "7.3.0" "vite": "7.3.1"
} }
} }

View File

@ -144,7 +144,15 @@ export default [
'vue/return-in-computed-property': 'warn', 'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-reactivity-loss': 'warn', 'vue/no-setup-props-reactivity-loss': 'warn',
'vue/max-attributes-per-line': 'off', 'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off', 'vue/html-self-closing': ['error', {
html: {
void: 'any',
normal: 'never',
component: 'any',
},
svg: 'any',
math: 'any',
}],
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'vue/v-on-event-hyphenation': ['error', 'never', { 'vue/v-on-event-hyphenation': ['error', 'never', {
autofix: true, autofix: true,

View File

@ -11,7 +11,6 @@
}, },
"dependencies": { "dependencies": {
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"i18n": "workspace:*",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3", "@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0", "@rollup/pluginutils": "5.3.0",
@ -20,17 +19,18 @@
"buraha": "0.0.1", "buraha": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
"i18n": "workspace:*",
"icons-subsetter": "workspace:*", "icons-subsetter": "workspace:*",
"json5": "2.2.3", "json5": "2.2.3",
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.54.0", "rollup": "4.55.1",
"sass": "1.97.1", "sass": "1.97.2",
"shiki": "3.20.0", "shiki": "3.21.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"uuid": "13.0.0", "uuid": "13.0.0",
"vite": "7.3.0", "vite": "7.3.1",
"vue": "3.5.26" "vue": "3.5.26"
}, },
"devDependencies": { "devDependencies": {
@ -39,29 +39,29 @@
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/micromatch": "4.0.10", "@types/micromatch": "4.0.10",
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"@vitest/coverage-v8": "4.0.16", "@vitest/coverage-v8": "4.0.17",
"@vue/runtime-core": "3.5.26", "@vue/runtime-core": "3.5.26",
"acorn": "8.15.0", "acorn": "8.15.0",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.7.0",
"happy-dom": "20.0.11", "happy-dom": "20.3.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.12.6", "msw": "2.12.7",
"nodemon": "3.1.11", "nodemon": "3.1.11",
"prettier": "3.7.4", "prettier": "3.8.0",
"start-server-and-test": "2.1.3", "start-server-and-test": "2.1.3",
"tsx": "4.21.0", "tsx": "4.21.0",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.2.1", "vue-component-type-helpers": "3.2.2",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.8" "vue-tsc": "3.2.2"
} }
} }

View File

@ -9,16 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="user.isCat" :class="[$style.ears]"> <div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft"> <div :class="$style.earLeft">
<div v-if="false" :class="$style.layer"> <div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
</div> </div>
</div> </div>
<div :class="$style.earRight"> <div :class="$style.earRight">
<div v-if="false" :class="$style.layer"> <div v-if="false" :class="$style.layer">
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"></canvas>
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> <img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
</div> </div>
</template> </template>

View File

@ -21,11 +21,11 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"esbuild": "0.27.2", "esbuild": "0.27.2",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.7.0",
"nodemon": "3.1.11", "nodemon": "3.1.11",
"vue-eslint-parser": "10.2.0" "vue-eslint-parser": "10.2.0"
}, },

View File

@ -147,7 +147,15 @@ export default [
'vue/return-in-computed-property': 'warn', 'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-reactivity-loss': 'warn', 'vue/no-setup-props-reactivity-loss': 'warn',
'vue/max-attributes-per-line': 'off', 'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off', 'vue/html-self-closing': ['error', {
html: {
void: 'any',
normal: 'never',
component: 'any',
},
svg: 'any',
math: 'any',
}],
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'vue/v-on-event-hyphenation': ['error', 'never', { 'vue/v-on-event-hyphenation': ['error', 'never', {
autofix: true, autofix: true,

View File

@ -19,12 +19,12 @@
"@analytics/google-analytics": "1.1.0", "@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1", "@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-rc2", "@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0", "@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3", "@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0", "@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.32.1", "@sentry/vue": "10.34.0",
"@syuilo/aiscript": "1.2.1", "@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
@ -39,13 +39,13 @@
"chartjs-chart-matrix": "3.0.0", "chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.3.4", "chromatic": "13.3.5",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.1.0", "cropperjs": "2.1.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"execa": "9.6.1", "execa": "9.6.1",
"exifreader": "4.33.1", "exifreader": "4.36.0",
"frontend-shared": "workspace:*", "frontend-shared": "workspace:*",
"i18n": "workspace:*", "i18n": "workspace:*",
"icons-subsetter": "workspace:*", "icons-subsetter": "workspace:*",
@ -55,7 +55,7 @@
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"matter-js": "0.20.0", "matter-js": "0.20.0",
"mediabunny": "1.27.2", "mediabunny": "1.28.0",
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*", "misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
@ -64,16 +64,16 @@
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"qr-code-styling": "1.9.2", "qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2", "qr-scanner": "1.4.2",
"rollup": "4.54.0", "rollup": "4.55.1",
"sanitize-html": "2.17.0", "sanitize-html": "2.17.0",
"sass": "1.97.1", "sass": "1.97.2",
"shiki": "3.20.0", "shiki": "3.21.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.182.0", "three": "0.182.0",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "7.3.0", "vite": "7.3.1",
"vue": "3.5.26", "vue": "3.5.26",
"wanakana": "5.3.1" "wanakana": "5.3.1"
}, },
@ -81,7 +81,7 @@
"@misskey-dev/summaly": "5.2.5", "@misskey-dev/summaly": "5.2.5",
"@storybook/addon-essentials": "8.6.15", "@storybook/addon-essentials": "8.6.15",
"@storybook/addon-interactions": "8.6.15", "@storybook/addon-interactions": "8.6.15",
"@storybook/addon-links": "10.1.10", "@storybook/addon-links": "10.1.11",
"@storybook/addon-mdx-gfm": "8.6.15", "@storybook/addon-mdx-gfm": "8.6.15",
"@storybook/addon-storysource": "8.6.15", "@storybook/addon-storysource": "8.6.15",
"@storybook/blocks": "8.6.15", "@storybook/blocks": "8.6.15",
@ -89,13 +89,13 @@
"@storybook/core-events": "8.6.15", "@storybook/core-events": "8.6.15",
"@storybook/manager-api": "8.6.15", "@storybook/manager-api": "8.6.15",
"@storybook/preview-api": "8.6.15", "@storybook/preview-api": "8.6.15",
"@storybook/react": "10.1.10", "@storybook/react": "10.1.11",
"@storybook/react-vite": "10.1.10", "@storybook/react-vite": "10.1.11",
"@storybook/test": "8.6.15", "@storybook/test": "8.6.15",
"@storybook/theming": "8.6.15", "@storybook/theming": "8.6.15",
"@storybook/types": "8.6.15", "@storybook/types": "8.6.15",
"@storybook/vue3": "10.1.10", "@storybook/vue3": "10.1.11",
"@storybook/vue3-vite": "10.1.10", "@storybook/vue3-vite": "10.1.11",
"@tabler/icons-webfont": "3.35.0", "@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "1.9.0",
@ -103,46 +103,46 @@
"@types/insert-text-at-cursor": "0.3.2", "@types/insert-text-at-cursor": "0.3.2",
"@types/matter-js": "0.20.2", "@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10", "@types/micromatch": "4.0.10",
"@types/node": "24.10.4", "@types/node": "24.10.9",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0", "@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/textarea-caret": "3.0.4", "@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.50.1", "@typescript-eslint/parser": "8.53.0",
"@vitest/coverage-v8": "4.0.16", "@vitest/coverage-v8": "4.0.17",
"@vue/compiler-core": "3.5.26", "@vue/compiler-core": "3.5.26",
"acorn": "8.15.0", "acorn": "8.15.0",
"astring": "1.9.0", "astring": "1.9.0",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"cypress": "15.8.1", "cypress": "15.9.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.7.0",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"happy-dom": "20.0.11", "happy-dom": "20.3.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"magic-string": "0.30.21", "magic-string": "0.30.21",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"minimatch": "10.1.1", "minimatch": "10.1.1",
"msw": "2.12.6", "msw": "2.12.7",
"msw-storybook-addon": "2.0.6", "msw-storybook-addon": "2.0.6",
"nodemon": "3.1.11", "nodemon": "3.1.11",
"prettier": "3.7.4", "prettier": "3.8.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.1.3", "start-server-and-test": "2.1.3",
"storybook": "10.1.10", "storybook": "10.1.11",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0", "tsx": "4.21.0",
"vite-plugin-glsl": "1.5.5", "vite-plugin-glsl": "1.5.5",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "4.0.16", "vitest": "4.0.17",
"vitest-fetch-mock": "0.4.5", "vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.2.1", "vue-component-type-helpers": "3.2.2",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.8" "vue-tsc": "3.2.2"
} }
} }

View File

@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div :class="$style.body"> <div :class="$style.body">
<div :class="$style.header"> <div :class="$style.header">
<span :class="$style.title">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].title }}</span> <span :class="$style.title">{{ i18n.ts._achievements._types[`_${achievement.name}`].title }}</span>
<span :class="$style.time"> <span :class="$style.time">
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time> <time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
</span> </span>
</div> </div>
<div :class="$style.description">{{ withDescription ? (i18n.ts._achievements._types as any)['_' + achievement.name].description : '???' }}</div> <div :class="$style.description">{{ withDescription ? i18n.ts._achievements._types[`_${achievement.name}`].description : '???' }}</div>
<div v-if="(i18n.ts._achievements._types as any)['_' + achievement.name].flavor && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].flavor }}</div> <div v-if="'flavor' in i18n.ts._achievements._types[`_${achievement.name}`] && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types[`_${achievement.name}`] as { flavor: string; }).flavor }}</div>
</div> </div>
</div> </div>
<template v-if="withLocked"> <template v-if="withLocked">

View File

@ -106,7 +106,7 @@ const containerStyle = computed(() => {
const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle; const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle;
const border = isBordered ? { const border = isBordered ? {
borderWidth: c.borderWidth ?? '1px', borderWidth: `${c.borderWidth ?? 1}px`,
borderColor: c.borderColor ?? 'var(--MI_THEME-divider)', borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
borderStyle: c.borderStyle ?? 'solid', borderStyle: c.borderStyle ?? 'solid',
} : undefined; } : undefined;

View File

@ -45,6 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts"> <script lang="ts">
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import * as Misskey from 'misskey-js';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { emojilist, getEmojiName } from '@@/js/emojilist.js'; import { emojilist, getEmojiName } from '@@/js/emojilist.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
@ -63,7 +64,7 @@ import { prefer } from '@/preferences.js';
export type CompleteInfo = { export type CompleteInfo = {
user: { user: {
payload: any; payload: Misskey.entities.User;
query: string | null; query: string | null;
}, },
hashtag: { hashtag: {
@ -185,9 +186,9 @@ const suggests = ref<Element>();
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
const fetching = ref(true); const fetching = ref(true);
const users = ref<any[]>([]); const users = ref<Misskey.entities.User[]>([]);
const hashtags = ref<any[]>([]); const hashtags = ref<string[]>([]);
const emojis = ref<(EmojiDef)[]>([]); const emojis = ref<EmojiDef[]>([]);
const items = ref<Element[] | HTMLCollection>([]); const items = ref<Element[] | HTMLCollection>([]);
const mfmTags = ref<string[]>([]); const mfmTags = ref<string[]>([]);
const mfmParams = ref<string[]>([]); const mfmParams = ref<string[]>([]);
@ -204,8 +205,8 @@ function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T][
emit('closed'); emit('closed');
if (type === 'emoji' || type === 'emojiComplete') { if (type === 'emoji' || type === 'emojiComplete') {
let recents = store.s.recentlyUsedEmojis; let recents = store.s.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== value); recents = recents.filter((emoji) => emoji !== value);
recents.unshift(value); recents.unshift(value as string);
store.set('recentlyUsedEmojis', recents.splice(0, 32)); store.set('recentlyUsedEmojis', recents.splice(0, 32));
} }
} }
@ -254,7 +255,7 @@ function exec() {
limit: 10, limit: 10,
detail: false, detail: false,
}).then(searchedUsers => { }).then(searchedUsers => {
users.value = searchedUsers as any[]; users.value = searchedUsers;
fetching.value = false; fetching.value = false;
// //
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers)); sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
@ -276,7 +277,7 @@ function exec() {
query: props.q, query: props.q,
limit: 30, limit: 30,
}).then(searchedHashtags => { }).then(searchedHashtags => {
hashtags.value = searchedHashtags as any[]; hashtags.value = searchedHashtags;
fetching.value = false; fetching.value = false;
// //
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags)); sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));

View File

@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div> <div>
<span v-if="!available">Loading<MkEllipsis/></span> <span v-if="!available">Loading<MkEllipsis/></span>
<div v-if="props.provider == 'mcaptcha'"> <div v-if="props.provider == 'mcaptcha'">
<div id="mcaptcha__widget-container" class="m-captcha-style"></div> <iframe
<div ref="captchaEl"></div> v-if="mCaptchaIframeUrl != null"
ref="mCaptchaIframe"
:src="mCaptchaIframeUrl"
style="border: none; max-width: 320px; width: 100%; height: 100%; max-height: 80px;"
></iframe>
</div> </div>
<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;"> <div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/> <img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
@ -26,7 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted, nextTick } from 'vue';
import type Reciever_typeReferenceOnly from '@mcaptcha/core-glue';
import { store } from '@/store.js'; import { store } from '@/store.js';
// APIs provided by Captcha services // APIs provided by Captcha services
@ -71,6 +76,19 @@ const available = ref(false);
const captchaEl = useTemplateRef('captchaEl'); const captchaEl = useTemplateRef('captchaEl');
const captchaWidgetId = ref<string | undefined>(undefined); const captchaWidgetId = ref<string | undefined>(undefined);
let mCaptchaReciever: Reciever_typeReferenceOnly | null = null;
const mCaptchaIframe = useTemplateRef('mCaptchaIframe');
const mCaptchaRemoveState = ref(false);
const mCaptchaIframeUrl = computed(() => {
if (props.provider === 'mcaptcha' && !mCaptchaRemoveState.value && props.instanceUrl && props.sitekey) {
const url = new URL('/widget', props.instanceUrl);
url.searchParams.set('sitekey', props.sitekey);
return url.toString();
}
return null;
});
const testcaptchaInput = ref(''); const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false); const testcaptchaPassed = ref(false);
@ -129,8 +147,14 @@ function reset() {
if (_DEV_) console.warn(error); if (_DEV_) console.warn(error);
} }
} }
testcaptchaPassed.value = false; testcaptchaPassed.value = false;
testcaptchaInput.value = ''; testcaptchaInput.value = '';
if (mCaptchaReciever != null) {
mCaptchaReciever.destroy();
mCaptchaReciever = null;
}
} }
function remove() { function remove() {
@ -143,6 +167,10 @@ function remove() {
if (_DEV_) console.warn(error); if (_DEV_) console.warn(error);
} }
} }
if (props.provider === 'mcaptcha') {
mCaptchaRemoveState.value = true;
}
} }
async function requestRender() { async function requestRender() {
@ -160,25 +188,23 @@ async function requestRender() {
'error-callback': () => callback(undefined), 'error-callback': () => callback(undefined),
}); });
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
const { default: Widget } = await import('@mcaptcha/vanilla-glue'); const { default: Reciever } = await import('@mcaptcha/core-glue');
new Widget({ mCaptchaReciever = new Reciever({
siteKey: { siteKey: {
instanceUrl: new URL(props.instanceUrl),
key: props.sitekey, key: props.sitekey,
instanceUrl: new URL(props.instanceUrl),
}, },
}, (token: string) => {
callback(token);
}); });
mCaptchaReciever.listen();
mCaptchaRemoveState.value = false;
} else { } else {
window.setTimeout(requestRender, 1); window.setTimeout(requestRender, 50);
} }
} }
function clearWidget() { function clearWidget() {
if (props.provider === 'mcaptcha') {
const container = window.document.getElementById('mcaptcha__widget-container');
if (container) {
container.innerHTML = '';
}
} else {
reset(); reset();
remove(); remove();
@ -187,7 +213,6 @@ function clearWidget() {
captchaEl.value.innerHTML = ''; captchaEl.value.innerHTML = '';
} }
} }
}
function callback(response?: string) { function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null); emit('update:modelValue', typeof response === 'string' ? response : null);

View File

@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
<template #caption> <template #caption>
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/> <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"></span>
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/> <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"></span>
</template> </template>
</MkInput> </MkInput>
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect> <MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
@ -52,7 +52,8 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue'; import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
import { useMkSelect } from '@/composables/use-mkselect.js'; import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
borderWidth ? { borderWidth: borderWidth } : {}, borderWidth ? { borderWidth: borderWidth } : {},
borderColor ? { borderColor: borderColor } : {}, borderColor ? { borderColor: borderColor } : {},
]" ]"
/> ></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>{{ i18n.ts.permission }}</template> <template #key>{{ i18n.ts.permission }}</template>
<template #value> <template #value>
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList"> <ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
<li v-for="permission in extension.meta.permissions" :key="permission">{{ (i18n.ts._permissions as any)[permission] ?? permission }}</li> <li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
</ul> </ul>
<template v-else>{{ i18n.ts.none }}</template> <template v-else>{{ i18n.ts.none }}</template>
</template> </template>
@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder> </MkFolder>
</div> </div>
<slot name="additionalInfo"/> <slot name="additionalInfo"></slot>
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton> <MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
@ -101,6 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import * as Misskey from 'misskey-js';
export type Extension = { export type Extension = {
type: 'plugin'; type: 'plugin';
raw: string; raw: string;
@ -109,7 +111,7 @@ export type Extension = {
version: string; version: string;
author: string; author: string;
description?: string; description?: string;
permissions?: string[]; permissions?: (typeof Misskey.permissions)[number][];
config?: Record<string, unknown>; config?: Record<string, unknown>;
}; };
} | { } | {
@ -125,7 +127,6 @@ export type Extension = {
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import MkCode from '@/components/MkCode.vue'; import MkCode from '@/components/MkCode.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';

View File

@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m"> <div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
<template v-for="v, k in form"> <template v-for="v, k in form">
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template> <template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
<MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave"> <MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput> </MkInput>
<MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave"> <MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput> </MkInput>
<MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave"> <MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm" :manualSave="v.manualSave" @savingStateChange="(changed, invalid) => onSavingStateChange(k, changed, invalid)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="v.description" #caption>{{ v.description }}</template> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkTextarea> </MkTextarea>
@ -26,9 +26,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)"> <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
</MkSelect> </MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]"> <MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
</MkRadios> </MkRadios>
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter"> <MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -49,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import XFile from '@/components/MkForm.file.vue'; import XFile from '@/components/MkForm.file.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
@ -59,15 +59,50 @@ import MkButton from '@/components/MkButton.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import type { MkSelectItem } from '@/components/MkSelect.vue'; import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { MkRadiosOption } from '@/components/MkRadios.vue';
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js'; import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
const props = defineProps<{ const props = defineProps<{
form: Form; form: Form;
}>(); }>();
const emit = defineEmits<{
(ev: 'canSaveStateChange', canSave: boolean): void;
}>();
// TODO: // TODO:
const values = defineModel<Record<string, any>>({ required: true }); const values = defineModel<Record<string, any>>({ required: true });
//
const inputSavingStates = ref<Record<string, { changed: boolean; invalid: boolean }>>({});
function onSavingStateChange(key: string, changed: boolean, invalid: boolean) {
inputSavingStates.value[key] = { changed, invalid };
}
const canSave = computed(() => {
for (const key in inputSavingStates.value) {
const state = inputSavingStates.value[key];
if (
('manualSave' in props.form[key] && props.form[key].manualSave && state.changed) ||
state.invalid
) {
return false;
}
if ('required' in props.form[key] && props.form[key].required) {
const val = values.value[key];
if (val === null || val === undefined || val === '') {
return false;
}
}
}
return true;
});
watch(canSave, (newCanSave) => {
emit('canSaveStateChange', newCanSave);
}, { immediate: true });
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] { function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
return def.enum.map((v) => { return def.enum.map((v) => {
if (typeof v === 'string') { if (typeof v === 'string') {
@ -78,7 +113,13 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
}); });
} }
function getRadioKey(e: RadioFormItem['options'][number]) { function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value); return def.options.map<MkRadiosOption>((v) => {
if (typeof v === 'string') {
return { value: v, label: v };
} else {
return { value: v.value, label: v.label };
}
});
} }
</script> </script>

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:width="450" :width="450"
:canClose="false" :canClose="false"
:withOkButton="true" :withOkButton="true"
:okButtonDisabled="false" :okButtonDisabled="!canSave"
@click="cancel()" @click="cancel()"
@ok="ok()" @ok="ok()"
@close="cancel()" @close="cancel()"
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;"> <div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 32px;">
<MkForm v-model="values" :form="form"/> <MkForm v-model="values" :form="form" @canSaveStateChange="onCanSaveStateChanged"/>
</div> </div>
</MkModalWindow> </MkModalWindow>
</template> </template>
@ -59,7 +59,15 @@ const values = ref((() => {
return obj; return obj;
})()); })());
const canSave = ref(true);
function onCanSaveStateChanged(newCanSave: boolean) {
canSave.value = newCanSave;
}
function ok() { function ok() {
if (!canSave.value) return;
emit('done', { emit('done', {
result: values.value, result: values.value,
}); });

View File

@ -28,13 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ v.label ?? k }}</template> <template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template> <template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkRange> </MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]"> <MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
<template #label>{{ v.label ?? k }}</template> <template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template> <template v-if="v.caption != null" #caption>{{ v.caption }}</template>
<option v-for="item in v.enum" :value="item.value">
<i v-if="item.icon" :class="item.icon"></i>
<template v-else>{{ item.label }}</template>
</option>
</MkRadios> </MkRadios>
<div v-else-if="v.type === 'seed'"> <div v-else-if="v.type === 'seed'">
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1"> <MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<template #header>{{ file.name }}</template> <template #header>{{ file.name }}</template>
<div :class="$style.container"> <div :class="$style.container">
<img :src="file.url" :alt="file.comment ?? file.name" :class="$style.img"/> <img :src="file.url" :alt="file.comment || file.name" :class="$style.img"/>
</div> </div>
</MkModalWindow> </MkModalWindow>
</template> </template>

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
draggable="false" draggable="false"
tabindex="-1" tabindex="-1"
style="-webkit-user-drag: none;" style="-webkit-user-drag: none;"
/> ></canvas>
<img <img
v-show="!hide" v-show="!hide"
key="img" key="img"

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@input="onInput" @input="onInput"
> >
<datalist v-if="datalist" :id="id"> <datalist v-if="datalist" :id="id">
<option v-for="data in datalist" :key="data" :value="data"/> <option v-for="data in datalist" :key="data" :value="data"></option>
</datalist> </datalist>
<div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div> <div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div>
</div> </div>
@ -92,6 +92,7 @@ const emit = defineEmits<{
(ev: 'keydown', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter', _ev: KeyboardEvent): void; (ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: ModelValueType<T>): void; (ev: 'update:modelValue', value: ModelValueType<T>): void;
(ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
}>(); }>();
const { modelValue } = toRefs(props); const { modelValue } = toRefs(props);
@ -152,6 +153,10 @@ watch(v, () => {
invalid.value = inputEl.value?.validity.badInput ?? true; invalid.value = inputEl.value?.validity.badInput ?? true;
}); });
watch([changed, invalid], ([newChanged, newInvalid]) => {
emit('savingStateChange', newChanged, newInvalid);
}, { immediate: true });
// //
// 0 // 0
useInterval(() => { useInterval(() => {

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal"> <div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal" @contextmenu.stop="onContextmenu">
<component <component
:is="disableImageLink ? 'div' : 'a'" :is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? { v-bind="disableImageLink ? {
@ -123,7 +123,7 @@ watch(() => props.image, (newImage) => {
immediate: true, immediate: true,
}); });
function showMenu(ev: PointerEvent) { function getMenu() {
const menuItems: MenuItem[] = []; const menuItems: MenuItem[] = [];
menuItems.push({ menuItems.push({
@ -188,9 +188,16 @@ function showMenu(ev: PointerEvent) {
}); });
} }
os.popupMenu(menuItems, ev.currentTarget ?? ev.target); return menuItems;
} }
function showMenu(ev: PointerEvent) {
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
function onContextmenu(ev: PointerEvent) {
os.contextMenu(getMenu(), ev);
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -4,13 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div> <div :class="$style.root">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container"> <div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div <div
ref="gallery" ref="gallery"
:class="[ :class="[
$style.medias, $style.medias,
...(prefer.s.showMediaListByGridInWideArea ? [$style.gridInWideArea] : []),
count === 1 ? [$style.n1, { count === 1 ? [$style.n1, {
[$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9', [$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9',
[$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1', [$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1',
@ -107,8 +108,10 @@ onMounted(() => {
src: media.url, src: media.url,
w: media.properties.width, w: media.properties.width,
h: media.properties.height, h: media.properties.height,
alt: media.comment ?? media.name, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
comment: media.comment ?? media.name, alt: media.comment || media.name,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
comment: media.comment || media.name,
}; };
if (media.properties.orientation != null && media.properties.orientation >= 5) { if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w]; [item.w, item.h] = [item.h, item.w];
@ -155,8 +158,10 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w]; [itemData.w, itemData.h] = [itemData.h, itemData.w];
} }
itemData.msrc = file.thumbnailUrl ?? undefined; itemData.msrc = file.thumbnailUrl ?? undefined;
itemData.alt = file.comment ?? file.name; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
itemData.comment = file.comment ?? file.name; itemData.alt = file.comment || file.name;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
itemData.comment = file.comment || file.name;
itemData.thumbCropped = true; itemData.thumbCropped = true;
return itemData; return itemData;
@ -226,6 +231,10 @@ defineExpose({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
container-type: inline-size;
}
.container { .container {
position: relative; position: relative;
width: 100%; width: 100%;
@ -309,6 +318,20 @@ defineExpose({
border-radius: 8px; border-radius: 8px;
} }
@container (min-width: 500px) {
.medias.gridInWideArea {
display: grid;
aspect-ratio: auto;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: auto;
grid-gap: 8px;
> .media {
aspect-ratio: 1 / 1;
}
}
}
:global(.pswp) { :global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important; --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
--pswp-bg: var(--MI_THEME-modalBg) !important; --pswp-bg: var(--MI_THEME-modalBg) !important;

View File

@ -705,7 +705,7 @@ onDeactivated(() => {
.controlButton { .controlButton {
padding: 6px; padding: 6px;
border-radius: calc(var(--MI-radius) / 2); border-radius: calc(var(--MI-radius) / 2);
transition: background-color .2s ease-in-out; transition: background-color .15s ease;
font-size: 1.05rem; font-size: 1.05rem;
&:hover { &:hover {
@ -760,4 +760,21 @@ onDeactivated(() => {
} }
} }
} }
@container (max-width: 300px) {
.videoControls {
grid-template-areas:
"left . right"
"seekbar seekbar seekbar";
grid-template-columns: auto 1fr auto;
}
.controlsTime {
display: none;
}
.controlsVolume {
display: none;
}
}
</style> </style>

View File

@ -323,9 +323,20 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
type: 'radioOption', type: 'radioOption',
text: key, text: key,
action: () => { action: () => {
if ('value' in item.ref) {
item.ref.value = value;
} else {
// @ts-expect-error
item.ref = value; item.ref = value;
}
}, },
active: computed(() => item.ref === value), active: computed(() => {
if ('value' in item.ref) {
return item.ref.value === value;
} else {
return item.ref === value;
}
}),
}; };
}); });

View File

@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-if="appearNote.files && appearNote.files.length > 0"> <div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div> </div>
<MkPoll <MkPoll
v-if="appearNote.poll" v-if="appearNote.poll"
@ -323,6 +323,7 @@ const renoteButton = useTemplateRef('renoteButton');
const renoteTime = useTemplateRef('renoteTime'); const renoteTime = useTemplateRef('renoteTime');
const reactButton = useTemplateRef('reactButton'); const reactButton = useTemplateRef('reactButton');
const clipButton = useTemplateRef('clipButton'); const clipButton = useTemplateRef('clipButton');
const galleryEl = useTemplateRef('galleryEl');
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
@ -356,7 +357,9 @@ const keymap = {
if (!prefer.s.showClipButtonInNoteFooter) return; if (!prefer.s.showClipButtonInNoteFooter) return;
clip(); clip();
}, },
//'o': () => galleryEl.value?.openGallery(), 'o': () => {
galleryEl.value?.openGallery();
},
'v|enter': () => { 'v|enter': () => {
if (appearNote.cw != null) { if (appearNote.cw != null) {
showContent.value = !showContent.value; showContent.value = !showContent.value;

View File

@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ notification.invitation.room.name }} {{ notification.invitation.room.name }}
</div> </div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ (i18n.ts._achievements._types as any)['_' + notification.achievement].title }} {{ i18n.ts._achievements._types[`_${notification.achievement}`].title }}
</MkA> </MkA>
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`"> <MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
{{ i18n.ts.showFile }} {{ i18n.ts.showFile }}
@ -143,8 +143,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="notification.type === 'receiveFollowRequest'"> <template v-else-if="notification.type === 'receiveFollowRequest'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> <div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> <MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> <MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
</div> </div>
</template> </template>
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span> <span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.buttons"> <div :class="$style.buttons">
<div v-if="prevDotVisible" :class="$style.headTailButtons"> <div v-if="prevDotVisible" :class="$style.headTailButtons">
<MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton> <MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
<span class="ti ti-dots"/> <span class="ti ti-dots"></span>
</div> </div>
<MkButton <MkButton
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton> </MkButton>
<div v-if="nextDotVisible" :class="$style.headTailButtons"> <div v-if="nextDotVisible" :class="$style.headTailButtons">
<span class="ti ti-dots"/> <span class="ti ti-dots"></span>
<MkButton @click="onToTailButtonClicked">{{ max }}</MkButton> <MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/> <div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.headerRightButtonText">{{ targetChannel.name }}</span> <span :class="$style.headerRightButtonText">{{ targetChannel.name }}</span>
</button> </button>
</template> </template>
<button v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="targetChannel != null || visibility === 'specified'" @click="toggleLocalOnly"> <button v-if="visibility !== 'specified'" v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="targetChannel != null" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span>
</button> </button>
@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="targetChannel" :class="$style.colorBar" :style="{ background: targetChannel.color }"></div> <div v-if="targetChannel" :class="$style.colorBar" :style="{ background: targetChannel.color }"></div>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"></textarea>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div> </div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</footer> </footer>
<datalist id="hashtags"> <datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"></option>
</datalist> </datalist>
</div> </div>
</template> </template>
@ -529,7 +529,6 @@ function setVisibility() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility.value, currentVisibility: visibility.value,
isSilenced: $i.isSilenced, isSilenced: $i.isSilenced,
localOnly: localOnly.value,
anchorElement: visibilityButton.value, anchorElement: visibilityButton.value,
...(replyTargetNote.value ? { isReplyVisibilitySpecified: replyTargetNote.value.visibility === 'specified' } : {}), ...(replyTargetNote.value ? { isReplyVisibilitySpecified: replyTargetNote.value.visibility === 'specified' } : {}),
}, { }, {
@ -1023,7 +1022,7 @@ async function post(ev?: PointerEvent) {
channelId: targetChannel.value ? targetChannel.value.id : undefined, channelId: targetChannel.value ? targetChannel.value.id : undefined,
poll: poll.value, poll: poll.value,
cw: useCw.value ? cw.value ?? '' : null, cw: useCw.value ? cw.value ?? '' : null,
localOnly: localOnly.value, localOnly: visibility.value === 'specified' ? false : localOnly.value,
visibility: visibility.value, visibility: visibility.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
reactionAcceptance: reactionAcceptance.value, reactionAcceptance: reactionAcceptance.value,

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
tabindex="0" tabindex="0"
@click="showFileMenu(item, $event)" @click="showFileMenu(item, $event)"
@keydown.space.enter="showFileMenu(item, $event)" @keydown.space.enter="showFileMenu(item, $event)"
@contextmenu.prevent="showFileMenu(item, $event)" @contextmenu.prevent.stop="showFileMenu(item, $event)"
> >
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる --> <!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/> <MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>

View File

@ -12,11 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button"> <MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
<span>Switch is now {{ flag ? 'on' : 'off' }}</span> <span>Switch is now {{ flag ? 'on' : 'off' }}</span>
</MkSwitch> </MkSwitch>
<div :class="$style.preview__content1__input">
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
</div>
<div :class="$style.preview__content1__button"> <div :class="$style.preview__content1__button">
<MkButton inline>This is</MkButton> <MkButton inline>This is</MkButton>
<MkButton inline primary>the button</MkButton> <MkButton inline primary>the button</MkButton>
@ -40,15 +35,12 @@ import * as config from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { chooseDriveFile } from '@/utility/drive.js'; import { chooseDriveFile } from '@/utility/drive.js';
const text = ref(''); const text = ref('');
const flag = ref(true); const flag = ref(true);
const radio = ref('misskey');
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`); const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
const openDialog = async () => { const openDialog = async () => {

View File

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<slot/> <slot></slot>
</div> </div>
</template> </template>

View File

@ -144,7 +144,7 @@ async function unsubscribe() {
} }
function encode(buffer: ArrayBuffer | null) { function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : [])); return btoa(String.fromCharCode(...(buffer != null ? new Uint8Array(buffer) : [])));
} }
/** /**

View File

@ -1,136 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
v-adaptive-border
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
role="checkbox"
@click="toggle"
>
<input
type="radio"
:disabled="disabled"
:class="$style.input"
>
<span :class="$style.button">
<span></span>
</span>
<span :class="$style.label"><slot></slot></span>
</div>
</template>
<script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue';
const props = defineProps<{
modelValue: T;
value: T;
disabled?: boolean;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: T): void;
}>();
const checked = computed(() => props.modelValue === props.value);
function toggle(): void {
if (props.disabled) return;
emit('update:modelValue', props.value);
}
</script>
<style lang="scss" module>
.root {
position: relative;
display: inline-flex;
align-items: center;
text-align: left;
cursor: pointer;
padding: 7px 10px;
min-width: 60px;
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
border: solid 1px var(--MI_THEME-panel);
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
user-select: none;
&.disabled {
opacity: 0.6;
cursor: not-allowed !important;
}
&:hover {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
background-color: var(--MI_THEME-accentedBg) !important;
border-color: var(--MI_THEME-accentedBg) !important;
color: var(--MI_THEME-accent);
cursor: default !important;
> .button {
border-color: var(--MI_THEME-accent);
&::after {
background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
}
}
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--MI_THEME-inputBorder);
border-radius: 100%;
transition: inherit;
&::after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.label {
margin-left: 8px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style>

View File

@ -3,70 +3,96 @@ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template>
<div :class="{ [$style.vertical]: vertical }">
<div :class="$style.label">
<slot name="label"></slot>
</div>
<div :class="$style.body">
<div
v-for="option in options"
:key="getKey(option.value)"
v-adaptive-border
:class="[$style.optionRoot, { [$style.disabled]: option.disabled, [$style.checked]: model === option.value }]"
:aria-checked="model === option.value"
:aria-disabled="option.disabled"
role="checkbox"
@click="toggle(option)"
>
<input
type="radio"
:disabled="option.disabled"
:class="$style.optionInput"
>
<span :class="$style.optionButton">
<span></span>
</span>
<div :class="$style.optionContent">
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
<div>
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
<template v-else>
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
</template>
</div>
</div>
</div>
</div>
<div :class="$style.caption">
<slot name="caption"></slot>
</div>
</div>
</template>
<script lang="ts"> <script lang="ts">
import { Comment, defineComponent, h, ref, watch } from 'vue'; import type { StyleValue } from 'vue';
import MkRadio from './MkRadio.vue'; import type { OptionValue } from '@/types/option-value.js';
import type { VNode } from 'vue';
export default defineComponent({ export type MkRadiosOption<T = OptionValue, S = string> = {
props: { value: T;
modelValue: { slotId?: S;
required: false, label?: string;
}, labelStyle?: StyleValue;
vertical: { icon?: string;
type: Boolean, iconStyle?: StyleValue;
default: false, caption?: string;
}, disabled?: boolean;
}, };
setup(props, context) {
const value = ref(props.modelValue);
watch(value, () => {
context.emit('update:modelValue', value.value);
});
watch(() => props.modelValue, v => {
value.value = v;
});
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
const caption = context.slots.caption && context.slots.caption();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodev-if=false(trueoptiontype)
options = options.filter(vnode => vnode.type !== Comment);
return () => h('div', {
class: [
'novjtcto',
...(props.vertical ? ['vertical'] : []),
],
}, [
...(label ? [h('div', {
class: 'label',
}, label)] : []),
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
key: option.key as string,
value: option.props?.value,
disabled: option.props?.disabled,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {
class: 'caption',
}, caption)] : []),
]);
},
});
</script> </script>
<style lang="scss"> <script setup lang="ts" generic="const T extends MkRadiosOption">
.novjtcto { defineProps<{
> .label { options: T[];
vertical?: boolean;
}>();
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
defineSlots<{
label?: () => void;
caption?: () => void;
} & {
[K in `option-${SlotNames}`]: () => void;
}>();
const model = defineModel<T['value']>({ required: true });
function getKey(value: OptionValue): PropertyKey {
if (value === null) return '___null___';
return value;
}
function toggle(o: MkRadiosOption): void {
if (o.disabled) return;
model.value = o.value;
}
</script>
<style lang="scss" module>
.label {
font-size: 0.85em; font-size: 0.85em;
padding: 0 0 8px 0; padding: 0 0 8px 0;
user-select: none; user-select: none;
@ -76,13 +102,13 @@ export default defineComponent({
} }
} }
> .body { .body {
display: flex; display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
} }
> .caption { .caption {
font-size: 0.85em; font-size: 0.85em;
padding: 8px 0 0 0; padding: 8px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75); color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
@ -92,10 +118,110 @@ export default defineComponent({
} }
} }
&.vertical { .vertical > .body {
> .body {
flex-direction: column; flex-direction: column;
} }
.optionRoot {
position: relative;
display: inline-flex;
align-items: center;
text-align: left;
cursor: pointer;
padding: 8px 10px;
min-width: 60px;
background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
border: solid 1px var(--MI_THEME-panel);
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
user-select: none;
&.disabled {
opacity: 0.6;
cursor: not-allowed !important;
} }
&:hover {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
background-color: var(--MI_THEME-accentedBg) !important;
border-color: var(--MI_THEME-accentedBg) !important;
color: var(--MI_THEME-accent);
cursor: default !important;
.optionButton {
border-color: var(--MI_THEME-accent);
&::after {
background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
}
.optionCaption {
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
}
}
}
.optionInput {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.optionButton {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--MI_THEME-inputBorder);
border-radius: 100%;
transition: inherit;
&::after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.optionContent {
display: flex;
align-items: center;
gap: 6px;
margin-left: 8px;
}
.optionCaption {
font-size: 0.85em;
padding: 2px 0 0 0;
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
transition: all 0.2s;
}
.optionIcon {
flex-shrink: 0;
} }
</style> </style>

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:myReaction="props.myReaction" :myReaction="props.myReaction"
@reactionToggled="onMockToggleReaction" @reactionToggled="onMockToggleReaction"
/> />
<slot v-if="hasMoreReactions" name="more"/> <slot v-if="hasMoreReactions" name="more"></slot>
</component> </component>
</template> </template>

View File

@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue'; import { onMounted, useTemplateRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import type { ScatterDataPoint } from 'chart.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js'; import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
@ -18,6 +19,12 @@ import { alpha } from '@/utility/color.js';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
interface RetentionPoint extends ScatterDataPoint {
x: number;
y: number;
d: string;
}
initChart(); initChart();
const chartEl = useTemplateRef('chartEl'); const chartEl = useTemplateRef('chartEl');
@ -62,14 +69,14 @@ onMounted(async () => {
fill: false, fill: false,
tension: 0.4, tension: 0.4,
data: [{ data: [{
x: '0', x: 0,
y: 100, y: 100,
d: getYYYYMMDD(new Date(record.createdAt)), d: getYYYYMMDD(new Date(record.createdAt)),
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({ }, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
x: (i + 1).toString(), x: i + 1,
y: (v / record.users) * 100, y: (v / record.users) * 100,
d: getYYYYMMDD(new Date(record.createdAt)), d: getYYYYMMDD(new Date(record.createdAt)),
}))] as any, }))],
})), })),
}, },
options: { options: {
@ -111,11 +118,11 @@ onMounted(async () => {
enabled: false, enabled: false,
callbacks: { callbacks: {
title(context) { title(context) {
const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string }; const v = context[0].dataset.data[context[0].dataIndex] as RetentionPoint;
return `${v.x} days later`; return `${v.x} days later`;
}, },
label(context) { label(context) {
const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string }; const v = context.dataset.data[context.dataIndex] as RetentionPoint;
const p = Math.round(v.y) + '%'; const p = Math.round(v.y) + '%';
return `${v.d} ${p}`; return `${v.d} ${p}`;
}, },

View File

@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
export type OptionValue = string | number | null; import type { OptionValue } from '@/types/option-value.js';
export type ItemOption<T extends OptionValue = OptionValue> = { export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option'; type?: 'option';

View File

@ -14,19 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-settings-question"></i></template> <template #icon><i class="ti ti-settings-question"></i></template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkRadios v-model="q_use" :vertical="true"> <MkRadios
<option value="single"> v-model="q_use"
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div> :options="[
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div> { value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description },
</option> { value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description },
<option value="group"> { value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description },
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div> ]"
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div> vertical
</option> >
<option value="open">
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
</option>
</MkRadios> </MkRadios>
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo> <MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
@ -40,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-users"></i></template> <template #icon><i class="ti ti-users"></i></template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkRadios v-model="q_scale" :vertical="true"> <MkRadios
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option> v-model="q_scale"
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option> :options="[
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option> { value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' },
{ value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' },
{ value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' },
]"
vertical
>
</MkRadios> </MkRadios>
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo> <MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
@ -57,9 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div> <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
<MkRadios v-model="q_federation" :vertical="true"> <MkRadios
<option value="yes">{{ i18n.ts.yes }}</option> v-model="q_federation"
<option value="no">{{ i18n.ts.no }}</option> :options="[
{ value: 'yes', label: i18n.ts.yes },
{ value: 'no', label: i18n.ts.no },
]"
vertical
>
</MkRadios> </MkRadios>
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo> <MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
@ -212,9 +218,9 @@ const props = withDefaults(defineProps<{
}); });
const q_name = ref(''); const q_name = ref('');
const q_use = ref('single'); const q_use = ref<'single' | 'group' | 'open'>('single');
const q_scale = ref('small'); const q_scale = ref<'small' | 'medium' | 'large'>('small');
const q_federation = ref('yes'); const q_federation = ref<'yes' | 'no'>('no');
const q_remoteContentsCleaning = ref(true); const q_remoteContentsCleaning = ref(true);
const q_adminName = ref(''); const q_adminName = ref('');
const q_adminEmail = ref(''); const q_adminEmail = ref('');
@ -239,7 +245,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
enableReactionsBuffering, enableReactionsBuffering,
clientOptions: { clientOptions: {
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple', entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
} as any, },
}; };
}); });

View File

@ -67,6 +67,7 @@ const emit = defineEmits<{
(ev: 'keydown', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter'): void; (ev: 'enter'): void;
(ev: 'update:modelValue', value: string): void; (ev: 'update:modelValue', value: string): void;
(ev: 'savingStateChange', saved: boolean, invalid: boolean): void;
}>(); }>();
const { modelValue, autofocus } = toRefs(props); const { modelValue, autofocus } = toRefs(props);
@ -131,6 +132,10 @@ watch(v, () => {
invalid.value = inputEl.value?.validity.badInput ?? true; invalid.value = inputEl.value?.validity.badInput ?? true;
}); });
watch([changed, invalid], ([newChanged, newInvalid]) => {
emit('savingStateChange', newChanged, newInvalid);
}, { immediate: true });
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
if (autofocus.value) { if (autofocus.value) {

View File

@ -22,18 +22,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="text"> <MkTextarea v-model="text">
<template #label>{{ i18n.ts.text }}</template> <template #label>{{ i18n.ts.text }}</template>
</MkTextarea> </MkTextarea>
<MkRadios v-model="icon"> <MkRadios
v-model="icon"
:options="[
{ value: 'info', icon: 'ti ti-info-circle' },
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
]"
>
<template #label>{{ i18n.ts.icon }}</template> <template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios> </MkRadios>
<MkRadios v-model="display"> <MkRadios
v-model="display"
:options="[
{ value: 'normal', label: i18n.ts.normal },
{ value: 'banner', label: i18n.ts.banner },
{ value: 'dialog', label: i18n.ts.dialog },
]"
>
<template #label>{{ i18n.ts.display }}</template> <template #label>{{ i18n.ts.display }}</template>
<option value="normal">{{ i18n.ts.normal }}</option>
<option value="banner">{{ i18n.ts.banner }}</option>
<option value="dialog">{{ i18n.ts.dialog }}</option>
</MkRadios> </MkRadios>
<MkSwitch v-model="needConfirmationToRead"> <MkSwitch v-model="needConfirmationToRead">
{{ i18n.ts._announcement.needConfirmationToRead }} {{ i18n.ts._announcement.needConfirmationToRead }}

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
</div> </div>
</button> </button>
<button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')"> <button key="specified" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
<div :class="$style.icon"><i class="ti ti-mail"></i></div> <div :class="$style.icon"><i class="ti ti-mail"></i></div>
<div :class="$style.body"> <div :class="$style.body">
<span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
@ -52,7 +52,6 @@ const modal = useTemplateRef('modal');
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number]; currentVisibility: typeof Misskey.noteVisibilities[number];
isSilenced: boolean; isSilenced: boolean;
localOnly: boolean;
anchorElement?: HTMLElement | null; anchorElement?: HTMLElement | null;
isReplyVisibilitySpecified?: boolean; isReplyVisibilitySpecified?: boolean;
}>(), { }>(), {

Some files were not shown because too many files have changed in this diff Show More