Compare commits

..
Author SHA1 Message Date
PiEgg 9acceea42b 🐛 Fix(ci): avoid AtomGit upload headers timeout 2026-08-15 14:35:28 +08:00
PiEgg dcc64b61d7 🐛 Fix(ci): buffer AtomGit uploads for large assets 2026-08-15 14:16:13 +08:00
PiEgg bad47fb145 Perf(ci): upload AtomGit assets concurrently 2026-08-15 13:06:06 +08:00
PiEgg e9d9ae66e9 Test(ci): trigger AtomGit release mirror 2026-08-15 11:52:34 +08:00
PiEgg 9506aadef5 🐛 Fix(core): make background throttling optional 2026-08-15 11:50:45 +08:00
PiEgg 3980ef89fa Feature(ci): mirror releases to AtomGit 2026-08-15 11:50:21 +08:00
45fd078e4e 🐛 Fix(gui): 移除 backgroundThrottling: false 修复 macOS GPU 进程空转 (#1435)
Co-authored-by: WayneVoyager <hanghaiwang@tencent.com>
2026-08-14 16:13:01 +08:00
PiEgg 4676326eb8 🎉 Release: v3.0.2 2026-08-14 15:49:03 +08:00
PiEggandGitHub 33bbc9a512 Perf(gui): stop idle GPU churn from uploader status dot (#1436) 2026-08-14 15:45:41 +08:00
PiEggandGitHub 77f1ed16b7 🐛 Fix(gui): protect persisted provider credentials 2026-07-21 14:21:10 +08:00
PiEgg e410afa9bd 🎉 Release: v3.0.1 2026-07-12 17:29:51 +08:00
PiEggandGitHub 33c9530658 Feature(i18n): add Japanese (ja) translation (#1422) 2026-07-12 13:59:51 +08:00
PiEggandGitHub 26fc8e46de 🎨 Style(gui): refine title bar layout, restore mini window button and fix related bugs (#1421) 2026-07-11 12:35:52 +08:00
moduvoiceandGitHub 622031f4ff Feature(i18n): add Korean (ko) translation (#1420) 2026-07-11 10:16:28 +08:00
CtrlKismetandGitHub 4d92ca199b 🐛 Fix(gui): mini window ignores startup HIDE mode on Linux (#1417) 2026-07-03 09:13:41 +08:00
PiEggandGitHub 80beead2a5 🎨 Style: improve tray & rename dialog layout and add GitHub Sponsors button (#1416) 2026-07-02 16:46:36 +08:00
PiEgg 4056447ba2 📝 Docs: update docs 2026-07-01 14:23:43 +08:00
PiEgg e26a21c4ea 📝 Docs: update docs 2026-07-01 14:22:14 +08:00
38 changed files with 2399 additions and 124 deletions
+2 -2
View File
@@ -333,10 +333,10 @@ jobs:
- name: List dist
run: ls -la dist/
- name: Upload to release.picgo.app
- name: Upload update metadata to release.picgo.app
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev
run: |
ARGS="--all"
ARGS="--all --version-files-only"
if [[ "${{ github.event.inputs.test_upload_dist_to_dev }}" == "true" ]]; then
ARGS="$ARGS --dev"
@@ -0,0 +1,64 @@
name: Mirror Release to AtomGit
on:
push:
branches:
- atomgit-release-ci-test
release:
types:
- published
workflow_dispatch:
inputs:
release_tag:
description: Published GitHub release tag to mirror
required: true
type: string
permissions:
contents: read
concurrency:
group: mirror-atomgit-${{ github.event.release.tag_name || inputs.release_tag }}
cancel-in-progress: false
jobs:
mirror-release:
name: Mirror GitHub Release to AtomGit
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag || 'v3.0.2' }}
ATOM_OWNER: ${{ vars.ATOM_OWNER || 'Molunerfinn' }}
ATOM_REPO: ${{ vars.ATOM_REPO || 'PicGo' }}
ATOM_TARGET_COMMITISH: ${{ vars.ATOM_TARGET_COMMITISH }}
ATOM_UPLOAD_TIMEOUT_MINUTES: ${{ vars.ATOM_UPLOAD_TIMEOUT_MINUTES || '70' }}
steps:
- name: Check out git repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Read GitHub release metadata
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release view "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--json tagName,name,body,isDraft,isPrerelease,targetCommitish \
> github-release.json
- name: Download GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p dist
gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--dir dist
- name: Publish AtomGit release
env:
ATOM_PAT: ${{ secrets.ATOM_PAT }}
run: node scripts/publish-atomgit-release.js
+35
View File
@@ -1,3 +1,38 @@
## :tada: 3.0.2 (2026-08-14)
### :bug: Bug Fixes
* **gui:** protect persisted provider credentials ([77f1ed1](https://github.com/Molunerfinn/PicGo/commit/77f1ed1))
### :zap: Performance Improvements
* **gui:** stop idle GPU churn from uploader status dot ([#1436](https://github.com/Molunerfinn/PicGo/issues/1436)) ([33bbc9a](https://github.com/Molunerfinn/PicGo/commit/33bbc9a))
## :tada: 3.0.1 (2026-07-12)
### :sparkles: Features
* **i18n:** add Japanese (ja) translation ([#1422](https://github.com/Molunerfinn/PicGo/issues/1422)) ([33c9530](https://github.com/Molunerfinn/PicGo/commit/33c9530))
* **i18n:** add Korean (ko) translation ([#1420](https://github.com/Molunerfinn/PicGo/issues/1420)) ([622031f](https://github.com/Molunerfinn/PicGo/commit/622031f))
### :bug: Bug Fixes
* **gui:** mini window ignores startup HIDE mode on Linux ([#1417](https://github.com/Molunerfinn/PicGo/issues/1417)) ([4d92ca1](https://github.com/Molunerfinn/PicGo/commit/4d92ca1))
### :pencil: Documentation
* update docs ([4056447](https://github.com/Molunerfinn/PicGo/commit/4056447))
* update docs ([e26a21c](https://github.com/Molunerfinn/PicGo/commit/e26a21c))
# :tada: 3.0.0 (2026-07-01)
+1 -1
View File
@@ -115,7 +115,7 @@ If youre new to PicGo, start with the [User Guide](https://docs.picgo.app/gui
## Screenshots
![](https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif)
![](https://raw.githubusercontent.com/Molunerfinn/PicGo/dev/assets/picgo-v3.gif)
![picgo-menubar](https://user-images.githubusercontent.com/12621342/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif)
Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "picgo",
"version": "3.0.0",
"version": "3.0.2",
"private": true,
"main": "dist_electron/main/index.js",
"description": "A powerful & simple image uploader for creators.",
@@ -72,7 +72,7 @@
"motion": "^12.35.1",
"multer": "^1.4.5-lts.1",
"next-themes": "^0.4.6",
"picgo": "^3.0.0",
"picgo": "^3.0.1",
"prismjs": "^1.30.0",
"qrcode.react": "^4.2.0",
"qrcode.vue": "^3.3.3",
+5 -5
View File
@@ -126,8 +126,8 @@ importers:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
picgo:
specifier: ^3.0.0
version: 3.0.0
specifier: ^3.0.1
version: 3.0.1
prismjs:
specifier: ^1.30.0
version: 1.30.0
@@ -5791,8 +5791,8 @@ packages:
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
picgo@3.0.0:
resolution: {integrity: sha512-QrhigSQvfnPB52/U2csrsvjUnyt9ItGy2lsEuClK8NN+6faFeivSovyDai6Xnek49V3pNo0O51aEt03hCr+f1A==}
picgo@3.0.1:
resolution: {integrity: sha512-6tovqMARqXJiLIDc6NpOO4pRJnftwdJPRB+S0qVcyfyHxWIgomrxes6EI4gL4H2r/EEA4oJ93RwVIbZM6j7YEA==}
engines: {node: '>= 20.19.0'}
hasBin: true
@@ -13634,7 +13634,7 @@ snapshots:
pend@1.2.0: {}
picgo@3.0.0:
picgo@3.0.1:
dependencies:
'@hono/node-server': 1.19.9(hono@4.12.3)
'@picgo/i18n': 1.0.0
+593
View File
@@ -0,0 +1,593 @@
LANG_DISPLAY_LABEL: "日本語"
ABOUT: 情報
OPEN_MAIN_WINDOW: メインウィンドウを開く
CHOOSE_DEFAULT_PICBED: デフォルトの画像ホスティングを選択
OPEN_UPDATE_HELPER: アップデートヘルパーを開く
PRIVACY_TERMS_AGREEMENT: プライバシーポリシーと利用規約への同意
RELOAD_APP: アプリを再読み込み
UPLOAD_FAILED: アップロード失敗
UPLOAD_SUCCEED: アップロード成功
UPLOAD_PROGRESS: アップロード進行状況
OPERATION_FAILED: 操作失敗
OPERATION_SUCCEED: 操作成功
UPLOADING: アップロード中
QUICK_UPLOAD: クイックアップロード
UPLOAD_BY_CLIPBOARD: クリップボードからアップロード
HIDE_WINDOW: ウィンドウを隠す
SPONSOR_PICGO: PicGoを支援する
SHOW_PICBED_QRCODE: 画像ホスティングQRコードを表示
PICBED_QRCODE: 画像ホスティングQRコード
ENABLE: 有効
DISABLE: 無効
CONFIG_THING: "${c}を設定"
FIND_NEW_VERSION: 新しいバージョンを発見
NO_MORE_NOTICE: 今後通知しない
MORE: その他
SHOW_DEVTOOLS: 開発者ツールを表示
CURRENT_PICBED: 現在の画像ホスティング
OPEN_TOOLBOX: ツールボックスを開く
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: "${d}をデフォルトの画像ホスティングに設定:"
SIDEBAR_DASHBOARD: ダッシュボード
UPLOAD_AREA: アップロードエリア
ALBUM: アルバム
ALBUM_PROVIDERS: プロバイダー
PICBEDS_SETTINGS: 画像ホスティング設定
PICGO_SETTINGS: PicGo設定
PLUGIN_SETTINGS: プラグイン設定
DASHBOARD_HISTORY_PANEL_TITLE: 履歴パネル
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: すべてのデバイスをつなぐPicGoのクラウドサービスです。
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloudエラー
PICGO_CLOUD_LOADING: PicGo Cloudの状態を読み込み中...
PICGO_CLOUD_NOT_LOGGED_IN: PicGo Cloudにログインしていません。
PICGO_CLOUD_LOGIN: ログイン
PICGO_CLOUD_LOGOUT: ログアウト
PICGO_CLOUD_CANCEL_LOGIN: ログインをキャンセル
PICGO_CLOUD_RETRY: リトライ
PICGO_CLOUD_CONFIG_SYNC: 設定同期
PICGO_CLOUD_LOGIN_IN_PROGRESS: ログイン処理中です。ブラウザでログインを完了してください。
PICGO_CLOUD_LOGGED_IN_AS: "ログイン中: ${user}"
PICGO_CLOUD_OPEN: PicGo Cloudを開く
PICGO_CLOUD_LOGIN_TIMEOUT: ログインがタイムアウトしました。もう一度お試しください。
PICGO_CLOUD_LOGIN_FAILED: ログインに失敗しました。もう一度お試しください。
PICGO_CLOUD_AGREE_PREFIX: "PicGoの以下に同意します: "
PICGO_CLOUD_TERMS_OF_SERVICE: 利用規約
PICGO_CLOUD_AGREE_AND: "および"
PICGO_CLOUD_PRIVACY_POLICY: プライバシーポリシー
PICGO_CLOUD_LOGIN_EXPIRED: ログインが期限切れです。再度ログインしてください。
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 暗号化モード
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 自動
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: サーバー側暗号化
PICGO_CLOUD_ENCRYPTION_MODE_E2E: エンドツーエンド暗号化
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: "自動: クラウドで最後に使用した暗号化方式に従います(デフォルトはサーバー側暗号化)。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: "サーバー側暗号化: 設定はサーバーに保存される前に暗号化されます。PINは不要です。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: "エンドツーエンド暗号化: PINを使用してデータを暗号化/復号化します。PicGoはPINを保存しません。紛失した場合、データは復元できません。"
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: ドキュメントを読む
PICGO_CLOUD_E2E_CHECKBOX_LABEL: E2E暗号化を有効にする
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: E2E暗号化を有効にする
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: "E2E暗号化を使用するにはPINを安全に保管する必要があります。PINを紛失すると、暗号化されたデータを復元できません。PicGoはPINや復旧データを保存しません。"
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: リモート設定がE2E暗号化されています。ローカルでもE2Eが有効になりました。
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: E2E暗号化PINの設定
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: E2E暗号化PINを入力
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PINが正しくありません。もう一度お試しください(${retryCount}回目)。"
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: PINを確認
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 設定の同期に成功しました。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 競合が検出されました。競合を解決してください。
PICGO_CLOUD_CONFIG_SYNC_FAILED: 設定の同期に失敗しました。
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 設定の同期がキャンセルされました。
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 暗号化方式を切り替えますか?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "「${from}」から「${to}」に切り替えます。\n\n注意: 暗号化モードを切り替えると、クラウドの履歴バージョンがすべて削除されます。\nこれは古い履歴バージョンが新しいモードで復号化または検証できないためです。\n切り替え後、システムは新しいバックアップを開始点として即座に作成します。"
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 切り替えを確認して履歴を削除
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: キャンセル
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: ユーザーにより暗号化の切り替えがキャンセルされました
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "設定同期失敗: ${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PINの入力回数が上限に達しました。もう一度お試しください。
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: ローカル設定が無効です。設定ファイルを確認してください。
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 設定の同期が進行中です。
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 保留中の競合セッションがあります。先に競合を解決してください。
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 競合セッションが見つかりません。
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: すべての競合項目についてローカルまたはクラウドを選択してください。
PICGO_CLOUD_CONFIG_SYNC_STARTING: 設定の同期を開始しています...
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "競合が検出されました(${count}件)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: すべてローカルを選択
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: すべてクラウドを選択
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: すべてリセット
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: ローカルバージョン
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: クラウドバージョン
PICGO_CLOUD_CONFIG_SYNC_ABORT: 中止
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 確認して同期
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (空)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 競合が解決されました。
PICGO_CLOUD_LAST_SYNC_TIME: "最終ローカル同期日時: ${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE: なし
PICGO_CLOUD_LAST_SYNC_LABEL: 最終同期
PICGO_CLOUD_PLAN_USAGE_TITLE: プランと使用量
PICGO_CLOUD_PLAN_USAGE_DESC: 現在のプランと使用量の概要を確認します。
PICGO_CLOUD_PLAN_PERIOD_LABEL: プラン期間
PICGO_CLOUD_STORAGE_LABEL: ストレージ
PICGO_CLOUD_FILES_LABEL: ファイル
PICGO_CLOUD_USAGE_PROGRESS: "${total}中${used}使用"
PICGO_CLOUD_USAGE_UNLIMITED: 無制限
PICGO_CLOUD_PLAN_PERIOD_RENEWS: "${date}に更新"
PICGO_CLOUD_PLAN_PERIOD_CANCELS: "${date}に解約"
PICGO_CLOUD_PLAN_PERIOD_UNTIL: "${date}まで有効"
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 永久ライセンス
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 猶予期限
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: "有料プランが期限切れとなり、現在猶予期間中です。この日付まではPicGo Cloud画像にアクセスできますが、使用量は一時的に無料枠に引き下げられます。ほとんどの有料機能は一時的に利用できません。この日付以降、アカウントは凍結状態になります。"
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 凍結期限
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: "アカウントが凍結されています。PicGo Cloud画像はエラーを返します。この日付以降、データが削除される場合があります。アクセスを復元するには更新してください。"
PICGO_CLOUD_QUOTA_DOWNGRADED: "猶予期間中、使用量は一時的に${plan}に引き下げられています。復元するには更新してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: プランが猶予期間中です
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: "有料プランが期限切れとなり、${days}日間の猶予期間に入りました。使用量は一時的に無料枠に引き下げられ、ほとんどの有料機能が一時的に利用できません。全機能を復元するには更新してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: アカウントが凍結されました
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: "アカウントが凍結されています。クラウド画像は一時的にアクセスできず、${days}日後にデータが削除されます。今すぐ更新してアクセスを復元してください。"
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: データ削除予定
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: アカウントは削除待ち状態であり、クラウドデータがまもなく完全に削除されます。データを保持するにはすぐに更新してください。
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 今すぐ更新
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 閉じる
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: プランが猶予期間または凍結期間中のため、自動インポートは一時停止されています。更新後に再開されます。
PICGO_CLOUD_IMAGE_UNAVAILABLE: 画像を利用できません
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 猶予期間中はこの操作を利用できません。プランを更新してからもう一度お試しください。
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: アカウントが凍結されています。アクセスを復元するにはプランを更新してください。
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 自動インポートが無効です。PicGo Cloud設定で先に有効にしてください。
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 現在のプランではこの機能をサポートしていません。アップグレードしてからもう一度お試しください。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: プランの使用量上限に達しました。上位プランにアップグレードしてください。
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 猶予期間中は使用量が無料枠に引き下げられます。復元するには更新してください。
PICGO_CLOUD_ERROR_PLAN_REQUIRED: この機能には有料プランが必要です。
PICGO_CLOUD_USAGE_LOAD_FAILED: 使用量データの読み込みに失敗しました。
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 同期状態の読み込みに失敗しました。
PICGO_CLOUD_FREE_PLAN_BANNER: 無料プランをご利用中です。アップグレードすると、より多くの容量と高度なクラウド機能を利用できます。
PICGO_CLOUD_VIEW_PLANS: プランを見る
PICGO_CLOUD_CONFIG_SYNC_TITLE: 設定同期
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: デバイス間で設定を安全に同期します。
PICGO_CLOUD_SYNC_NOW: 今すぐ同期
PICGO_CLOUD_SYNC_QUOTA_LABEL: 同期クォータ
PICGO_CLOUD_SYNC_QUOTA_TIP: "プランでは最新の${limit}個の設定スナップショットが保持されます。${limit}個中${limit}個を使用していても同期は引き続き可能です。上限を超えた古いスナップショットは同期のたびに自動的に削除されます。"
PICGO_CLOUD_LOGIN_PANEL_TITLE: PicGo Cloudにログイン
PICGO_CLOUD_LOGIN_PANEL_DESC: PicGo Cloudを公式画像ホスティングとして使用し、クラウド機能を有効にするにはログインしてください。
PICGO_CLOUD_LOGIN_FEATURES_TITLE: クラウド機能
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 公式画像ホスティング
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: アルバムクラウド同期が組み込まれたPicGoの公式画像ホスティングです。
PICGO_CLOUD_PAID_PLAN_BADGE: 有料プラン
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 再起動が必要ですか?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 一部の設定は再起動が必要な場合があります。今すぐ再起動しますか?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 今すぐ再起動
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: あとで
INPUT_BOX_CONFIRM_MISMATCH: 2つの入力値が一致しません。
PICGO_SPONSOR_TEXT: PicGoはフリーソフトウェアです。気に入っていただけたら、コーヒー一杯の支援をお忘れなく。
ALIPAY: Alipay
WECHATPAY: WeChat Pay
CHOOSE_PICBED: 画像ホスティングを選択
COPY_PICBED_CONFIG: 画像ホスティング設定をコピー
COPY_PICBED_CONFIG_SUCCEED: 画像ホスティング設定のコピーに成功
INPUT: 入力
CANCEL: キャンセル
CONFIRM: 確認
CHOOSE_SHOWED_PICBED: 表示する画像ホスティングを選択
CHOOSE_PASTE_FORMAT: 貼り付け形式を選択
SEARCH: 検索
COPY: コピー
DELETE: 削除
SELECT_ALL: すべて選択
CHANGE_IMAGE_URL: 画像URLを変更
CHANGE_IMAGE_URL_SUCCEED: 画像URLの変更に成功
COPY_LINK_SUCCEED: リンクのコピーに成功
BATCH_COPY_LINK_SUCCEED: リンクの一括コピーに成功
FILE_RENAME: ファイル名を変更
COPY_FILE_PATH: ファイルパスをコピー
OPEN_FILE_PATH: ファイルパスを開く
SUCCESS: 成功
FAILED: 失敗
# settings
SETTINGS: 設定
SETTINGS_OPEN_CONFIG_FILE: 設定ファイルを開く
SETTINGS_CLICK_TO_OPEN: クリックして開く
SETTINGS_SET_LOG_FILE: ログファイルの設定
SETTINGS_CLICK_TO_SET: クリックして設定
SETTINGS_CLICK_TO_CHECK: クリックして確認
SETTINGS_SET_SHORTCUT: ショートカットの設定
SETTINGS_URL_REWRITE: URLリライト
SETTINGS_CUSTOM_LINK_FORMAT: カスタムリンク形式
SETTINGS_SET_PROXY_AND_MIRROR: プロキシとミラーの設定
SETTINGS_SET_SERVER: サーバーの設定
SETTINGS_CHECK_UPDATE: アップデートを確認
SETTINGS_UPDATE_CHECK_RESULT: "最新バージョンは${version}です。${action}"
SETTINGS_UPDATE_CHECK_NO_UPDATE: 更新の必要はありません
SETTINGS_UPDATE_CHECK_CAN_UPDATE: アップデート可能
SETTINGS_OPEN_UPDATE_HELPER: アップデートヘルパーを開く
SETTINGS_OPEN: 開く
SETTINGS_CLOSE: 閉じる
SETTINGS_ACCEPT_BETA_UPDATE: ベータ版アップデートを許可
SETTINGS_LAUNCH_ON_BOOT: 起動時に自動実行
SETTINGS_RENAME_BEFORE_UPLOAD: アップロード前にリネーム
SETTINGS_TIMESTAMP_RENAME: タイムスタンプでリネーム
SETTINGS_OPEN_UPLOAD_TIPS: アップロードのヒントを表示
SETTINGS_NOTIFICATION_SOUND: 通知音を再生
SETTINGS_MINI_WINDOW_ON_TOP: ミニウィンドウを常に最前面に
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: アップロード後にURLを自動コピー
SETTINGS_TIPS_PLACEHOLDER_URL: $urlでURL位置を表します
SETTINGS_TIPS_PLACEHOLDER_FILENAME: $fileNameでファイル名の位置を表します
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: $extNameでファイル拡張子の位置を表します
SETTINGS_TIPS_SUCH_AS: "例: $url/$fileName"
SETTINGS_UPLOAD_PROXY: アップロードプロキシ
SETTINGS_PLUGIN_INSTALL_PROXY: プラグインインストール用プロキシ
SETTINGS_PLUGIN_INSTALL_MIRROR: プラグインインストール用ミラー
SETTINGS_CURRENT_VERSION: 現在のバージョン
SETTINGS_NEWEST_VERSION: 最新バージョン
SETTINGS_GETING: 取得中...
SETTINGS_TIPS_HAS_NEW_VERSION: PicGoに新しいバージョンがあります。確認をクリックしてダウンロードページを開いてください
SETTINGS_LOG_FILE: ログファイル
SETTINGS_LOG_LEVEL: ログレベル
SETTINGS_LOG_FILE_SIZE: ログファイルサイズ
SETTINGS_SET_PICGO_SERVER: PicGoサーバーの設定
SETTINGS_TIPS_SERVER_NOTICE: サーバー機能がわからない場合は、ドキュメントを参照するか、設定を変更しないでください。
SETTINGS_ENABLE_SERVER: サーバーを有効にする
SETTINGS_SET_SERVER_HOST: サーバーホストを設定
SETTINGS_SET_SERVER_PORT: サーバーポートを設定
SETTINGS_TIP_PLACEHOLDER_HOST: デフォルト:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: デフォルト:36677
SETTINGS_LOG_LEVEL_ALL: すべて
SETTINGS_LOG_LEVEL_SUCCESS: 成功
SETTINGS_LOG_LEVEL_ERROR: エラー
SETTINGS_LOG_LEVEL_INFO: 情報
SETTINGS_LOG_LEVEL_WARN: 警告
SETTINGS_LOG_LEVEL_NONE: なし
SETTINGS_RESULT: 結果
SETTINGS_DEFAULT_PICBED: デフォルトの画像ホスティング
SETTINGS_SET_DEFAULT_PICBED: デフォルトのアップローダーを設定
SETTINGS_NOT_CONFIG_OPTIONS: 設定項目なし
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 内蔵クリップボードを使用してアップロード
SETTINGS_CHOOSE_LANGUAGE: 言語を選択
UPLOADER_CONFIG_NAME: 設定名
BUILTIN_CLIPBOARD_TIPS: スクリプトの代わりに内蔵クリップボード機能を使用してアップロード
# url rewrite
URL_REWRITE_HELP: アップロードされた画像URLをリライトします。ルールは順番に評価され、最初にマッチしたルールが適用されます。
URL_REWRITE_ADD_RULE: ルールを追加
URL_REWRITE_EDIT_RULE: ルールを編集
URL_REWRITE_EMPTY: ルールなし
URL_REWRITE_ORDER: 順序
URL_REWRITE_MATCH: マッチパターン
URL_REWRITE_REPLACE: 置換文字列
URL_REWRITE_FLAGS: フラグ
URL_REWRITE_ENABLED: 有効
URL_REWRITE_ACTIONS: 操作
URL_REWRITE_MOVE_UP: 上へ
URL_REWRITE_MOVE_DOWN: 下へ
URL_REWRITE_EDIT: 編集
URL_REWRITE_DELETE: 削除
URL_REWRITE_DELETE_CONFIRM: このルールを削除しますか?
URL_REWRITE_MATCH_TIPS: 正規表現(JavaScript RegExp)に対応しています
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 置換文字列($1, $2, ... に対応)
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: オプション
URL_REWRITE_RULE_ENABLED: このルールを有効にする
URL_REWRITE_FLAG_GLOBAL_LABEL: グローバル (g)
URL_REWRITE_FLAG_GLOBAL_DESC: 最初のマッチだけでなく、すべてのマッチを置換します
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 大文字小文字を区別しない (i)
URL_REWRITE_FLAG_IGNORE_CASE_DESC: "大文字小文字を区別せずにマッチします(例: JPGとjpgを同一視)"
URL_REWRITE_MATCH_REQUIRED: マッチパターンは必須です
URL_REWRITE_REPLACE_REQUIRED: 置換文字列は必須です
URL_REWRITE_INVALID_REGEX: 無効な正規表現です
URL_REWRITE_PREVIEW_TITLE: プレビュー
URL_REWRITE_PREVIEW_TIPS: URLを入力すると、現在のルールがどのようにリライトするかを確認できます(順番にマッチし、最初のマッチのみ適用されます)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: プレビュー
URL_REWRITE_PREVIEW_OUTPUT: 出力URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: プレビューするURLを入力してください
URL_REWRITE_PREVIEW_RULE_INVALID: 無効なルールです
URL_REWRITE_PREVIEW_MATCHED_RULE: マッチしたルール
URL_REWRITE_PREVIEW_NO_MATCH: マッチするルールなし
UPLOADER_CONFIG_PLACEHOLDER: 設定名を入力してください
SELECTED_SETTING_HINT: 選択済み
SETTINGS_ENCODE_OUTPUT_URL: 出力(またはコピーした)URLをエンコード
SETTINGS_SHOW_DOCK_ICON: Dockアイコンを表示
SETTINGS_SHOW_MENUBAR_ICON: メニューバーアイコンを表示
SETTINGS_SHOW_MENUBAR_ICON_TIPS: "「Dockアイコンを表示」と「メニューバーアイコンを表示」の両方がオフの場合、PicGoのメインウィンドウが見つからなくなります。設定ファイルを編集してshowDockIconまたはshowMenubarIconをtrueに設定すると復元できます。"
SETTINGS_STARTUP_MODE: 起動モード
SETTINGS_STARTUP_MODE_MAIN_WINDOW: メインウィンドウを開く
SETTINGS_STARTUP_MODE_MINI_WINDOW: ミニウィンドウを開く
SETTINGS_STARTUP_MODE_HIDE: サイレント起動
# shortcut-page
SHORTCUT_NAME: ショートカット名
SHORTCUT_BIND: ショートカットバインド
SHORTCUT_STATUS: ステータス
SHORTCUT_ENABLED: 有効
SHORTCUT_DISABLED: 無効
SHORTCUT_SOURCE: ソース
SHORTCUT_HANDLE: ハンドル
SHORTCUT_ENABLE: 有効にする
SHORTCUT_DISABLE: 無効にする
SHORTCUT_EDIT: 編集
SHORTCUT_CHANGE_UPLOAD: アップロードショートカットを変更
# album-page
ALBUM_URL_REWRITE_TITLE: 選択した画像URLをリライト
ALBUM_URL_REWRITE_RESULT_TITLE: 画像URLリライト結果
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 最低1枚の画像を選択してください
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: グローバルURLリライトルールを適用
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: グローバルルール
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 任意項目です。一時ルールをスキップするには空のままにしてください。一時ルールはグローバルルールより優先されます。
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 一時ルールにはマッチパターンと置換文字列の両方が必要です
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 有効なグローバルルールがなく、一時ルールも入力されていません
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 一時ルールをグローバルURLリライトルールに保存しますか?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 適用して保存
ALBUM_URL_REWRITE_APPLY_ONLY: 適用のみ
ALBUM_URL_REWRITE_NO_CHANGES: 変更されたURLはありません
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: リライト結果が空のためスキップしました
# tray-page
WAIT_TO_UPLOAD: アップロード待ち
ALREADY_UPLOAD: アップロード済み
# upload-page
PICTURE_UPLOAD: 画像アップロード
DRAG_FILE_TO_HERE: ファイルをここにドラッグ、または
CLICK_TO_UPLOAD: クリックしてアップロード
LINK_FORMAT: リンク形式
CUSTOM: カスタム
CLIPBOARD_PICTURE: クリップボード
TIPS_DRAG_VALID_PICTURE_OR_URL: 有効な画像またはURLをここにドラッグしてください
TIPS_INPUT_URL: URLを入力
TIPS_HTTP_PREFIX: http://またはhttps://で始めてください。複数URL対応(1行に1つ)
TIPS_INPUT_VALID_URL: 有効なURLを入力してください
# plugins
PLUGIN_SEARCH_PLACEHOLDER: npmでpicgoプラグインを検索するか、ボタンをクリックしておすすめプラグイン一覧を確認してください
PLUGIN_SEARCH_EXACT_MATCH: プラグイン名の完全一致
PLUGIN_INSTALL: インストール
PLUGIN_INSTALLING: インストール中...
PLUGIN_INSTALLED: インストール済み
PLUGIN_DEPRECATED_BADGE: 非推奨
PLUGIN_DEPRECATED_TITLE: このプラグインは作者によりサポートが終了しています
PLUGIN_DOING_SOMETHING: 処理中...
PLUGIN_LIST: プラグイン一覧
PLUGIN_IMPORT_LOCAL: ローカルプラグインをインポート
# tips
TIPS_REMOVE_LINK: この操作で画像がアルバムから削除されます。続行しますか?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: この操作で画像がアルバムから削除されます。続行しますか?
TIPS_MUST_CONTAINS_URL: $url、$fileName、$extNameのいずれかを含める必要があります
TIPS_NETWORK_ERROR: ネットワークエラー
TIPS_NEED_RELOAD: アプリの再読み込みが必要です
TIPS_PLEASE_CHOOSE_LOG_LEVEL: ログレベルを選択してください
TIPS_SET_SUCCEED: 設定成功
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: このプラグインはGUIに最適化されていません。続行しますか?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 通知をクリックしてアプリを再読み込み
TIPS_GET_PLUGIN_LIST_FAILED: プラグイン一覧の取得に失敗しました
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: プラグインのインストールに成功
PLUGIN_INSTALL_FAILED: プラグインのインストールに失敗
PLUGIN_UNINSTALL_SUCCEED: プラグインのアンインストールに成功
PLUGIN_UNINSTALL_FAILED: プラグインのアンインストールに失敗
PLUGIN_UPDATE_SUCCEED: プラグインのアップデートに成功
PLUGIN_UPDATE_FAILED: プラグインのアップデートに失敗
PLUGIN_IMPORT_SUCCEED: プラグインのインポートに成功
PLUGIN_IMPORT_FAILED: プラグインのインポートに失敗
ENABLE_PLUGIN: プラグインを有効にする
DISABLE_PLUGIN: プラグインを無効にする
UNINSTALL_PLUGIN: プラグインをアンインストール
UPDATE_PLUGIN: プラグインをアップデート
# toolbox
TOOLBOX: ツールボックス
TOOLBOX_TITLE: PicGoの実行問題を診断
TOOLBOX_SUB_TITLE: 以下の項目を即座に検査して使用上の問題を修正します
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 設定ファイルの破損を確認
TOOLBOX_CHECK_ALBUM_FILE_BROKEN: アルバムファイルの破損を確認
TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: クリップボード画像アップロードの問題を確認
TOOLBOX_CHECK_PROBLEM_WITH_PROXY: プロキシ設定の正常性を確認
TOOLBOX_FIX_DONE_NEED_RELOAD: 修復が完了しました。適用するには再起動が必要です。再起動しますか?
TOOLBOX_CANT_AUTO_FIX: 自動修復できません。以下の問題を手動で修正してください
TOOLBOX_START_SCAN: スキャン開始
TOOLBOX_RE_SCAN: 再スキャン
TOOLBOX_START_FIX: 修復開始
TOOLBOX_SUCCESS_TIPS: おめでとうございます。問題は見つかりませんでした
TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: "設定ファイルのパス: ${path}"
TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 設定ファイルが破損しています
TOOLBOX_CHECK_ALBUM_FILE_PATH_TIPS: "アルバムファイルのパス: ${path}"
TOOLBOX_CHECK_ALBUM_FILE_BROKEN_TIPS: アルバムファイルが破損しています
TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: プロキシ設定正常
TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: プロキシ設定なし
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: プロキシ設定が正しくありません
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: プロキシ設定が利用できません
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: "クリップボード画像の一時フォルダのパス: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: "クリップボード画像の一時フォルダが存在しません: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: "次のフォルダを手動で作成してください: ${path}"
# tips
TIPS_NOTICE: お知らせ
TIPS_WARNING: 警告
TIPS_ERROR: エラー
TIPS_SKIPPED_INVALID_URLS: "無効なURL${n}件をスキップしました。詳細はログを確認してください"
TIPS_TOO_MANY_URLS_CONFIRM: "一度にURL${n}件をアップロードしようとしています。遅延が発生する可能性があるため、分割してアップロードすることをお勧めします。続行しますか?"
TIPS_NO_VALID_URLS: 有効なURLが見つかりません
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Node.jsをインストールしてPicGoを再起動してください
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: プラグインがアルバムから一部の画像を削除しようとしています。続行しますか?
TIPS_PLUGIN_OVERWRITE_ALBUM: プラグインがアルバムを上書きしようとしています。続行しますか?
TIPS_UPLOAD_NOT_PICTURES: 最新のクリップボード項目は画像ではありません
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo設定ファイルが破損したため、デフォルトに復元されました
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo設定ファイルが破損したため、バックアップから復元されました
TIPS_PICGO_BACKUP_FILE_VERSION: "バックアップファイルのバージョン: ${v}"
TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: カスタム設定ファイルの解析エラーです。パスの内容を確認してください
TIPS_SHORTCUT_MODIFIED_SUCCEED: ショートカットの変更に成功しました
TIPS_SHORTCUT_MODIFIED_CONFLICT: ショートカットが競合しています。再設定してください
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: カスタムリンクスタイルの変更に成功しました
TIPS_FIND_NEW_VERSION: "新しいバージョン${v}が見つかりました。多くの新機能が追加されています。最新バージョンをダウンロードしますか?"
TIPS_DELETE_UPLOADER_CONFIG: この設定を削除しますか?
TIPS_COPY_UPLOADER_CONFIG: この設定をコピーしますか?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 設定名を空にすることはできません
TIPS_UPLOADER_CONFIG_NOT_FOUND: 設定が見つかりません
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 最後の設定は削除できません
# privacy
PRIVACY: "ご利用の前にプライバシーポリシー${privacyUrl}と利用規約${termsUrl}をお読みいただき、同意してください。"
PRIVACY_TIPS: アップロードするにはプライバシーポリシーに同意してください
QUIT: 終了
ALBUM_ALL_PHOTOS: すべての写真
ALBUM_COLLECTIONS: コレクション
ALBUM_TAGS: タグ
ALBUM_MENU: メニュー
ALBUM_OPEN_INSPECTOR: インスペクターを開く
ALBUM_INSPECTOR_TITLE: アルバムインスペクター
ALBUM_INSPECTOR_DESCRIPTION: 選択した画像を検査・編集します。
ALBUM_CLEAR_SELECTION: 選択を解除
ALBUM_SELECTED_COUNT: "${count}件選択中"
ALBUM_GRID_VIEW: グリッド表示
ALBUM_LIST_VIEW: リスト表示
ALBUM_PREVIEW: フルスクリーンプレビュー
ALBUM_PREVIEW_EMPTY: プレビューする画像がありません
ALBUM_PREVIEW_PREV: 前へ
ALBUM_PREVIEW_NEXT: 次へ
ALBUM_PREVIEW_CLOSE: プレビューを閉じる
ALBUM_PREVIEW_COUNT: "${current} / ${total}"
ALBUM_QUICK_ACTIONS: クイック操作
ALBUM_EXPORT: ダウンロード
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 一括リライト
ALBUM_COLLECTION: コレクション
ALBUM_ADD_TAG: タグを追加
ALBUM_TAG_SUGGESTIONS: おすすめタグ
ALBUM_COLUMN_NAME: 名前
ALBUM_COLUMN_PROVIDER: プロバイダー
ALBUM_COLUMN_SIZE: サイズ
ALBUM_COLUMN_DATE: 日付
ALBUM_ADD: 追加
ALBUM_SOURCE_LOCAL: ローカル
ALBUM_SOURCE_CLOUD: クラウド
ALBUM_CLOUD_LOAD_FAILED: クラウドアルバムの読み込みに失敗しました
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: ログインが必要です
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: クラウドアルバムを使用するには有料のPicGo Cloudアカウントでログインしてください
ALBUM_CLOUD_LOGIN_BUTTON: ログイン
ALBUM_CLOUD_UPGRADE_TITLE: アップグレードが必要です
ALBUM_CLOUD_UPGRADE_DESC: クラウドアルバムには有料プランが必要です
ALBUM_CLOUD_UPGRADE_BUTTON: アップグレード
ALBUM_CLOUD_FEATURES_TITLE: クラウドアルバムを使う理由
ALBUM_CLOUD_FEATURE_SYNC: すべてのデバイスからアップロード履歴にアクセス
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 新しいアップロード情報を自動インポート(オプトインが必要)
ALBUM_CLOUD_FEATURE_SECURE: ローカル履歴をワンクリックでクラウドにインポート
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: ローカル記録をインポート
ALBUM_CLOUD_IMPORT_GUIDE_DESC: ローカルのアップロード履歴をクラウドアルバムにインポートします。実際の画像ファイルではなく、記録のみがインポートされます。
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: インポート開始
ALBUM_CLOUD_EMPTY_TITLE: 項目なし
ALBUM_CLOUD_EMPTY_DESC: クラウドアルバムは空です。画像をアップロードして始めましょう。
ALBUM_CLOUD_IMPORTING: クラウドアルバムにインポート中...
ALBUM_CLOUD_IMPORT_SUCCESS: "${num}件をクラウドアルバムにインポートしました"
ALBUM_CLOUD_IMPORT_FAILED: クラウドアルバムへのインポートに失敗しました
ALBUM_CLOUD_REFRESH: 更新
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 自動インポートを有効にする
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: この操作を実行すると自動インポートが有効になり、以降のアップロードがクラウドアルバムに自動記録されます。既存のローカル記録も同時にインポートされます。
ALBUM_CLOUD_DELETE_FAILED: クラウドアルバムからの削除に失敗しました
ALBUM_INSPECTOR_DETAILS_TITLE: 詳細情報
ALBUM_INSPECTOR_FILE_NAME: ファイル名
ALBUM_INSPECTOR_UPLOADER: アップローダー
ALBUM_INSPECTOR_CONTENT_TYPE: コンテンツタイプ
ALBUM_INSPECTOR_CREATED_AT: 作成日
ALBUM_INSPECTOR_UPDATED_AT: 更新日
ALBUM_INSPECTOR_FILE_SIZE: サイズ
ALBUM_CLOUD_IMPORT_STATUS_TITLE: クラウドアルバム
ALBUM_CLOUD_IMPORTED: クラウドにインポート済み
ALBUM_CLOUD_IMPORTED_TOOLTIP: これはローカルマーカーです。クラウドから記録が削除されても、このステータスは更新されません。必要に応じて再インポートできます。
ALBUM_CLOUD_REIMPORT: 再インポート
ALBUM_CLOUD_NATIVE: PicGo Cloud経由でアップロード
ALBUM_CLOUD_IMPORT_BUTTON: クラウドアルバムにインポート
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: "${num}件をクラウドアルバムにインポート"
ALBUM_CLOUD_ALL_IMPORTED: すべてクラウドアルバムに登録済み
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 自動インポートを有効にする
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: クラウドアルバムにインポートするには、まず自動インポートを有効にする必要があります。今すぐ有効にして続行しますか?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 有効にしてインポート
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: クラウドアルバムへのインポートに成功しました
PICGO_CLOUD_AUTO_IMPORT_LABEL: クラウドアルバムへ自動インポート
PICGO_CLOUD_AUTO_IMPORT_DESC: 他のアップローダーのアップロード記録をクラウドアルバムに自動インポートします(PicGo Cloudはデフォルトで保存されます)
SIDEBAR_PLUGINS: プラグイン
SIDEBAR_SYSTEM: システム
SIDEBAR_NOTIFICATIONS: 通知
SIDEBAR_EXPAND: クリックして展開
SIDEBAR_COLLAPSE: サイドバーを折りたたむ
HISTORY_PANEL_TITLE: 履歴
HISTORY_PANEL_FILTER_PLACEHOLDER: フィルター...
HISTORY_PANEL_TODAY: 今日
HISTORY_PANEL_YESTERDAY: 昨日
DASHBOARD_NO_VISIBLE_PROVIDERS: 表示するプロバイダーがありません
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: すべてのプロバイダーが非表示になっています。設定でプロバイダーの表示を変更してください。
DASHBOARD_OPEN_SETTINGS: 設定を開く
DASHBOARD_DROP_IMAGES_HERE: ファイルをここにドロップ
DASHBOARD_OR: または
DASHBOARD_CLICK_TO_UPLOAD: クリックしてアップロード
DASHBOARD_PASTE_FROM_CLIPBOARD: クリップボードから貼り付け
DASHBOARD_PASTE_FROM_URL: URLから貼り付け
DASHBOARD_CLIPBOARD: クリップボード
FIELD_IS_REQUIRED: ${field}は必須です
CONFIG_RENAME: 設定名を変更
SETTINGS_SECTION_GENERAL: 一般
SETTINGS_SECTION_APPEARANCE: 外観
SETTINGS_SECTION_UPLOAD_WORKFLOW: アップロードワークフロー
SETTINGS_SECTION_NETWORK: ネットワーク
SETTINGS_SECTION_ADVANCED: 詳細設定
SETTINGS_SECTION_ABOUT: 情報
SETTINGS_NO_RESULTS_TITLE: 設定が見つかりません
SETTINGS_NO_RESULTS_DESCRIPTION: 別のキーワードを試すか、検索入力をクリアしてください。
SETTINGS_OPEN_SHORTCUTS: ショートカット
SETTINGS_LINK_WEBSITE: Webサイト
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: ドキュメント
SETTINGS_LINK_PRIVACY: プライバシーポリシー
SETTINGS_LINK_TERMS: 利用規約
SETTINGS_APPEARANCE_MODE: 外観
SETTINGS_APPEARANCE_MODE_LIGHT: ライト
SETTINGS_APPEARANCE_MODE_DARK: ダーク
SETTINGS_APPEARANCE_MODE_AUTO: 自動
COMMON_FOR_EXAMPLE: "例: ${value}"
SETTINGS_SET_DEFAULT_CONFIG: デフォルトに設定
PROVIDER_DRAFT_CONFIG: 下書き
PROVIDER_ACTIVE_UPLOADER_LABEL: アクティブ
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: ${uploaderName}がアクティブなアップローダーです
PROVIDER_DEFAULT_CONFIG_LABEL: デフォルト
PROVIDER_DEFAULT_CONFIG_TOOLTIP: ${uploaderName}がアクティブな場合に使用されます。
PROVIDER_SIDEBAR_EMPTY: アップローダーまたは設定が見つかりません。
PROVIDER_UPLOADER_ACTIONS: ${uploaderName}の操作
PROVIDER_SIDEBAR_COLLAPSE: 折りたたむ
PROVIDER_SIDEBAR_EXPAND: 展開
PROVIDER_CONFIG_ACTIONS: 設定の操作
PROVIDER_CREATE_CONFIG: 設定を作成
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: このアップローダーには設定項目がないため、追加の設定を作成する必要はありません
PROVIDER_INSTALL_MORE_UPLOADERS: アップローダーを追加インストール
PROVIDER_NO_UPLOADER_SELECTED: アップローダーが選択されていません。
PROVIDER_NO_CONFIG_YET: 設定はまだありません
PROVIDER_NO_CONFIG_DESCRIPTION: ${uploaderName}のスキーマベースのオプションを編集するには、新しい設定を作成してください。
PROVIDER_CONFIGURATION: 設定
PROVIDER_UPDATED_AT_LABEL: 更新日
PROVIDER_DELETE_CONFIG_HINT: この設定を完全に削除します。
UPLOADER_SWITCHER_SELECT_PROVIDER: プロバイダーを選択
UPLOADER_SWITCHER_CONFIG: 設定
UPLOADER_SWITCHER_NO_CONFIG: 設定なし
ALBUM_URL_REWRITE_DESCRIPTION: 選択した画像のURLを一括リライトします。
ALBUM_URL_REWRITE_CHANGED: 変更あり
ALBUM_URL_REWRITE_UNCHANGED: 変更なし
TRAY_ALREADY_UPLOAD_EMPTY: アップロードされた画像はまだありません
PLUGIN_EMPTY: プラグインが見つかりません。
PLUGIN_EMPTY_DESCRIPTION: npmを検索してプラグインをインストールして始めましょう。
PLUGIN_DETAIL: 詳細
PLUGIN_NO_CONFIG_SCHEMA: このプラグインは設定スキーマを提供していません。
PLUGIN_NO_TRANSFORMER_SCHEMA: このプラグインはトランスフォーマースキーマを提供していません。
PLUGIN_NO_README: このプラグインのREADMEはありません。
NO_OPTIONS_FOUND: オプションが見つかりません。
+593
View File
@@ -0,0 +1,593 @@
LANG_DISPLAY_LABEL: "한국어"
ABOUT: 정보
OPEN_MAIN_WINDOW: 메인 창 열기
CHOOSE_DEFAULT_PICBED: 기본 이미지 호스팅 선택
OPEN_UPDATE_HELPER: 업데이트 도우미 열기
PRIVACY_TERMS_AGREEMENT: 개인정보 처리방침 및 이용약관 동의
RELOAD_APP: 앱 새로고침
UPLOAD_FAILED: 업로드 실패
UPLOAD_SUCCEED: 업로드 성공
UPLOAD_PROGRESS: 업로드 진행률
OPERATION_FAILED: 작업 실패
OPERATION_SUCCEED: 작업 성공
UPLOADING: 업로드 중
QUICK_UPLOAD: 빠른 업로드
UPLOAD_BY_CLIPBOARD: 클립보드로 업로드
HIDE_WINDOW: 창 숨기기
SPONSOR_PICGO: PicGo 후원하기
SHOW_PICBED_QRCODE: 이미지 호스팅 QR 코드 표시
PICBED_QRCODE: 이미지 호스팅 QR 코드
ENABLE: 사용
DISABLE: 사용 안 함
CONFIG_THING: "${c} 설정"
FIND_NEW_VERSION: 새 버전 찾기
NO_MORE_NOTICE: 다시 알리지 않기
MORE: 더 보기
SHOW_DEVTOOLS: 개발자 도구 표시
CURRENT_PICBED: 현재 이미지 호스팅
OPEN_TOOLBOX: 도구 상자 열기
# ---renderer i18n begin---
CHOOSE_YOUR_DEFAULT_PICBED: "${d}을(를) 기본 이미지 호스팅으로 선택:"
SIDEBAR_DASHBOARD: 대시보드
UPLOAD_AREA: 업로드 영역
ALBUM: 앨범
ALBUM_PROVIDERS: 제공자
PICBEDS_SETTINGS: 이미지 호스팅 설정
PICGO_SETTINGS: PicGo 설정
PLUGIN_SETTINGS: 플러그인 설정
DASHBOARD_HISTORY_PANEL_TITLE: 기록 패널
PICGO_CLOUD_TITLE: PicGo Cloud
PICGO_CLOUD_DESCRIPTION: 모든 기기를 연결하는 PicGo의 클라우드 서비스입니다.
PICGO_CLOUD_BRAND_NAME: PicGo Cloud
PICGO_CLOUD_ERROR_TITLE: PicGo Cloud 오류
PICGO_CLOUD_LOADING: PicGo Cloud 상태를 불러오는 중...
PICGO_CLOUD_NOT_LOGGED_IN: PicGo Cloud에 로그인되어 있지 않습니다.
PICGO_CLOUD_LOGIN: 로그인
PICGO_CLOUD_LOGOUT: 로그아웃
PICGO_CLOUD_CANCEL_LOGIN: 로그인 취소
PICGO_CLOUD_RETRY: 재시도
PICGO_CLOUD_CONFIG_SYNC: 설정 동기화
PICGO_CLOUD_LOGIN_IN_PROGRESS: 로그인이 진행 중입니다. 브라우저에서 로그인을 완료해 주세요.
PICGO_CLOUD_LOGGED_IN_AS: "로그인 계정: ${user}"
PICGO_CLOUD_OPEN: PicGo Cloud 열기
PICGO_CLOUD_LOGIN_TIMEOUT: 로그인 시간이 초과되었습니다. 다시 시도해 주세요.
PICGO_CLOUD_LOGIN_FAILED: 로그인에 실패했습니다. 다시 시도해 주세요.
PICGO_CLOUD_AGREE_PREFIX: "PicGo의 "
PICGO_CLOUD_TERMS_OF_SERVICE: 이용약관
PICGO_CLOUD_AGREE_AND: " 및 "
PICGO_CLOUD_PRIVACY_POLICY: 개인정보 처리방침
PICGO_CLOUD_LOGIN_EXPIRED: 로그인이 만료되었습니다. 다시 로그인해 주세요.
PICGO_CLOUD_ENCRYPTION_MODE_LABEL: 암호화 모드
PICGO_CLOUD_ENCRYPTION_MODE_AUTO: 자동
PICGO_CLOUD_ENCRYPTION_MODE_SERVER: 서버 측 암호화
PICGO_CLOUD_ENCRYPTION_MODE_E2E: 종단간 암호화
PICGO_CLOUD_ENCRYPTION_MODE_TIP_AUTO: "자동: 클라우드에서 마지막으로 사용한 암호화 방식을 따릅니다(기본값은 서버 측 암호화)."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_SERVER: "서버 측 암호화: 설정이 서버에 저장되기 전에 암호화됩니다. PIN이 필요하지 않습니다."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_E2E: "종단간 암호화: PIN을 사용해 데이터를 암호화/복호화합니다. PicGo는 PIN을 저장하지 않으며, 분실 시 데이터를 복구할 수 없습니다."
PICGO_CLOUD_ENCRYPTION_MODE_TIP_DOC: 전체 문서 보기
PICGO_CLOUD_E2E_CHECKBOX_LABEL: 종단간 암호화 사용
PICGO_CLOUD_E2E_ENABLE_WARNING_TITLE: 종단간 암호화 사용
PICGO_CLOUD_E2E_ENABLE_WARNING_MESSAGE: "종단간 암호화를 사용하려면 PIN을 안전하게 보관해야 합니다. PIN을 분실하면 암호화된 데이터를 복구할 수 없습니다. PicGo는 PIN이나 복구 데이터를 저장하지 않습니다."
PICGO_CLOUD_REMOTE_E2E_AUTO_ENABLED: 원격 설정이 종단간 암호화되어 있습니다. 로컬에서도 종단간 암호화가 활성화되었습니다.
PICGO_CLOUD_E2E_PIN_SETUP_TITLE: 종단간 암호화 PIN 설정
PICGO_CLOUD_E2E_PIN_DECRYPT_TITLE: 종단간 암호화 PIN 입력
PICGO_CLOUD_E2E_PIN_RETRY_TITLE: "PIN이 올바르지 않습니다. 다시 시도해 주세요 (시도 ${retryCount}회)."
PICGO_CLOUD_E2E_PIN_PLACEHOLDER: PIN
PICGO_CLOUD_E2E_PIN_CONFIRM_PLACEHOLDER: PIN 확인
PICGO_CLOUD_CONFIG_SYNC_SUCCESS: 설정 동기화에 성공했습니다.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_DETECTED: 충돌이 발견되었습니다. 충돌을 해결해 주세요.
PICGO_CLOUD_CONFIG_SYNC_FAILED: 설정 동기화에 실패했습니다.
PICGO_CLOUD_CONFIG_SYNC_ABORTED: 설정 동기화가 취소되었습니다.
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_TITLE: 암호화 방식을 전환하시겠습니까?
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_BODY: "\"${from}\"에서 \"${to}\"(으)로 전환합니다.\n\n참고: 암호화 모드를 전환하면 클라우드 기록 버전이 모두 삭제됩니다.\n이는 이전 기록 버전을 새 모드에서 복호화하거나 검증할 수 없기 때문입니다.\n전환 후에는 시스템이 즉시 새로운 백업을 시작점으로 생성합니다."
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CONFIRM: 전환 확인 및 기록 삭제
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCEL: 취소
PICGO_CLOUD_CONFIG_SYNC_ENCRYPTION_SWITCH_CANCELLED: 사용자가 암호화 전환을 취소했습니다
PICGO_CLOUD_CONFIG_SYNC_FAILED_WITH_REASON: "설정 동기화 실패: ${reason}"
PICGO_CLOUD_CONFIG_SYNC_PIN_MAX_RETRY: PIN을 너무 많이 잘못 입력했습니다. 다시 시도해 주세요.
PICGO_CLOUD_CONFIG_SYNC_LOCAL_CONFIG_INVALID: 로컬 설정이 유효하지 않습니다. 설정 파일을 확인해 주세요.
PICGO_CLOUD_CONFIG_SYNC_IN_PROGRESS: 설정 동기화가 진행 중입니다.
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_PENDING: 대기 중인 충돌 세션이 있습니다. 먼저 충돌을 해결해 주세요.
PICGO_CLOUD_CONFIG_SYNC_NO_CONFLICT_SESSION: 충돌 세션을 찾을 수 없습니다.
PICGO_CLOUD_CONFIG_SYNC_RESOLUTION_INCOMPLETE: 모든 충돌 항목에 대해 로컬 또는 클라우드를 선택해 주세요.
PICGO_CLOUD_CONFIG_SYNC_STARTING: 설정 동기화를 시작합니다...
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_TITLE: "충돌 발견 (${count}개)"
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_LOCAL: 모두 로컬 선택
PICGO_CLOUD_CONFIG_SYNC_CHOOSE_ALL_CLOUD: 모두 클라우드 선택
PICGO_CLOUD_CONFIG_SYNC_RESET_ALL: 전체 초기화
PICGO_CLOUD_CONFIG_SYNC_LOCAL_VERSION: 로컬 버전
PICGO_CLOUD_CONFIG_SYNC_CLOUD_VERSION: 클라우드 버전
PICGO_CLOUD_CONFIG_SYNC_ABORT: 중단
PICGO_CLOUD_CONFIG_SYNC_CONFIRM_AND_SYNC: 확인 및 동기화
PICGO_CLOUD_CONFIG_SYNC_VALUE_UNDEFINED: (비어 있음)
PICGO_CLOUD_CONFIG_SYNC_CONFLICT_RESOLVED: 충돌이 해결되었습니다.
PICGO_CLOUD_LAST_SYNC_TIME: "마지막 로컬 동기화 시간: ${time}"
PICGO_CLOUD_LAST_SYNC_TIME_NONE: 없음
PICGO_CLOUD_LAST_SYNC_LABEL: 마지막 동기화
PICGO_CLOUD_PLAN_USAGE_TITLE: 요금제 및 사용량
PICGO_CLOUD_PLAN_USAGE_DESC: 현재 요금제와 사용량 개요를 확인하세요.
PICGO_CLOUD_PLAN_PERIOD_LABEL: 요금제 기간
PICGO_CLOUD_STORAGE_LABEL: 저장 공간
PICGO_CLOUD_FILES_LABEL: 파일
PICGO_CLOUD_USAGE_PROGRESS: "${total} 중 ${used}"
PICGO_CLOUD_USAGE_UNLIMITED: 무제한
PICGO_CLOUD_PLAN_PERIOD_RENEWS: "${date}에 갱신"
PICGO_CLOUD_PLAN_PERIOD_CANCELS: "${date}에 해지"
PICGO_CLOUD_PLAN_PERIOD_UNTIL: "${date}까지 유효"
PICGO_CLOUD_PLAN_PERIOD_LIFETIME: 평생 이용
PICGO_CLOUD_PLAN_PERIOD_GRACE_LABEL: 유예 기간 종료일
PICGO_CLOUD_PLAN_PERIOD_GRACE_TOOLTIP: "유료 요금제가 만료되어 현재 유예 기간입니다. 이 날짜까지는 PicGo Cloud 이미지에 계속 접근할 수 있지만, 할당량은 일시적으로 무료 등급으로 낮아집니다. 대부분의 유료 기능은 일시적으로 사용할 수 없습니다. 이 날짜 이후에는 계정이 정지 상태가 됩니다."
PICGO_CLOUD_PLAN_PERIOD_FROZEN_LABEL: 정지 종료일
PICGO_CLOUD_PLAN_PERIOD_FROZEN_TOOLTIP: "계정이 정지되었습니다. PicGo Cloud 이미지는 오류를 반환합니다. 이 날짜 이후에는 데이터가 삭제될 수 있습니다. 접근 권한을 복구하려면 갱신해 주세요."
PICGO_CLOUD_QUOTA_DOWNGRADED: "유예 기간 동안 할당량이 일시적으로 ${plan}(으)로 낮아졌습니다. 복구하려면 갱신해 주세요."
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_TITLE: 요금제가 유예 기간입니다
PICGO_CLOUD_LIFECYCLE_BANNER_GRACE_DESC: "유료 요금제가 만료되어 ${days}일간의 유예 기간에 들어갔습니다. 할당량은 일시적으로 무료 등급으로 낮아지고, 대부분의 유료 기능을 사용할 수 없습니다. 전체 기능을 복구하려면 갱신해 주세요."
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_TITLE: 계정이 정지되었습니다
PICGO_CLOUD_LIFECYCLE_BANNER_FROZEN_DESC: "계정이 정지되어 클라우드 이미지에 일시적으로 접근할 수 없으며, ${days}일 후 데이터가 삭제됩니다. 지금 갱신하면 접근 권한을 복구할 수 있습니다."
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_TITLE: 데이터 삭제 예정
PICGO_CLOUD_LIFECYCLE_BANNER_PENDING_CLEANUP_DESC: 계정이 삭제 대기 상태이며 클라우드 데이터가 곧 영구적으로 삭제됩니다. 데이터를 유지하려면 즉시 갱신해 주세요.
PICGO_CLOUD_LIFECYCLE_BANNER_CTA: 지금 갱신
PICGO_CLOUD_LIFECYCLE_BANNER_DISMISS: 닫기
PICGO_CLOUD_AUTO_IMPORT_DISABLED_BY_LIFECYCLE: 요금제가 유예 또는 정지 상태인 동안에는 자동 가져오기가 일시 중지됩니다. 갱신 후 다시 시작됩니다.
PICGO_CLOUD_IMAGE_UNAVAILABLE: 이미지를 사용할 수 없음
PICGO_CLOUD_ERROR_GRACE_RESTRICTED: 유예 기간 동안에는 이 작업을 사용할 수 없습니다. 요금제를 갱신한 후 다시 시도해 주세요.
PICGO_CLOUD_ERROR_ACCOUNT_FROZEN: 계정이 정지되었습니다. 접근 권한을 복구하려면 요금제를 갱신해 주세요.
PICGO_CLOUD_ERROR_IMPORT_DISABLED: 자동 가져오기가 비활성화되어 있습니다. PicGo Cloud 설정에서 먼저 활성화해 주세요.
PICGO_CLOUD_ERROR_PLAN_INELIGIBLE: 현재 요금제에서는 이 기능을 지원하지 않습니다. 업그레이드 후 다시 시도해 주세요.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_ACTIVE: 요금제 할당량에 도달했습니다. 더 많은 용량을 사용하려면 상위 요금제로 업그레이드하세요.
PICGO_CLOUD_ERROR_QUOTA_EXCEEDED_GRACE: 유예 기간 동안에는 할당량이 무료 등급으로 낮아집니다. 복구하려면 갱신해 주세요.
PICGO_CLOUD_ERROR_PLAN_REQUIRED: 이 기능은 유료 요금제가 필요합니다.
PICGO_CLOUD_USAGE_LOAD_FAILED: 사용량 데이터를 불러오지 못했습니다.
PICGO_CLOUD_CONFIG_SYNC_LOAD_FAILED: 동기화 상태를 불러오지 못했습니다.
PICGO_CLOUD_FREE_PLAN_BANNER: 무료 요금제를 사용 중입니다. 업그레이드하면 더 많은 할당량과 고급 클라우드 기능을 이용할 수 있습니다.
PICGO_CLOUD_VIEW_PLANS: 요금제 보기
PICGO_CLOUD_CONFIG_SYNC_TITLE: 설정 동기화
PICGO_CLOUD_CONFIG_SYNC_CARD_DESC: 기기 간 설정을 안전하게 동기화하세요.
PICGO_CLOUD_SYNC_NOW: 지금 동기화
PICGO_CLOUD_SYNC_QUOTA_LABEL: 동기화 할당량
PICGO_CLOUD_SYNC_QUOTA_TIP: "요금제는 최근 ${limit}개의 설정 스냅샷을 보관합니다. ${limit}개 중 ${limit}개를 모두 사용해도 동기화는 계속 가능합니다 — 한도를 초과한 오래된 스냅샷은 매번 동기화 후 자동으로 정리됩니다."
PICGO_CLOUD_LOGIN_PANEL_TITLE: PicGo Cloud에 로그인
PICGO_CLOUD_LOGIN_PANEL_DESC: PicGo Cloud를 PicGo의 공식 이미지 호스팅으로 사용하고 클라우드 기능을 활성화하려면 로그인하세요.
PICGO_CLOUD_LOGIN_FEATURES_TITLE: 클라우드 기능
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_TITLE: 공식 이미지 호스팅
PICGO_CLOUD_OFFICIAL_IMAGE_HOST_DESC: 앨범 클라우드 동기화가 내장된 PicGo의 공식 이미지 호스팅입니다.
PICGO_CLOUD_PAID_PLAN_BADGE: 유료 요금제
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_TITLE: 재시작이 필요한가요?
PICGO_CLOUD_CONFIG_SYNC_RESTART_PROMPT_MESSAGE: 일부 설정은 적용하려면 재시작이 필요할 수 있습니다. 지금 재시작하시겠습니까?
PICGO_CLOUD_CONFIG_SYNC_RESTART_NOW: 지금 재시작
PICGO_CLOUD_CONFIG_SYNC_RESTART_LATER: 나중에
INPUT_BOX_CONFIRM_MISMATCH: 두 입력값이 일치하지 않습니다.
PICGO_SPONSOR_TEXT: PicGo는 무료 소프트웨어입니다. 마음에 드신다면 커피 한 잔 후원하는 것을 잊지 말아 주세요.
ALIPAY: 알리페이
WECHATPAY: 위챗페이
CHOOSE_PICBED: 이미지 호스팅 선택
COPY_PICBED_CONFIG: 이미지 호스팅 설정 복사
COPY_PICBED_CONFIG_SUCCEED: 이미지 호스팅 설정 복사 성공
INPUT: 입력
CANCEL: 취소
CONFIRM: 확인
CHOOSE_SHOWED_PICBED: 표시할 이미지 호스팅 선택
CHOOSE_PASTE_FORMAT: 붙여넣기 형식 선택
SEARCH: 검색
COPY: 복사
DELETE: 삭제
SELECT_ALL: 전체 선택
CHANGE_IMAGE_URL: 이미지 URL 변경
CHANGE_IMAGE_URL_SUCCEED: 이미지 URL 변경 성공
COPY_LINK_SUCCEED: 링크 복사 성공
BATCH_COPY_LINK_SUCCEED: 링크 일괄 복사 성공
FILE_RENAME: 파일 이름 변경
COPY_FILE_PATH: 파일 경로 복사
OPEN_FILE_PATH: 파일 경로 열기
SUCCESS: 성공
FAILED: 실패
# settings
SETTINGS: 설정
SETTINGS_OPEN_CONFIG_FILE: 설정 파일 열기
SETTINGS_CLICK_TO_OPEN: 클릭하여 열기
SETTINGS_SET_LOG_FILE: 로그 파일 설정
SETTINGS_CLICK_TO_SET: 클릭하여 설정
SETTINGS_CLICK_TO_CHECK: 클릭하여 확인
SETTINGS_SET_SHORTCUT: 단축키 설정
SETTINGS_URL_REWRITE: URL 재작성
SETTINGS_CUSTOM_LINK_FORMAT: 사용자 지정 링크 형식
SETTINGS_SET_PROXY_AND_MIRROR: 프록시 및 미러 설정
SETTINGS_SET_SERVER: 서버 설정
SETTINGS_CHECK_UPDATE: 업데이트 확인
SETTINGS_UPDATE_CHECK_RESULT: "최신 버전은 ${version}이며, ${action}"
SETTINGS_UPDATE_CHECK_NO_UPDATE: 업데이트가 필요하지 않습니다
SETTINGS_UPDATE_CHECK_CAN_UPDATE: 업데이트 가능
SETTINGS_OPEN_UPDATE_HELPER: 업데이트 도우미 열기
SETTINGS_OPEN: 열기
SETTINGS_CLOSE: 닫기
SETTINGS_ACCEPT_BETA_UPDATE: 베타 업데이트 허용
SETTINGS_LAUNCH_ON_BOOT: 부팅 시 자동 실행
SETTINGS_RENAME_BEFORE_UPLOAD: 업로드 전 이름 변경
SETTINGS_TIMESTAMP_RENAME: 타임스탬프로 이름 변경
SETTINGS_OPEN_UPLOAD_TIPS: 업로드 팁 표시
SETTINGS_NOTIFICATION_SOUND: 알림 소리 재생
SETTINGS_MINI_WINDOW_ON_TOP: 미니 창 항상 위
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 업로드 후 URL 자동 복사
SETTINGS_TIPS_PLACEHOLDER_URL: $url를 사용하여 URL 위치를 표시
SETTINGS_TIPS_PLACEHOLDER_FILENAME: $fileName을 사용하여 파일 이름 위치를 표시
SETTINGS_TIPS_PLACEHOLDER_EXTNAME: $extName을 사용하여 파일 확장자 위치를 표시
SETTINGS_TIPS_SUCH_AS: "예: $url/$fileName"
SETTINGS_UPLOAD_PROXY: 업로드 프록시
SETTINGS_PLUGIN_INSTALL_PROXY: 플러그인 설치 프록시
SETTINGS_PLUGIN_INSTALL_MIRROR: 플러그인 설치 미러
SETTINGS_CURRENT_VERSION: 현재 버전
SETTINGS_NEWEST_VERSION: 최신 버전
SETTINGS_GETING: 가져오는 중...
SETTINGS_TIPS_HAS_NEW_VERSION: PicGo에 새 버전이 있습니다. 확인을 클릭하여 다운로드 페이지를 여세요
SETTINGS_LOG_FILE: 로그 파일
SETTINGS_LOG_LEVEL: 로그 수준
SETTINGS_LOG_FILE_SIZE: 로그 파일 크기
SETTINGS_SET_PICGO_SERVER: PicGo 서버 설정
SETTINGS_TIPS_SERVER_NOTICE: 서버 기능이 무엇인지 모른다면 문서를 참고하거나 설정을 변경하지 마세요.
SETTINGS_ENABLE_SERVER: 서버 사용
SETTINGS_SET_SERVER_HOST: 서버 호스트 설정
SETTINGS_SET_SERVER_PORT: 서버 포트 설정
SETTINGS_TIP_PLACEHOLDER_HOST: 기본값:127.0.0.1
SETTINGS_TIP_PLACEHOLDER_PORT: 기본값:36677
SETTINGS_LOG_LEVEL_ALL: 전체
SETTINGS_LOG_LEVEL_SUCCESS: 성공
SETTINGS_LOG_LEVEL_ERROR: 오류
SETTINGS_LOG_LEVEL_INFO: 정보
SETTINGS_LOG_LEVEL_WARN: 경고
SETTINGS_LOG_LEVEL_NONE: 없음
SETTINGS_RESULT: 결과
SETTINGS_DEFAULT_PICBED: 기본 이미지 호스팅
SETTINGS_SET_DEFAULT_PICBED: 기본 업로더 설정
SETTINGS_NOT_CONFIG_OPTIONS: 설정 옵션 없음
SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 내장 클립보드로 업로드 사용
SETTINGS_CHOOSE_LANGUAGE: 언어 선택
UPLOADER_CONFIG_NAME: 설정 이름
BUILTIN_CLIPBOARD_TIPS: 스크립트 대신 내장 클립보드 기능을 사용하여 업로드
# url rewrite
URL_REWRITE_HELP: 업로드된 이미지 URL을 재작성합니다. 규칙은 순서대로 평가되며, 처음 일치한 규칙이 적용됩니다.
URL_REWRITE_ADD_RULE: 규칙 추가
URL_REWRITE_EDIT_RULE: 규칙 편집
URL_REWRITE_EMPTY: 규칙 없음
URL_REWRITE_ORDER: 순서
URL_REWRITE_MATCH: 일치 패턴
URL_REWRITE_REPLACE: 대체 문자열
URL_REWRITE_FLAGS: 플래그
URL_REWRITE_ENABLED: 사용함
URL_REWRITE_ACTIONS: 작업
URL_REWRITE_MOVE_UP: 위로
URL_REWRITE_MOVE_DOWN: 아래로
URL_REWRITE_EDIT: 편집
URL_REWRITE_DELETE: 삭제
URL_REWRITE_DELETE_CONFIRM: 이 규칙을 삭제하시겠습니까?
URL_REWRITE_MATCH_TIPS: 정규식(JavaScript RegExp)을 지원합니다
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
URL_REWRITE_REPLACE_TIPS: 대체 문자열($1, $2, ... 지원)
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
URL_REWRITE_OPTIONS: 옵션
URL_REWRITE_RULE_ENABLED: 이 규칙 사용
URL_REWRITE_FLAG_GLOBAL_LABEL: 전역(g)
URL_REWRITE_FLAG_GLOBAL_DESC: 첫 번째 항목만이 아니라 일치하는 모든 항목을 대체합니다
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 대소문자 무시(i)
URL_REWRITE_FLAG_IGNORE_CASE_DESC: "대소문자를 구분하지 않고 일치시킵니다(예: JPG와 jpg를 동일하게 취급)"
URL_REWRITE_MATCH_REQUIRED: 일치 패턴은 필수입니다
URL_REWRITE_REPLACE_REQUIRED: 대체 문자열은 필수입니다
URL_REWRITE_INVALID_REGEX: 잘못된 정규식입니다
URL_REWRITE_PREVIEW_TITLE: 미리보기
URL_REWRITE_PREVIEW_TIPS: URL을 입력하면 현재 규칙이 어떻게 재작성하는지 확인할 수 있습니다(순서대로 일치하며, 첫 번째 일치 항목만 적용됩니다)
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
URL_REWRITE_PREVIEW_RUN: 미리보기
URL_REWRITE_PREVIEW_OUTPUT: 출력 URL
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 미리보기할 URL을 입력해 주세요
URL_REWRITE_PREVIEW_RULE_INVALID: 잘못된 규칙입니다
URL_REWRITE_PREVIEW_MATCHED_RULE: 일치한 규칙
URL_REWRITE_PREVIEW_NO_MATCH: 일치하는 규칙 없음
UPLOADER_CONFIG_PLACEHOLDER: 설정 이름을 입력해 주세요
SELECTED_SETTING_HINT: 선택됨
SETTINGS_ENCODE_OUTPUT_URL: 출력(또는 복사한) URL 인코딩
SETTINGS_SHOW_DOCK_ICON: Dock 아이콘 표시
SETTINGS_SHOW_MENUBAR_ICON: 메뉴바 아이콘 표시
SETTINGS_SHOW_MENUBAR_ICON_TIPS: "\"Dock 아이콘 표시\"와 \"메뉴바 아이콘 표시\"가 모두 꺼져 있으면 PicGo의 메인 창을 찾을 수 없게 됩니다. 설정 파일을 편집하여 showDockIcon 또는 showMenubarIcon을 true로 설정하면 복구할 수 있습니다."
SETTINGS_STARTUP_MODE: 시작 모드
SETTINGS_STARTUP_MODE_MAIN_WINDOW: 메인 창 열기
SETTINGS_STARTUP_MODE_MINI_WINDOW: 미니 창 열기
SETTINGS_STARTUP_MODE_HIDE: 조용히 시작
# shortcut-page
SHORTCUT_NAME: 단축키 이름
SHORTCUT_BIND: 단축키 바인딩
SHORTCUT_STATUS: 상태
SHORTCUT_ENABLED: 사용함
SHORTCUT_DISABLED: 사용 안 함
SHORTCUT_SOURCE: 소스
SHORTCUT_HANDLE: 처리
SHORTCUT_ENABLE: 사용
SHORTCUT_DISABLE: 사용 안 함
SHORTCUT_EDIT: 편집
SHORTCUT_CHANGE_UPLOAD: 업로드 단축키 변경
# album-page
ALBUM_URL_REWRITE_TITLE: 선택한 이미지 URL 재작성
ALBUM_URL_REWRITE_RESULT_TITLE: 이미지 URL 재작성 결과
ALBUM_URL_REWRITE_WARN_NO_SELECTION: 먼저 사진을 하나 이상 선택해야 합니다
ALBUM_URL_REWRITE_APPLY_GLOBAL_RULES: 전역 URL 재작성 규칙 적용
ALBUM_URL_REWRITE_GLOBAL_RULES_COUNT: 전역 규칙
ALBUM_URL_REWRITE_TEMP_RULE_TIPS: 선택 사항입니다. 임시 규칙을 건너뛰려면 비워 두세요. 임시 규칙은 전역 규칙보다 우선순위가 높습니다.
ALBUM_URL_REWRITE_TEMP_RULE_REQUIRED: 임시 규칙에는 일치 패턴과 대체 문자열이 모두 필요합니다
ALBUM_URL_REWRITE_NO_RULES_TO_APPLY: 사용 중인 전역 규칙이 없고 임시 규칙도 입력되지 않았습니다
ALBUM_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 임시 규칙을 전역 URL 재작성 규칙에 저장하시겠습니까?
ALBUM_URL_REWRITE_APPLY_AND_SAVE: 적용 및 저장
ALBUM_URL_REWRITE_APPLY_ONLY: 적용만
ALBUM_URL_REWRITE_NO_CHANGES: 변경된 URL이 없습니다
ALBUM_URL_REWRITE_EMPTY_RESULT_WARN: 재작성 결과가 비어 있어 건너뛰었습니다
# tray-page
WAIT_TO_UPLOAD: 업로드 대기 중
ALREADY_UPLOAD: 업로드 완료
# upload-page
PICTURE_UPLOAD: 사진 업로드
DRAG_FILE_TO_HERE: 파일을 여기로 드래그하거나
CLICK_TO_UPLOAD: 클릭하여 업로드
LINK_FORMAT: 링크 형식
CUSTOM: 사용자 지정
CLIPBOARD_PICTURE: 클립보드
TIPS_DRAG_VALID_PICTURE_OR_URL: 유효한 사진 또는 URL을 여기로 드래그하세요
TIPS_INPUT_URL: URL 입력
TIPS_HTTP_PREFIX: http:// 또는 https://로 시작해야 합니다. 여러 URL 지원(줄당 하나씩)
TIPS_INPUT_VALID_URL: 유효한 URL을 입력하세요
# plugins
PLUGIN_SEARCH_PLACEHOLDER: npm에서 picgo 플러그인을 검색하거나, 버튼을 클릭하여 추천 플러그인 목록을 확인하세요
PLUGIN_SEARCH_EXACT_MATCH: 플러그인 이름 정확히 일치
PLUGIN_INSTALL: 설치
PLUGIN_INSTALLING: 설치 중...
PLUGIN_INSTALLED: 설치됨
PLUGIN_DEPRECATED_BADGE: 지원 종료
PLUGIN_DEPRECATED_TITLE: 이 플러그인은 제작자가 지원을 종료했습니다
PLUGIN_DOING_SOMETHING: 처리 중...
PLUGIN_LIST: 플러그인 목록
PLUGIN_IMPORT_LOCAL: 로컬 플러그인 가져오기
# tips
TIPS_REMOVE_LINK: 이 작업은 사진을 앨범에서 제거합니다. 계속하시겠습니까?
TIPS_WILL_REMOVE_CHOOSED_IMAGES: 이 작업은 사진을 앨범에서 제거합니다. 계속하시겠습니까?
TIPS_MUST_CONTAINS_URL: $url, $fileName, $extName 중 하나를 포함해야 합니다
TIPS_NETWORK_ERROR: 네트워크 오류
TIPS_NEED_RELOAD: 앱을 새로고침해야 합니다
TIPS_PLEASE_CHOOSE_LOG_LEVEL: 로그 수준을 선택해 주세요
TIPS_SET_SUCCEED: 설정 성공
TIPS_PLUGIN_NOT_GUI_IMPLEMENT: 이 플러그인은 GUI에 최적화되어 있지 않습니다. 계속하시겠습니까?
TIPS_CLICK_NOTIFICATION_TO_RELOAD: 알림을 클릭하여 앱을 새로고침하세요
TIPS_GET_PLUGIN_LIST_FAILED: 플러그인 목록을 가져오지 못했습니다
# ---renderer i18n end---
# plugins
PLUGIN_INSTALL_SUCCEED: 플러그인 설치 성공
PLUGIN_INSTALL_FAILED: 플러그인 설치 실패
PLUGIN_UNINSTALL_SUCCEED: 플러그인 제거 성공
PLUGIN_UNINSTALL_FAILED: 플러그인 제거 실패
PLUGIN_UPDATE_SUCCEED: 플러그인 업데이트 성공
PLUGIN_UPDATE_FAILED: 플러그인 업데이트 실패
PLUGIN_IMPORT_SUCCEED: 플러그인 가져오기 성공
PLUGIN_IMPORT_FAILED: 플러그인 가져오기 실패
ENABLE_PLUGIN: 플러그인 사용
DISABLE_PLUGIN: 플러그인 사용 안 함
UNINSTALL_PLUGIN: 플러그인 제거
UPDATE_PLUGIN: 플러그인 업데이트
# toolbox
TOOLBOX: 도구 상자
TOOLBOX_TITLE: PicGo 실행 문제 진단
TOOLBOX_SUB_TITLE: 다음 항목을 즉시 검사하여 사용 문제를 해결하세요
TOOLBOX_CHECK_CONFIG_FILE_BROKEN: 설정 파일 손상 여부 확인
TOOLBOX_CHECK_ALBUM_FILE_BROKEN: 앨범 파일 손상 여부 확인
TOOLBOX_CHECK_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD: 클립보드 사진 업로드 문제 확인
TOOLBOX_CHECK_PROBLEM_WITH_PROXY: 프록시 설정 정상 여부 확인
TOOLBOX_FIX_DONE_NEED_RELOAD: 수리가 완료되었습니다. 적용하려면 재시작이 필요합니다. 재시작하시겠습니까?
TOOLBOX_CANT_AUTO_FIX: 자동으로 수리할 수 없습니다. 아래 문제를 직접 수리해 주세요
TOOLBOX_START_SCAN: 검사 시작
TOOLBOX_RE_SCAN: 다시 검사
TOOLBOX_START_FIX: 수리 시작
TOOLBOX_SUCCESS_TIPS: 축하합니다, 문제가 발견되지 않았습니다
TOOLBOX_CHECK_CONFIG_FILE_PATH_TIPS: "설정 파일 경로: ${path}"
TOOLBOX_CHECK_CONFIG_FILE_BROKEN_TIPS: 설정 파일이 손상되었습니다
TOOLBOX_CHECK_ALBUM_FILE_PATH_TIPS: "앨범 파일 경로: ${path}"
TOOLBOX_CHECK_ALBUM_FILE_BROKEN_TIPS: 앨범 파일이 손상되었습니다
TOOLBOX_CHECK_PROXY_SUCCESS_TIPS: 프록시 설정 정상
TOOLBOX_CHECK_PROXY_NO_PROXY_TIPS: 프록시 설정 없음
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_CORRECT: 프록시 설정이 올바르지 않습니다
TOOLBOX_CHECK_PROXY_PROXY_IS_NOT_WORKING: 프록시 설정을 사용할 수 없습니다
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_TIPS: "클립보드 사진의 임시 폴더 경로: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_NOT_EXIST_TIPS: "클립보드 사진의 임시 폴더가 존재하지 않습니다: ${path}"
TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: "다음 폴더를 직접 생성해 주세요: ${path}"
# tips
TIPS_NOTICE: 알림
TIPS_WARNING: 경고
TIPS_ERROR: 오류
TIPS_SKIPPED_INVALID_URLS: "잘못된 URL ${n}개를 건너뛰었습니다. 자세한 내용은 로그를 확인하세요"
TIPS_TOO_MANY_URLS_CONFIRM: "한 번에 URL ${n}개를 업로드하려고 합니다. 지연이 발생할 수 있으니 나누어 업로드하는 것을 권장합니다. 계속하시겠습니까?"
TIPS_NO_VALID_URLS: 유효한 URL을 찾을 수 없습니다
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Node.js를 설치한 후 PicGo를 재시작해 주세요
TIPS_PLUGIN_REMOVE_ALBUM_ITEM: 플러그인이 앨범에서 일부 이미지를 제거하려고 합니다. 계속하시겠습니까?
TIPS_PLUGIN_OVERWRITE_ALBUM: 플러그인이 앨범을 덮어쓰려고 합니다. 계속하시겠습니까?
TIPS_UPLOAD_NOT_PICTURES: 최근 클립보드 항목이 사진이 아닙니다
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_DEFAULT: PicGo 설정 파일이 손상되어 기본값으로 복원되었습니다
TIPS_PICGO_CONFIG_FILE_BROKEN_WITH_BACKUP: PicGo 설정 파일이 손상되어 백업본으로 복원되었습니다
TIPS_PICGO_BACKUP_FILE_VERSION: "백업 파일 버전: ${v}"
TIPS_CUSTOM_CONFIG_FILE_PATH_ERROR: 사용자 지정 설정 파일 구문 분석 오류입니다. 경로 내용을 확인해 주세요
TIPS_SHORTCUT_MODIFIED_SUCCEED: 단축키가 성공적으로 변경되었습니다
TIPS_SHORTCUT_MODIFIED_CONFLICT: 단축키가 충돌합니다. 다시 설정해 주세요
TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED: 사용자 지정 링크 스타일이 성공적으로 변경되었습니다
TIPS_FIND_NEW_VERSION: "새 버전 ${v}을(를) 찾았습니다. 다양한 새 기능이 추가되었습니다. 최신 버전을 다운로드하시겠습니까?"
TIPS_DELETE_UPLOADER_CONFIG: 이 설정을 삭제하시겠습니까?
TIPS_COPY_UPLOADER_CONFIG: 이 설정을 복사하시겠습니까?
TIPS_UPLOADER_CONFIG_NAME_EMPTY: 설정 이름은 비워 둘 수 없습니다
TIPS_UPLOADER_CONFIG_NOT_FOUND: 설정을 찾을 수 없습니다
TIPS_UPLOADER_CONFIG_CANNOT_DELETE_LAST: 마지막 설정은 삭제할 수 없습니다
# privacy
PRIVACY: "이용하기 전에 개인정보 처리방침 ${privacyUrl} 및 이용약관 ${termsUrl}을(를) 읽고 동의해 주세요."
PRIVACY_TIPS: 업로드하려면 개인정보 처리방침에 동의해 주세요
QUIT: 종료
ALBUM_ALL_PHOTOS: 전체 사진
ALBUM_COLLECTIONS: 컬렉션
ALBUM_TAGS: 태그
ALBUM_MENU: 메뉴
ALBUM_OPEN_INSPECTOR: 인스펙터 열기
ALBUM_INSPECTOR_TITLE: 앨범 인스펙터
ALBUM_INSPECTOR_DESCRIPTION: 선택한 이미지를 검사하고 편집합니다.
ALBUM_CLEAR_SELECTION: 선택 해제
ALBUM_SELECTED_COUNT: "${count}개 선택됨"
ALBUM_GRID_VIEW: 그리드 보기
ALBUM_LIST_VIEW: 목록 보기
ALBUM_PREVIEW: 전체 화면 미리보기
ALBUM_PREVIEW_EMPTY: 미리볼 이미지가 없습니다
ALBUM_PREVIEW_PREV: 이전
ALBUM_PREVIEW_NEXT: 다음
ALBUM_PREVIEW_CLOSE: 미리보기 닫기
ALBUM_PREVIEW_COUNT: "${current} / ${total}"
ALBUM_QUICK_ACTIONS: 빠른 작업
ALBUM_EXPORT: 다운로드
ALBUM_URL: URL
ALBUM_BATCH_REWRITE: 일괄 재작성
ALBUM_COLLECTION: 컬렉션
ALBUM_ADD_TAG: 태그 추가
ALBUM_TAG_SUGGESTIONS: 추천 태그
ALBUM_COLUMN_NAME: 이름
ALBUM_COLUMN_PROVIDER: 제공자
ALBUM_COLUMN_SIZE: 크기
ALBUM_COLUMN_DATE: 날짜
ALBUM_ADD: 추가
ALBUM_SOURCE_LOCAL: 로컬
ALBUM_SOURCE_CLOUD: 클라우드
ALBUM_CLOUD_LOAD_FAILED: 클라우드 앨범을 불러오지 못했습니다
ALBUM_CLOUD_LOGIN_REQUIRED_TITLE: 로그인 필요
ALBUM_CLOUD_LOGIN_REQUIRED_DESC: 클라우드 앨범을 사용하려면 유료 PicGo Cloud 계정으로 로그인하세요
ALBUM_CLOUD_LOGIN_BUTTON: 로그인
ALBUM_CLOUD_UPGRADE_TITLE: 업그레이드 필요
ALBUM_CLOUD_UPGRADE_DESC: 클라우드 앨범은 유료 요금제가 필요합니다
ALBUM_CLOUD_UPGRADE_BUTTON: 업그레이드
ALBUM_CLOUD_FEATURES_TITLE: 클라우드 앨범을 사용해야 하는 이유
ALBUM_CLOUD_FEATURE_SYNC: 모든 기기에서 업로드 기록에 접근
ALBUM_CLOUD_FEATURE_AUTO_IMPORT: 새 업로드 정보 자동 가져오기(옵트인 필요)
ALBUM_CLOUD_FEATURE_SECURE: 로컬 기록을 클라우드로 원클릭 가져오기
ALBUM_CLOUD_IMPORT_GUIDE_TITLE: 로컬 기록 가져오기
ALBUM_CLOUD_IMPORT_GUIDE_DESC: 로컬 업로드 기록을 클라우드 앨범으로 가져옵니다. 실제 이미지 파일이 아닌 기록만 가져옵니다.
ALBUM_CLOUD_IMPORT_GUIDE_BUTTON: 가져오기 시작
ALBUM_CLOUD_EMPTY_TITLE: 항목 없음
ALBUM_CLOUD_EMPTY_DESC: 클라우드 앨범이 비어 있습니다. 이미지를 업로드하여 시작하세요.
ALBUM_CLOUD_IMPORTING: 클라우드 앨범으로 가져오는 중...
ALBUM_CLOUD_IMPORT_SUCCESS: "클라우드 앨범으로 ${num}개 항목을 성공적으로 가져왔습니다"
ALBUM_CLOUD_IMPORT_FAILED: 클라우드 앨범으로 가져오지 못했습니다
ALBUM_CLOUD_REFRESH: 새로고침
ALBUM_CLOUD_IMPORT_CONFIRM_TITLE: 자동 가져오기 사용
ALBUM_CLOUD_IMPORT_CONFIRM_DESC: 이 작업을 사용하면 자동 가져오기가 활성화되어 이후 업로드가 클라우드 앨범에 자동으로 기록됩니다. 기존 로컬 기록도 지금 함께 가져옵니다.
ALBUM_CLOUD_DELETE_FAILED: 클라우드 앨범에서 삭제하지 못했습니다
ALBUM_INSPECTOR_DETAILS_TITLE: 상세 정보
ALBUM_INSPECTOR_FILE_NAME: 파일 이름
ALBUM_INSPECTOR_UPLOADER: 업로더
ALBUM_INSPECTOR_CONTENT_TYPE: 콘텐츠 유형
ALBUM_INSPECTOR_CREATED_AT: 생성일
ALBUM_INSPECTOR_UPDATED_AT: 수정일
ALBUM_INSPECTOR_FILE_SIZE: 크기
ALBUM_CLOUD_IMPORT_STATUS_TITLE: 클라우드 앨범
ALBUM_CLOUD_IMPORTED: 클라우드로 가져옴
ALBUM_CLOUD_IMPORTED_TOOLTIP: 이것은 로컬 표시자입니다. 클라우드에서 기록이 삭제되어도 이 상태는 갱신되지 않습니다. 필요하면 다시 가져올 수 있습니다.
ALBUM_CLOUD_REIMPORT: 다시 가져오기
ALBUM_CLOUD_NATIVE: PicGo Cloud를 통해 업로드됨
ALBUM_CLOUD_IMPORT_BUTTON: 클라우드 앨범으로 가져오기
ALBUM_CLOUD_IMPORT_BUTTON_COUNT: "항목 ${num}개를 클라우드 앨범으로 가져오기"
ALBUM_CLOUD_ALL_IMPORTED: 모두 클라우드 앨범에 있음
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_TITLE: 자동 가져오기 사용
ALBUM_CLOUD_AUTO_IMPORT_REQUIRED_DESC: 항목을 클라우드 앨범으로 가져오려면 먼저 자동 가져오기를 사용해야 합니다. 지금 사용하고 계속하시겠습니까?
ALBUM_CLOUD_AUTO_IMPORT_ENABLE_AND_IMPORT: 사용 및 가져오기
ALBUM_CLOUD_IMPORT_SINGLE_SUCCESS: 클라우드 앨범으로 성공적으로 가져왔습니다
PICGO_CLOUD_AUTO_IMPORT_LABEL: 클라우드 앨범으로 자동 가져오기
PICGO_CLOUD_AUTO_IMPORT_DESC: 다른 업로더의 업로드 기록을 클라우드 앨범으로 자동 가져오기(PicGo Cloud는 기본적으로 저장됨)
SIDEBAR_PLUGINS: 플러그인
SIDEBAR_SYSTEM: 시스템
SIDEBAR_NOTIFICATIONS: 알림
SIDEBAR_EXPAND: 클릭하여 펼치기
SIDEBAR_COLLAPSE: 사이드바 접기
HISTORY_PANEL_TITLE: 기록
HISTORY_PANEL_FILTER_PLACEHOLDER: 필터...
HISTORY_PANEL_TODAY: 오늘
HISTORY_PANEL_YESTERDAY: 어제
DASHBOARD_NO_VISIBLE_PROVIDERS: 표시할 제공자가 없습니다
DASHBOARD_NO_VISIBLE_PROVIDERS_DESCRIPTION: 모든 제공자가 숨겨져 있습니다. 설정에서 제공자 표시 여부를 변경하세요.
DASHBOARD_OPEN_SETTINGS: 설정 열기
DASHBOARD_DROP_IMAGES_HERE: 파일을 여기로 드롭하세요
DASHBOARD_OR: 또는
DASHBOARD_CLICK_TO_UPLOAD: 클릭하여 업로드
DASHBOARD_PASTE_FROM_CLIPBOARD: 클립보드에서 붙여넣기
DASHBOARD_PASTE_FROM_URL: URL에서 붙여넣기
DASHBOARD_CLIPBOARD: 클립보드
FIELD_IS_REQUIRED: "${field}은(는) 필수입니다"
CONFIG_RENAME: 설정 이름 변경
SETTINGS_SECTION_GENERAL: 일반
SETTINGS_SECTION_APPEARANCE: 외관
SETTINGS_SECTION_UPLOAD_WORKFLOW: 업로드 워크플로
SETTINGS_SECTION_NETWORK: 네트워크
SETTINGS_SECTION_ADVANCED: 고급
SETTINGS_SECTION_ABOUT: 정보
SETTINGS_NO_RESULTS_TITLE: 설정을 찾을 수 없습니다
SETTINGS_NO_RESULTS_DESCRIPTION: 다른 키워드를 시도하거나 검색어를 지워 보세요.
SETTINGS_OPEN_SHORTCUTS: 단축키
SETTINGS_LINK_WEBSITE: 웹사이트
SETTINGS_LINK_GITHUB: GitHub
SETTINGS_LINK_DOCS: 문서
SETTINGS_LINK_PRIVACY: 개인정보 처리방침
SETTINGS_LINK_TERMS: 이용약관
SETTINGS_APPEARANCE_MODE: 외관
SETTINGS_APPEARANCE_MODE_LIGHT: 밝게
SETTINGS_APPEARANCE_MODE_DARK: 어둡게
SETTINGS_APPEARANCE_MODE_AUTO: 자동
COMMON_FOR_EXAMPLE: "예: ${value}"
SETTINGS_SET_DEFAULT_CONFIG: 기본값으로 설정
PROVIDER_DRAFT_CONFIG: 임시 저장
PROVIDER_ACTIVE_UPLOADER_LABEL: 활성
PROVIDER_ACTIVE_UPLOADER_TOOLTIP: "${uploaderName}이(가) 활성 업로더입니다"
PROVIDER_DEFAULT_CONFIG_LABEL: 기본값
PROVIDER_DEFAULT_CONFIG_TOOLTIP: "${uploaderName}이(가) 활성 상태일 때 사용됩니다."
PROVIDER_SIDEBAR_EMPTY: 업로더 또는 설정을 찾을 수 없습니다.
PROVIDER_UPLOADER_ACTIONS: "${uploaderName} 작업"
PROVIDER_SIDEBAR_COLLAPSE: 접기
PROVIDER_SIDEBAR_EXPAND: 펼치기
PROVIDER_CONFIG_ACTIONS: 설정 작업
PROVIDER_CREATE_CONFIG: 설정 생성
PROVIDER_CREATE_CONFIG_DISABLED_EMPTY_SCHEMA: 이 업로더에는 설정 옵션이 없어 추가 설정을 생성할 필요가 없습니다
PROVIDER_INSTALL_MORE_UPLOADERS: 업로더 추가 설치
PROVIDER_NO_UPLOADER_SELECTED: 선택된 업로더가 없습니다.
PROVIDER_NO_CONFIG_YET: 아직 설정이 없습니다
PROVIDER_NO_CONFIG_DESCRIPTION: "${uploaderName}의 스키마 기반 옵션을 편집하려면 새 설정을 생성하세요."
PROVIDER_CONFIGURATION: 설정
PROVIDER_UPDATED_AT_LABEL: 수정일
PROVIDER_DELETE_CONFIG_HINT: 이 설정을 영구적으로 삭제합니다.
UPLOADER_SWITCHER_SELECT_PROVIDER: 제공자 선택
UPLOADER_SWITCHER_CONFIG: 설정
UPLOADER_SWITCHER_NO_CONFIG: 설정 없음
ALBUM_URL_REWRITE_DESCRIPTION: 선택한 이미지의 URL을 일괄 재작성합니다.
ALBUM_URL_REWRITE_CHANGED: 변경됨
ALBUM_URL_REWRITE_UNCHANGED: 변경 없음
TRAY_ALREADY_UPLOAD_EMPTY: 아직 업로드된 이미지가 없습니다
PLUGIN_EMPTY: 플러그인을 찾을 수 없습니다.
PLUGIN_EMPTY_DESCRIPTION: npm을 검색하여 플러그인을 설치해 시작하세요.
PLUGIN_DETAIL: 상세 정보
PLUGIN_NO_CONFIG_SCHEMA: 이 플러그인은 설정 스키마를 제공하지 않습니다.
PLUGIN_NO_TRANSFORMER_SCHEMA: 이 플러그인은 트랜스포머 스키마를 제공하지 않습니다.
PLUGIN_NO_README: 이 플러그인에 대한 README가 없습니다.
NO_OPTIONS_FOUND: 옵션을 찾을 수 없습니다.
+335
View File
@@ -0,0 +1,335 @@
const fs = require('fs')
const http = require('http')
const https = require('https')
const path = require('path')
const API_URL = process.env.ATOM_API_URL || 'https://api.atomgit.com/api/v5'
const ATOM_PAT = process.env.ATOM_PAT
const ATOM_OWNER = process.env.ATOM_OWNER
const ATOM_REPO = process.env.ATOM_REPO
const RELEASE_TAG = process.env.RELEASE_TAG
const TARGET_COMMITISH = process.env.ATOM_TARGET_COMMITISH
const DIST_DIR = path.resolve(process.env.DIST_DIR || 'dist')
const RELEASE_METADATA_PATH = path.resolve(process.env.GITHUB_RELEASE_JSON || 'github-release.json')
const MAX_ATTEMPTS = 3
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
const UPLOAD_TIMEOUT_MINUTES = Number.parseInt(process.env.ATOM_UPLOAD_TIMEOUT_MINUTES || '70', 10)
function requireEnvironmentVariable(name, value) {
if (!value) {
throw new Error(`Missing required environment variable: ${name}`)
}
}
function encodePathSegment(value) {
return encodeURIComponent(value)
}
function wait(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function formatFileSize(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
function formatDuration(milliseconds) {
return `${Math.round(milliseconds / 1000)}s`
}
async function requestWithRetry(url, createOptions, label) {
let lastError
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
try {
const response = await fetch(url, createOptions())
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
return response
}
await response.text()
console.warn(`[AtomGit] ${label} returned ${response.status}; retrying (${attempt}/${MAX_ATTEMPTS})`)
} catch (error) {
lastError = error
if (attempt === MAX_ATTEMPTS) {
throw error
}
console.warn(`[AtomGit] ${label} failed; retrying (${attempt}/${MAX_ATTEMPTS})`)
}
await wait(1000 * attempt)
}
throw lastError || new Error(`${label} failed after ${MAX_ATTEMPTS} attempts`)
}
function atomGitApiUrl(relativePath) {
return `${API_URL}/repos/${encodePathSegment(ATOM_OWNER)}/${encodePathSegment(ATOM_REPO)}${relativePath}`
}
function atomGitHeaders(additionalHeaders = {}) {
return {
Authorization: `Bearer ${ATOM_PAT}`,
Accept: 'application/json',
...additionalHeaders
}
}
function parseResponseText(text) {
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch {
return text
}
}
async function readResponseBody(response) {
return parseResponseText(await response.text())
}
function formatResponseBody(body) {
return typeof body === 'string' ? body : JSON.stringify(body)
}
async function assertSuccessfulResponse(response, label) {
const body = await readResponseBody(response)
if (!response.ok) {
throw new Error(`${label} failed with HTTP ${response.status}: ${formatResponseBody(body)}`)
}
return body
}
async function verifyCredentials() {
const response = await requestWithRetry(
`${API_URL}/user`,
() => ({ headers: atomGitHeaders() }),
'credential verification'
)
await assertSuccessfulResponse(response, 'AtomGit credential verification')
}
async function getRelease() {
const response = await requestWithRetry(
atomGitApiUrl(`/releases/tags/${encodePathSegment(RELEASE_TAG)}`),
() => ({ headers: atomGitHeaders() }),
`get release ${RELEASE_TAG}`
)
if (response.status === 404) {
await response.text()
return null
}
return assertSuccessfulResponse(response, `Get AtomGit release ${RELEASE_TAG}`)
}
async function createRelease(metadata) {
const name = metadata.name || RELEASE_TAG
const payload = {
tag_name: RELEASE_TAG,
name,
body: metadata.body || `Release ${name}`,
release_status: metadata.isPrerelease ? 'pre' : 'latest'
}
const targetCommitish = TARGET_COMMITISH || metadata.targetCommitish
if (targetCommitish) {
payload.target_commitish = targetCommitish
}
const response = await requestWithRetry(
atomGitApiUrl('/releases'),
() => ({
method: 'POST',
headers: atomGitHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(payload)
}),
`create release ${RELEASE_TAG}`
)
if (response.status === 409) {
await response.text()
return getRelease()
}
return assertSuccessfulResponse(response, `Create AtomGit release ${RELEASE_TAG}`)
}
async function getOrCreateRelease(metadata) {
const existingRelease = await getRelease()
if (existingRelease) {
console.log(`[AtomGit] Release ${RELEASE_TAG} already exists`)
return existingRelease
}
console.log(`[AtomGit] Creating release ${RELEASE_TAG}`)
const createdRelease = await createRelease(metadata)
if (!createdRelease) {
throw new Error(`AtomGit release ${RELEASE_TAG} was not available after creation`)
}
return createdRelease
}
async function getUploadRequest(fileName) {
const url = new URL(atomGitApiUrl(`/releases/${encodePathSegment(RELEASE_TAG)}/upload_url`))
url.searchParams.set('file_name', fileName)
const response = await requestWithRetry(
url,
() => ({ headers: atomGitHeaders() }),
`request upload URL for ${fileName}`
)
const body = await assertSuccessfulResponse(response, `Get upload URL for ${fileName}`)
if (!body || typeof body !== 'object' || typeof body.url !== 'string' || !body.headers || typeof body.headers !== 'object') {
throw new Error(`AtomGit returned an invalid upload request for ${fileName}`)
}
return body
}
function putAssetBuffer(url, headers, fileBuffer) {
return new Promise((resolve, reject) => {
const targetUrl = new URL(url)
const requestClient = targetUrl.protocol === 'http:' ? http : https
const request = requestClient.request(targetUrl, {
method: 'PUT',
headers: Object.fromEntries(headers.entries())
}, response => {
const chunks = []
response.on('data', chunk => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
})
response.on('end', () => {
resolve({
status: response.statusCode || 0,
body: Buffer.concat(chunks).toString('utf8')
})
})
response.on('error', reject)
})
request.setTimeout(UPLOAD_TIMEOUT_MINUTES * 60 * 1000, () => {
request.destroy(new Error(`Upload request timed out after ${UPLOAD_TIMEOUT_MINUTES} minutes`))
})
request.on('error', reject)
request.end(fileBuffer)
})
}
async function uploadAsset(filePath) {
const fileName = path.basename(filePath)
const fileBuffer = await fs.promises.readFile(filePath)
const fileSize = fileBuffer.byteLength
const startedAt = Date.now()
console.log(`[AtomGit] Uploading ${fileName} (${formatFileSize(fileSize)})`)
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
const uploadRequest = await getUploadRequest(fileName)
const uploadHeaders = new Headers(uploadRequest.headers)
uploadHeaders.set('Content-Length', String(fileSize))
let response
try {
response = await putAssetBuffer(uploadRequest.url, uploadHeaders, fileBuffer)
} catch (error) {
if (attempt === MAX_ATTEMPTS) {
throw error
}
console.warn(`[AtomGit] Upload ${fileName} failed; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
await wait(1000 * attempt)
continue
}
if (response.status >= 200 && response.status < 300) {
console.log(`[AtomGit] Uploaded ${fileName} in ${formatDuration(Date.now() - startedAt)}`)
return
}
const responseBody = parseResponseText(response.body)
if (!RETRYABLE_STATUS_CODES.has(response.status) || attempt === MAX_ATTEMPTS) {
throw new Error(`Upload ${fileName} failed with HTTP ${response.status}: ${formatResponseBody(responseBody)}`)
}
console.warn(`[AtomGit] Upload ${fileName} returned ${response.status}; requesting a new upload URL (${attempt}/${MAX_ATTEMPTS})`)
await wait(1000 * attempt)
}
}
function readReleaseMetadata() {
const metadata = JSON.parse(fs.readFileSync(RELEASE_METADATA_PATH, 'utf8'))
if (metadata.isDraft) {
throw new Error(`GitHub release ${RELEASE_TAG} is still a draft and will not be mirrored`)
}
if (metadata.tagName !== RELEASE_TAG) {
throw new Error(`GitHub release tag ${metadata.tagName} does not match requested tag ${RELEASE_TAG}`)
}
return metadata
}
function findAssets() {
return fs.readdirSync(DIST_DIR, { withFileTypes: true })
.filter(entry => entry.isFile())
.map(entry => path.join(DIST_DIR, entry.name))
.sort((left, right) => left.localeCompare(right))
}
function getExistingAssetNames(release) {
if (!Array.isArray(release.assets)) {
return new Set()
}
return new Set(release.assets
.map(asset => asset && asset.name)
.filter(name => typeof name === 'string'))
}
async function publishRelease() {
requireEnvironmentVariable('ATOM_PAT', ATOM_PAT)
requireEnvironmentVariable('ATOM_OWNER', ATOM_OWNER)
requireEnvironmentVariable('ATOM_REPO', ATOM_REPO)
requireEnvironmentVariable('RELEASE_TAG', RELEASE_TAG)
if (!Number.isInteger(UPLOAD_TIMEOUT_MINUTES) || UPLOAD_TIMEOUT_MINUTES < 1 || UPLOAD_TIMEOUT_MINUTES > 180) {
throw new Error('ATOM_UPLOAD_TIMEOUT_MINUTES must be an integer between 1 and 180')
}
const metadata = readReleaseMetadata()
const assetPaths = findAssets()
if (assetPaths.length === 0) {
throw new Error(`No GitHub release assets found in ${DIST_DIR}`)
}
console.log(`[AtomGit] Mirroring ${assetPaths.length} assets to ${ATOM_OWNER}/${ATOM_REPO}@${RELEASE_TAG}`)
await verifyCredentials()
const release = await getOrCreateRelease(metadata)
const existingAssetNames = getExistingAssetNames(release)
for (const assetPath of assetPaths) {
const fileName = path.basename(assetPath)
if (existingAssetNames.has(fileName)) {
console.log(`[AtomGit] Skipping existing asset ${fileName}`)
continue
}
await uploadAsset(assetPath)
}
console.log(`[AtomGit] Release ${RELEASE_TAG} mirror completed`)
}
publishRelease().catch(error => {
console.error('[AtomGit] Publish failed:', error)
process.exitCode = 1
})
+4 -2
View File
@@ -11,6 +11,7 @@ const distPath = path.join(__dirname, '../dist')
const S3Client = require('@aws-sdk/client-s3').S3Client
const Upload = require('@aws-sdk/lib-storage').Upload
const uploadToDev = process.argv.includes('--dev')
const uploadVersionFilesOnly = process.argv.includes('--version-files-only')
const S3_BUCKET = 'release'
const S3_LEGACY_BUCKET = 'picgo'
@@ -103,6 +104,7 @@ const uploadDist = async () => {
console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`)
console.log(`[PicGo] Version: ${VERSION}`)
console.log(`[PicGo] Install package upload: ${uploadVersionFilesOnly ? 'SKIPPED' : 'ENABLED'}`)
console.log(`[PicGo] Total files to upload: ${configs.length}\n`)
const uploadedVersionFiles = new Set()
@@ -117,11 +119,11 @@ const uploadDist = async () => {
console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`)
// 上传构建产物
if (fs.existsSync(filePath)) {
if (!uploadVersionFilesOnly && fs.existsSync(filePath)) {
console.log(` Uploading to S3: ${FILE_PATH}${fileName}`)
await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath)
console.log(` ✅ Uploaded: ${fileName}`)
} else {
} else if (!uploadVersionFilesOnly) {
console.warn(` ⚠️ File not found: ${fileName}`)
}
@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest'
import { createSchemaOnlyUploaderContext } from '~/main/utils/schemaOnlyUploaderContext'
type ConfigRecord = Record<string, unknown>
function isConfigRecord(value: unknown): value is ConfigRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function getByPath(value: unknown, path: string): unknown {
return path.split('.').reduce<unknown>((current, key) => {
if (!isConfigRecord(current)) {
return undefined
}
return current[key]
}, value)
}
describe('createSchemaOnlyUploaderContext', () => {
it('hides only the target uploader configuration without mutating the source', () => {
const config = {
picBed: {
current: 'tcyun',
tcyun: {
secretId: 'existing-secret-id',
secretKey: 'existing-secret-key'
},
github: {
token: 'github-token'
}
},
uploader: {
tcyun: {
defaultId: 'config-1'
},
github: {
defaultId: 'config-2'
}
},
settings: {
proxy: 'http://localhost:7890'
}
}
const context = {
marker: 'original-context',
getConfig<T>(name?: string): T {
return (name ? getByPath(config, name) : config) as T
}
}
const schemaContext = createSchemaOnlyUploaderContext(context, 'tcyun')
expect(schemaContext.getConfig('picBed.tcyun')).toBeUndefined()
expect(schemaContext.getConfig('picBed.tcyun.secretKey')).toBeUndefined()
expect(schemaContext.getConfig('uploader.tcyun')).toBeUndefined()
expect(schemaContext.getConfig('settings.proxy')).toBe('http://localhost:7890')
expect(schemaContext.marker).toBe('original-context')
expect(schemaContext.getConfig<ConfigRecord>('picBed')).toEqual({
current: 'tcyun',
github: {
token: 'github-token'
}
})
expect(schemaContext.getConfig<ConfigRecord>('uploader')).toEqual({
github: {
defaultId: 'config-2'
}
})
expect(schemaContext.getConfig<ConfigRecord>()).toEqual({
picBed: {
current: 'tcyun',
github: {
token: 'github-token'
}
},
uploader: {
github: {
defaultId: 'config-2'
}
},
settings: {
proxy: 'http://localhost:7890'
}
})
expect(context.getConfig('picBed.tcyun')).toEqual({
secretId: 'existing-secret-id',
secretKey: 'existing-secret-key'
})
})
})
@@ -1,10 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SHOW_PLUGIN_PAGE_MENU, OPEN_URL } from '#/events/constants'
import { SHOW_PLUGIN_PAGE_MENU } from '#/events/constants'
import { IRPCActionType } from '~/universal/types/enum'
vi.mock('@/utils/dataSender', () => ({
getConfig: vi.fn(),
invokeRPC: vi.fn(),
openURL: vi.fn(),
saveConfig: vi.fn(),
sendRPC: vi.fn(),
sendToMain: vi.fn()
@@ -14,6 +15,7 @@ import { pluginsAdapter } from '@/adapters/plugins'
import {
getConfig,
invokeRPC,
openURL,
saveConfig,
sendRPC,
sendToMain
@@ -152,15 +154,13 @@ describe('renderer/adapters plugins', () => {
expect(sendRPCMock).toHaveBeenCalledWith(IRPCActionType.RELOAD_APP)
})
it('opens homepage and awesome list through main events', () => {
it('opens homepage and awesome list through openURL', () => {
const openURLMock = vi.mocked(openURL)
pluginsAdapter.openPluginHomepage('https://example.com')
pluginsAdapter.openAwesomeList()
expect(sendToMainMock).toHaveBeenNthCalledWith(1, OPEN_URL, 'https://example.com')
expect(sendToMainMock).toHaveBeenNthCalledWith(
2,
OPEN_URL,
'https://github.com/PicGo/Awesome-PicGo'
)
expect(openURLMock).toHaveBeenNthCalledWith(1, 'https://example.com')
expect(openURLMock).toHaveBeenNthCalledWith(2, 'https://github.com/PicGo/Awesome-PicGo')
})
})
@@ -0,0 +1,178 @@
// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
ensureExpanded: vi.fn(),
ensureHydrated: vi.fn(async () => {}),
navigate: vi.fn(),
refreshConfigSchema: vi.fn(),
setHydrating: vi.fn()
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
vi.mock('sonner', () => ({
toast: {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn()
}
}))
vi.mock('@tanstack/react-router', () => ({
useNavigate: () => mocks.navigate,
useSearch: () => ({
uploader: 'tcyun',
configId: 'config-1'
})
}))
vi.mock('@/adapters/plugins', () => ({
pluginsAdapter: {
refreshConfigSchema: mocks.refreshConfigSchema
}
}))
vi.mock('@/store', () => ({
appActions: {
ensureHydrated: mocks.ensureHydrated
},
providerStoreActions: {
ensureExpanded: mocks.ensureExpanded,
setHydrating: mocks.setHydrating
},
useAppStore: {
use: {
appConfig: () => ({
picBed: {
uploader: 'tcyun'
},
uploader: {
tcyun: {
defaultId: 'config-1',
configList: [
{
_id: 'config-1',
_configName: 'Existing Config',
_createdAt: 1700000000000,
_updatedAt: 1700000000000,
secretKey: 'existing-secret-key'
}
]
}
}
}),
providers: () => [
{
id: 'tcyun',
name: 'Tencent Cloud',
visible: true,
isDefaultUploader: true
}
],
hasHydrated: () => true
}
},
useProviderStore: {
use: {
isHydrating: () => false
}
}
}))
vi.mock('@/components/main/providers/provider-sidebar', () => ({
ProviderSidebar: ({
onCreateIntent
}: {
onCreateIntent: (uploaderId: string) => void
}) => (
<button type='button' onClick={() => onCreateIntent('tcyun')}>
Open create dialog
</button>
)
}))
vi.mock('@/components/main/providers/provider-config-panel', () => ({
ProviderConfigPanel: ({
draftConfigMap
}: {
draftConfigMap: Record<string, unknown>
}) => (
<output data-testid='draft-config'>{JSON.stringify(draftConfigMap)}</output>
)
}))
vi.mock('@/components/main/providers/provider-config-name-dialog', () => ({
ProviderConfigNameDialog: ({
state,
onSubmit
}: {
state: { name: string } | null
onSubmit: () => Promise<void>
}) => state
? (
<button type='button' onClick={async () => await onSubmit()}>
Submit create dialog
</button>
)
: null
}))
vi.mock('@/components/main/providers/provider-delete-config-dialog', () => ({
ProviderDeleteConfigDialog: () => null
}))
import { PicGoProviders } from '@/components/main/providers/picgo-providers'
describe('PicGoProviders create config', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.refreshConfigSchema.mockResolvedValue([
{
name: 'version',
type: 'list',
choices: ['v4', 'v5'],
default: 'v5',
required: false
},
{
name: 'secretKey',
type: 'password',
default: '',
required: true
}
])
})
it('requests schema-only defaults and creates an empty credential draft', async () => {
render(<PicGoProviders />)
fireEvent.click(screen.getByRole('button', { name: 'Open create dialog' }))
fireEvent.click(screen.getByRole('button', { name: 'Submit create dialog' }))
await waitFor(() => {
expect(mocks.refreshConfigSchema).toHaveBeenCalledWith({
target: 'uploader',
uploaderName: 'tcyun',
draftValues: {},
schemaOnly: true
})
})
await waitFor(() => {
const draftMap = JSON.parse(
screen.getByTestId('draft-config').textContent ?? '{}'
) as Record<string, Record<string, unknown>>
expect(draftMap.tcyun).toMatchObject({
_configName: 'New Config',
_isDraft: true,
version: 'v5',
secretKey: ''
})
})
})
})
@@ -4,6 +4,13 @@ import { act, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { evaluatePluginConfig } from 'picgo'
const routerMocks = vi.hoisted(() => ({
search: {
uploader: 'picgo-plugin-test',
configId: 'config-1'
}
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
@@ -15,10 +22,7 @@ vi.mock('@tanstack/react-router', async () => {
return {
...actual,
useNavigate: () => vi.fn(),
useSearch: () => ({
uploader: 'picgo-plugin-test',
configId: 'config-1'
})
useSearch: () => routerMocks.search
}
})
@@ -39,6 +43,7 @@ vi.mock('@/store/providers/actions', () => ({
}))
import { ProviderConfigPanel } from '@/components/main/providers/provider-config-panel'
import type { ProviderDraftConfigItem } from '@/components/main/providers/types'
import { useAppStore } from '@/store/app-store'
import { useProviderStoreBase as useProviderStore } from '@/store/providers/store'
import { pluginsAdapter } from '@/adapters/plugins'
@@ -46,6 +51,11 @@ import { normalizePluginConfigSchema } from '@/components/common/normalize-plugi
import { IPasteStyle, IStartupMode } from '~/universal/types/enum'
import { buildCascadeRawSchema } from '../fixtures/cascade-fixture'
beforeEach(() => {
routerMocks.search.uploader = 'picgo-plugin-test'
routerMocks.search.configId = 'config-1'
})
const baseAppConfig = {
picBed: {
uploader: 'picgo-plugin-test',
@@ -88,17 +98,21 @@ const baseAppConfig = {
needReload: false
}
function setupStoreWithSavedConfig(savedConfig: Record<string, unknown>) {
function setupStoreWithSavedConfig(
savedConfig: Record<string, unknown>,
schemaOverride?: unknown[]
) {
// Mimics what the main process sends at startup: schema evaluated with
// empty answers, so the plugin's `default(answers)` for downstream fields
// computes against synthAnswers defaults — NOT the user's saved values.
// Provider-config-panel then has to issue an initial sync to bring the
// schema in line with the saved values.
const initialSchema = evaluatePluginConfig(
buildCascadeRawSchema(savedConfig.region as string) as Parameters<
typeof evaluatePluginConfig
>[0]
) as unknown[]
const initialSchema = schemaOverride ??
evaluatePluginConfig(
buildCascadeRawSchema(savedConfig.region as string) as Parameters<
typeof evaluatePluginConfig
>[0]
) as unknown[]
useAppStore.setState({
defaultPicBed: 'picgo-plugin-test',
@@ -182,10 +196,12 @@ function getFieldSelectTrigger(label: string) {
return trigger as HTMLElement
}
function renderPanel() {
function renderPanel(
draftConfigMap: Record<string, ProviderDraftConfigItem | undefined> = {}
) {
return render(
<ProviderConfigPanel
draftConfigMap={{}}
draftConfigMap={draftConfigMap}
setDraftConfigMap={vi.fn()}
onCreateConfigIntent={vi.fn()}
onDeleteConfigIntent={vi.fn()}
@@ -363,3 +379,119 @@ describe('ProviderConfigPanel — editor field rendering', () => {
})
})
})
describe('ProviderConfigPanel — saved password rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('keeps saved password fields masked without a reveal button', async () => {
const passwordSchema = [
{
name: 'secret',
type: 'password',
alias: 'Secret',
required: true,
message: 'Enter secret'
}
]
setupStoreWithSavedConfig(
{ secret: 'saved-secret' },
passwordSchema
)
vi.mocked(pluginsAdapter.refreshConfigSchema).mockResolvedValue(
normalizePluginConfigSchema(passwordSchema)
)
renderPanel()
await waitFor(() => {
const label = screen.getByText('Secret')
const field = label.closest('[data-slot="field"]')
if (!field) throw new Error('No field container for password field')
const input = field.querySelector('input')
expect(input?.value).toBe('saved-secret')
expect(input?.type).toBe('password')
expect(field.querySelector('button')).toBeNull()
})
})
})
describe('ProviderConfigPanel — draft schema rendering', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('keeps schema refreshes isolated from persisted uploader defaults', async () => {
const draftId = 'draft:picgo-plugin-test'
const cachedSchema = [
{
name: 'version',
type: 'list',
choices: ['v4', 'v5'],
default: 'v5',
required: false
},
{
name: 'secret',
type: 'password',
alias: 'Secret',
default: 'persisted-secret',
required: true
}
]
const cleanSchema = [
cachedSchema[0],
{
...cachedSchema[1],
default: ''
}
]
setupStoreWithSavedConfig(
{
version: 'v5',
secret: 'persisted-secret'
},
cachedSchema
)
routerMocks.search.configId = draftId
vi.mocked(pluginsAdapter.refreshConfigSchema).mockResolvedValue(
normalizePluginConfigSchema(cleanSchema)
)
renderPanel({
'picgo-plugin-test': {
_id: draftId,
_configName: 'New Config',
_createdAt: 1700000000001,
_updatedAt: 1700000000001,
_isDraft: true,
version: 'v5',
secret: ''
}
})
await waitFor(() => {
expect(pluginsAdapter.refreshConfigSchema).toHaveBeenCalledWith(
expect.objectContaining({
target: 'uploader',
uploaderName: 'picgo-plugin-test',
schemaOnly: true
})
)
})
await waitFor(() => {
const label = screen.getByText('Secret')
const field = label.closest('[data-slot="field"]')
if (!field) throw new Error('No field container for draft password field')
const input = field.querySelector('input')
expect(input?.value).toBe('')
expect(input?.type).toBe('password')
expect(field.querySelector('button')).not.toBeNull()
})
})
})
@@ -116,6 +116,88 @@ describe('SchemaFormFields editor field', () => {
})
describe('SchemaFormFields password field', () => {
const passwordSchema: ProviderPluginConfig[] = [
{
name: 'secret',
type: 'password',
required: true,
alias: 'Secret'
}
]
it('allows password reveal by default', () => {
renderSchema(passwordSchema, { secret: 'saved-secret' })
const input = screen.getByDisplayValue('saved-secret') as HTMLInputElement
expect(input.type).toBe('password')
fireEvent.click(screen.getByRole('button'))
expect(input.type).toBe('text')
})
it('keeps the password masked and removes the reveal button when disabled', () => {
const onValueChange = vi.fn()
render(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal={false}
onValueChange={onValueChange}
/>
)
const input = screen.getByDisplayValue('saved-secret') as HTMLInputElement
expect(input.type).toBe('password')
expect(screen.queryByRole('button')).toBeNull()
fireEvent.change(input, { target: { value: 'replacement-secret' } })
expect(onValueChange).toHaveBeenCalledWith('secret', 'replacement-secret')
})
it('clears visible password state when reveal becomes disabled', () => {
const onValueChange = vi.fn()
const { rerender } = render(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal
onValueChange={onValueChange}
/>
)
fireEvent.click(screen.getByRole('button'))
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('text')
rerender(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal={false}
onValueChange={onValueChange}
/>
)
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('password')
rerender(
<SchemaFormFields
schema={passwordSchema}
values={{ secret: 'saved-secret' }}
allowPasswordReveal
onValueChange={onValueChange}
/>
)
expect(
(screen.getByDisplayValue('saved-secret') as HTMLInputElement).type
).toBe('password')
})
})
describe('SchemaFormFields unknown field type', () => {
it('does not render any input control for an unrecognized type, label only', () => {
renderSchema([
+3 -5
View File
@@ -25,8 +25,7 @@ const defaultWebPreferences = {
preload: path.join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true,
nodeIntegrationInWorker: false,
backgroundThrottling: false
nodeIntegrationInWorker: false
}
const handleWindowParams = (windowURL: string) => {
@@ -162,13 +161,12 @@ windowList.set(IWindowList.MINI_WINDOW, {
const obj: IBrowserWindowOptions = {
height: 64,
width: 64,
show: isLinux,
show: false,
frame: false,
fullscreenable: false,
skipTaskbar: true,
resizable: false,
transparent: false,
backgroundColor: '#0f172a',
transparent: true,
icon: getStaticPath('logo.png'),
webPreferences: {
...defaultWebPreferences
+11 -2
View File
@@ -12,6 +12,7 @@ import { notifyAppConfigUpdated } from '~/main/utils/appConfigNotifier'
import { dialog } from 'electron'
import windowManager from '~/main/apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { createSchemaOnlyUploaderContext } from '~/main/utils/schemaOnlyUploaderContext'
const README_FILE_CANDIDATES = ['README.md', 'readme.md', 'Readme.md'] as const
@@ -210,7 +211,10 @@ pluginsRouter
} else if (payload.target === 'uploader') {
const handler = picgo.helper.uploader.get(payload.uploaderName)
if (handler?.config) {
rawSchema = handler.config(picgo)
const configContext = payload.schemaOnly
? createSchemaOnlyUploaderContext(picgo, payload.uploaderName)
: picgo
rawSchema = handler.config(configContext)
}
}
@@ -235,7 +239,12 @@ pluginsRouter
type IRefreshConfigSchemaArgs =
| { target: 'plugin', pluginFullName: string, draftValues?: Record<string, unknown> }
| { target: 'transformer', pluginFullName: string, draftValues?: Record<string, unknown> }
| { target: 'uploader', uploaderName: string, draftValues?: Record<string, unknown> }
| {
target: 'uploader'
uploaderName: string
draftValues?: Record<string, unknown>
schemaOnly?: boolean
}
export {
pluginsRouter
@@ -0,0 +1,87 @@
interface ConfigReadableContext {
getConfig<T>(name?: string): T
}
type ConfigRecord = Record<string, unknown>
function isConfigRecord(value: unknown): value is ConfigRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function omitUploaderConfig(
value: unknown,
uploaderName: string
): unknown {
if (!isConfigRecord(value)) {
return value
}
const nextValue = { ...value }
delete nextValue[uploaderName]
return nextValue
}
function sanitizeFullConfig(value: unknown, uploaderName: string): unknown {
if (!isConfigRecord(value)) {
return value
}
const nextValue = { ...value }
if ('picBed' in value) {
nextValue.picBed = omitUploaderConfig(value.picBed, uploaderName)
}
if ('uploader' in value) {
nextValue.uploader = omitUploaderConfig(value.uploader, uploaderName)
}
return nextValue
}
export function createSchemaOnlyUploaderContext<T extends ConfigReadableContext>(
context: T,
uploaderName: string
): T {
const hiddenConfigPaths = [
`picBed.${uploaderName}`,
`uploader.${uploaderName}`
]
const getConfig = <V>(name?: string): V => {
if (
name &&
hiddenConfigPaths.some(
(configPath) => name === configPath || name.startsWith(`${configPath}.`)
)
) {
return undefined as V
}
if (name === 'picBed' || name === 'uploader') {
return omitUploaderConfig(
context.getConfig<unknown>(name),
uploaderName
) as V
}
if (name === undefined) {
return sanitizeFullConfig(
context.getConfig<unknown>(),
uploaderName
) as V
}
return context.getConfig<V>(name)
}
return new Proxy(context, {
get(target, property, receiver) {
if (property === 'getConfig') {
return getConfig
}
return Reflect.get(target, property, receiver)
}
})
}
+10 -5
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import { OPEN_URL, SHOW_PLUGIN_PAGE_MENU } from '#/events/constants'
import { SHOW_PLUGIN_PAGE_MENU } from '#/events/constants'
import { IRPCActionType } from '~/universal/types/enum'
import { getConfig, invokeRPC, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
import { getConfig, invokeRPC, openURL, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
import { ipc } from '@/utils/bridge'
import type { ProviderPluginConfig } from '@/components/main/providers/types'
import { normalizePluginConfigSchema } from '@/components/common/normalize-plugin-schema'
@@ -9,7 +9,12 @@ import { normalizePluginConfigSchema } from '@/components/common/normalize-plugi
export type IRefreshConfigSchemaArgs =
| { target: 'plugin', pluginFullName: string, draftValues: Record<string, unknown> }
| { target: 'transformer', pluginFullName: string, draftValues: Record<string, unknown> }
| { target: 'uploader', uploaderName: string, draftValues: Record<string, unknown> }
| {
target: 'uploader'
uploaderName: string
draftValues: Record<string, unknown>
schemaOnly?: boolean
}
interface PluginInstallResult {
success: boolean
@@ -222,9 +227,9 @@ export const pluginsAdapter = {
if (!url) {
return
}
sendToMain(OPEN_URL, url)
openURL(url)
},
openAwesomeList () {
sendToMain(OPEN_URL, 'https://github.com/PicGo/Awesome-PicGo')
openURL('https://github.com/PicGo/Awesome-PicGo')
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import { GET_PICBEDS, OPEN_URL, PICGO_OPEN_FILE, TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { GET_PICBEDS, PICGO_OPEN_FILE, TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
import { IRPCActionType } from '~/universal/types/enum'
import { appConfigAdapter } from './app-config'
import { getConfig, invokeRPC, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
import { getConfig, invokeRPC, openURL, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
import { ipc } from '@/utils/bridge'
export const settingsAdapter = {
@@ -27,7 +27,7 @@ export const settingsAdapter = {
sendToMain(PICGO_OPEN_FILE, 'picgo.log')
},
openExternalUrl (url: string) {
sendToMain(OPEN_URL, url)
openURL(url)
},
updateServer () {
sendToMain('updateServer')
+3
View File
@@ -19,5 +19,8 @@ export const windowControlsAdapter = {
},
minimizeWindow () {
sendToMain(MINIMIZE_WINDOW)
},
openMiniWindow () {
sendToMain('openMiniWindow')
}
}
@@ -39,6 +39,7 @@ interface SchemaFormFieldsProps {
schema: ProviderPluginConfig[]
values: SchemaFormValues
fieldErrors?: SchemaFieldErrorMap
allowPasswordReveal?: boolean
onValueChange: (name: string, value: unknown) => void
}
@@ -49,6 +50,14 @@ interface CheckboxFieldProps {
onValueChange: (name: string, value: unknown) => void
}
interface PasswordFieldProps {
field: ProviderPluginConfig
selectedValue: unknown
isInvalid: boolean
allowPasswordReveal: boolean
onValueChange: (name: string, value: unknown) => void
}
function renderSanitizedTips(markdown: string) {
const parsed = marked.parse(markdown)
const html = typeof parsed === "string" ? parsed : markdown
@@ -151,16 +160,51 @@ function CheckboxField({
)
}
function PasswordField({
field,
selectedValue,
isInvalid,
allowPasswordReveal,
onValueChange,
}: PasswordFieldProps) {
const [isPasswordVisible, setIsPasswordVisible] = useState(false)
return (
<div className="relative">
<Input
value={String(selectedValue ?? "")}
type={isPasswordVisible ? "text" : "password"}
placeholder={field.message || field.name}
className={allowPasswordReveal ? "pr-10" : undefined}
aria-invalid={isInvalid}
onChange={(event) => onValueChange(field.name, event.target.value)}
/>
{allowPasswordReveal && (
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute top-1/2 right-1 -translate-y-1/2"
onClick={() => setIsPasswordVisible((prev) => !prev)}
>
{isPasswordVisible ? (
<EyeOffIcon className="size-4" />
) : (
<EyeIcon className="size-4" />
)}
</Button>
)}
</div>
)
}
export function SchemaFormFields({
schema,
values,
fieldErrors = {},
allowPasswordReveal = true,
onValueChange,
}: SchemaFormFieldsProps) {
const [visiblePasswords, setVisiblePasswords] = useState<Record<string, boolean>>(
{}
)
return (
<div className="space-y-5">
{schema.map((field) => {
@@ -169,7 +213,6 @@ export function SchemaFormFields({
const optionValueMap = new Map(
choices.map((choice) => [String(choice.value), choice.value] as const)
)
const isPasswordVisible = Boolean(visiblePasswords[field.name])
const fieldError = fieldErrors[field.name]
const isInvalid = Boolean(fieldError)
@@ -230,34 +273,14 @@ export function SchemaFormFields({
)}
{field.type === "password" && (
<div className="relative">
<Input
value={String(value ?? "")}
type={isPasswordVisible ? "text" : "password"}
placeholder={field.message || field.name}
className="pr-10"
aria-invalid={isInvalid}
onChange={(event) => onValueChange(field.name, event.target.value)}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute top-1/2 right-1 -translate-y-1/2"
onClick={() => {
setVisiblePasswords((prev) => ({
...prev,
[field.name]: !prev[field.name],
}))
}}
>
{isPasswordVisible ? (
<EyeOffIcon className="size-4" />
) : (
<EyeIcon className="size-4" />
)}
</Button>
</div>
<PasswordField
key={`${field.name}:${allowPasswordReveal ? "reveal" : "locked"}`}
field={field}
selectedValue={value}
isInvalid={isInvalid}
allowPasswordReveal={allowPasswordReveal}
onValueChange={onValueChange}
/>
)}
{field.type === "list" && (
+11 -7
View File
@@ -1,10 +1,9 @@
import { useEffect, useState, type CSSProperties } from 'react'
import { Copy, Maximize2, Minus, Square, X } from 'lucide-react'
import { Copy, Maximize2, Minus, Shrink, Square, X } from 'lucide-react'
import { WINDOW_STATE_CHANGED } from '#/events/constants'
import { windowControlsAdapter } from '@/adapters/window-controls'
import { ipc } from '@/utils/bridge'
import { cn } from '@/lib/utils'
import pkg from 'root/package.json'
interface TitleBarProps {
isMac?: boolean
@@ -12,7 +11,6 @@ interface TitleBarProps {
export function TitleBar ({ isMac = false }: TitleBarProps) {
const [isMaximized, setIsMaximized] = useState(false)
const version = pkg.version
useEffect(() => {
async function syncWindowState () {
@@ -103,6 +101,16 @@ export function TitleBar ({ isMac = false }: TitleBarProps) {
>
<Minus className="size-4" />
</button>
<button
type="button"
aria-label="Open mini window"
className="flex h-full w-12 cursor-pointer items-center justify-center transition-colors hover:bg-black/5 dark:hover:bg-white/10"
onClick={() => {
windowControlsAdapter.openMiniWindow()
}}
>
<Shrink className="size-4" />
</button>
<button
type="button"
aria-label={isMaximized ? 'Restore window' : 'Maximize window'}
@@ -135,10 +143,6 @@ export function TitleBar ({ isMac = false }: TitleBarProps) {
{isMac ? macWindowControls : null}
</div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-xs font-medium opacity-50">
{`PicGo - ${version}`}
</div>
<div className="flex h-full items-center">
{!isMac ? desktopWindowControls : null}
</div>
@@ -41,6 +41,7 @@ export function PicGoMiniPage() {
const { t } = useTranslation()
const isLinuxPlatform = isLinux()
const fileInputRef = useRef<HTMLInputElement>(null)
const didDragRef = useRef(false)
const [isDragOver, setIsDragOver] = useState(false)
const [dragging, setDragging] = useState(false)
const [mouseDownX, setMouseDownX] = useState(-1)
@@ -49,6 +50,11 @@ export function PicGoMiniPage() {
const [mouseDownScreenY, setMouseDownScreenY] = useState(-1)
const { showProgress, progress, hasError, uploadFiles } = useMiniUpload()
useEffect(() => {
document.body.style.background = 'transparent'
document.documentElement.style.background = 'transparent'
}, [])
const handleOpenFilePicker = () => {
fileInputRef.current?.click()
}
@@ -56,6 +62,7 @@ export function PicGoMiniPage() {
useEffect(() => {
const handleMouseDown = (event: MouseEvent) => {
setDragging(true)
didDragRef.current = false
setMouseDownX(event.pageX)
setMouseDownY(event.pageY)
setMouseDownScreenX(event.screenX)
@@ -70,6 +77,7 @@ export function PicGoMiniPage() {
return
}
didDragRef.current = true
miniPageAdapter.moveMiniWindow({
x: event.screenX - mouseDownX,
y: event.screenY - mouseDownY,
@@ -166,7 +174,7 @@ export function PicGoMiniPage() {
}
return (
<UtilityWindowLayout page="mini" shellClassName="flex">
<UtilityWindowLayout page="mini" shellClassName="flex" className="bg-transparent">
<div
id="mini-page"
className={cn(
@@ -202,8 +210,6 @@ export function PicGoMiniPage() {
tabIndex={0}
aria-label={t("CLICK_TO_UPLOAD")}
className="h-full w-full"
onClick={handleOpenFilePicker}
onDoubleClick={handleOpenFilePicker}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
@@ -86,13 +86,13 @@ export function PicGoRenamePage() {
return (
<UtilityWindowLayout page="rename">
<AppMainCard className="h-(--app-utility-shell-height) w-full flex-none rounded-xl border-(--app-panel-border) bg-(--app-panel-bg) px-4 py-4">
<form className="flex h-full flex-col" onSubmit={handleConfirm}>
<AppMainCard className="h-(--app-utility-shell-height) w-full flex-none rounded-xl border-(--app-panel-border) bg-(--app-panel-bg) px-4 py-2">
<form className="flex h-full flex-col gap-2" onSubmit={handleConfirm}>
<Label htmlFor="rename-file-name" className="text-sm font-semibold">
{t("FILE_RENAME")}
</Label>
<div className="flex flex-1 items-center">
<div>
<div className="relative w-full">
<Input
id="rename-file-name"
@@ -127,7 +127,7 @@ export function PicGoRenamePage() {
</div>
</div>
<div className="flex justify-end gap-1.5">
<div className="flex justify-end gap-1.5 pt-3">
<Button
type="button"
variant="outline"
@@ -124,13 +124,15 @@ export function PicGoTrayPage () {
return (
<UtilityWindowLayout page='tray'>
<AppMainCard className='h-(--app-utility-shell-height) w-full gap-0 overflow-hidden rounded-xl border-(--app-panel-border) bg-(--app-panel-bg) py-0'>
<button
type='button'
className='h-8 w-full cursor-pointer bg-primary/80 px-3 text-center text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary dark:bg-primary/75 dark:hover:bg-primary'
onClick={handleOpenMainWindow}
>
{t('OPEN_MAIN_WINDOW')}
</button>
<div className='px-2 pt-2 pb-2'>
<button
type='button'
className='h-7 w-full cursor-pointer rounded-full border-none bg-primary/80 text-center text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary dark:bg-primary/75 dark:hover:bg-primary'
onClick={handleOpenMainWindow}
>
{t('OPEN_MAIN_WINDOW')}
</button>
</div>
<ScrollArea className='min-h-0 flex-1'>
<div className='space-y-2 p-2'>
@@ -98,7 +98,7 @@ function buildGroupKey (timestamp: number) {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
}
function formatGroupLabel (timestamp: number, t: (key: string) => string) {
function formatGroupLabel (timestamp: number, t: (key: string) => string, locale?: string) {
const date = new Date(timestamp)
const now = new Date()
const yesterday = new Date(now)
@@ -112,7 +112,7 @@ function formatGroupLabel (timestamp: number, t: (key: string) => string) {
return t('HISTORY_PANEL_YESTERDAY')
}
return new Intl.DateTimeFormat(undefined, {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric'
@@ -121,7 +121,8 @@ function formatGroupLabel (timestamp: number, t: (key: string) => string) {
function buildHistoryGroups (
items: DashboardHistoryItem[],
t: (key: string) => string
t: (key: string) => string,
locale?: string
): DashboardHistoryGroup[] {
const groups: DashboardHistoryGroup[] = []
@@ -136,7 +137,7 @@ function buildHistoryGroups (
groups.push({
key,
label: formatGroupLabel(item.timestamp, t),
label: formatGroupLabel(item.timestamp, t, locale),
items: [item]
})
})
@@ -198,7 +199,7 @@ export function HistoryPanel ({
onStartImport?: () => Promise<void> | void
onCloudRefresh?: () => Promise<void> | void
}) {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const albumSource = useAlbumStore.use.albumSource()
const { userInfo: cloudUserInfo, isPaid: isCloudPaidUser } = usePicGoCloudUserInfo()
const isCloud = albumSource === AlbumSource.CLOUD
@@ -223,7 +224,7 @@ export function HistoryPanel ({
}
return yesterdayLabel
})
}, i18n.language)
const entries = buildHistoryEntries(groups)
const virtuosoKey = buildVirtuosoResetKey(groups, keyword)
const previewItems: AlbumPhoto[] = filteredItems
@@ -71,7 +71,8 @@ export function UploaderSwitcher({
className="group h-10 cursor-pointer bg-card/80 px-4 backdrop-blur transition-all hover:border-primary/30"
disabled={disabled}
>
<span className="size-2 animate-pulse rounded-full bg-green-500" />
{/* 刻意不加 animate-pulse2s infinite 会让主窗口空闲时也持续产出合成帧,把周围几层 backdrop-blur 一起钉在满帧率上重算,GPU 进程无法进入空闲。静态圆点表意相同。 */}
<span className="size-2 rounded-full bg-green-500" />
<span className="text-sm font-semibold text-foreground transition-colors group-hover:text-primary">
{current.providerName}
<span className="mx-1 font-normal text-muted-foreground">/</span>
@@ -13,7 +13,7 @@ import {
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { getConfig, getPicBeds } from '@/utils/dataSender'
import { getConfig, getPicBeds, openURL } from '@/utils/dataSender'
import { clipboard, ipc } from '@/utils/bridge'
import { toast } from 'sonner'
import { mainMoreAdapter } from '@/adapters/main-more'
@@ -182,6 +182,18 @@ export function MainMoreDialog ({ open, onOpenChange }: MainMoreDialogProps) {
<DialogDescription>{t('PICGO_SPONSOR_TEXT')}</DialogDescription>
</DialogHeader>
<div className='flex justify-center'>
<Button
type='button'
variant='outline'
className='gap-2'
onClick={() => openURL('https://github.com/sponsors/Molunerfinn')}
>
<HeartHandshakeIcon className='size-4' />
GitHub Sponsors
</Button>
</div>
<div className='grid gap-4 sm:grid-cols-2'>
<div className='space-y-3 rounded-xl border border-border p-4 text-center'>
<img
@@ -16,6 +16,7 @@ import {
import { useMatchRoute, useNavigate } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import pkg from 'root/package.json'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
@@ -95,11 +96,12 @@ export function PicGoAppSidebar({
/>
<span
className={cn(
'text-xl font-bold tracking-tight transition-all duration-300 whitespace-nowrap overflow-hidden origin-left',
'inline-flex items-center text-xl font-bold tracking-tight transition-all duration-300 whitespace-nowrap overflow-hidden origin-left',
collapsed ? 'w-0 opacity-0 scale-90' : 'w-auto opacity-100 scale-100'
)}
>
PicGo
<span className="ml-1.5 text-xs font-normal text-muted-foreground">v{pkg.version}</span>
</span>
</div>
@@ -3,6 +3,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { pluginsAdapter } from "@/adapters/plugins"
import { appActions, providerStoreActions, useAppStore, useProviderStore } from "@/store"
import { ProviderConfigNameDialog } from "./provider-config-name-dialog"
import { ProviderConfigPanel } from "./provider-config-panel"
@@ -35,7 +36,6 @@ export function PicGoProviders() {
const appConfig = useAppStore.use.appConfig()
const providers = useAppStore.use.providers()
const providerSchemas = useAppStore.use.providerSchemas()
const hasHydrated = useAppStore.use.hasHydrated()
const isLoadingUploaders = useProviderStore.use.isHydrating()
@@ -167,14 +167,12 @@ export function PicGoProviders() {
const handleCreateConfigDraft = async (uploaderId: string, configName: string) => {
try {
const resolvedSchema =
(await providerStoreActions.ensureSchema(uploaderId)).config ??
providerSchemas[uploaderId]?.config
if (!resolvedSchema) {
toast.error(t("FAILED"))
return
}
const resolvedSchema = await pluginsAdapter.refreshConfigSchema({
target: "uploader",
uploaderName: uploaderId,
draftValues: {},
schemaOnly: true,
})
const now = Date.now()
const draftId = createDraftConfigId(uploaderId)
@@ -300,9 +300,11 @@ export function ProviderConfigPanel({
</div>
<ProviderFormFields
key={`${selectedUploaderId}:${selectedConfig._id}`}
schema={schema}
values={formValues}
fieldErrors={fieldErrors}
allowPasswordReveal={isDraftSelected}
onValueChange={handleValueChange}
/>
@@ -335,4 +337,3 @@ export function ProviderConfigPanel({
</AppMainCard>
)
}
@@ -78,9 +78,10 @@ export function useProviderConfigForm({
target: "uploader",
uploaderName: refreshTarget.uploaderName,
draftValues,
schemaOnly: isDraftSelected,
})
},
[]
[isDraftSelected]
)
// Re-hydrate when the persisted config's _updatedAt changes (initial load,
@@ -113,6 +114,9 @@ export function useProviderConfigForm({
.then((nextSchema) => {
if (cancelled) return
setLiveSchema(nextSchema)
if (isDraftSelected) {
setValues(buildFormValues(nextSchema, selectedConfig))
}
})
.catch((error) => {
if (cancelled) return
@@ -62,6 +62,8 @@ export function SettingsSectionGeneral({
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh-CN"></SelectItem>
<SelectItem value="zh-TW"></SelectItem>
<SelectItem value="ko"></SelectItem>
<SelectItem value="ja"></SelectItem>
</SelectContent>
</Select>
}
+6 -2
View File
@@ -7,6 +7,8 @@ import { ipc } from '@/utils/bridge'
import enRaw from '../../../public/i18n/en.yml?raw'
import zhCNRaw from '../../../public/i18n/zh-CN.yml?raw'
import zhTWRaw from '../../../public/i18n/zh-TW.yml?raw'
import koRaw from '../../../public/i18n/ko.yml?raw'
import jaRaw from '../../../public/i18n/ja.yml?raw'
type TranslationMap = Record<string, string>
@@ -15,7 +17,7 @@ function parseLocale (raw: string): TranslationMap {
}
function applyLanguage (lang: string) {
const nextLanguage = ['en', 'zh-CN', 'zh-TW'].includes(lang) ? lang : 'en'
const nextLanguage = ['en', 'zh-CN', 'zh-TW', 'ko', 'ja'].includes(lang) ? lang : 'en'
return i18n.changeLanguage(nextLanguage).catch(() => {
return i18n.changeLanguage('en')
})
@@ -34,7 +36,9 @@ export function initializeI18n () {
resources: {
en: { translation: parseLocale(enRaw) },
'zh-CN': { translation: parseLocale(zhCNRaw) },
'zh-TW': { translation: parseLocale(zhTWRaw) }
'zh-TW': { translation: parseLocale(zhTWRaw) },
ko: { translation: parseLocale(koRaw) },
ja: { translation: parseLocale(jaRaw) }
},
lng: 'en',
fallbackLng: 'en',
+6
View File
@@ -7,4 +7,10 @@ export const builtinI18nList: II18nItem[] = [{
}, {
label: 'English',
value: 'en'
}, {
label: '한국어',
value: 'ko'
}, {
label: '日本語',
value: 'ja'
}]
+1 -1
View File
@@ -144,7 +144,7 @@ interface IBrowserWindowOptions {
nodeIntegration: boolean,
nodeIntegrationInWorker: boolean,
contextIsolation: boolean,
backgroundThrottling: boolean
backgroundThrottling?: boolean
webSecurity?: boolean
},
vibrancy?: string | any,