mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-09-21 19:20:52 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a716ecdbc | ||
|
|
089884b10a | ||
|
|
2ed1dd5abd | ||
|
|
c84d542557 | ||
|
|
dfe92d496b | ||
|
|
3a9ff25071 | ||
|
|
2c9a188dbb | ||
|
|
525492bed1 | ||
|
|
50881b071a | ||
|
|
2936b19008 |
+4
-1
@@ -25,4 +25,7 @@ scripts/*.yml
|
||||
/dist_electron
|
||||
.serena/
|
||||
dist/*
|
||||
test.js
|
||||
test.js
|
||||
specs/
|
||||
.cache/
|
||||
openspec/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm commitlint ${1}
|
||||
@@ -0,0 +1 @@
|
||||
pnpm check
|
||||
@@ -0,0 +1 @@
|
||||
pnpm test
|
||||
@@ -4,13 +4,14 @@
|
||||
PicGo is an Electron + Vue 3 desktop client. Source lives in `src/`: `src/main` for main-process and IPC logic, `src/renderer` for Vue views, and `src/universal` for shared helpers (`types/`, `events/constants.ts`). `background.ts` wires Electron Builder. Static assets and locale YAML files stay in `public/` (add languages under `public/i18n/`), while `docs/` hosts user-facing guides. Automation scripts live in `scripts/`, and legacy tests sit under `test/unit` (Karma) and `test/e2e` (Spectron).
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `pnpm install` — install dependencies; `npm install` is unsupported.
|
||||
- `pnpm install` — install dependencies; `npm install` is unsupported. Only run this when the user explicitly asks/coordinates it.
|
||||
- Always add/remove dependencies with `pnpm` (never edit package.json versions by hand then install).
|
||||
- `pnpm dev` — electron-vite dev server for main/preload/renderer.
|
||||
- `pnpm build` — electron-vite build outputs to `dist/main`, `dist/preload`, `dist/renderer`; `pnpm preview` for preview mode.
|
||||
- Packaging config lives in `electron-builder.yml` (read by electron-builder via package.json `build` field/extraResources); set `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` if downloads are slow.
|
||||
- `pnpm lint` / `pnpm lint:fix` — run or auto-fix ESLint (Standard, TypeScript, Vue rules).
|
||||
- `pnpm lint:dpdm` — fail fast on circular dependencies in `src/`.
|
||||
- `pnpm check` — run `tsc` + `lint` (run once before finishing a task).
|
||||
- `pnpm gen-i18n` — regenerate typed locales after touching `public/i18n/*.yml`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
## :tada: 2.4.3 (2026-01-12)
|
||||
|
||||
|
||||
### :sparkles: Features
|
||||
|
||||
* add global url rewrite support ([#1377](https://github.com/Molunerfinn/PicGo/issues/1377)) ([2ed1dd5](https://github.com/Molunerfinn/PicGo/commit/2ed1dd5)), closes [#1255](https://github.com/Molunerfinn/PicGo/issues/1255) [#1281](https://github.com/Molunerfinn/PicGo/issues/1281)
|
||||
* **upload:** add batch url upload support ([#1376](https://github.com/Molunerfinn/PicGo/issues/1376)) ([c84d542](https://github.com/Molunerfinn/PicGo/commit/c84d542)), closes [#1302](https://github.com/Molunerfinn/PicGo/issues/1302)
|
||||
|
||||
|
||||
### :bug: Bug Fixes
|
||||
|
||||
* **gallery:** fix picbed list visible status not sync with gallery ([#1373](https://github.com/Molunerfinn/PicGo/issues/1373)) ([dfe92d4](https://github.com/Molunerfinn/PicGo/commit/dfe92d4)), closes [#1372](https://github.com/Molunerfinn/PicGo/issues/1372)
|
||||
* **ui:** some ui bugs ([089884b](https://github.com/Molunerfinn/PicGo/commit/089884b))
|
||||
|
||||
|
||||
|
||||
## :tada: 2.4.2 (2026-01-07)
|
||||
|
||||
|
||||
### :sparkles: Features
|
||||
|
||||
* **notification:** refactor notification and add notificationSound settings ([#1370](https://github.com/Molunerfinn/PicGo/issues/1370)) ([2936b19](https://github.com/Molunerfinn/PicGo/commit/2936b19)), closes [#1229](https://github.com/Molunerfinn/PicGo/issues/1229)
|
||||
|
||||
|
||||
### :bug: Bug Fixes
|
||||
|
||||
* **tray:** clamp image titles to two lines ([525492b](https://github.com/Molunerfinn/PicGo/commit/525492b))
|
||||
|
||||
|
||||
### :pencil: Documentation
|
||||
|
||||
* **2.4.2:** update changelog ([2c9a188](https://github.com/Molunerfinn/PicGo/commit/2c9a188))
|
||||
|
||||
|
||||
|
||||
## :tada: 2.4.2-beta.0 (2025-12-31)
|
||||
|
||||
|
||||
|
||||
+48
-48
@@ -1,66 +1,66 @@
|
||||
# PicGo 2.4.0 Changelog
|
||||
|
||||
## Features
|
||||
- Add filename display in the gallery (#1050).
|
||||
- Add default placeholders when images/URLs cannot be shown in the gallery or tray window (#1050).
|
||||
- Add filename display in the gallery (#1050)
|
||||
- Add default placeholders when images/URLs cannot be shown in the gallery or tray window (#1050)
|
||||

|
||||
- Add filename display in the macOS tray window (#1054).
|
||||
- Allow multiple configs per uploader type and choose one for upload (thanks @STDSuperman, #1016).
|
||||
- Add filename display in the macOS tray window (#1054)
|
||||
- Allow multiple configs per uploader type and choose one for upload (thanks @STDSuperman, #1016)
|
||||

|
||||
- Allow the uploader selection menu to pick a specific config entry for that uploader type.
|
||||
- Allow the uploader selection menu to pick a specific config entry for that uploader type
|
||||

|
||||
- Add Dock icon visibility option (#1045).
|
||||
- Add Dock icon visibility option (#1045)
|
||||
<img width="787" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/bb9492f1-6522-45ce-ae5d-c614901d8b06" />
|
||||
- Add PicGo Repair Toolbox for self-diagnosis.
|
||||
- Add PicGo Repair Toolbox for self-diagnosis
|
||||
<img width="324" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/98cf6e64-7313-4ebe-83d0-aa86e4514109" />
|
||||
- Add option to encode/escape output URLs (#731).
|
||||
- Add option to encode/escape output URLs (#731)
|
||||
<img width="775" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/6e0c13dd-3404-4b07-9c99-412321835b27" />
|
||||
- Support drag-and-drop uploads for arbitrary file formats (#1052).
|
||||
- Tencent COS: add Endpoint and Intelligent Compression settings (thanks @palmcivet @yc910920).
|
||||
- Support drag-and-drop uploads for arbitrary file formats (#1052)
|
||||
- Tencent COS: add Endpoint and Intelligent Compression settings (thanks @palmcivet @yc910920)
|
||||

|
||||
- Add Markdown-rendered tips for uploader config (see tcyun uploader config).
|
||||
- Add Markdown-rendered tips for uploader config (see tcyun uploader config)
|
||||

|
||||
- Show the active uploader config name in the upload screen.
|
||||
- Show the active uploader config name in the upload screen
|
||||

|
||||
- Gallery toolbar: bulk replace image URL host (#875).
|
||||
Note: select images first; you can filter by uploader; e.g., replace `https://www.a.com/...` with `https://www.b.com/...`.
|
||||
- Gallery toolbar: bulk replace image URL host (#875)
|
||||
Note: select images first; you can filter by uploader; e.g., replace `https://www.a.com/...` with `https://www.b.com/...`
|
||||

|
||||
- Add “Startup Mode”: Silent (default) / Main Window; Windows/Linux also support Mini Window (#915).
|
||||
- Add “Startup Mode”: Silent (default) / Main Window; Windows/Linux also support Mini Window (#915)
|
||||
<img width="577" alt="image" src="https://github.com/Molunerfinn/PicGo/assets/12621342/7e63bb5c-44b0-480e-824f-3c2edbed4fbb" />
|
||||
- PicGo Server: support multipart form uploads (field `files`, #428, thanks @happy-game).
|
||||
- PicGo Server: support multipart form uploads (field `files`, #428, thanks @happy-game)
|
||||
<img width="951" alt="image" src="https://github.com/user-attachments/assets/14244f1d-60f5-487f-bde6-6d0e009645fb" />
|
||||
|
||||
## Bug Fixes
|
||||
- Fix Windows context-menu config script generation (#1019).
|
||||
- Fix custom link URL encoding (#1112).
|
||||
- Fix potential logging dead loop (#1101).
|
||||
- Fix drag-to-tray upload error (#1107).
|
||||
- Fix filename encoding (#1121).
|
||||
- Prevent errors on GitHub uploads with duplicate filenames.
|
||||
- Fix beta.2 style issues.
|
||||
- Fix slow filename display in the rename window (#1130).
|
||||
- Fix “open config file” actually opening the log file (#1163).
|
||||
- Fix plugin config dialog not reading/saving correctly.
|
||||
- Fix inability to add new uploader configs (created as edit) (#1198, #1196).
|
||||
- Fix macOS context menu disappearing (thanks @muwoo, #1179).
|
||||
- Fix macOS tray window flashing after right-click (thanks @QThans, #1217).
|
||||
- Fix uploader config page not scrollable when many fields (#1237).
|
||||
- Fix Tencent COS URL encoding (#1265).
|
||||
- Fix plugin list search not working (#1297).
|
||||
- Fix clipboard filename losing “seconds” (#1293).
|
||||
- Fix macOS tray copy-link failing on click (#1280, #1210).
|
||||
- Fix auto-copy URL toggle not turning off (#1294, thanks @happy-game).
|
||||
- Fix Wayland clipboard image upload issue (#1261, thanks @happy-game).
|
||||
- Fix URL uploads with Chinese characters being encoded in filenames (#1339).
|
||||
- Fix Windows context-menu config script generation (#1019)
|
||||
- Fix custom link URL encoding (#1112)
|
||||
- Fix potential logging dead loop (#1101)
|
||||
- Fix drag-to-tray upload error (#1107)
|
||||
- Fix filename encoding (#1121)
|
||||
- Prevent errors on GitHub uploads with duplicate filenames
|
||||
- Fix beta.2 style issues
|
||||
- Fix slow filename display in the rename window (#1130)
|
||||
- Fix “open config file” actually opening the log file (#1163)
|
||||
- Fix plugin config dialog not reading/saving correctly
|
||||
- Fix inability to add new uploader configs (created as edit) (#1198, #1196)
|
||||
- Fix macOS context menu disappearing (thanks @muwoo, #1179)
|
||||
- Fix macOS tray window flashing after right-click (thanks @QThans, #1217)
|
||||
- Fix uploader config page not scrollable when many fields (#1237)
|
||||
- Fix Tencent COS URL encoding (#1265)
|
||||
- Fix plugin list search not working (#1297)
|
||||
- Fix clipboard filename losing “seconds” (#1293)
|
||||
- Fix macOS tray copy-link failing on click (#1280, #1210)
|
||||
- Fix auto-copy URL toggle not turning off (#1294, thanks @happy-game)
|
||||
- Fix Wayland clipboard image upload issue (#1261, thanks @happy-game)
|
||||
- Fix URL uploads with Chinese characters being encoded in filenames (#1339)
|
||||
|
||||
## Other
|
||||
- COS distribution paused due to malicious traffic charges.
|
||||
- Upgrade to Vue3.
|
||||
- Refactor parts of the code.
|
||||
- Refactor parts of the code (again).
|
||||
- Add more detailed hotkey logs (#1031).
|
||||
- Known issue: some button styles were incorrect (fixed in later betas).
|
||||
- Update README scoop installation section (thanks @wuhang2003).
|
||||
- COS distribution paused due to malicious traffic charges
|
||||
- Upgrade to Vue3
|
||||
- Refactor parts of the code
|
||||
- Refactor parts of the code (again)
|
||||
- Add more detailed hotkey logs (#1031)
|
||||
- Known issue: some button styles were incorrect (fixed in later betas)
|
||||
- Update README scoop installation section (thanks @wuhang2003)
|
||||
|
||||
----------
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
- 新增 上传界面展示当前图床使用的配置名
|
||||

|
||||
- 新增 相册页工具栏,目前内置 `批量修改图片 URL HOST 的功能` 。参考 #875
|
||||
注意:需要先选中指定的图片,然后会根据已选中的图片进行修改,你可以通过图床筛选功能只筛选出需要修改的图片。
|
||||
注意:需要先选中指定的图片,然后会根据已选中的图片进行修改,你可以通过图床筛选功能只筛选出需要修改的图片
|
||||
例如,你有一批图片都是 `https://www.a.com/...` 打头的 URL,你想把 `www.a.com` 批量修改成 `www.b.com` ,就可以用这个功能
|
||||

|
||||
- 新增 `启动模式`,可以设置启动的时候是否要打开窗口。全平台支持 `静默启动`(默认值) & `打开主窗口`,Windows 和 Linux 额外支持 `打开 Mini 窗口`。参考 #915
|
||||
@@ -102,11 +102,11 @@
|
||||
- 修复 日志写入可能存在死循环问题,参考 #1101
|
||||
- 修复 拖拽文件到顶部栏图标报错问题,参考 #1107
|
||||
- 修复 文件名 encode 问题。参考 #1121
|
||||
- 修复 GitHub 重名文件上传不再报错。
|
||||
- 修复 GitHub 重名文件上传不再报错
|
||||
- 修复 beta.2 版本部分样式问题
|
||||
- 修复 重命名窗口某些情况下显示文件名过慢的问题,参考 #1130
|
||||
- 修复 打开配置文件打开的是日志文件的 bug,参考 #1163
|
||||
- 修复 插件配置弹窗打开后无法正确读取和保存配置的问题。
|
||||
- 修复 插件配置弹窗打开后无法正确读取和保存配置的问题
|
||||
- 修复 无法新增图床配置的问题(新增变成了编辑)。参考 #1198,#1196
|
||||
- 修复 macOS 右键菜单消失问题。感谢 @muwoo 。参考 #1179
|
||||
- 修复 macOS 顶部栏窗口右键之后一闪而过的问题。 感谢 @QThans。 参考 #1217
|
||||
@@ -120,7 +120,7 @@
|
||||
- 修复 直接通过 URL 上传图片的时候,带有汉字的 URL 上传后,文件名被 encode 的问题。 参考 #1339
|
||||
|
||||
## Other
|
||||
- 由于 PicGo 存储的 COS 空间被恶意刷大量流量导致欠费,暂时停止 COS 渠道的 PicGo 分发。
|
||||
- 由于 PicGo 存储的 COS 空间被恶意刷大量流量导致欠费,暂时停止 COS 渠道的 PicGo 分发
|
||||
- 更新至 Vue3
|
||||
- 重构部分代码
|
||||
- 重构部分代码
|
||||
|
||||
+8
-8
@@ -2,23 +2,23 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Add `showMenubarIcon` setting to control the visibility of the macOS menu bar icon. See #1222 for details.
|
||||
- Add `showMenubarIcon` setting to control the visibility of the macOS menu bar icon. See #1222 for details
|
||||
|
||||
## Bug Fixes
|
||||
- Fix: An issue where the macOS menu bar window could not read images from the clipboard. See https://github.com/Molunerfinn/PicGo/issues/1310 for details.
|
||||
- Fix: An issue where the macOS Intel build could not be opened. Please take a look at #1363 and #1310 for details.
|
||||
- Fix: An issue where the macOS menu bar window could not read images from the clipboard. See https://github.com/Molunerfinn/PicGo/issues/1310 for details
|
||||
- Fix: An issue where the macOS Intel build could not be opened. Please take a look at #1363 and #1310 for details
|
||||
|
||||
## Other
|
||||
- Upgraded Electron to v38 and migrated the underlying build framework to Electron-vite.
|
||||
- Unified the main window UI on Windows/Linux to match the macOS style.
|
||||
- Upgraded Electron to v38 and migrated the underlying build framework to Electron-vite
|
||||
- Unified the main window UI on Windows/Linux to match the macOS style
|
||||
<img width="821" height="472" alt="image" src="https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902" />
|
||||
- Updated ARM64 builds for Windows & Linux, and added Linux deb packages.
|
||||
- Updated ARM64 builds for Windows & Linux, and added Linux deb packages
|
||||
|
||||
----------
|
||||
|
||||
## Features
|
||||
|
||||
- 新增 `showMenubarIcon` 配置项,用于控制 macOS 顶部栏图标的显示与隐藏。参考 #1222 。
|
||||
- 新增 `showMenubarIcon` 配置项,用于控制 macOS 顶部栏图标的显示与隐藏。参考 #1222
|
||||
|
||||
## Bug Fixes
|
||||
- 修复 macOS 顶部栏窗口无法获取剪贴板图片的问题。参考 https://github.com/Molunerfinn/PicGo/issues/1310
|
||||
@@ -28,4 +28,4 @@
|
||||
- 更新 Electron 版本到 38 以及切换底层开发框架到 Electron-vite
|
||||
- 更新 Windows\Linux 系统现在主窗口的样式跟 macOS 一致了
|
||||
<img width="821" height="472" alt="image" src="https://github.com/user-attachments/assets/dbafadeb-7a9f-40e5-bd87-d4012c9a3902" />
|
||||
- 更新 Windows & Linux 平台的 ARM64 架构构建产物;新增 Linux `deb` 构建产物。
|
||||
- 更新 Windows & Linux 平台的 ARM64 架构构建产物;新增 Linux `deb` 构建产物
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# PicGo 2.4.2 Changelog
|
||||
|
||||
## Features
|
||||
- New: macOS version with signature && notarization. No more manual handling
|
||||
<img width="260" height="260" alt="image" src="https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399" />
|
||||
- Add duplicate button for image host config item. And add a double confirm dialog for `duplicate` and `delete` config item action
|
||||
<img width="800" height="450" alt="image" src="https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02" />
|
||||
- Add: Refactor notifications and add `notificationSound` setting (#1370)
|
||||
|
||||
## Bug Fixes
|
||||
- Fix: An issue that the app icon will be too big under macOS 26. See #1367 for more details
|
||||
- Fix: Clamp tray image titles to two lines or the style will be broken
|
||||
|
||||
## Other
|
||||
- Refactor: Remove config store (#1371)
|
||||
|
||||
----------
|
||||
|
||||
## Features
|
||||
- 新增:macOS 版本签名+公证,现在不再需要手动放行,可以直接下载安装使用
|
||||
<img width="260" height="260" alt="image" src="https://github.com/user-attachments/assets/dc1b7114-1f10-40e3-b45e-e3b330712399" />
|
||||
- 新增:图床配置界面新增复制按钮,可以一键复制已有配置,方便进行多样化操作。同时复制和删除均增加二次确认弹窗
|
||||
<img width="800" height="450" alt="image" src="https://github.com/user-attachments/assets/238881e3-e993-4cb7-9699-fd2f7441ce02" />
|
||||
- 新增:重构通知并新增 `notificationSound` 设置(#1370)
|
||||
|
||||
## Bug Fixes
|
||||
- 修复:在 macOS 26 版本下 app 图标过大的问题。参考 #1367
|
||||
- 修复:托盘图片标题最多显示两行否则样式会被破坏的问题
|
||||
|
||||
## Other
|
||||
- 重构:移除 config store(#1371)
|
||||
+37
-35
@@ -1,48 +1,50 @@
|
||||
# gen_changelog.md
|
||||
|
||||
This guide describes how to generate a consolidated changelog for any PicGo release series (e.g., 2.4.0, 2.5.0) from the GitHub release notes of its betas and finals.
|
||||
This guide describes how to generate a consolidated changelog for any PicGo release series (e.g., 2.4.0, 2.5.0) from the GitHub release notes of its betas and finals
|
||||
|
||||
## Source of truth
|
||||
- Use GitHub release pages only (e.g., `https://github.com/Molunerfinn/PicGo/releases/tag/vX.Y.Z-beta.N` and `vX.Y.Z` if present). You can fetch these via MCP GitHub APIs (e.g., `get_release_by_tag`, `list_releases`) instead of manual browsing.
|
||||
- Do **not** use `CHANGELOG.md` in the repo.
|
||||
- Preserve all text and images exactly as written in the releases.
|
||||
- Use GitHub release pages only (e.g., `https://github.com/Molunerfinn/PicGo/releases/tag/vX.Y.Z-beta.N` and `vX.Y.Z` if present). You can fetch these via MCP GitHub APIs (e.g., `get_release_by_tag`, `list_releases`) instead of manual browsing
|
||||
- Do **not** use `CHANGELOG.md` in the repo
|
||||
- Preserve all text and images exactly as written in the releases
|
||||
- Exception: Remove holiday greetings (e.g., "Happy New Year!", "新年快乐!") from the generated changelog. These can stay in GitHub release notes but should not appear in `changelog/X.Y.Z.md`
|
||||
|
||||
## Structure
|
||||
- Target file: `changelog/X.Y.Z.md` (replace with the series version).
|
||||
- Three top-level sections: `## Features`, `## Bug Fixes`, `## Other`.
|
||||
- Do **not** nest content under individual beta version headers; merge all items into these sections.
|
||||
- Keep items in chronological order (early → late) within each section, mirroring the beta sequence.
|
||||
- Keep inline issue references, thanks, and notes as-is.
|
||||
- Remove download-link sections entirely.
|
||||
- After the English sections, insert a line with `----------`, then add a full Chinese translation with the same structure/content (including images).
|
||||
- Target file: `changelog/X.Y.Z.md` (replace with the series version)
|
||||
- Three top-level sections: `## Features`, `## Bug Fixes`, `## Other`
|
||||
- Do **not** nest content under individual beta version headers; merge all items into these sections
|
||||
- Keep items in chronological order (early → late) within each section, mirroring the beta sequence
|
||||
- Keep inline issue references, thanks, and notes as-is
|
||||
- Remove download-link sections entirely
|
||||
- After the English sections, insert a line with `----------`, then add a full Chinese translation with the same structure/content (including images)
|
||||
|
||||
## Formatting rules
|
||||
- Markdown, plain ASCII.
|
||||
- Bullet lists only; no numbered lists required.
|
||||
- Indent images under their bullet with two spaces to keep association clear.
|
||||
- Keep inline HTML image tags from the releases (e.g., `<img width="...">`) untouched.
|
||||
- Keep line breaks and multi-line notes intact.
|
||||
- Do not reword or summarize; copy content verbatim except for removing beta headers and download sections.
|
||||
- Chinese translation must mirror the English bullets in order and content (keep images alongside the translated bullets).
|
||||
- Markdown, plain ASCII
|
||||
- Bullet lists only; no numbered lists required
|
||||
- Indent images under their bullet with two spaces to keep association clear
|
||||
- Keep inline HTML image tags from the releases (e.g., `<img width="...">`) untouched
|
||||
- Keep line breaks and multi-line notes intact
|
||||
- Do not reword or summarize; copy content verbatim except for removing beta headers and download sections
|
||||
- If you need to add items from git commit logs (for the same release series), do not paste raw commit messages. Normalize them to the existing changelog style (e.g., use `Add:`, `Fix:`, `Refactor:` prefixes without emoji)
|
||||
- Chinese translation must mirror the English bullets in order and content (keep images alongside the translated bullets)
|
||||
|
||||
## Regeneration steps
|
||||
1) Collect all release bodies for the target series (e.g., `vX.Y.Z-beta.0…N` and, if present, `vX.Y.Z`) from GitHub releases.
|
||||
1) Collect all release bodies for the target series (e.g., `vX.Y.Z-beta.0…N` and, if present, `vX.Y.Z`) from GitHub releases
|
||||
2) For each release, copy bullets and images into the appropriate section:
|
||||
- Features ↔ “Feature(s)” or “Features” blocks.
|
||||
- Bug Fixes ↔ “Bug Fixes” blocks.
|
||||
- Other ↔ “Other”, “Notice”, or misc notes that are not features/bugs.
|
||||
3) Omit “国内可下载链接” (or any download links) entirely.
|
||||
4) Drop beta subheadings; keep only section-level bullets in chronological order.
|
||||
5) Ensure images remain adjacent to their bullets with indentation.
|
||||
6) After the English sections are complete, insert `----------` on its own line.
|
||||
7) Append the Chinese translation, preserving bullet order and images, under `## Features`, `## Bug Fixes`, `## Other` again (same section titles, just Chinese content; no “(Chinese)” suffix).
|
||||
8) Save the result to `changelog/X.Y.Z.md`.
|
||||
- Features ↔ “Feature(s)” or “Features” blocks
|
||||
- Bug Fixes ↔ “Bug Fixes” blocks
|
||||
- Other ↔ “Other”, “Notice”, or misc notes that are not features/bugs
|
||||
3) Omit “国内可下载链接” (or any download links) entirely
|
||||
4) Drop beta subheadings; keep only section-level bullets in chronological order
|
||||
5) Ensure images remain adjacent to their bullets with indentation
|
||||
6) After the English sections are complete, insert `----------` on its own line
|
||||
7) Append the Chinese translation, preserving bullet order and images, under `## Features`, `## Bug Fixes`, `## Other` again (same section titles, just Chinese content; no “(Chinese)” suffix)
|
||||
8) Save the result to `changelog/X.Y.Z.md`
|
||||
|
||||
## Quick checklist
|
||||
- [ ] All features present with images kept.
|
||||
- [ ] All bug fixes present.
|
||||
- [ ] All “Other” notes present.
|
||||
- [ ] No beta headers.
|
||||
- [ ] No download links.
|
||||
- [ ] Chronological ordering preserved.
|
||||
- [ ] Chinese translation present with matching bullets/images after `----------`.
|
||||
- [ ] All features present with images kept
|
||||
- [ ] All bug fixes present
|
||||
- [ ] All “Other” notes present
|
||||
- [ ] No beta headers
|
||||
- [ ] No download links
|
||||
- [ ] Chronological ordering preserved
|
||||
- [ ] Chinese translation present with matching bullets/images after `----------`
|
||||
|
||||
+7
-2
@@ -6,6 +6,7 @@ const tsParser = require('@typescript-eslint/parser')
|
||||
const importPlugin = require('eslint-plugin-import')
|
||||
const promisePlugin = require('eslint-plugin-promise')
|
||||
const vuePlugin = require('eslint-plugin-vue')
|
||||
const stylistic = require('@stylistic/eslint-plugin')
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const vueConfigs = vuePlugin.configs['flat/recommended'].map(config => ({
|
||||
@@ -76,7 +77,8 @@ module.exports = [
|
||||
},
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
promise: promisePlugin
|
||||
promise: promisePlugin,
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
@@ -98,7 +100,10 @@ module.exports = [
|
||||
'no-debugger': isProduction ? 'error' : 'off',
|
||||
'no-async-promise-executor': 'off',
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'no-unused-vars': 'off'
|
||||
'no-unused-vars': 'off',
|
||||
'@stylistic/indent': ['error', 2],
|
||||
'@stylistic/semi': ['error', 'never'],
|
||||
'no-unexpected-multiline': 'error'
|
||||
}
|
||||
},
|
||||
...vueConfigs,
|
||||
|
||||
+12
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picgo",
|
||||
"version": "2.4.2-beta.0",
|
||||
"version": "2.4.3",
|
||||
"private": true,
|
||||
"main": "dist_electron/main/index.js",
|
||||
"description": "A powerful & simple image uploader for creators.",
|
||||
@@ -14,7 +14,7 @@
|
||||
"build:mac": "npm run build && electron-builder --config electron-builder.config.ts --mac --publish never",
|
||||
"build:linux": "npm run build && electron-builder --config electron-builder.config.ts --linux --publish never",
|
||||
"build:local": "dotenv -e .env -- electron-vite build && electron-builder --config electron-builder.config.ts --publish never",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src/",
|
||||
"lint": "pnpm lint:dpdm && eslint --ext .js,.jsx,.ts,.tsx,.vue src/",
|
||||
"tsc": "tsc --noEmit",
|
||||
"bump": "bump-version",
|
||||
"cz": "git-cz",
|
||||
@@ -25,7 +25,11 @@
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps",
|
||||
"upload-dist": "node ./scripts/upload-dist.js",
|
||||
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts"
|
||||
"lint:dpdm": "dpdm -T --tsconfig ./tsconfig.json --no-tree --no-warning --exit-code circular:1 src/background.ts",
|
||||
"check": "pnpm run tsc && pnpm run lint",
|
||||
"test": "vitest run src/__tests__",
|
||||
"prepare": "husky",
|
||||
"commitlint": "commitlint --edit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
@@ -49,7 +53,7 @@
|
||||
"mime-types": "^3.0.2",
|
||||
"mitt": "^3.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"picgo": "^1.6.4",
|
||||
"picgo": "^1.8.1",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"semver": "^7.7.3",
|
||||
"shell-path": "2.1.0",
|
||||
@@ -68,6 +72,7 @@
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@molunerfinn/vite-plugin-electron-renderer": "^0.14.7",
|
||||
"@picgo/bump-version": "^2.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.6.1",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
@@ -100,13 +105,14 @@
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"husky": "^3.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"postcss": "^8.4.23",
|
||||
"stylus": "^0.54.7",
|
||||
"stylus-loader": "^3.0.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6"
|
||||
"vite": "^7.2.6",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
|
||||
Generated
+283
-968
File diff suppressed because it is too large
Load Diff
+60
-6
@@ -68,6 +68,7 @@ SETTINGS_SET_LOG_FILE: Set Log File
|
||||
SETTINGS_CLICK_TO_SET: Click to Set
|
||||
SETTINGS_CLICK_TO_CHECK: Click to Check
|
||||
SETTINGS_SET_SHORTCUT: Set Shortcut
|
||||
SETTINGS_URL_REWRITE: URL Rewrite
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: Custom Link Format
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: Set Proxy and Mirror
|
||||
SETTINGS_SET_SERVER: Set Server
|
||||
@@ -80,6 +81,7 @@ SETTINGS_LAUNCH_ON_BOOT: Launch On Boot
|
||||
SETTINGS_RENAME_BEFORE_UPLOAD: Rename Before Upload
|
||||
SETTINGS_TIMESTAMP_RENAME: Timestamp Rename
|
||||
SETTINGS_OPEN_UPLOAD_TIPS: Open Upload Tips
|
||||
SETTINGS_NOTIFICATION_SOUND: Play Notification Sound
|
||||
SETTINGS_MINI_WINDOW_ON_TOP: Mini Window On Top
|
||||
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: Auto Copy URL After Upload
|
||||
SETTINGS_TIPS_PLACEHOLDER_URL: Use $url to represent url position
|
||||
@@ -117,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: Use Builtin Clipboard to Upload
|
||||
SETTINGS_CHOOSE_LANGUAGE: Choose Language
|
||||
UPLOADER_CONFIG_NAME: Configuration Name
|
||||
BUILTIN_CLIPBOARD_TIPS: Use builtin clipboard function to upload instead of using scripts
|
||||
|
||||
# url rewrite
|
||||
|
||||
URL_REWRITE_HELP: Rewrites uploaded image URLs. Rules are evaluated in order; the first matched rule wins.
|
||||
URL_REWRITE_ADD_RULE: Add Rule
|
||||
URL_REWRITE_EDIT_RULE: Edit Rule
|
||||
URL_REWRITE_EMPTY: No rules
|
||||
URL_REWRITE_ORDER: Order
|
||||
URL_REWRITE_MATCH: Match
|
||||
URL_REWRITE_REPLACE: Replace
|
||||
URL_REWRITE_FLAGS: Flags
|
||||
URL_REWRITE_ENABLED: Enabled
|
||||
URL_REWRITE_ACTIONS: Actions
|
||||
URL_REWRITE_MOVE_UP: Up
|
||||
URL_REWRITE_MOVE_DOWN: Down
|
||||
URL_REWRITE_EDIT: Edit
|
||||
URL_REWRITE_DELETE: Delete
|
||||
URL_REWRITE_DELETE_CONFIRM: Delete this rule?
|
||||
URL_REWRITE_MATCH_TIPS: Supports regex (JavaScript RegExp)
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
|
||||
URL_REWRITE_REPLACE_TIPS: Replacement string (supports $1, $2, ...)
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
|
||||
URL_REWRITE_OPTIONS: Options
|
||||
URL_REWRITE_RULE_ENABLED: Enable this rule
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: Global (g)
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: Replace all occurrences, not just the first one
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: Ignore case (i)
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: Case-insensitive matching (e.g. JPG equals jpg)
|
||||
URL_REWRITE_MATCH_REQUIRED: Match is required
|
||||
URL_REWRITE_REPLACE_REQUIRED: Replace is required
|
||||
URL_REWRITE_INVALID_REGEX: Invalid regex
|
||||
URL_REWRITE_PREVIEW_TITLE: Preview
|
||||
URL_REWRITE_PREVIEW_TIPS: Enter a URL to see how the current rules rewrite it (matched in order; only the first match is applied)
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
|
||||
URL_REWRITE_PREVIEW_RUN: Preview
|
||||
URL_REWRITE_PREVIEW_OUTPUT: Output URL
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: Please enter a URL to preview
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: Invalid rule
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: Matched rule
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: No rules matched
|
||||
UPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name
|
||||
SELECTED_SETTING_HINT: Selected
|
||||
SETTINGS_ENCODE_OUTPUT_URL: Encode Output(or Copyed) URL
|
||||
@@ -143,11 +185,20 @@ SHORTCUT_EDIT: Edit
|
||||
SHORTCUT_CHANGE_UPLOAD: Change Upload Shortcut
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: Change Image URL HOST
|
||||
NEW_IMAGE_URL_HOST: New Image URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: Selected Image URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: The result of changing image URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_WARN: You must select at least one picture first
|
||||
GALLERY_URL_REWRITE_TITLE: Rewrite Selected Image URLs
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: Rewrite Image URL Result
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: You must select at least one picture first
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: Apply global URL rewrite rules
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: Global rules
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: Optional. Leave empty to skip the temporary rule. Temporary rule has higher priority than global rules.
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: Match and Replace are required for the temporary rule
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: No global rules enabled and no temporary rule provided
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: Save the temporary rule to global URL rewrite rules?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: Apply and Save
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: Apply Only
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: No URLs were changed
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: The rewrite result is empty; skipped
|
||||
|
||||
# tray-page
|
||||
|
||||
@@ -164,7 +215,7 @@ CUSTOM: Custom
|
||||
CLIPBOARD_PICTURE: Clipboard
|
||||
TIPS_DRAG_VALID_PICTURE_OR_URL: Drag valid picture or url to here
|
||||
TIPS_INPUT_URL: Input URL
|
||||
TIPS_HTTP_PREFIX: http:// or https://
|
||||
TIPS_HTTP_PREFIX: Starts with http:// or https://. Multiple URLs supported (one per line)
|
||||
TIPS_INPUT_VALID_URL: Input valid URL
|
||||
|
||||
# plugins
|
||||
@@ -236,6 +287,9 @@ TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: "Please create the folder yourself
|
||||
TIPS_NOTICE: Tips
|
||||
TIPS_WARNING: Warning
|
||||
TIPS_ERROR: Error
|
||||
TIPS_SKIPPED_INVALID_URLS: Skipped ${n} invalid URL(s), see logs for details
|
||||
TIPS_TOO_MANY_URLS_CONFIRM: You are about to upload ${n} URLs at once. This may cause lag. It's recommended to upload in batches. Continue?
|
||||
TIPS_NO_VALID_URLS: No valid URL found
|
||||
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: Please install Node.js and restart PicGo to continue
|
||||
TIPS_PLUGIN_REMOVE_GALLERY_ITEM: Plugin is trying to remove some images from the album gallery, continue?
|
||||
TIPS_PLUGIN_OVERWRITE_GALLERY: Plugin is trying to overwrite the album gallery, continue?
|
||||
|
||||
+60
-6
@@ -68,6 +68,7 @@ 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: 设置Server
|
||||
@@ -80,6 +81,7 @@ SETTINGS_LAUNCH_ON_BOOT: 开机自启
|
||||
SETTINGS_RENAME_BEFORE_UPLOAD: 上传前重命名
|
||||
SETTINGS_TIMESTAMP_RENAME: 时间戳重命名
|
||||
SETTINGS_OPEN_UPLOAD_TIPS: 开启上传提示
|
||||
SETTINGS_NOTIFICATION_SOUND: 开启通知提示音
|
||||
SETTINGS_MINI_WINDOW_ON_TOP: Mini窗口置顶
|
||||
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上传后自动复制URL
|
||||
SETTINGS_TIPS_PLACEHOLDER_URL: 用占位符 $url 来表示url的位置
|
||||
@@ -117,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用内置剪贴板上传
|
||||
SETTINGS_CHOOSE_LANGUAGE: 选择语言
|
||||
BUILTIN_CLIPBOARD_TIPS: 使用内置剪贴板函数而不是调用脚本获取剪贴板图片
|
||||
UPLOADER_CONFIG_NAME: 图床配置名
|
||||
|
||||
# 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 时进行转义
|
||||
@@ -143,11 +185,20 @@ SHORTCUT_EDIT: 编辑
|
||||
SHORTCUT_CHANGE_UPLOAD: 修改上传快捷键
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: 修改图片 URL HOST
|
||||
NEW_IMAGE_URL_HOST: 新的图片 URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: 已选中的图片 URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: 修改图片 URL HOST 结果
|
||||
CHANGE_IMAGE_URL_HOST_WARN: 你必须先选中至少一张图片
|
||||
GALLERY_URL_REWRITE_TITLE: 重写选中图片 URL
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: 重写图片 URL 结果
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必须先选中至少一张图片
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 应用全局 URL 重写规则
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全局规则数量
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可选,留空则不使用临时规则(临时规则优先级高于全局规则)。
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 临时规则需要同时填写「匹配」和「替换」
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 没有可用的全局规则,且未填写临时规则
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否将临时规则写入全局 URL 重写规则列表?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: 应用并写入
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: 仅应用
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: 没有任何 URL 被修改
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重写结果为空,已跳过
|
||||
|
||||
# tray-page
|
||||
|
||||
@@ -164,7 +215,7 @@ CUSTOM: 自定义
|
||||
CLIPBOARD_PICTURE: 剪贴板图片
|
||||
TIPS_DRAG_VALID_PICTURE_OR_URL: 请拖入合法的图片文件或者图片URL地址
|
||||
TIPS_INPUT_URL: 请输入URL
|
||||
TIPS_HTTP_PREFIX: http://或者https://开头
|
||||
TIPS_HTTP_PREFIX: http:// 或者 https:// 开头,支持上传多条 URL(请换行输入)
|
||||
TIPS_INPUT_VALID_URL: 请输入合法的URL
|
||||
|
||||
# plugins
|
||||
@@ -236,6 +287,9 @@ TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 请自行创建文件夹:${path}
|
||||
TIPS_NOTICE: 注意
|
||||
TIPS_WARNING: 警告
|
||||
TIPS_ERROR: 发生错误
|
||||
TIPS_SKIPPED_INVALID_URLS: 已跳过 ${n} 条非法 URL,请查看日志了解详情
|
||||
TIPS_TOO_MANY_URLS_CONFIRM: 你将一次上传 ${n} 条 URL,可能会引起卡顿,建议分批上传。是否继续?
|
||||
TIPS_NO_VALID_URLS: 未检测到合法的 URL
|
||||
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 请安装Node.js并重启PicGo再继续操作
|
||||
TIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在试图删除一些相册图片,是否继续
|
||||
TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在试图覆盖相册列表,是否继续
|
||||
|
||||
+60
-6
@@ -68,6 +68,7 @@ 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: 設定PROXY和鏡像地址
|
||||
SETTINGS_SET_SERVER: 設定Server
|
||||
@@ -80,6 +81,7 @@ SETTINGS_LAUNCH_ON_BOOT: 開機時啟動
|
||||
SETTINGS_RENAME_BEFORE_UPLOAD: 上傳前重新命名
|
||||
SETTINGS_TIMESTAMP_RENAME: 以時間戳命名
|
||||
SETTINGS_OPEN_UPLOAD_TIPS: 開啟上傳提示
|
||||
SETTINGS_NOTIFICATION_SOUND: 開啟通知提示音
|
||||
SETTINGS_MINI_WINDOW_ON_TOP: Mini視窗置頂
|
||||
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: 上傳後自動複製URL
|
||||
SETTINGS_TIPS_PLACEHOLDER_URL: 用佔位符 $url 來表示URL的位置
|
||||
@@ -117,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用內建剪貼簿上傳
|
||||
SETTINGS_CHOOSE_LANGUAGE: 選擇語言
|
||||
BUILTIN_CLIPBOARD_TIPS: 使用內建剪貼簿函數而不是調用腳本取得剪貼簿內的照片
|
||||
UPLOADER_CONFIG_NAME: 圖床配置名
|
||||
|
||||
# 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 時進行轉義
|
||||
@@ -143,11 +185,20 @@ SHORTCUT_EDIT: 編輯
|
||||
SHORTCUT_CHANGE_UPLOAD: 修改上傳快捷鍵
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: 修改圖片 URL HOST
|
||||
NEW_IMAGE_URL_HOST: 新的圖片 URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: 已選中的圖片 URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: 修改圖片 URL HOST 結果
|
||||
CHANGE_IMAGE_URL_HOST_WARN: 你必須先選中至少一張圖片
|
||||
GALLERY_URL_REWRITE_TITLE: 重寫選中圖片 URL
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: 重寫圖片 URL 結果
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必須先選中至少一張圖片
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 套用全域 URL 重寫規則
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全域規則數量
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可選,留空則不使用臨時規則(臨時規則優先級高於全域規則)。
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 臨時規則需要同時填寫「匹配」和「替換」
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 沒有可用的全域規則,且未填寫臨時規則
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否將臨時規則寫入全域 URL 重寫規則列表?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: 套用並寫入
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: 僅套用
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: 沒有任何 URL 被修改
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重寫結果為空,已跳過
|
||||
|
||||
# tray-page
|
||||
|
||||
@@ -164,7 +215,7 @@ CUSTOM: 自訂
|
||||
CLIPBOARD_PICTURE: 剪貼簿圖片
|
||||
TIPS_DRAG_VALID_PICTURE_OR_URL: 請拖入合法的圖片檔案或者圖片URL地址
|
||||
TIPS_INPUT_URL: 請輸入URL
|
||||
TIPS_HTTP_PREFIX: http://或者https://開頭
|
||||
TIPS_HTTP_PREFIX: http:// 或者 https:// 開頭,支援上傳多條 URL(請換行輸入)
|
||||
TIPS_INPUT_VALID_URL: 請輸入合法的URL
|
||||
|
||||
# plugins
|
||||
@@ -236,6 +287,9 @@ TOOLBOX_CHECK_CLIPBOARD_FILE_PATH_ERROR_TIPS: 請自行創建文件夾:${path}
|
||||
TIPS_NOTICE: 注意
|
||||
TIPS_WARNING: 警告
|
||||
TIPS_ERROR: 發生錯誤
|
||||
TIPS_SKIPPED_INVALID_URLS: 已跳過 ${n} 條非法 URL,請查看日誌了解詳情
|
||||
TIPS_TOO_MANY_URLS_CONFIRM: 你將一次上傳 ${n} 條 URL,可能會引起卡頓,建議分批上傳。是否繼續?
|
||||
TIPS_NO_VALID_URLS: 未偵測到合法的 URL
|
||||
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: 請安裝Node.js並重新啟動PicGo再繼續操作
|
||||
TIPS_PLUGIN_REMOVE_GALLERY_ITEM: 有插件正在試圖刪除一些相簿圖片,是否繼續?
|
||||
TIPS_PLUGIN_OVERWRITE_GALLERY: 有插件正在試圖覆蓋相簿列表,是否繼續?
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { extractHttpUrlsFromText, parseNewlineSeparatedUrls } from '../../../universal/utils/common'
|
||||
|
||||
describe('universal/utils/common', () => {
|
||||
describe('parseNewlineSeparatedUrls', () => {
|
||||
it('parses newline-separated urls, trims lines, ignores empty lines, and de-duplicates', () => {
|
||||
const input = [
|
||||
'https://a.example/1.png',
|
||||
'',
|
||||
' https://a.example/2.png ',
|
||||
'https://a.example/1.png',
|
||||
'not-a-url'
|
||||
].join('\n')
|
||||
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(input)
|
||||
|
||||
expect(urls).toEqual([
|
||||
'https://a.example/1.png',
|
||||
'https://a.example/2.png'
|
||||
])
|
||||
expect(invalidLines).toEqual(['not-a-url'])
|
||||
})
|
||||
|
||||
it('ignores uri-list comment lines when source is uri-list', () => {
|
||||
const input = [
|
||||
'# comment',
|
||||
'https://a.example/1.png',
|
||||
'# another comment',
|
||||
'https://a.example/2.png'
|
||||
].join('\n')
|
||||
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(input, { source: 'uri-list' })
|
||||
|
||||
expect(urls).toEqual([
|
||||
'https://a.example/1.png',
|
||||
'https://a.example/2.png'
|
||||
])
|
||||
expect(invalidLines).toEqual([])
|
||||
})
|
||||
|
||||
it('supports NUL-separated drag payloads by normalizing to newlines', () => {
|
||||
const input = `https://a.example/1.png\u0000https://a.example/2.png`
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(input)
|
||||
|
||||
expect(urls).toEqual([
|
||||
'https://a.example/1.png',
|
||||
'https://a.example/2.png'
|
||||
])
|
||||
expect(invalidLines).toEqual([])
|
||||
})
|
||||
|
||||
it('splits concatenated urls in a single line as a fallback', () => {
|
||||
const input = 'https://a.example/1.pnghttps://b.example/2.webphttps://c.example/3.png'
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(input)
|
||||
|
||||
expect(urls).toEqual([
|
||||
'https://a.example/1.png',
|
||||
'https://b.example/2.webp',
|
||||
'https://c.example/3.png'
|
||||
])
|
||||
expect(invalidLines).toEqual([])
|
||||
})
|
||||
|
||||
it('does not split embedded urls in query parameters', () => {
|
||||
const input = 'https://example.com/?url=https://a.example/1.png'
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(input)
|
||||
|
||||
expect(urls).toEqual(['https://example.com/?url=https://a.example/1.png'])
|
||||
expect(invalidLines).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractHttpUrlsFromText', () => {
|
||||
it('extracts urls from mixed text, strips trailing punctuation, and de-duplicates', () => {
|
||||
const input = [
|
||||
'hello',
|
||||
'https://a.example/1.png)',
|
||||
'https://b.example/2.webp]',
|
||||
'https://a.example/1.png'
|
||||
].join(' ')
|
||||
|
||||
expect(extractHttpUrlsFromText(input)).toEqual([
|
||||
'https://a.example/1.png',
|
||||
'https://b.example/2.webp'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns empty array when no urls are present', () => {
|
||||
expect(extractHttpUrlsFromText('no urls here')).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -116,7 +116,7 @@ class RemoteNoticeHandler {
|
||||
body: action.data?.content || '',
|
||||
clickToCopy: !!action.data?.copyToClipboard,
|
||||
copyContent: action.data?.copyToClipboard || '',
|
||||
clickFn () {
|
||||
callback () {
|
||||
if (action.data?.url) {
|
||||
shell.openExternal(action.data.url)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from 'electron'
|
||||
import logger from '@core/picgo/logger'
|
||||
import GuiApi from '../../gui'
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
|
||||
import shortKeyService from './shortKeyService'
|
||||
import picgo from '@core/picgo'
|
||||
@@ -25,7 +24,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
|
||||
private initBuiltInShortKey () {
|
||||
const commands = db.get('settings.shortKey') as IShortKeyConfigs
|
||||
const commands = picgo.getConfig<IShortKeyConfigs>('settings.shortKey') || {}
|
||||
Object.keys(commands)
|
||||
.filter(item => item.includes('picgo:'))
|
||||
.forEach(command => {
|
||||
@@ -57,8 +56,8 @@ class ShortKeyHandler {
|
||||
const commands = plugin.commands(picgo) as IPluginShortKeyConfig[]
|
||||
for (const cmd of commands) {
|
||||
const command = `${item}:${cmd.name}`
|
||||
if (db.has(`settings.shortKey[${command}]`)) {
|
||||
const commandConfig = db.get(`settings.shortKey.${command}`) as IShortKeyConfig
|
||||
const commandConfig = picgo.getConfig<IShortKeyConfig | undefined>(`settings.shortKey.${command}`)
|
||||
if (commandConfig) {
|
||||
// if disabled, don't register #534
|
||||
if (commandConfig.enable) {
|
||||
this.registerShortKey(commandConfig, command, cmd.handle, false)
|
||||
@@ -168,8 +167,8 @@ class ShortKeyHandler {
|
||||
const commands = plugin.commands(picgo) as IPluginShortKeyConfig[]
|
||||
for (const cmd of commands) {
|
||||
const command = `${pluginName}:${cmd.name}`
|
||||
if (db.has(`settings.shortKey[${command}]`)) {
|
||||
const commandConfig = db.get(`settings.shortKey[${command}]`) as IShortKeyConfig
|
||||
const commandConfig = picgo.getConfig<IShortKeyConfig | undefined>(`settings.shortKey.${command}`)
|
||||
if (commandConfig) {
|
||||
this.registerShortKey(commandConfig, command, cmd.handle, false)
|
||||
} else {
|
||||
this.registerShortKey(cmd, command, cmd.handle, true)
|
||||
@@ -179,7 +178,7 @@ class ShortKeyHandler {
|
||||
}
|
||||
|
||||
unregisterPluginShortKey (pluginName: string) {
|
||||
const commands = db.get('settings.shortKey') as IShortKeyConfigs
|
||||
const commands = picgo.getConfig<IShortKeyConfigs>('settings.shortKey') || {}
|
||||
const keyList = Object.keys(commands)
|
||||
.filter(command => command.includes(pluginName))
|
||||
.map(command => {
|
||||
@@ -191,7 +190,7 @@ class ShortKeyHandler {
|
||||
keyList.forEach(item => {
|
||||
globalShortcut.unregister(item.key)
|
||||
shortKeyService.unregisterCommand(item.command)
|
||||
db.unset('settings.shortKey', item.command)
|
||||
picgo.removeConfig('settings.shortKey', item.command)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
Tray,
|
||||
dialog,
|
||||
clipboard,
|
||||
Notification,
|
||||
nativeTheme
|
||||
} from 'electron'
|
||||
import uploader from 'apis/app/uploader'
|
||||
import db, { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { IWindowList } from '#/types/enum'
|
||||
import { IPasteStyle, IWindowList } from '#/types/enum'
|
||||
import pasteTemplate from '~/main/utils/pasteTemplate'
|
||||
import pkg from 'root/package.json'
|
||||
import { ensureFilePath, getClipboardFilePathList, handleCopyUrl } from '~/main/utils/common'
|
||||
import { ensureFilePath, getClipboardFilePathList, handleCopyUrl, showNotification } from '~/main/utils/common'
|
||||
import { privacyManager } from '~/main/utils/privacyManager'
|
||||
import { T } from '~/main/i18n'
|
||||
import { isMacOSVersionGreaterThanOrEqualTo } from '~/main/utils/getMacOSVersion'
|
||||
@@ -59,10 +59,10 @@ export function createContextMenu () {
|
||||
{
|
||||
label: T('OPEN_UPDATE_HELPER'),
|
||||
type: 'checkbox',
|
||||
checked: db.get('settings.showUpdateTip'),
|
||||
checked: picgo.getConfig<boolean>('settings.showUpdateTip') !== false,
|
||||
click () {
|
||||
const value = db.get('settings.showUpdateTip')
|
||||
db.set('settings.showUpdateTip', !value)
|
||||
const value = picgo.getConfig<boolean>('settings.showUpdateTip') !== false
|
||||
picgo.saveConfig({ 'settings.showUpdateTip': !value })
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -108,10 +108,10 @@ export function createContextMenu () {
|
||||
{
|
||||
label: T('OPEN_UPDATE_HELPER'),
|
||||
type: 'checkbox',
|
||||
checked: db.get('settings.showUpdateTip'),
|
||||
checked: picgo.getConfig<boolean>('settings.showUpdateTip') !== false,
|
||||
click () {
|
||||
const value = db.get('settings.showUpdateTip')
|
||||
db.set('settings.showUpdateTip', !value)
|
||||
const value = picgo.getConfig<boolean>('settings.showUpdateTip') !== false
|
||||
picgo.saveConfig({ 'settings.showUpdateTip': !value })
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -217,7 +217,7 @@ export function createTray () {
|
||||
// drop-files only be supported in macOS
|
||||
// so the tray window must be available
|
||||
tray.on('drop-files', async (event, files: string[]) => {
|
||||
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)!
|
||||
const imgs = await uploader
|
||||
.setWebContents(trayWindow.webContents)
|
||||
@@ -226,15 +226,14 @@ export function createTray () {
|
||||
const pasteText: string[] = []
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
pasteText.push(
|
||||
pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink'))
|
||||
pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink'))
|
||||
)
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl!
|
||||
// icon: files[i]
|
||||
})
|
||||
setTimeout(() => {
|
||||
notification.show()
|
||||
showNotification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl!
|
||||
// icon: files[i]
|
||||
})
|
||||
}, i * 100)
|
||||
await GalleryDB.getInstance().insert(imgs[i])
|
||||
}
|
||||
@@ -273,7 +272,7 @@ export function handleMenubarIcon (visible?: boolean) {
|
||||
}
|
||||
return
|
||||
}
|
||||
const shouldShow = visible !== undefined ? visible : (db.get('settings.showMenubarIcon') !== false)
|
||||
const shouldShow = visible !== undefined ? visible : (picgo.getConfig<boolean>('settings.showMenubarIcon') !== false)
|
||||
if (shouldShow) {
|
||||
if (!tray) {
|
||||
createTray()
|
||||
@@ -285,7 +284,7 @@ export function handleMenubarIcon (visible?: boolean) {
|
||||
|
||||
export function handleDockIcon () {
|
||||
if (isMacOS) {
|
||||
if (db.get('settings.showDockIcon') !== false) {
|
||||
if (picgo.getConfig<boolean>('settings.showDockIcon') !== false) {
|
||||
app.dock?.show()
|
||||
app.dock?.setMenu(createContextMenu())
|
||||
} else {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {
|
||||
Notification,
|
||||
WebContents
|
||||
} from 'electron'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { IRPCActionType, IWindowList } from '#/types/enum'
|
||||
import { IPasteStyle, IRPCActionType, IWindowList } from '#/types/enum'
|
||||
import uploader from '.'
|
||||
import pasteTemplate from '~/main/utils/pasteTemplate'
|
||||
import db, { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/main/utils/common'
|
||||
import { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import { handleCopyUrl, handleUrlEncodeWithSetting, showNotification } from '~/main/utils/common'
|
||||
import { T } from '~/main/i18n/index'
|
||||
import logger from '@core/picgo/logger'
|
||||
import picgo from '@core/picgo'
|
||||
// import dayjs from 'dayjs'
|
||||
|
||||
const handleClipboardUploading = async (): Promise<false | ImgInfo[]> => {
|
||||
const useBuiltinClipboard = !!db.get('settings.useBuiltinClipboard')
|
||||
const useBuiltinClipboard = !!picgo.getConfig<boolean>('settings.useBuiltinClipboard')
|
||||
const win = windowManager.getAvailableWindow()
|
||||
if (useBuiltinClipboard) {
|
||||
return await uploader.setWebContents(win!.webContents).uploadWithBuildInClipboard()
|
||||
@@ -27,15 +27,14 @@ export const uploadClipboardFiles = async (): Promise<string> => {
|
||||
if (img !== false) {
|
||||
if (img.length > 0) {
|
||||
const trayWindow = windowManager.get(IWindowList.TRAY_WINDOW)
|
||||
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
|
||||
handleCopyUrl(pasteTemplate(pasteStyle, img[0], db.get('settings.customLink')))
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: img[0].imgUrl!
|
||||
// icon: img[0].imgUrl
|
||||
})
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'
|
||||
handleCopyUrl(pasteTemplate(pasteStyle, img[0], picgo.getConfig<string>('settings.customLink')))
|
||||
setTimeout(() => {
|
||||
notification.show()
|
||||
showNotification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: img[0].imgUrl!
|
||||
// icon: img[0].imgUrl
|
||||
})
|
||||
}, 100)
|
||||
await GalleryDB.getInstance().insert(img[0])
|
||||
// trayWindow just be created in mac/windows, not in linux
|
||||
@@ -46,11 +45,10 @@ export const uploadClipboardFiles = async (): Promise<string> => {
|
||||
}
|
||||
return handleUrlEncodeWithSetting(img[0].imgUrl as string)
|
||||
} else {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('UPLOAD_FAILED'),
|
||||
body: T('TIPS_UPLOAD_NOT_PICTURES')
|
||||
})
|
||||
notification.show()
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
@@ -63,17 +61,16 @@ export const uploadSelectedFiles = async (webContents: WebContents, files: IFile
|
||||
const imgs = await uploader.setWebContents(webContents).upload(input)
|
||||
const result = []
|
||||
if (imgs !== false) {
|
||||
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'
|
||||
const pasteText: string[] = []
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
pasteText.push(pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink')))
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl!
|
||||
// icon: files[i].path
|
||||
})
|
||||
pasteText.push(pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink')))
|
||||
setTimeout(() => {
|
||||
notification.show()
|
||||
showNotification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl!
|
||||
// icon: files[i].path
|
||||
})
|
||||
}, i * 100)
|
||||
await GalleryDB.getInstance().insert(imgs[i])
|
||||
result.push(handleUrlEncodeWithSetting(imgs[i].imgUrl!))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Notification,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
WebContents,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
} from 'electron'
|
||||
import dayjs from 'dayjs'
|
||||
import picgo from '@core/picgo'
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { IWindowList } from '#/types/enum'
|
||||
import util from 'util'
|
||||
@@ -48,27 +46,26 @@ class Uploader {
|
||||
}
|
||||
|
||||
init () {
|
||||
picgo.on('notification', (message: Electron.NotificationConstructorOptions | undefined) => {
|
||||
const notification = new Notification(message)
|
||||
notification.show()
|
||||
picgo.on('notification', (message: IShowNotificationOption | undefined) => {
|
||||
if (!message) return
|
||||
showNotification(message)
|
||||
})
|
||||
|
||||
picgo.on('uploadProgress', (progress: any) => {
|
||||
this.webContents?.send('uploadProgress', progress)
|
||||
})
|
||||
picgo.on('beforeTransform', () => {
|
||||
if (db.get('settings.uploadNotification')) {
|
||||
const notification = new Notification({
|
||||
if (picgo.getConfig<boolean>('settings.uploadNotification')) {
|
||||
showNotification({
|
||||
title: T('UPLOAD_PROGRESS'),
|
||||
body: T('UPLOADING')
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
})
|
||||
picgo.helper.beforeUploadPlugins.register('renameFn', {
|
||||
handle: async (ctx: IPicGo) => {
|
||||
const rename = db.get('settings.rename')
|
||||
const autoRename = db.get('settings.autoRename')
|
||||
const rename = picgo.getConfig<boolean>('settings.rename')
|
||||
const autoRename = picgo.getConfig<boolean>('settings.autoRename')
|
||||
if (autoRename || rename) {
|
||||
await Promise.all(ctx.output.map(async (item, index) => {
|
||||
let name: undefined | string | null
|
||||
@@ -146,7 +143,7 @@ class Uploader {
|
||||
fromClipboard: !img,
|
||||
duration: Date.now() - startTime,
|
||||
outputList: output
|
||||
});
|
||||
})
|
||||
}
|
||||
return output.filter(item => item.imgUrl)
|
||||
} else {
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
import { IStartupMode, IWindowList } from '#/types/enum'
|
||||
import bus from '@core/bus'
|
||||
import { CREATE_APP_MENU } from '@core/bus/constants'
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
|
||||
import { app } from 'electron'
|
||||
import { T } from '~/main/i18n'
|
||||
import { isLinux } from '~/universal/utils/common'
|
||||
import { getStaticPath } from '#/utils/staticPath'
|
||||
import picgo from '@core/picgo'
|
||||
// import { URLSearchParams } from 'url'
|
||||
|
||||
const windowList = new Map<IWindowList, IWindowListItem>()
|
||||
@@ -37,7 +37,7 @@ const handleWindowParams = (windowURL: string) => {
|
||||
}
|
||||
|
||||
export const isWindowShouldShowOnStartup = (currentWindow: IWindowList) => {
|
||||
const startupMode = db.get('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
|
||||
const startupMode = picgo.getConfig<IStartupMode | undefined>('settings.startupMode') || (isLinux ? IStartupMode.SHOW_MINI_WINDOW : IStartupMode.HIDE)
|
||||
switch (currentWindow) {
|
||||
case IWindowList.MINI_WINDOW: {
|
||||
return startupMode === IStartupMode.SHOW_MINI_WINDOW
|
||||
@@ -82,7 +82,7 @@ windowList.set(IWindowList.SETTING_WINDOW, {
|
||||
isValid: true,
|
||||
multiple: false,
|
||||
options () {
|
||||
const showDockIcon = db.get('settings.showDockIcon') !== false
|
||||
const showDockIcon = picgo.getConfig<boolean>('settings.showDockIcon') !== false
|
||||
const options: IBrowserWindowOptions = {
|
||||
height: 450,
|
||||
width: 800,
|
||||
@@ -142,7 +142,7 @@ windowList.set(IWindowList.MINI_WINDOW, {
|
||||
}
|
||||
}
|
||||
|
||||
if (db.get('settings.miniWindowOnTop')) {
|
||||
if (picgo.getConfig<boolean>('settings.miniWindowOnTop')) {
|
||||
obj.alwaysOnTop = true
|
||||
}
|
||||
return obj
|
||||
|
||||
@@ -1,85 +1,14 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import { dbPathChecker, dbPathDir, getGalleryDBPath } from './dbChecker'
|
||||
import { DBStore, JSONStore } from '@picgo/store'
|
||||
import { T } from '~/main/i18n'
|
||||
import { DBStore } from '@picgo/store'
|
||||
import { getGalleryDBPath } from './dbChecker'
|
||||
|
||||
const STORE_PATH = dbPathDir()
|
||||
const DB_PATH: string = getGalleryDBPath().dbPath
|
||||
fs.ensureDirSync(path.dirname(DB_PATH))
|
||||
|
||||
if (!fs.pathExistsSync(STORE_PATH)) {
|
||||
fs.mkdirpSync(STORE_PATH)
|
||||
}
|
||||
const CONFIG_PATH: string = dbPathChecker()
|
||||
export const DB_PATH: string = getGalleryDBPath().dbPath
|
||||
|
||||
class ConfigStore {
|
||||
private db: JSONStore
|
||||
constructor () {
|
||||
this.db = new JSONStore(CONFIG_PATH)
|
||||
|
||||
if (!this.db.has('picBed')) {
|
||||
this.db.set('picBed', {
|
||||
current: 'smms', // deprecated
|
||||
uploader: 'smms',
|
||||
smms: {
|
||||
token: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!this.db.has('settings.shortKey')) {
|
||||
this.db.set('settings.shortKey[picgo:upload]', {
|
||||
enable: true,
|
||||
key: 'CommandOrControl+Shift+P',
|
||||
name: 'upload',
|
||||
label: T('QUICK_UPLOAD')
|
||||
})
|
||||
}
|
||||
this.read()
|
||||
}
|
||||
|
||||
flush () {
|
||||
this.db = new JSONStore(CONFIG_PATH)
|
||||
}
|
||||
|
||||
read () {
|
||||
this.db.read()
|
||||
return this.db
|
||||
}
|
||||
|
||||
get (key = ''): any {
|
||||
if (key === '') {
|
||||
return this.db.read()
|
||||
}
|
||||
return this.db.get(key)
|
||||
}
|
||||
|
||||
set (key: string, value: any): void {
|
||||
return this.db.set(key, value)
|
||||
}
|
||||
|
||||
has (key: string) {
|
||||
return this.db.has(key)
|
||||
}
|
||||
|
||||
unset (key: string, value: any): boolean {
|
||||
return this.db.unset(key, value)
|
||||
}
|
||||
|
||||
getConfigPath () {
|
||||
return CONFIG_PATH
|
||||
}
|
||||
}
|
||||
|
||||
const db = new ConfigStore()
|
||||
|
||||
export default db
|
||||
|
||||
// v2.3.0 add gallery db
|
||||
class GalleryDB {
|
||||
private static instance: DBStore
|
||||
private constructor () {
|
||||
console.log('init gallery db')
|
||||
}
|
||||
private constructor () {}
|
||||
|
||||
public static getInstance (): DBStore {
|
||||
if (!GalleryDB.instance) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { dbChecker, dbPathChecker } from 'apis/core/datastore/dbChecker'
|
||||
import pkg from 'root/package.json'
|
||||
import { PicGo } from 'picgo'
|
||||
import db from 'apis/core/datastore'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
const CONFIG_PATH = dbPathChecker()
|
||||
|
||||
@@ -17,18 +15,18 @@ picgo.saveConfig({
|
||||
global.PICGO_GUI_VERSION = pkg.version
|
||||
picgo.GUI_VERSION = global.PICGO_GUI_VERSION
|
||||
|
||||
const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
|
||||
// const originPicGoSaveConfig = picgo.saveConfig.bind(picgo)
|
||||
|
||||
function flushDB () {
|
||||
db.flush()
|
||||
}
|
||||
// function flushDB () {
|
||||
// db.flush()
|
||||
// }
|
||||
|
||||
const debounced = debounce(flushDB, 1000)
|
||||
// const debounced = debounce(flushDB, 1000)
|
||||
|
||||
picgo.saveConfig = (config: IStringKeyMap) => {
|
||||
originPicGoSaveConfig(config)
|
||||
// flush electron's db
|
||||
debounced()
|
||||
}
|
||||
// picgo.saveConfig = (config: IStringKeyMap) => {
|
||||
// originPicGoSaveConfig(config)
|
||||
// // flush electron's db
|
||||
// debounced()
|
||||
// }
|
||||
|
||||
export default picgo
|
||||
|
||||
+12
-17
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
Notification,
|
||||
ipcMain
|
||||
} from 'electron'
|
||||
import db, { GalleryDB } from 'apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import { GalleryDB } from 'apis/core/datastore'
|
||||
import { dbPathChecker, defaultConfigPath, getGalleryDBPath } from 'apis/core/datastore/dbChecker'
|
||||
import uploader from 'apis/app/uploader'
|
||||
import pasteTemplate from '~/main/utils/pasteTemplate'
|
||||
import { handleCopyUrl } from '~/main/utils/common'
|
||||
import { handleCopyUrl, showNotification as showMainNotification } from '~/main/utils/common'
|
||||
import {
|
||||
getWindowId,
|
||||
getSettingWindowId
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '~/universal/events/constants'
|
||||
import { DBStore } from '@picgo/store'
|
||||
import { T } from '~/main/i18n'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { IPasteStyle, IRPCActionType } from '~/universal/types/enum'
|
||||
|
||||
class GuiApi implements IGuiApi {
|
||||
private static instance: GuiApi
|
||||
@@ -77,17 +77,16 @@ class GuiApi implements IGuiApi {
|
||||
const webContents = this.getWebContentsByWindowId(this.windowId)
|
||||
const imgs = await uploader.setWebContents(webContents!).upload(input)
|
||||
if (imgs !== false) {
|
||||
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'
|
||||
const pasteText: string[] = []
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
pasteText.push(pasteTemplate(pasteStyle, imgs[i], db.get('settings.customLink')))
|
||||
const notification = new Notification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl as string
|
||||
// icon: imgs[i].imgUrl
|
||||
})
|
||||
pasteText.push(pasteTemplate(pasteStyle, imgs[i], picgo.getConfig<string>('settings.customLink')))
|
||||
setTimeout(() => {
|
||||
notification.show()
|
||||
showMainNotification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: imgs[i].imgUrl as string
|
||||
// icon: imgs[i].imgUrl
|
||||
})
|
||||
}, i * 100)
|
||||
await GalleryDB.getInstance().insert(imgs[i])
|
||||
}
|
||||
@@ -103,11 +102,7 @@ class GuiApi implements IGuiApi {
|
||||
title: '',
|
||||
body: ''
|
||||
}) {
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
})
|
||||
notification.show()
|
||||
showMainNotification(options)
|
||||
}
|
||||
|
||||
showMessageBox (options: IShowMessageBoxOption = {
|
||||
|
||||
+23
-20
@@ -2,15 +2,16 @@ import {
|
||||
app,
|
||||
ipcMain,
|
||||
shell,
|
||||
Notification,
|
||||
IpcMainEvent,
|
||||
BrowserWindow
|
||||
} from 'electron'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { IRPCActionType, IWindowList } from '#/types/enum'
|
||||
import { IPasteStyle, IRPCActionType, IWindowList } from '#/types/enum'
|
||||
import uploader from 'apis/app/uploader'
|
||||
import pasteTemplate from '~/main/utils/pasteTemplate'
|
||||
import db, { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import logger from '@core/picgo/logger'
|
||||
import { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import server from '~/main/server'
|
||||
import getPicBeds from '~/main/utils/getPicBeds'
|
||||
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
|
||||
@@ -27,14 +28,15 @@ import {
|
||||
OPEN_URL,
|
||||
SHOW_PLUGIN_PAGE_MENU,
|
||||
SET_MINI_WINDOW_POS,
|
||||
GET_PICBEDS
|
||||
GET_PICBEDS,
|
||||
LOG_INVALID_URL_LINES
|
||||
} from '#/events/constants'
|
||||
import {
|
||||
uploadClipboardFiles,
|
||||
uploadSelectedFiles
|
||||
} from '~/main/apis/app/uploader/apis'
|
||||
import picgoCoreIPC from './picgoCoreIPC'
|
||||
import { handleCopyUrl } from '~/main/utils/common'
|
||||
import { handleCopyUrl, showNotification } from '~/main/utils/common'
|
||||
import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu'
|
||||
import path from 'path'
|
||||
import { T } from '~/main/i18n'
|
||||
@@ -49,15 +51,14 @@ export default {
|
||||
// macOS use builtin clipboard is OK
|
||||
const img = await uploader.setWebContents(trayWindow.webContents).uploadWithBuildInClipboard()
|
||||
if (img !== false) {
|
||||
const pasteStyle = db.get('settings.pasteStyle') || 'markdown'
|
||||
handleCopyUrl(pasteTemplate(pasteStyle, img[0], db.get('settings.customLink')))
|
||||
const notification = new Notification({
|
||||
const pasteStyle = picgo.getConfig<IPasteStyle>('settings.pasteStyle') || 'markdown'
|
||||
handleCopyUrl(pasteTemplate(pasteStyle, img[0], picgo.getConfig<string>('settings.customLink')))
|
||||
showNotification({
|
||||
title: T('UPLOAD_SUCCEED'),
|
||||
body: img[0].imgUrl!
|
||||
// icon: file[0]
|
||||
// icon: img[0].imgUrl
|
||||
})
|
||||
notification.show()
|
||||
await GalleryDB.getInstance().insert(img[0])
|
||||
trayWindow.webContents.send('clipboardFiles', [])
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
@@ -80,43 +81,38 @@ export default {
|
||||
const result = shortKeyHandler.updateShortKey(item, oldKey, from)
|
||||
evt.sender.send('updateShortKeyResponse', result)
|
||||
if (result) {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('OPERATION_SUCCEED'),
|
||||
body: T('TIPS_SHORTCUT_MODIFIED_SUCCEED')
|
||||
})
|
||||
notification.show()
|
||||
} else {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('OPERATION_FAILED'),
|
||||
body: T('TIPS_SHORTCUT_MODIFIED_CONFLICT')
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('bindOrUnbindShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, from: string) => {
|
||||
const result = shortKeyHandler.bindOrUnbindShortKey(item, from)
|
||||
if (result) {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('OPERATION_SUCCEED'),
|
||||
body: T('TIPS_SHORTCUT_MODIFIED_SUCCEED')
|
||||
})
|
||||
notification.show()
|
||||
} else {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('OPERATION_FAILED'),
|
||||
body: T('TIPS_SHORTCUT_MODIFIED_CONFLICT')
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('updateCustomLink', () => {
|
||||
const notification = new Notification({
|
||||
showNotification({
|
||||
title: T('OPERATION_SUCCEED'),
|
||||
body: T('TIPS_CUSTOM_LINK_STYLE_MODIFIED_SUCCEED')
|
||||
})
|
||||
notification.show()
|
||||
})
|
||||
|
||||
ipcMain.on('autoStart', (evt: IpcMainEvent, val: boolean) => {
|
||||
@@ -136,7 +132,7 @@ export default {
|
||||
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)!
|
||||
const settingWindow = windowManager.get(IWindowList.SETTING_WINDOW)!
|
||||
|
||||
if (db.get('settings.miniWindowOnTop')) {
|
||||
if (picgo.getConfig<boolean>('settings.miniWindowOnTop')) {
|
||||
miniWindow.setAlwaysOnTop(true)
|
||||
}
|
||||
|
||||
@@ -222,6 +218,13 @@ export default {
|
||||
const window = BrowserWindow.getFocusedWindow()
|
||||
window?.setBounds(pos)
|
||||
})
|
||||
|
||||
ipcMain.on(LOG_INVALID_URL_LINES, (_evt: IpcMainEvent, lines: string[]) => {
|
||||
if (!Array.isArray(lines) || !lines.length) return
|
||||
lines.forEach((line, index) => {
|
||||
logger.warn(`[Batch URL Upload] invalid url line #${index + 1}: ${line}`)
|
||||
})
|
||||
})
|
||||
},
|
||||
dispose () {}
|
||||
}
|
||||
|
||||
@@ -257,8 +257,9 @@ const handleRemoveFiles = () => {
|
||||
}
|
||||
|
||||
const handlePicGoSaveConfig = () => {
|
||||
ipcMain.on(PICGO_SAVE_CONFIG, (event: IpcMainEvent, data: IObj) => {
|
||||
ipcMain.handle(PICGO_SAVE_CONFIG, (_event, data: IObj) => {
|
||||
picgo.saveConfig(data)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { logger } from '@picgo/i18n'
|
||||
import { IPicGo } from 'picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
import { getHost, removeProtocolAndSuffix, replaceHost } from '~/main/utils/common'
|
||||
|
||||
export const galleryMenu = () => {
|
||||
return [{
|
||||
label: T('CHANGE_IMAGE_URL_HOST'),
|
||||
async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {
|
||||
const hostList = [...new Set(selectedList.map((item) => {
|
||||
return getHost(item.imgUrl)
|
||||
}).filter(Boolean) as string[])]
|
||||
if (hostList.length === 0) {
|
||||
guiApi.showNotification({
|
||||
title: T('CHANGE_IMAGE_URL_HOST'),
|
||||
body: T('CHANGE_IMAGE_URL_HOST_WARN')
|
||||
})
|
||||
logger.warn(T('CHANGE_IMAGE_URL_HOST_WARN'))
|
||||
return
|
||||
}
|
||||
const config: IPicGoPluginConfig[] = [
|
||||
{
|
||||
alias: T('SELECTED_IMAGE_URL_HOST'),
|
||||
name: 'selectedHost',
|
||||
type: 'checkbox',
|
||||
choices: hostList.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
checked: true
|
||||
})),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
alias: T('NEW_IMAGE_URL_HOST'),
|
||||
name: 'newHost',
|
||||
type: 'input',
|
||||
message: 'www.example.com',
|
||||
default: '',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
const options: IPicGoPluginShowConfigDialogOption = {
|
||||
title: T('CHANGE_IMAGE_URL_HOST'),
|
||||
config
|
||||
}
|
||||
const res = await guiApi.showConfigDialog<{
|
||||
selectedHost: string[]
|
||||
newHost: string
|
||||
}>(options)
|
||||
|
||||
if (res) {
|
||||
const selectedHost = res.selectedHost
|
||||
const newHost = removeProtocolAndSuffix(res.newHost)
|
||||
const changedList = selectedList.map((item) => {
|
||||
try {
|
||||
const url = new URL(item.imgUrl || '')
|
||||
const host = url.host
|
||||
if (selectedHost.includes(host)) {
|
||||
item.imgUrl = replaceHost(item.imgUrl!, host, newHost)
|
||||
return item
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.log.error(e)
|
||||
return false
|
||||
}
|
||||
}).filter(Boolean) as ImgInfo[]
|
||||
const updateRes = await guiApi.galleryDB.updateMany(changedList)
|
||||
guiApi.showNotification({
|
||||
title: T('CHANGE_IMAGE_URL_HOST_RESULT'),
|
||||
body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { logger } from '@picgo/i18n'
|
||||
import { PicGoUtils, type IPicGo } from 'picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
|
||||
interface IUrlRewriteRule {
|
||||
match: string
|
||||
replace: string
|
||||
enable?: boolean
|
||||
global?: boolean
|
||||
ignoreCase?: boolean
|
||||
}
|
||||
|
||||
interface IUrlRewriteDialogResult {
|
||||
applyGlobalRules?: boolean
|
||||
match?: string
|
||||
replace?: string
|
||||
global?: boolean
|
||||
ignoreCase?: boolean
|
||||
}
|
||||
|
||||
function normalizeRules (value: unknown): IUrlRewriteRule[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map(item => {
|
||||
const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>
|
||||
return {
|
||||
match: String(raw.match ?? ''),
|
||||
replace: String(raw.replace ?? ''),
|
||||
enable: raw.enable === false ? false : true,
|
||||
global: raw.global === true,
|
||||
ignoreCase: raw.ignoreCase === true
|
||||
}
|
||||
}).filter(rule => rule.match.length > 0)
|
||||
}
|
||||
|
||||
function buildFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>): string {
|
||||
return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`
|
||||
}
|
||||
|
||||
function validateRuleOrThrow (rule: IUrlRewriteRule) {
|
||||
if (!rule.match.trim() || !rule.replace.trim()) {
|
||||
throw new Error(T('GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED'))
|
||||
}
|
||||
try {
|
||||
new RegExp(rule.match, buildFlags(rule))
|
||||
} catch (error) {
|
||||
const message = `Invalid URL rewrite regex pattern "${rule.match}": ${error instanceof Error ? error.message : String(error)}`
|
||||
logger.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function applyFirstMatchRewrite (ctx: IPicGo, imgItem: ImgInfo, rules: IUrlRewriteRule[]): ImgInfo {
|
||||
const imgInfo = {
|
||||
imgUrl: imgItem.imgUrl,
|
||||
originImgUrl: imgItem.originImgUrl
|
||||
}
|
||||
PicGoUtils.applyUrlRewriteToImgInfo(imgInfo, rules, {
|
||||
log: {
|
||||
error: (...args: Parameters<IPicGo['log']['error']>) => ctx.log.error(...args),
|
||||
warn: () => ctx.log.warn(T('GALLERY_URL_REWRITE_EMPTY_RESULT_WARN'))
|
||||
}
|
||||
})
|
||||
if (imgInfo.imgUrl === '') return imgItem
|
||||
return imgInfo
|
||||
}
|
||||
|
||||
export const galleryMenu = () => {
|
||||
return [{
|
||||
label: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {
|
||||
if (!selectedList.length) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_WARN_NO_SELECTION')
|
||||
})
|
||||
logger.warn(T('GALLERY_URL_REWRITE_WARN_NO_SELECTION'))
|
||||
return
|
||||
}
|
||||
|
||||
const globalRules = normalizeRules(ctx.getConfig('settings.urlRewrite.rules'))
|
||||
|
||||
const config: IPicGoPluginConfig[] = [
|
||||
{
|
||||
alias: T('GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES'),
|
||||
name: 'applyGlobalRules',
|
||||
type: 'confirm',
|
||||
default: globalRules.length > 0,
|
||||
required: false,
|
||||
confirmText: T('SETTINGS_OPEN'),
|
||||
cancelText: T('SETTINGS_CLOSE'),
|
||||
tips: `${T('GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT')}: ${globalRules.length}`
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_MATCH'),
|
||||
name: 'match',
|
||||
type: 'input',
|
||||
message: T('URL_REWRITE_MATCH_PLACEHOLDER'),
|
||||
default: '',
|
||||
required: false,
|
||||
tips: `${T('GALLERY_URL_REWRITE_TEMP_RULE_TIPS')}\n\n${T('URL_REWRITE_MATCH_TIPS')}`
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_REPLACE'),
|
||||
name: 'replace',
|
||||
type: 'input',
|
||||
message: T('URL_REWRITE_REPLACE_PLACEHOLDER'),
|
||||
default: '',
|
||||
required: false,
|
||||
tips: T('URL_REWRITE_REPLACE_TIPS')
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_FLAG_GLOBAL_LABEL'),
|
||||
name: 'global',
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
required: false,
|
||||
confirmText: 'g',
|
||||
cancelText: '-',
|
||||
tips: T('URL_REWRITE_FLAG_GLOBAL_DESC')
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL'),
|
||||
name: 'ignoreCase',
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
required: false,
|
||||
confirmText: 'i',
|
||||
cancelText: '-',
|
||||
tips: T('URL_REWRITE_FLAG_IGNORE_CASE_DESC')
|
||||
}
|
||||
]
|
||||
const options: IPicGoPluginShowConfigDialogOption = {
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
config
|
||||
}
|
||||
const res = await guiApi.showConfigDialog<IUrlRewriteDialogResult>(options)
|
||||
if (!res) return
|
||||
|
||||
const applyGlobalRules = res.applyGlobalRules === true
|
||||
const tempMatch = String(res.match ?? '').trim()
|
||||
const tempReplace = String(res.replace ?? '').trim()
|
||||
|
||||
const hasTempRuleInput = tempMatch.length > 0 || tempReplace.length > 0
|
||||
let tempRule: IUrlRewriteRule | null = null
|
||||
if (hasTempRuleInput) {
|
||||
tempRule = {
|
||||
match: tempMatch,
|
||||
replace: tempReplace,
|
||||
enable: true,
|
||||
global: res.global === true,
|
||||
ignoreCase: res.ignoreCase === true
|
||||
}
|
||||
}
|
||||
|
||||
if (tempRule) {
|
||||
try {
|
||||
validateRuleOrThrow(tempRule)
|
||||
} catch (e: any) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: e.message
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!applyGlobalRules && !tempRule) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_NO_RULES_TO_APPLY')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let shouldSaveTempRule = false
|
||||
if (tempRule) {
|
||||
const saveRes = await guiApi.showMessageBox({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
message: T('GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT'),
|
||||
type: 'info',
|
||||
buttons: [
|
||||
T('GALLERY_URL_REWRITE_APPLY_AND_SAVE'),
|
||||
T('GALLERY_URL_REWRITE_APPLY_ONLY'),
|
||||
T('CANCEL')
|
||||
]
|
||||
})
|
||||
if (saveRes.result === 2) return
|
||||
shouldSaveTempRule = saveRes.result === 0
|
||||
}
|
||||
|
||||
const rulesToApply: IUrlRewriteRule[] = [
|
||||
...(tempRule ? [tempRule] : []),
|
||||
...(applyGlobalRules ? globalRules : [])
|
||||
]
|
||||
|
||||
const changedList = selectedList.map((item) => {
|
||||
const current = item.imgUrl || ''
|
||||
if (!current) return false
|
||||
const next = applyFirstMatchRewrite(ctx, item, rulesToApply)
|
||||
if (next.imgUrl === current) return false
|
||||
return {
|
||||
id: item.id,
|
||||
imgUrl: next.imgUrl,
|
||||
originImgUrl: next.originImgUrl
|
||||
} as ImgInfo
|
||||
}).filter(Boolean) as ImgInfo[]
|
||||
|
||||
if (changedList.length === 0) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_NO_CHANGES')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldSaveTempRule && tempRule) {
|
||||
const nextGlobalRules = [...globalRules, tempRule]
|
||||
ctx.saveConfig({
|
||||
'settings.urlRewrite.rules': nextGlobalRules
|
||||
})
|
||||
}
|
||||
|
||||
const updateRes = await guiApi.galleryDB.updateMany(changedList)
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),
|
||||
body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}`
|
||||
})
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { galleryMenu as changeHostGalleryMenu } from './changeHost'
|
||||
import { galleryMenu as changeURLGalleryMenu } from './changeURL'
|
||||
export const builtInGalleryToolboxMenu = () => {
|
||||
const menuList = [...changeHostGalleryMenu()]
|
||||
const menuList = [...changeURLGalleryMenu()]
|
||||
|
||||
return menuList
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { RPCRouter } from '../router'
|
||||
import { app, clipboard, shell } from 'electron'
|
||||
import windowManager from '~/main/apis/app/window/windowManager'
|
||||
import { handleMenubarIcon } from '~/main/apis/app/system'
|
||||
import { PICGO_NOTIFICATION_CLICKED } from '~/universal/events/constants'
|
||||
import { showNotification } from '~/main/utils/common'
|
||||
|
||||
const systemRouter = new RPCRouter()
|
||||
|
||||
@@ -35,6 +37,24 @@ systemRouter
|
||||
const [visible] = args as IShowMenubarIconArgs
|
||||
handleMenubarIcon(visible)
|
||||
})
|
||||
.add(IRPCActionType.SHOW_NOTIFICATION, async (args, event) => {
|
||||
const [title, body, id] = args as IShowNotificationArgs
|
||||
|
||||
const options: IPrivateShowNotificationOption = {
|
||||
title,
|
||||
body
|
||||
}
|
||||
|
||||
if (id) {
|
||||
options.callback = () => {
|
||||
if (!event.sender.isDestroyed()) {
|
||||
event.sender.send(PICGO_NOTIFICATION_CLICKED, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(options)
|
||||
})
|
||||
|
||||
export {
|
||||
systemRouter
|
||||
|
||||
@@ -11,7 +11,7 @@ const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_
|
||||
const defaultClipboardImagePath = path.join(defaultConfigPath, CLIPBOARD_IMAGE_FOLDER)
|
||||
|
||||
export const checkClipboardUploadMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async (event) => {
|
||||
sendToolboxRes(event, {
|
||||
@@ -51,7 +51,7 @@ IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
}
|
||||
|
||||
export const fixClipboardUploadMap: IToolboxFixMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD
|
||||
> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_CLIPBOARD_PIC_UPLOAD]: async () => {
|
||||
const configFilePath = dbPathChecker()
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from 'fs-extra'
|
||||
import { IpcMainEvent } from 'electron'
|
||||
import { IToolboxItemCheckStatus, IToolboxItemType } from '~/universal/types/enum'
|
||||
import { sendToolboxResWithType } from './utils'
|
||||
import { dbPathChecker } from '~/main/apis/core/datastore/dbChecker'
|
||||
import { GalleryDB, DB_PATH } from '~/main/apis/core/datastore'
|
||||
import { dbPathChecker, getGalleryDBPath } from '~/main/apis/core/datastore/dbChecker'
|
||||
import { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import path from 'path'
|
||||
import { T } from '~/main/i18n'
|
||||
|
||||
@@ -40,20 +40,21 @@ IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.LOADING
|
||||
})
|
||||
const { dbPath } = getGalleryDBPath()
|
||||
const galleryDB = GalleryDB.getInstance()
|
||||
if (galleryDB.errorList.length === 0) {
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.SUCCESS,
|
||||
msg: T('TOOLBOX_CHECK_GALLERY_FILE_PATH_TIPS', {
|
||||
path: DB_PATH
|
||||
path: dbPath
|
||||
}),
|
||||
value: path.dirname(DB_PATH)
|
||||
value: path.dirname(dbPath)
|
||||
})
|
||||
} else {
|
||||
sendToolboxRes(event, {
|
||||
status: IToolboxItemCheckStatus.ERROR,
|
||||
msg: T('TOOLBOX_CHECK_GALLERY_FILE_BROKEN_TIPS'),
|
||||
value: path.dirname(DB_PATH)
|
||||
value: path.dirname(dbPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -75,7 +76,7 @@ IToolboxItemType.IS_CONFIG_FILE_BROKEN | IToolboxItemType.IS_GALLERY_FILE_BROKEN
|
||||
},
|
||||
[IToolboxItemType.IS_GALLERY_FILE_BROKEN]: async () => {
|
||||
try {
|
||||
fs.unlinkSync(DB_PATH)
|
||||
fs.unlinkSync(getGalleryDBPath().dbPath)
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const getProxy = (proxyStr: string): AxiosRequestConfig['proxy'] | false => {
|
||||
const sendToolboxRes = sendToolboxResWithType(IToolboxItemType.HAS_PROBLEM_WITH_PROXY)
|
||||
|
||||
export const checkProxyMap: IToolboxCheckerMap<
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_PROXY
|
||||
IToolboxItemType.HAS_PROBLEM_WITH_PROXY
|
||||
> = {
|
||||
[IToolboxItemType.HAS_PROBLEM_WITH_PROXY]: async (event) => {
|
||||
sendToolboxRes(event, {
|
||||
|
||||
+11
-10
@@ -2,8 +2,7 @@ import './errorHandler'
|
||||
import {
|
||||
app,
|
||||
globalShortcut,
|
||||
protocol,
|
||||
Notification
|
||||
protocol
|
||||
} from 'electron'
|
||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import beforeOpen from '~/main/utils/beforeOpen'
|
||||
@@ -26,7 +25,7 @@ import server from '~/main/server/index'
|
||||
import updateChecker from '~/main/utils/updateChecker'
|
||||
import shortKeyHandler from 'apis/app/shortKey/shortKeyHandler'
|
||||
import { getUploadFiles } from '~/main/utils/handleArgv'
|
||||
import db, { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import { GalleryDB } from '~/main/apis/core/datastore'
|
||||
import bus from '@core/bus'
|
||||
import logger from 'apis/core/picgo/logger'
|
||||
import picgo from 'apis/core/picgo'
|
||||
@@ -35,6 +34,7 @@ import { initI18n } from '~/main/utils/handleI18n'
|
||||
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
|
||||
import { isMacOS } from '../utils/getMacOSVersion'
|
||||
import { isWindowShouldShowOnStartup } from '../apis/app/window/windowList'
|
||||
import { showNotification } from '../utils/common'
|
||||
import { initStaticPath, isDev } from '../utils/env'
|
||||
|
||||
const isDevelopment = isDev
|
||||
@@ -65,8 +65,8 @@ class LifeCycle {
|
||||
initI18n()
|
||||
ipcList.listen()
|
||||
busEventList.listen()
|
||||
updateShortKeyFromVersion212(db, db.get('settings.shortKey'))
|
||||
await migrateGalleryFromVersion230(db, GalleryDB.getInstance(), picgo)
|
||||
updateShortKeyFromVersion212(picgo)
|
||||
await migrateGalleryFromVersion230(GalleryDB.getInstance(), picgo)
|
||||
}
|
||||
|
||||
private onReady () {
|
||||
@@ -98,7 +98,7 @@ class LifeCycle {
|
||||
}
|
||||
handleMenubarIcon()
|
||||
handleDockIcon()
|
||||
db.set('needReload', false)
|
||||
picgo.saveConfig({ needReload: false })
|
||||
updateChecker()
|
||||
// 不需要阻塞
|
||||
process.nextTick(() => {
|
||||
@@ -112,8 +112,9 @@ class LifeCycle {
|
||||
if (global.notificationList && global.notificationList?.length > 0) {
|
||||
while (global.notificationList?.length) {
|
||||
const option = global.notificationList.pop()
|
||||
const notice = new Notification(option!)
|
||||
notice.show()
|
||||
if (option) {
|
||||
showNotification(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
await remoteNoticeHandler.init()
|
||||
@@ -146,13 +147,13 @@ class LifeCycle {
|
||||
// click dock to open setting window
|
||||
if (isMacOS) {
|
||||
handleDockIcon()
|
||||
if (db.get('settings.showDockIcon') !== false) {
|
||||
if (picgo.getConfig<boolean>('settings.showDockIcon') !== false) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)?.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: db.get('settings.autoStart') || false
|
||||
openAtLogin: picgo.getConfig<boolean>('settings.autoStart') || false
|
||||
})
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId('com.molunerfinn.picgo')
|
||||
|
||||
+19
-14
@@ -1,12 +1,12 @@
|
||||
import { DBStore } from '@picgo/store'
|
||||
import ConfigStore from '~/main/apis/core/datastore'
|
||||
import path from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import { PicGo as PicGoCore } from 'picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
import { SHORTKEY_COMMAND_UPLOAD } from 'apis/core/bus/constants'
|
||||
// from v2.1.2
|
||||
const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IShortKeyConfigs | IOldShortKeyConfigs) => {
|
||||
const updateShortKeyFromVersion212 = (picgo: PicGoCore) => {
|
||||
const shortKeyConfig = picgo.getConfig<IShortKeyConfigs | IOldShortKeyConfigs | undefined>('settings.shortKey')
|
||||
// #557 极端情况可能会出现配置不存在,需要重新写入
|
||||
if (shortKeyConfig === undefined) {
|
||||
const defaultShortKeyConfig = {
|
||||
@@ -15,33 +15,38 @@ const updateShortKeyFromVersion212 = (db: typeof ConfigStore, shortKeyConfig: IS
|
||||
name: 'upload',
|
||||
label: T('QUICK_UPLOAD')
|
||||
}
|
||||
db.set('settings.shortKey[picgo:upload]', defaultShortKeyConfig)
|
||||
picgo.saveConfig({
|
||||
[`settings.shortKey[${SHORTKEY_COMMAND_UPLOAD}]`]: defaultShortKeyConfig
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (shortKeyConfig.upload) {
|
||||
// @ts-ignore
|
||||
shortKeyConfig[SHORTKEY_COMMAND_UPLOAD] = {
|
||||
if (typeof (shortKeyConfig as IOldShortKeyConfigs).upload === 'string') {
|
||||
const oldKey = (shortKeyConfig as IOldShortKeyConfigs).upload
|
||||
const nextConfig = Object.fromEntries(
|
||||
Object.entries(shortKeyConfig).filter(([key]) => key !== 'upload')
|
||||
) as IShortKeyConfigs
|
||||
nextConfig[SHORTKEY_COMMAND_UPLOAD] = {
|
||||
enable: true,
|
||||
key: shortKeyConfig.upload,
|
||||
key: oldKey,
|
||||
name: 'upload',
|
||||
label: T('QUICK_UPLOAD')
|
||||
}
|
||||
// @ts-ignore
|
||||
delete shortKeyConfig.upload
|
||||
db.set('settings.shortKey', shortKeyConfig)
|
||||
picgo.saveConfig({
|
||||
'settings.shortKey': nextConfig
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const migrateGalleryFromVersion230 = async (configDB: typeof ConfigStore, galleryDB: DBStore, picgo: PicGoCore) => {
|
||||
const originGallery: ImgInfo[] = picgo.getConfig('uploaded')
|
||||
const migrateGalleryFromVersion230 = async (galleryDB: DBStore, picgo: PicGoCore) => {
|
||||
const originGallery = picgo.getConfig<ImgInfo[] | undefined>('uploaded')
|
||||
// if hasMigrate, we don't need to migrate
|
||||
const hasMigrate: boolean = configDB.get('__migrateUploaded')
|
||||
const hasMigrate = picgo.getConfig<boolean | undefined>('__migrateUploaded') === true
|
||||
if (hasMigrate) {
|
||||
return
|
||||
}
|
||||
const configPath = configDB.getConfigPath()
|
||||
const configPath = picgo.configPath
|
||||
const configBakPath = path.join(path.dirname(configPath), 'config.bak.json')
|
||||
// migrate gallery from config to gallery db
|
||||
if (originGallery && Array.isArray(originGallery) && originGallery?.length > 0) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export type MulterMiddleware = (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
callback: (error?: MulterError) => void
|
||||
) => void;
|
||||
) => void
|
||||
|
||||
// 扩展 multer 函数的返回类型
|
||||
declare module 'multer' {
|
||||
|
||||
+28
-17
@@ -1,12 +1,13 @@
|
||||
import fs from 'fs-extra'
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import logger from '@core/picgo/logger'
|
||||
import { clipboard, Notification, dialog } from 'electron'
|
||||
import { handleUrlEncode } from '~/universal/utils/common'
|
||||
import { readClipboardFilePaths } from 'clip-filepaths'
|
||||
import crypto from 'node:crypto'
|
||||
import picgo from '@core/picgo'
|
||||
|
||||
export const handleCopyUrl = (str: string): void => {
|
||||
if (db.get('settings.autoCopyUrl') !== false) {
|
||||
if (picgo.getConfig<boolean>('settings.autoCopyUrl') !== false) {
|
||||
clipboard.writeText(str)
|
||||
}
|
||||
}
|
||||
@@ -18,21 +19,31 @@ export const handleCopyUrl = (str: string): void => {
|
||||
export const showNotification = (options: IPrivateShowNotificationOption = {
|
||||
title: '',
|
||||
body: '',
|
||||
text: '',
|
||||
clickToCopy: false,
|
||||
copyContent: '',
|
||||
clickFn: () => {}
|
||||
callback: () => {}
|
||||
}) => {
|
||||
if (options.text) {
|
||||
logger.info('[PicGo Notification]', options.text)
|
||||
clipboard.writeText(options.text)
|
||||
}
|
||||
|
||||
const title = options.title || ''
|
||||
const body = options.body || options.text || ''
|
||||
const silent = picgo.getConfig('settings.notificationSound') === false
|
||||
const notification = new Notification({
|
||||
title: options.title,
|
||||
body: options.body
|
||||
title,
|
||||
body,
|
||||
silent
|
||||
// icon: options.icon || undefined
|
||||
})
|
||||
const handleClick = () => {
|
||||
if (options.clickToCopy) {
|
||||
clipboard.writeText(options.copyContent || options.body)
|
||||
clipboard.writeText(options.copyContent || body)
|
||||
}
|
||||
if (options.clickFn) {
|
||||
options.clickFn()
|
||||
if (options.callback) {
|
||||
options.callback()
|
||||
}
|
||||
}
|
||||
notification.once('click', handleClick)
|
||||
@@ -83,18 +94,18 @@ export const calcUploadProcessDurationRange = (duration: number) => {
|
||||
export const calcUploadBigFileSizeRange = (fileSizeMB: number) => {
|
||||
if (fileSizeMB < 10) {
|
||||
// 3.2 -> 3, 3.6 -> 4
|
||||
const result = Math.round(fileSizeMB);
|
||||
return result === 0 && fileSizeMB > 0 ? 1 : result;
|
||||
const result = Math.round(fileSizeMB)
|
||||
return result === 0 && fileSizeMB > 0 ? 1 : result
|
||||
}
|
||||
else if (fileSizeMB < 100) {
|
||||
// 13 -> 1.3 -> 1 -> 10
|
||||
// 17 -> 1.7 -> 2 -> 20
|
||||
return Math.round(fileSizeMB / 10) * 10;
|
||||
return Math.round(fileSizeMB / 10) * 10
|
||||
}
|
||||
else {
|
||||
// 135 -> 1.35 -> 1 -> 100
|
||||
// 160 -> 1.60 -> 2 -> 200
|
||||
return Math.round(fileSizeMB / 100) * 100;
|
||||
return Math.round(fileSizeMB / 100) * 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,18 +113,18 @@ export const calcUploadBigFileSizeRange = (fileSizeMB: number) => {
|
||||
export const calcVideoDurationRange = (durationSec: number) => {
|
||||
if (durationSec < 10) {
|
||||
// 3.2 -> 3, 3.6 -> 4
|
||||
const result = Math.round(durationSec);
|
||||
return result === 0 && durationSec > 0 ? 1 : result;
|
||||
const result = Math.round(durationSec)
|
||||
return result === 0 && durationSec > 0 ? 1 : result
|
||||
}
|
||||
else if (durationSec < 100) {
|
||||
// 13 -> 1.3 -> 1 -> 10
|
||||
// 17 -> 1.7 -> 2 -> 20
|
||||
return Math.round(durationSec / 10) * 10;
|
||||
return Math.round(durationSec / 10) * 10
|
||||
}
|
||||
else {
|
||||
// 135 -> 1.35 -> 1 -> 100
|
||||
// 160 -> 1.60 -> 2 -> 200
|
||||
return Math.round(durationSec / 100) * 100;
|
||||
return Math.round(durationSec / 100) * 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +154,7 @@ export const getClipboardFilePathList = (): string[] => {
|
||||
}
|
||||
|
||||
export const handleUrlEncodeWithSetting = (url: string) => {
|
||||
if (db.get('settings.encodeOutputURL') === true) {
|
||||
if (picgo.getConfig<boolean>('settings.encodeOutputURL') === true) {
|
||||
url = handleUrlEncode(url)
|
||||
}
|
||||
return url
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ipcMain, IpcMainEvent, type WebContents } from "electron";
|
||||
import { deviceIdManager } from "./deviceId";
|
||||
import { REGISTER_DEVICE_ID, TALKING_DATA_EVENT } from "~/universal/events/constants";
|
||||
import type { IImgInfo } from "picgo";
|
||||
import { calcUploadBigFileSizeRange, calcUploadProcessDurationRange, calcVideoDurationRange } from "./common";
|
||||
import { app } from "electron/main";
|
||||
import db from "~/main/apis/core/datastore";
|
||||
import { getVideoDuration } from "@picgo/video-duration";
|
||||
import { MB, SECOND } from "./constants";
|
||||
import { ipcMain, IpcMainEvent, type WebContents } from "electron"
|
||||
import { deviceIdManager } from "./deviceId"
|
||||
import { REGISTER_DEVICE_ID, TALKING_DATA_EVENT } from "~/universal/events/constants"
|
||||
import type { IImgInfo } from "picgo"
|
||||
import { calcUploadBigFileSizeRange, calcUploadProcessDurationRange, calcVideoDurationRange } from "./common"
|
||||
import { app } from "electron/main"
|
||||
import picgo from "@core/picgo"
|
||||
import { getVideoDuration } from "@picgo/video-duration"
|
||||
import { MB, SECOND } from "./constants"
|
||||
|
||||
export interface IReportUploadDataOptions {
|
||||
fromClipboard: boolean;
|
||||
@@ -15,26 +15,26 @@ export interface IReportUploadDataOptions {
|
||||
}
|
||||
|
||||
class DataReportManager {
|
||||
private deviceId: string | null = null;
|
||||
private hasRegisterDeviceID: boolean = false;
|
||||
private deviceId: string | null = null
|
||||
private hasRegisterDeviceID: boolean = false
|
||||
constructor () {
|
||||
this.init()
|
||||
this.handleRegisterDeviceID()
|
||||
}
|
||||
private handleRegisterDeviceID() {
|
||||
ipcMain.once(REGISTER_DEVICE_ID, (_evt: IpcMainEvent) => {
|
||||
this.hasRegisterDeviceID = true;
|
||||
console.log('Device ID registered');
|
||||
});
|
||||
this.hasRegisterDeviceID = true
|
||||
console.log('Device ID registered')
|
||||
})
|
||||
}
|
||||
private async init () {
|
||||
if (this.deviceId) return;
|
||||
if (this.deviceId) return
|
||||
this.deviceId = await deviceIdManager.getId()
|
||||
}
|
||||
public async reportUploadData(webContents: WebContents, options: IReportUploadDataOptions) {
|
||||
await this.init();
|
||||
await this.registerDeviceID(webContents);
|
||||
const { fromClipboard, duration, outputList } = options;
|
||||
await this.init()
|
||||
await this.registerDeviceID(webContents)
|
||||
const { fromClipboard, duration, outputList } = options
|
||||
const fileList = outputList.map(item => {
|
||||
return {
|
||||
fileName: item.fileName,
|
||||
@@ -50,56 +50,56 @@ class DataReportManager {
|
||||
by: fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件
|
||||
count: fileList.length, // 上传的数量
|
||||
duration: calcUploadProcessDurationRange(duration || 0), // 上传耗时
|
||||
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
|
||||
type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',
|
||||
}
|
||||
}
|
||||
this.reportDataToWebContents(webContents, uploadEventData);
|
||||
this.reportDataToWebContents(webContents, uploadEventData)
|
||||
fileList.forEach(async file => {
|
||||
if (file?.mimeType?.startsWith('video/') && file.filePath) {
|
||||
const metadata = await getVideoDuration(file.filePath);
|
||||
const sizeMB = metadata.size / MB;
|
||||
const durationSec = (metadata.duration / SECOND) || 0;
|
||||
const metadata = await getVideoDuration(file.filePath)
|
||||
const sizeMB = metadata.size / MB
|
||||
const durationSec = (metadata.duration / SECOND) || 0
|
||||
const videoEventData: ITalkingDataOptions = {
|
||||
EventId: 'upload_video',
|
||||
Label: '',
|
||||
MapKv: {
|
||||
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
|
||||
type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',
|
||||
mimeType: file.mimeType,
|
||||
sizeRange: calcUploadBigFileSizeRange(sizeMB),
|
||||
durationRange: calcVideoDurationRange(durationSec),
|
||||
}
|
||||
};
|
||||
this.reportDataToWebContents(webContents, videoEventData);
|
||||
}
|
||||
this.reportDataToWebContents(webContents, videoEventData)
|
||||
} else if (file?.mimeType?.startsWith('image/') || fromClipboard) {
|
||||
const imageEventData: ITalkingDataOptions = {
|
||||
EventId: 'upload_image',
|
||||
Label: '',
|
||||
MapKv: {
|
||||
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
|
||||
type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',
|
||||
mimeType: file.mimeType || 'image/png',
|
||||
sizeRange: calcUploadBigFileSizeRange(file.size / MB)
|
||||
}
|
||||
};
|
||||
this.reportDataToWebContents(webContents, imageEventData);
|
||||
}
|
||||
this.reportDataToWebContents(webContents, imageEventData)
|
||||
} else {
|
||||
const otherEventData: ITalkingDataOptions = {
|
||||
EventId: 'upload_file',
|
||||
Label: '',
|
||||
MapKv: {
|
||||
type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms',
|
||||
type: picgo.getConfig<string>('picBed.uploader') || picgo.getConfig<string>('picBed.current') || 'smms',
|
||||
mimeType: file.mimeType || 'UNKNOWN',
|
||||
sizeRange: calcUploadBigFileSizeRange(file.size / MB)
|
||||
}
|
||||
};
|
||||
this.reportDataToWebContents(webContents, otherEventData);
|
||||
}
|
||||
this.reportDataToWebContents(webContents, otherEventData)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
public async registerDeviceID(webContents: WebContents) {
|
||||
if (this.hasRegisterDeviceID) return;
|
||||
await this.init();
|
||||
webContents.send(REGISTER_DEVICE_ID, this.deviceId);
|
||||
if (this.hasRegisterDeviceID) return
|
||||
await this.init()
|
||||
webContents.send(REGISTER_DEVICE_ID, this.deviceId)
|
||||
}
|
||||
|
||||
private reportDataToWebContents(webContents: WebContents, data: ITalkingDataOptions) {
|
||||
@@ -111,22 +111,22 @@ class DataReportManager {
|
||||
version: app.getVersion(),
|
||||
area: this.getAreaFromTimezone()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private getAreaFromTimezone() {
|
||||
try {
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (!timeZone) return 'UNKNOWN';
|
||||
try {
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
if (!timeZone) return 'UNKNOWN'
|
||||
|
||||
if (timeZone === 'Asia/Shanghai') return 'CN';
|
||||
if (timeZone === 'Asia/Shanghai') return 'CN'
|
||||
|
||||
const region = timeZone.split('/')[0]; // Asia, America, Europe
|
||||
return region;
|
||||
} catch (e) {
|
||||
return 'UNKNOWN';
|
||||
const region = timeZone.split('/')[0] // Asia, America, Europe
|
||||
return region
|
||||
} catch (e) {
|
||||
return 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dataReportManager = new DataReportManager()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import { i18nManager } from '~/main/i18n'
|
||||
export const initI18n = () => {
|
||||
const currentLanguage = db.get('settings.language') || 'zh-CN'
|
||||
const currentLanguage = picgo.getConfig<string>('settings.language') || 'zh-CN'
|
||||
i18nManager.setCurrentLanguage(currentLanguage)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import { showMessageBox } from '~/main/utils/common'
|
||||
import { T } from '~/main/i18n'
|
||||
|
||||
class PrivacyManager {
|
||||
async check () {
|
||||
if (db.get('settings.privacyEnsure') !== true) {
|
||||
if (picgo.getConfig<boolean>('settings.privacyEnsure') !== true) {
|
||||
const res = await this.show(true)
|
||||
// cancel
|
||||
if (res.result === 1) {
|
||||
return false
|
||||
} else {
|
||||
db.set('settings.privacyEnsure', true)
|
||||
picgo.saveConfig({ 'settings.privacyEnsure': true })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dialog, shell } from 'electron'
|
||||
import db from '~/main/apis/core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import pkg from 'root/package.json'
|
||||
import { lt } from 'semver'
|
||||
import { T } from '~/main/i18n'
|
||||
@@ -10,13 +10,13 @@ const version = pkg.version
|
||||
const downloadUrl = 'https://github.com/Molunerfinn/PicGo/releases/latest'
|
||||
|
||||
const checkVersion = async () => {
|
||||
let showTip = db.get('settings.showUpdateTip')
|
||||
let showTip = picgo.getConfig<boolean | undefined>('settings.showUpdateTip')
|
||||
if (showTip === undefined) {
|
||||
db.set('settings.showUpdateTip', true)
|
||||
picgo.saveConfig({ 'settings.showUpdateTip': true })
|
||||
showTip = true
|
||||
}
|
||||
if (showTip) {
|
||||
const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false
|
||||
const isCheckBetaUpdate = picgo.getConfig<boolean>('settings.checkBetaUpdate') !== false
|
||||
const res: string = await getLatestVersion(isCheckBetaUpdate)
|
||||
if (res !== '') {
|
||||
const latest = res
|
||||
@@ -35,7 +35,7 @@ const checkVersion = async () => {
|
||||
if (res.response === 0) { // if selected yes
|
||||
shell.openExternal(downloadUrl)
|
||||
}
|
||||
db.set('settings.showUpdateTip', !res.checkboxChecked)
|
||||
picgo.saveConfig({ 'settings.showUpdateTip': !res.checkboxChecked })
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -50,7 +50,7 @@ const checkVersion = async () => {
|
||||
const compareVersion2Update = (current: string, latest: string) => {
|
||||
try {
|
||||
if (latest.includes('beta')) {
|
||||
const isCheckBetaUpdate = db.get('settings.checkBetaUpdate') !== false
|
||||
const isCheckBetaUpdate = picgo.getConfig<boolean>('settings.checkBetaUpdate') !== false
|
||||
if (!isCheckBetaUpdate) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<ConfirmDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:width="width"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
@close="handleCancel"
|
||||
@@ -31,6 +32,7 @@ const configList = ref<IPicGoPluginConfig[]>([])
|
||||
const formModel = reactive<IStringKeyMap>({})
|
||||
const $form = ref<IFormInstance>()
|
||||
const title = ref('')
|
||||
const width = ref(500)
|
||||
|
||||
const handleConfigForm = useConfigForm()
|
||||
|
||||
@@ -39,6 +41,7 @@ useIPCOn(IRPCActionType.OPEN_CONFIG_DIALOG, (event, options: IPicGoPluginShowCon
|
||||
visible.value = true
|
||||
configList.value = handleConfigForm(options.config, formModel)
|
||||
title.value = options.title
|
||||
width.value = options.width || 500
|
||||
})
|
||||
const handleConfirm = async () => {
|
||||
const res = await $form.value?.validate() || false
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:width="width + 'px'"
|
||||
:append-to-body="true"
|
||||
@close="handleClose"
|
||||
>
|
||||
<slot name="body" />
|
||||
@@ -39,10 +41,12 @@ interface IProps {
|
||||
title: string
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
width?: number
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL')
|
||||
cancelButtonText: $T('CANCEL'),
|
||||
width: 500
|
||||
})
|
||||
const $emit = defineEmits(['confirm', 'cancel', 'close'])
|
||||
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
<el-dialog
|
||||
v-model="showInputBoxVisible"
|
||||
:title="inputBoxOptions.title || $T('INPUT')"
|
||||
:modal-append-to-body="false"
|
||||
:append-to-body="true"
|
||||
:width="inputBoxOptions.width + 'px'"
|
||||
>
|
||||
<el-input
|
||||
v-model="inputBoxValue"
|
||||
:placeholder="inputBoxOptions.placeholder"
|
||||
:type="inputBoxOptions.inputType || 'text'"
|
||||
:rows="inputBoxOptions.inputType === 'textarea' ? 6 : undefined"
|
||||
:class="{ 'input-box__textarea': inputBoxOptions.inputType === 'textarea' }"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button
|
||||
@@ -38,7 +42,9 @@ const inputBoxValue = ref('')
|
||||
const showInputBoxVisible = ref(false)
|
||||
const inputBoxOptions = reactive({
|
||||
title: '',
|
||||
placeholder: ''
|
||||
placeholder: '',
|
||||
inputType: 'text' as 'text' | 'textarea',
|
||||
width: 500
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
@@ -54,6 +60,8 @@ function initInputBoxValue (options: IShowInputBoxOption) {
|
||||
inputBoxValue.value = options.value || ''
|
||||
inputBoxOptions.title = options.title || ''
|
||||
inputBoxOptions.placeholder = options.placeholder || ''
|
||||
inputBoxOptions.inputType = options.inputType || 'text'
|
||||
inputBoxOptions.width = options.width || 400
|
||||
showInputBoxVisible.value = true
|
||||
}
|
||||
|
||||
@@ -82,4 +90,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style lang='stylus'>
|
||||
.input-box__textarea
|
||||
.el-textarea__inner
|
||||
resize vertical
|
||||
max-height 240px
|
||||
</style>
|
||||
|
||||
@@ -89,6 +89,7 @@ import { ref } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const $form = ref<FormInstance>()
|
||||
|
||||
@@ -117,7 +118,6 @@ async function validate (): Promise<IStringKeyMap | false> {
|
||||
resolve(form.value)
|
||||
} else {
|
||||
resolve(false)
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
import { T as $T } from '@/i18n'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { showNotification } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
|
||||
interface IProps {
|
||||
@@ -50,11 +50,14 @@ const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const value = useVModel(props, 'modelValue')
|
||||
|
||||
const handleChange = (value: ISwitchValueType) => {
|
||||
saveConfig(`settings.${props.settingProps}`, value)
|
||||
const handleChange = async (value: ISwitchValueType) => {
|
||||
await saveConfig(`settings.${props.settingProps}`, value)
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
showNotification(props.label, $T('TIPS_SET_SUCCEED'))
|
||||
showNotification({
|
||||
title: props.label,
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
:placeholder="$T('CHOOSE_SHOWED_PICBED')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in picBed"
|
||||
v-for="item in visiblePicBedList"
|
||||
:key="item.type"
|
||||
:label="item.name"
|
||||
:value="item.type"
|
||||
@@ -203,6 +203,7 @@ import $$db from '@/utils/db'
|
||||
import GalleryToolbar from './components/gallery/GalleryToolbar.vue'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { getRawData } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
const images = ref<ImgInfo[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const imgInfo = reactive({
|
||||
@@ -229,6 +230,7 @@ const pasteStyleMap = {
|
||||
Custom: 'Custom'
|
||||
}
|
||||
const picBed = ref<IPicBedType[]>([])
|
||||
const visiblePicBedList = computed(() => picBed.value.filter(item => item.visible))
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
if (from.name === 'gallery') {
|
||||
clearSelectedList()
|
||||
@@ -279,6 +281,9 @@ const isAllSelected = computed(() => {
|
||||
|
||||
function getPicBeds (event: IpcRendererEvent, picBeds: IPicBedType[]) {
|
||||
picBed.value = picBeds
|
||||
if (selectedPicBed.value.length === 0) return
|
||||
const visibleTypes = new Set(picBeds.filter(item => item.visible).map(item => item.type))
|
||||
selectedPicBed.value = selectedPicBed.value.filter(type => visibleTypes.has(type))
|
||||
}
|
||||
|
||||
function getGallery (): IGalleryItem[] {
|
||||
@@ -375,10 +380,10 @@ async function copy (item: ImgInfo) {
|
||||
// sometimes will cause lagging
|
||||
// icon: item.url || item.imgUrl
|
||||
}
|
||||
const myNotification = new Notification(obj.title, obj)
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: obj.title,
|
||||
body: obj.body
|
||||
})
|
||||
}
|
||||
|
||||
function remove (id?: string) {
|
||||
@@ -395,10 +400,10 @@ function remove (id?: string) {
|
||||
title: $T('OPERATION_SUCCEED'),
|
||||
body: ''
|
||||
}
|
||||
const myNotification = new Notification(obj.title, obj)
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: obj.title,
|
||||
body: obj.body
|
||||
})
|
||||
updateGallery()
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
@@ -422,10 +427,10 @@ async function confirmModify () {
|
||||
body: imgInfo.imgUrl
|
||||
// icon: this.imgInfo.imgUrl
|
||||
}
|
||||
const myNotification = new Notification(obj.title, obj)
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: obj.title,
|
||||
body: obj.body
|
||||
})
|
||||
dialogVisible.value = false
|
||||
updateGallery()
|
||||
}
|
||||
@@ -478,10 +483,10 @@ function multiRemove () {
|
||||
body: ''
|
||||
}
|
||||
sendToMain('removeFiles', files)
|
||||
const myNotification = new Notification(obj.title, obj)
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: obj.title,
|
||||
body: obj.body
|
||||
})
|
||||
updateGallery()
|
||||
}).catch(() => {
|
||||
return true
|
||||
@@ -509,11 +514,11 @@ async function multiCopy () {
|
||||
title: $T('BATCH_COPY_LINK_SUCCEED'),
|
||||
body: copyString.join('\n')
|
||||
}
|
||||
const myNotification = new Notification(obj.title, obj)
|
||||
clipboard.writeText(copyString.join('\n'))
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: obj.title,
|
||||
body: obj.body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,16 @@
|
||||
// import mixin from '@/utils/mixin'
|
||||
// import { Component, Vue, Watch } from 'vue-property-decorator'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { ElMessage as $message } from 'element-plus'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import {
|
||||
ipcRenderer,
|
||||
IpcRendererEvent
|
||||
} from 'electron'
|
||||
import { onBeforeUnmount, onBeforeMount, ref, watch } from 'vue'
|
||||
import { SHOW_MINI_PAGE_MENU, SET_MINI_WINDOW_POS } from '~/universal/events/constants'
|
||||
import { LOG_INVALID_URL_LINES, SHOW_MINI_PAGE_MENU, SET_MINI_WINDOW_POS } from '~/universal/events/constants'
|
||||
import {
|
||||
isUrl
|
||||
isUrl,
|
||||
parseNewlineSeparatedUrls
|
||||
} from '~/universal/utils/common'
|
||||
import { sendToMain } from '@/utils/dataSender'
|
||||
import { getFilePath } from '@/utils/common'
|
||||
@@ -84,42 +85,78 @@ watch(progress, (val) => {
|
||||
}
|
||||
})
|
||||
|
||||
function onDrop (e: DragEvent) {
|
||||
async function onDrop (e: DragEvent) {
|
||||
dragover.value = false
|
||||
const items = e.dataTransfer?.items!
|
||||
const files = e.dataTransfer?.files!
|
||||
|
||||
// send files first
|
||||
if (files?.length) {
|
||||
ipcSendFiles(e.dataTransfer?.files!)
|
||||
} else {
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer!)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer!.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
sendToMain('uploadChoosedFiles', [{ path: str }])
|
||||
} else {
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const dataTransfer = e.dataTransfer
|
||||
if (!dataTransfer) return
|
||||
|
||||
const uriList = dataTransfer.getData('text/uri-list')
|
||||
if (uriList) {
|
||||
await handleUriListDrop(uriList, dataTransfer.getData('text/html'))
|
||||
return
|
||||
}
|
||||
|
||||
const plainText = dataTransfer.getData('text/plain')
|
||||
if (plainText) {
|
||||
await handlePlainTextDrop(plainText)
|
||||
return
|
||||
}
|
||||
|
||||
showNotification({
|
||||
title: $T('TIPS_ERROR'),
|
||||
body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')
|
||||
})
|
||||
}
|
||||
|
||||
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
sendToMain('uploadChoosedFiles', [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
} else {
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
async function uploadUrls (urls: string[], invalidLines: string[]) {
|
||||
if (invalidLines.length) {
|
||||
sendToMain(LOG_INVALID_URL_LINES, invalidLines)
|
||||
showNotification({
|
||||
title: $T('TIPS_WARNING'),
|
||||
body: $T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length })
|
||||
})
|
||||
}
|
||||
|
||||
sendToMain('uploadChoosedFiles', urls.map((url) => ({ path: url })))
|
||||
}
|
||||
|
||||
async function handlePlainTextDrop (plainText: string) {
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(plainText, { source: 'plain' })
|
||||
if (!urls.length) {
|
||||
showNotification({
|
||||
title: $T('TIPS_ERROR'),
|
||||
body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')
|
||||
})
|
||||
return
|
||||
}
|
||||
await uploadUrls(urls, invalidLines)
|
||||
}
|
||||
|
||||
async function handleUriListDrop (uriListText: string, urlString: string) {
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(uriListText, { source: 'uri-list' })
|
||||
if (urls.length) {
|
||||
await uploadUrls(urls, invalidLines)
|
||||
return
|
||||
}
|
||||
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch && isUrl(urlMatch[1])) {
|
||||
await uploadUrls([urlMatch[1]], invalidLines)
|
||||
return
|
||||
}
|
||||
|
||||
showNotification({
|
||||
title: $T('TIPS_ERROR'),
|
||||
body: $T('TIPS_DRAG_VALID_PICTURE_OR_URL')
|
||||
})
|
||||
}
|
||||
|
||||
function openUploadWindow () {
|
||||
|
||||
@@ -65,6 +65,7 @@ const form = reactive<ISettingForm>({
|
||||
rename: false,
|
||||
autoRename: false,
|
||||
uploadNotification: false,
|
||||
notificationSound: true,
|
||||
miniWindowOnTop: false,
|
||||
logLevel: ['all'],
|
||||
autoCopyUrl: true,
|
||||
@@ -102,6 +103,7 @@ async function initData () {
|
||||
form.rename = settings.rename || false
|
||||
form.autoRename = settings.autoRename || false
|
||||
form.uploadNotification = settings.uploadNotification || false
|
||||
form.notificationSound = settings.notificationSound === undefined ? true : settings.notificationSound
|
||||
form.miniWindowOnTop = settings.miniWindowOnTop || false
|
||||
form.logLevel = initLogLevel(settings.logLevel || [])
|
||||
form.autoCopyUrl = settings.autoCopyUrl === undefined ? true : settings.autoCopyUrl
|
||||
|
||||
@@ -214,6 +214,7 @@ import {
|
||||
} from '#/events/constants'
|
||||
import { computed, ref, onBeforeMount, onBeforeUnmount, watch } from 'vue'
|
||||
import { getConfig, saveConfig, sendRPC, sendToMain } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import { getRendererStaticFileUrl } from '@/utils/static'
|
||||
@@ -405,12 +406,11 @@ async function handleReload () {
|
||||
needReload: true
|
||||
})
|
||||
needReload.value = true
|
||||
const successNotification = new Notification($T('PLUGIN_UPDATE_SUCCEED'), {
|
||||
body: $T('TIPS_NEED_RELOAD')
|
||||
showNotification({
|
||||
title: $T('PLUGIN_UPDATE_SUCCEED'),
|
||||
body: $T('TIPS_NEED_RELOAD'),
|
||||
callback: reloadApp
|
||||
})
|
||||
successNotification.onclick = () => {
|
||||
reloadApp()
|
||||
}
|
||||
}
|
||||
|
||||
function cleanSearch () {
|
||||
@@ -437,12 +437,10 @@ async function handleConfirmConfig () {
|
||||
})
|
||||
break
|
||||
}
|
||||
const successNotification = new Notification($T('SETTINGS_RESULT'), {
|
||||
showNotification({
|
||||
title: $T('SETTINGS_RESULT'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
successNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getPluginList()
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import { PASTE_TEXT, OPEN_WINDOW } from '#/events/constants'
|
||||
import { IWindowList } from '#/types/enum'
|
||||
import { sendToMain } from '@/utils/dataSender'
|
||||
import { getRawData } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { IpcRendererEvent } from 'electron/renderer'
|
||||
|
||||
const files = ref<IResult<ImgInfo>[]>([])
|
||||
@@ -95,10 +96,10 @@ async function getData () {
|
||||
async function copyTheLink (item: ImgInfo) {
|
||||
notification.body = item.imgUrl!
|
||||
await ipcRenderer.invoke(PASTE_TEXT, getRawData(item))
|
||||
const myNotification = new Notification(notification.title, notification)
|
||||
myNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
showNotification({
|
||||
title: notification.title,
|
||||
body: notification.body
|
||||
})
|
||||
}
|
||||
|
||||
// function calcHeight (width: number, height: number): number {
|
||||
@@ -238,9 +239,14 @@ body::-webkit-scrollbar
|
||||
cursor not-allowed
|
||||
&__title
|
||||
text-align center
|
||||
width 100%
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
white-space normal
|
||||
word-break break-all
|
||||
display -webkit-box
|
||||
-webkit-box-orient vertical
|
||||
-webkit-line-clamp 2
|
||||
color #ddd
|
||||
font-size 14px
|
||||
margin-top 4px
|
||||
|
||||
+104
-39
@@ -109,16 +109,19 @@ import {
|
||||
IpcRendererEvent,
|
||||
ipcRenderer
|
||||
} from 'electron'
|
||||
import { ElMessage as $message } from 'element-plus'
|
||||
import { ElMessage as $message, ElMessageBox } from 'element-plus'
|
||||
import { onBeforeMount, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import {
|
||||
GET_PICBEDS,
|
||||
LOG_INVALID_URL_LINES,
|
||||
SHOW_INPUT_BOX,
|
||||
SHOW_INPUT_BOX_RESPONSE,
|
||||
SHOW_UPLOAD_PAGE_MENU
|
||||
} from '~/universal/events/constants'
|
||||
import {
|
||||
isUrl
|
||||
extractHttpUrlsFromText,
|
||||
isUrl,
|
||||
parseNewlineSeparatedUrls
|
||||
} from '~/universal/utils/common'
|
||||
const dragover = ref(false)
|
||||
const progress = ref(0)
|
||||
@@ -128,6 +131,7 @@ const pasteStyle = ref('')
|
||||
const picBed = ref<IPicBedType[]>([])
|
||||
const picBedName = ref('')
|
||||
const configName = ref('')
|
||||
const $confirm = ElMessageBox.confirm
|
||||
onBeforeMount(() => {
|
||||
ipcRenderer.on('uploadProgress', (event: IpcRendererEvent, _progress: number) => {
|
||||
if (_progress !== -1) {
|
||||
@@ -169,42 +173,88 @@ onBeforeUnmount(() => {
|
||||
ipcRenderer.removeListener(GET_PICBEDS, getPicBeds)
|
||||
})
|
||||
|
||||
function onDrop (e: DragEvent) {
|
||||
async function onDrop (e: DragEvent) {
|
||||
dragover.value = false
|
||||
const items = e.dataTransfer?.items!
|
||||
const files = e.dataTransfer?.files!
|
||||
|
||||
// send files first
|
||||
if (files?.length) {
|
||||
ipcSendFiles(e.dataTransfer?.files!)
|
||||
} else {
|
||||
if (items.length === 2 && items[0].type === 'text/uri-list') {
|
||||
handleURLDrag(items, e.dataTransfer!)
|
||||
} else if (items[0].type === 'text/plain') {
|
||||
const str = e.dataTransfer!.getData(items[0].type)
|
||||
if (isUrl(str)) {
|
||||
sendToMain('uploadChoosedFiles', [{ path: str }])
|
||||
} else {
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
return
|
||||
}
|
||||
|
||||
const dataTransfer = e.dataTransfer
|
||||
if (!dataTransfer) return
|
||||
|
||||
const uriList = dataTransfer.getData('text/uri-list')
|
||||
if (uriList) {
|
||||
await handleUriListDrop(uriList, dataTransfer.getData('text/html'))
|
||||
return
|
||||
}
|
||||
|
||||
const plainText = dataTransfer.getData('text/plain')
|
||||
if (plainText) {
|
||||
await handlePlainTextDrop(plainText)
|
||||
return
|
||||
}
|
||||
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
}
|
||||
|
||||
async function confirmLargeUrlBatch (count: number, onCancel?: () => void): Promise<boolean> {
|
||||
if (count <= 10) return true
|
||||
try {
|
||||
await $confirm(
|
||||
$T('TIPS_TOO_MANY_URLS_CONFIRM', { n: count }),
|
||||
$T('TIPS_WARNING'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL')
|
||||
}
|
||||
}
|
||||
)
|
||||
return true
|
||||
} catch (e) {
|
||||
onCancel?.()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function handleURLDrag (items: DataTransferItemList, dataTransfer: DataTransfer) {
|
||||
// text/html
|
||||
// Use this data to get a more precise URL
|
||||
const urlString = dataTransfer.getData(items[1].type)
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch) {
|
||||
sendToMain('uploadChoosedFiles', [
|
||||
{
|
||||
path: urlMatch[1]
|
||||
}
|
||||
])
|
||||
} else {
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
async function uploadUrls (urls: string[], invalidLines: string[], onCancel?: () => void) {
|
||||
if (invalidLines.length) {
|
||||
sendToMain(LOG_INVALID_URL_LINES, invalidLines)
|
||||
$message.warning($T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length }))
|
||||
}
|
||||
|
||||
const canUpload = await confirmLargeUrlBatch(urls.length, onCancel)
|
||||
if (!canUpload) return
|
||||
|
||||
sendToMain('uploadChoosedFiles', urls.map((url) => ({ path: url })))
|
||||
}
|
||||
|
||||
async function handlePlainTextDrop (plainText: string) {
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(plainText, { source: 'plain' })
|
||||
if (!urls.length) {
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
return
|
||||
}
|
||||
await uploadUrls(urls, invalidLines)
|
||||
}
|
||||
|
||||
async function handleUriListDrop (uriListText: string, urlString: string) {
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(uriListText, { source: 'uri-list' })
|
||||
if (urls.length) {
|
||||
await uploadUrls(urls, invalidLines)
|
||||
return
|
||||
}
|
||||
|
||||
const urlMatch = urlString.match(/<img.*src="(.*?)"/)
|
||||
if (urlMatch && isUrl(urlMatch[1])) {
|
||||
await uploadUrls([urlMatch[1]], invalidLines)
|
||||
return
|
||||
}
|
||||
|
||||
$message.error($T('TIPS_DRAG_VALID_PICTURE_OR_URL'))
|
||||
}
|
||||
|
||||
function openUploadWindow () {
|
||||
@@ -234,7 +284,7 @@ async function getPasteStyle () {
|
||||
pasteStyle.value = await getConfig('settings.pasteStyle') || 'markdown'
|
||||
}
|
||||
|
||||
function handlePasteStyleChange (val: string | number | boolean) {
|
||||
function handlePasteStyleChange (val: string | number | boolean | undefined) {
|
||||
saveConfig({
|
||||
'settings.pasteStyle': val
|
||||
})
|
||||
@@ -244,24 +294,39 @@ function uploadClipboardFiles () {
|
||||
sendToMain('uploadClipboardFilesFromUploadPage')
|
||||
}
|
||||
|
||||
async function uploadURLFiles () {
|
||||
const str = await navigator.clipboard.readText()
|
||||
function openUrlInputBox (value: string) {
|
||||
$bus.emit(SHOW_INPUT_BOX, {
|
||||
value: isUrl(str) ? str : '',
|
||||
value,
|
||||
title: $T('TIPS_INPUT_URL'),
|
||||
placeholder: $T('TIPS_HTTP_PREFIX')
|
||||
placeholder: $T('TIPS_HTTP_PREFIX'),
|
||||
inputType: 'textarea'
|
||||
})
|
||||
}
|
||||
|
||||
function handleInputBoxValue (val: string) {
|
||||
async function uploadURLFiles () {
|
||||
let str = ''
|
||||
try {
|
||||
str = await navigator.clipboard.readText()
|
||||
} catch (e) {}
|
||||
const urls = extractHttpUrlsFromText(str)
|
||||
openUrlInputBox(urls.join('\n'))
|
||||
}
|
||||
|
||||
async function handleInputBoxValue (val: string) {
|
||||
if (val === '') return
|
||||
if (isUrl(val)) {
|
||||
sendToMain('uploadChoosedFiles', [{
|
||||
path: val
|
||||
}])
|
||||
} else {
|
||||
$message.error($T('TIPS_INPUT_VALID_URL'))
|
||||
|
||||
const { urls, invalidLines } = parseNewlineSeparatedUrls(val, { source: 'plain' })
|
||||
if (!urls.length) {
|
||||
if (invalidLines.length) {
|
||||
sendToMain(LOG_INVALID_URL_LINES, invalidLines)
|
||||
$message.error($T('TIPS_SKIPPED_INVALID_URLS', { n: invalidLines.length }))
|
||||
return
|
||||
}
|
||||
$message.error($T('TIPS_NO_VALID_URLS'))
|
||||
return
|
||||
}
|
||||
|
||||
await uploadUrls(urls, invalidLines, () => openUrlInputBox(val))
|
||||
}
|
||||
|
||||
async function getDefaultPicBed () {
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
import { Delete, DocumentCopy, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { saveConfig, triggerRPC } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import dayjs from 'dayjs'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
@@ -207,12 +208,10 @@ function setDefaultPicBed (type: string) {
|
||||
})
|
||||
|
||||
store?.setDefaultPicBed(type)
|
||||
const successNotification = new Notification($T('SETTINGS_DEFAULT_PICBED'), {
|
||||
showNotification({
|
||||
title: $T('SETTINGS_DEFAULT_PICBED'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
successNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
<template>
|
||||
<div id="url-rewrite-page">
|
||||
<div class="view-title">
|
||||
{{ $T('SETTINGS_URL_REWRITE') }}
|
||||
</div>
|
||||
<el-row
|
||||
class="url-rewrite-list"
|
||||
justify="center"
|
||||
>
|
||||
<el-col
|
||||
:span="20"
|
||||
:offset="2"
|
||||
>
|
||||
<div class="flex mb-[12px] justify-between align-middle">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_HELP') }}
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
<el-icon class="mr-[4px]">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_ADD_RULE') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
class="url-rewrite-table-border"
|
||||
:data="rules"
|
||||
size="small"
|
||||
header-cell-class-name="url-rewrite-table-border"
|
||||
cell-class-name="url-rewrite-table-border"
|
||||
:empty-text="$T('URL_REWRITE_EMPTY')"
|
||||
>
|
||||
<el-table-column
|
||||
width="50px"
|
||||
:label="$T('URL_REWRITE_ORDER')"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
min-width="300px"
|
||||
:label="$T('URL_REWRITE_MATCH')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span class="font-mono text-[12px] break-all">
|
||||
{{ scope.row.match }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="80px"
|
||||
:label="$T('URL_REWRITE_FLAGS')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
v-if="scope.row.global"
|
||||
size="small"
|
||||
>
|
||||
G
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="scope.row.ignoreCase"
|
||||
size="small"
|
||||
class="ml-[4px]"
|
||||
>
|
||||
I
|
||||
</el-tag>
|
||||
<span v-if="!scope.row.global && !scope.row.ignoreCase">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="70px"
|
||||
:label="$T('URL_REWRITE_ENABLED')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.enable"
|
||||
@change="handleToggleEnable(scope.$index, $event as boolean)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="220px"
|
||||
:label="$T('URL_REWRITE_ACTIONS')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-row class="mb-[2px]">
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="scope.$index === 0"
|
||||
@click="moveRule(scope.$index, scope.$index - 1)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_MOVE_UP') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="scope.$index === rules.length - 1"
|
||||
@click="moveRule(scope.$index, scope.$index + 1)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_MOVE_DOWN') }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="openEditDialog(scope.row, scope.$index)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_EDIT') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="danger"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="confirmDelete(scope.$index)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_DELETE') }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] rounded-[8px] url-rewrite-panel p-[12px]">
|
||||
<div class="text-[14px] mb-[8px] text-[#bbb] font-bold">
|
||||
{{ $T('URL_REWRITE_PREVIEW_TITLE') }}
|
||||
</div>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mb-[10px]">
|
||||
{{ $T('URL_REWRITE_PREVIEW_TIPS') }}
|
||||
</div>
|
||||
|
||||
<el-row
|
||||
class="mb-[10px]"
|
||||
:gutter="10"
|
||||
>
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
v-model="previewInputUrl"
|
||||
:placeholder="$T('URL_REWRITE_PREVIEW_PLACEHOLDER')"
|
||||
size="small"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@click="runPreview"
|
||||
>
|
||||
{{ $T('URL_REWRITE_PREVIEW_RUN') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-alert
|
||||
v-if="previewStatus === 'error'"
|
||||
:title="previewMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="previewStatus === 'matched'"
|
||||
:title="previewMessage"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="previewStatus === 'noMatch'"
|
||||
:title="previewMessage"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="previewStatus !== 'idle'"
|
||||
class="mt-[10px]"
|
||||
>
|
||||
<div class="text-[12px] text-[#bbb] mb-[6px]">
|
||||
{{ $T('URL_REWRITE_PREVIEW_OUTPUT') }}
|
||||
</div>
|
||||
<div class="rounded-[6px] url-rewrite-mono-box p-[10px] font-mono text-[12px] break-all text-[#bbb]">
|
||||
{{ previewOutputUrl }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
:title="editDialogTitle"
|
||||
width="500px"
|
||||
:append-to-body="true"
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
label-width="80px"
|
||||
size="small"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_MATCH')"
|
||||
>
|
||||
<div class="flex flex-col gap-[6px] w-full">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_MATCH_TIPS') }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="ruleForm.match"
|
||||
class="align-center"
|
||||
:autofocus="true"
|
||||
:placeholder="$T('URL_REWRITE_MATCH_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_REPLACE')"
|
||||
>
|
||||
<div class="flex flex-col gap-[6px] w-full">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_REPLACE_TIPS') }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="ruleForm.replace"
|
||||
class="align-center"
|
||||
:placeholder="$T('URL_REWRITE_REPLACE_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_OPTIONS')"
|
||||
>
|
||||
<div class="flex flex-col gap-[10px] w-full">
|
||||
<el-row
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
>
|
||||
<div class="text-[13px]">
|
||||
{{ $T('URL_REWRITE_RULE_ENABLED') }}
|
||||
</div>
|
||||
<el-switch v-model="ruleForm.enable" />
|
||||
</el-row>
|
||||
|
||||
<div class="grid grid-cols-2 gap-[12px]">
|
||||
<div class="rounded-[6px] url-rewrite-option-card p-[10px]">
|
||||
<el-checkbox
|
||||
v-model="ruleForm.global"
|
||||
>
|
||||
{{ $T('URL_REWRITE_FLAG_GLOBAL_LABEL') }}
|
||||
</el-checkbox>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mt-[6px]">
|
||||
{{ $T('URL_REWRITE_FLAG_GLOBAL_DESC') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[6px] url-rewrite-option-card p-[10px]">
|
||||
<el-checkbox
|
||||
v-model="ruleForm.ignoreCase"
|
||||
>
|
||||
{{ $T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL') }}
|
||||
</el-checkbox>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mt-[6px]">
|
||||
{{ $T('URL_REWRITE_FLAG_IGNORE_CASE_DESC') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button
|
||||
round
|
||||
@click="cancelEditDialog"
|
||||
>
|
||||
{{ $T('CANCEL') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
@click="confirmEditDialog"
|
||||
>
|
||||
{{ $T('CONFIRM') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowDown, ArrowUp, Delete, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage as $message, ElMessageBox } from 'element-plus'
|
||||
import { computed, onBeforeMount, reactive, ref } from 'vue'
|
||||
import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { T as $T } from '@/i18n'
|
||||
|
||||
interface IUrlRewriteRule {
|
||||
match: string
|
||||
replace: string
|
||||
enable: boolean
|
||||
global: boolean
|
||||
ignoreCase: boolean
|
||||
}
|
||||
|
||||
const $confirm = ElMessageBox.confirm
|
||||
|
||||
const rules = ref<IUrlRewriteRule[]>([])
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const editDialogMode = ref<'add' | 'edit'>('add')
|
||||
const editRuleIndex = ref(-1)
|
||||
|
||||
const previewInputUrl = ref('')
|
||||
const previewOutputUrl = ref('')
|
||||
const previewStatus = ref<'idle' | 'matched' | 'noMatch' | 'error'>('idle')
|
||||
const previewMessage = ref('')
|
||||
|
||||
const ruleForm = reactive<IUrlRewriteRule>({
|
||||
match: '',
|
||||
replace: '',
|
||||
enable: true,
|
||||
global: false,
|
||||
ignoreCase: false
|
||||
})
|
||||
|
||||
const editDialogTitle = computed(() => {
|
||||
return editDialogMode.value === 'add' ? $T('URL_REWRITE_ADD_RULE') : $T('URL_REWRITE_EDIT_RULE')
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initRules()
|
||||
})
|
||||
|
||||
async function initRules () {
|
||||
const configRules = await getConfig<unknown>('settings.urlRewrite.rules')
|
||||
rules.value = normalizeRules(configRules)
|
||||
}
|
||||
|
||||
function normalizeRules (value: unknown): IUrlRewriteRule[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map(item => {
|
||||
const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>
|
||||
return {
|
||||
match: String(raw.match ?? ''),
|
||||
replace: String(raw.replace ?? ''),
|
||||
enable: raw.enable === false ? false : true,
|
||||
global: raw.global === true,
|
||||
ignoreCase: raw.ignoreCase === true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function persistRules () {
|
||||
try {
|
||||
await saveConfig('settings.urlRewrite.rules', rules.value)
|
||||
} catch (e) {
|
||||
$message.error($T('OPERATION_FAILED'))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnable (index: number, value: boolean) {
|
||||
if (!rules.value[index]) return
|
||||
rules.value[index].enable = value
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
async function moveRule (fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex) return
|
||||
if (toIndex < 0 || toIndex >= rules.value.length) return
|
||||
const next = [...rules.value]
|
||||
const [item] = next.splice(fromIndex, 1)
|
||||
next.splice(toIndex, 0, item)
|
||||
rules.value = next
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
async function confirmDelete (index: number) {
|
||||
if (!rules.value[index]) return
|
||||
try {
|
||||
await $confirm($T('URL_REWRITE_DELETE_CONFIRM'), $T('TIPS_WARNING'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL')
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
rules.value.splice(index, 1)
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
function openAddDialog () {
|
||||
editDialogMode.value = 'add'
|
||||
editRuleIndex.value = -1
|
||||
ruleForm.match = ''
|
||||
ruleForm.replace = ''
|
||||
ruleForm.enable = true
|
||||
ruleForm.global = false
|
||||
ruleForm.ignoreCase = false
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog (rule: IUrlRewriteRule, index: number) {
|
||||
editDialogMode.value = 'edit'
|
||||
editRuleIndex.value = index
|
||||
ruleForm.match = rule.match
|
||||
ruleForm.replace = rule.replace
|
||||
ruleForm.enable = rule.enable
|
||||
ruleForm.global = rule.global
|
||||
ruleForm.ignoreCase = rule.ignoreCase
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
function cancelEditDialog () {
|
||||
editDialogVisible.value = false
|
||||
}
|
||||
|
||||
function buildRuleFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>) {
|
||||
return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`
|
||||
}
|
||||
|
||||
function validateRuleOrThrow (rule: IUrlRewriteRule) {
|
||||
if (!rule.match.trim()) {
|
||||
throw new Error($T('URL_REWRITE_MATCH_REQUIRED'))
|
||||
}
|
||||
if (!rule.replace.trim()) {
|
||||
throw new Error($T('URL_REWRITE_REPLACE_REQUIRED'))
|
||||
}
|
||||
new RegExp(rule.match, buildRuleFlags(rule))
|
||||
}
|
||||
|
||||
function runPreview () {
|
||||
previewStatus.value = 'idle'
|
||||
previewMessage.value = ''
|
||||
previewOutputUrl.value = previewInputUrl.value
|
||||
|
||||
const url = previewInputUrl.value
|
||||
if (!url) {
|
||||
previewStatus.value = 'error'
|
||||
previewMessage.value = $T('URL_REWRITE_PREVIEW_INPUT_REQUIRED')
|
||||
return
|
||||
}
|
||||
|
||||
for (const [index, rule] of rules.value.entries()) {
|
||||
if (rule.enable === false) continue
|
||||
let regexp: RegExp
|
||||
try {
|
||||
regexp = new RegExp(rule.match, buildRuleFlags(rule))
|
||||
} catch (e) {
|
||||
previewStatus.value = 'error'
|
||||
previewMessage.value = `${$T('URL_REWRITE_PREVIEW_RULE_INVALID')} #${index + 1}: ${(e as Error).message}`
|
||||
return
|
||||
}
|
||||
|
||||
const matched = regexp.test(url)
|
||||
regexp.lastIndex = 0
|
||||
if (!matched) continue
|
||||
|
||||
const output = url.replace(regexp, rule.replace)
|
||||
previewOutputUrl.value = output
|
||||
previewStatus.value = 'matched'
|
||||
previewMessage.value = `${$T('URL_REWRITE_PREVIEW_MATCHED_RULE')} #${index + 1}`
|
||||
return
|
||||
}
|
||||
|
||||
previewStatus.value = 'noMatch'
|
||||
previewMessage.value = $T('URL_REWRITE_PREVIEW_NO_MATCH')
|
||||
}
|
||||
|
||||
async function confirmEditDialog () {
|
||||
const nextRule: IUrlRewriteRule = {
|
||||
match: ruleForm.match,
|
||||
replace: ruleForm.replace,
|
||||
enable: ruleForm.enable,
|
||||
global: ruleForm.global,
|
||||
ignoreCase: ruleForm.ignoreCase
|
||||
}
|
||||
|
||||
try {
|
||||
validateRuleOrThrow(nextRule)
|
||||
} catch (e) {
|
||||
$message.error((e as Error).message || $T('URL_REWRITE_INVALID_REGEX'))
|
||||
return
|
||||
}
|
||||
|
||||
if (editDialogMode.value === 'add') {
|
||||
rules.value.push(nextRule)
|
||||
} else if (editRuleIndex.value >= 0 && rules.value[editRuleIndex.value]) {
|
||||
rules.value.splice(editRuleIndex.value, 1, nextRule)
|
||||
}
|
||||
|
||||
await persistRules()
|
||||
editDialogVisible.value = false
|
||||
$message.success($T('TIPS_SET_SUCCEED'))
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UrlRewritePage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
#url-rewrite-page
|
||||
.url-rewrite-list
|
||||
height 360px
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
overflow-x hidden
|
||||
width 100%
|
||||
.url-rewrite-panel
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid darken(#eee, 50%)
|
||||
.url-rewrite-option-card
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid rgba(255, 255, 255, .06)
|
||||
.url-rewrite-mono-box
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid rgba(255, 255, 255, .06)
|
||||
.url-rewrite-table-border
|
||||
border-color darken(#eee, 50%)
|
||||
.el-checkbox__label
|
||||
color #aaa
|
||||
.el-table
|
||||
background-color: transparent
|
||||
color #ddd
|
||||
&::before
|
||||
background-color darken(#eee, 50%)
|
||||
thead
|
||||
color #bbb
|
||||
th,tr
|
||||
background-color: transparent
|
||||
&__body
|
||||
tr.el-table__row--striped
|
||||
td
|
||||
background transparent
|
||||
&--enable-row-hover
|
||||
.el-table__body
|
||||
tr:hover
|
||||
&>td
|
||||
background #333
|
||||
.el-button+.el-button
|
||||
margin-left 4px
|
||||
.el-button
|
||||
&.danger
|
||||
color: #F56C6C
|
||||
</style>
|
||||
@@ -14,6 +14,11 @@
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@click="goShortCutPage"
|
||||
/>
|
||||
<ButtonFormItem
|
||||
:label="$T('SETTINGS_URL_REWRITE')"
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@click="goUrlRewritePage"
|
||||
/>
|
||||
<ButtonFormItem
|
||||
:label="$T('SETTINGS_CUSTOM_LINK_FORMAT')"
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@@ -72,7 +77,7 @@ import { reactive, ref } from 'vue'
|
||||
import { PICGO_OPEN_FILE } from '~/universal/events/constants'
|
||||
import LogSettingDialog from './LogSettingDialog.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SHORTKEY_PAGE } from '@/router/config'
|
||||
import { SHORTKEY_PAGE, URL_REWRITE_PAGE } from '@/router/config'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
|
||||
const $router = useRouter()
|
||||
@@ -103,6 +108,12 @@ function goShortCutPage () {
|
||||
})
|
||||
}
|
||||
|
||||
function goUrlRewritePage () {
|
||||
$router.push({
|
||||
name: URL_REWRITE_PAGE
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
import { T as $T } from '@/i18n'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { openFile, showNotification } from '@/utils/common'
|
||||
import { openFile } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { ElMessage as $message } from 'element-plus'
|
||||
import { useVModelValues } from '@/hooks/useVModelValues'
|
||||
|
||||
@@ -121,7 +122,10 @@ function confirmLogLevelSetting () {
|
||||
'settings.logLevel': form.logLevel,
|
||||
'settings.logFileSizeLimit': form.logFileSizeLimit
|
||||
})
|
||||
showNotification($T('SETTINGS_SET_LOG_FILE'), $T('TIPS_SET_SUCCEED'))
|
||||
showNotification({
|
||||
title: $T('SETTINGS_SET_LOG_FILE'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
updateProps()
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ import { T as $T } from '@/i18n'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { saveConfig } from '@/utils/dataSender'
|
||||
import { useVModelValues } from '@/hooks/useVModelValues'
|
||||
import { showNotification } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
|
||||
interface IProps {
|
||||
modelValue: boolean
|
||||
@@ -82,7 +82,10 @@ function confirmProxy () {
|
||||
'settings.npmProxy': form.npmProxy,
|
||||
'settings.npmRegistry': form.npmRegistry
|
||||
})
|
||||
showNotification($T('SETTINGS_SET_PROXY_AND_MIRROR'), $T('TIPS_SET_SUCCEED'))
|
||||
showNotification({
|
||||
title: $T('SETTINGS_SET_PROXY_AND_MIRROR'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
updateProps()
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ import { T as $T } from '@/i18n'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { useVModelValues } from '@/hooks/useVModelValues'
|
||||
import { saveConfig, sendToMain } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/common'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
|
||||
interface IProps {
|
||||
modelValue: boolean
|
||||
@@ -86,7 +86,10 @@ function confirmServerSetting () {
|
||||
saveConfig({
|
||||
'settings.server': form
|
||||
})
|
||||
showNotification($T('SETTINGS_SET_PICGO_SERVER'), $T('TIPS_SET_SUCCEED'))
|
||||
showNotification({
|
||||
title: $T('SETTINGS_SET_PICGO_SERVER'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
dialogVisible.value = false
|
||||
sendToMain('updateServer')
|
||||
updateProps()
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
setting-props="uploadNotification"
|
||||
:label="$T('SETTINGS_OPEN_UPLOAD_TIPS')"
|
||||
/>
|
||||
<SwitchFormItem
|
||||
v-model="form.notificationSound"
|
||||
setting-props="notificationSound"
|
||||
:label="$T('SETTINGS_NOTIFICATION_SOUND')"
|
||||
/>
|
||||
<SwitchFormItem
|
||||
v-if="os !== 'darwin'"
|
||||
v-model="form.miniWindowOnTop"
|
||||
|
||||
@@ -46,6 +46,7 @@ import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { ref, onBeforeMount } from 'vue'
|
||||
import { T as $T } from '@/i18n/index'
|
||||
import { sendToMain, triggerRPC } from '@/utils/dataSender'
|
||||
import { showNotification } from '@/utils/notification'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ConfigForm from '@/components/ConfigForm.vue'
|
||||
// import mixin from '@/utils/ConfirmButtonMixin'
|
||||
@@ -72,12 +73,10 @@ const handleConfirm = async () => {
|
||||
const result = (await $configForm.value?.validate()) || false
|
||||
if (result !== false) {
|
||||
await triggerRPC<void>(IRPCActionType.UPDATE_UPLOADER_CONFIG, type.value, result?._id, result)
|
||||
const successNotification = new Notification($T('SETTINGS_RESULT'), {
|
||||
showNotification({
|
||||
title: $T('SETTINGS_RESULT'),
|
||||
body: $T('TIPS_SET_SUCCEED')
|
||||
})
|
||||
successNotification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
$router.back()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ export const PICBEDS_PAGE = 'PicbedsPage'
|
||||
export const SETTING_PAGE = 'SettingPage'
|
||||
export const PLUGIN_PAGE = 'PluginPage'
|
||||
export const SHORTKEY_PAGE = 'ShortkeyPage'
|
||||
export const URL_REWRITE_PAGE = 'UrlRewritePage'
|
||||
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
||||
export const TOOLBOX_CONFIG_PAGE = 'ToolBoxPage'
|
||||
|
||||
@@ -57,6 +57,11 @@ export default createRouter({
|
||||
component: () => import(/* webpackChunkName: "ShortkeyPage" */ '@/pages/ShortKey.vue'),
|
||||
name: config.SHORTKEY_PAGE
|
||||
},
|
||||
{
|
||||
path: 'urlRewrite',
|
||||
component: () => import(/* webpackChunkName: "UrlRewritePage" */ '@/pages/UrlRewrite.vue'),
|
||||
name: config.URL_REWRITE_PAGE
|
||||
},
|
||||
{
|
||||
path: 'uploader-config-page/:type',
|
||||
component: () => import(/* webpackChunkName: "Other" */ '@/pages/UploaderConfigPage.vue'),
|
||||
|
||||
@@ -24,13 +24,13 @@ ipcRenderer.on(TALKING_DATA_EVENT, (_, data: ITalkingDataOptions) => {
|
||||
// 0:ANONYMOUS,匿名账号;
|
||||
// 1:REGISTERED,自有帐户显性注册;
|
||||
ipcRenderer.on(TALKING_DATA_DEVICE_ID_EVENT, (_, deviceId: string) => {
|
||||
window.TDAPP.register({
|
||||
window.TDAPP.register({
|
||||
profileId: deviceId,
|
||||
profileType: 1
|
||||
})
|
||||
window.TDAPP.login({
|
||||
})
|
||||
window.TDAPP.login({
|
||||
profileId: deviceId,
|
||||
profileType: 1
|
||||
})
|
||||
ipcRenderer.send(REGISTER_DEVICE_ID)
|
||||
});
|
||||
})
|
||||
ipcRenderer.send(REGISTER_DEVICE_ID)
|
||||
})
|
||||
|
||||
@@ -7,11 +7,7 @@ import {
|
||||
|
||||
type IEvent ={
|
||||
[SHOW_INPUT_BOX_RESPONSE]: string
|
||||
[SHOW_INPUT_BOX]: {
|
||||
value: string
|
||||
title: string
|
||||
placeholder: string
|
||||
},
|
||||
[SHOW_INPUT_BOX]: IShowInputBoxOption,
|
||||
[FORCE_UPDATE]: void
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,14 @@ export const trimValues = (obj: IStringKeyMap) => {
|
||||
* get raw data from reactive or ref
|
||||
*/
|
||||
export const getRawData = (args: any): any => {
|
||||
if (args === null) return args
|
||||
if (Array.isArray(args)) {
|
||||
const data = args.map((item: any) => {
|
||||
if (isRef(item)) {
|
||||
return unref(item)
|
||||
return getRawData(unref(item))
|
||||
}
|
||||
if (isReactive(item)) {
|
||||
return toRaw(item)
|
||||
return getRawData(toRaw(item))
|
||||
}
|
||||
return getRawData(item)
|
||||
})
|
||||
@@ -42,9 +43,9 @@ export const getRawData = (args: any): any => {
|
||||
Object.keys(args).forEach(key => {
|
||||
const item = args[key]
|
||||
if (isRef(item)) {
|
||||
data[key] = unref(item)
|
||||
data[key] = getRawData(unref(item))
|
||||
} else if (isReactive(item)) {
|
||||
data[key] = toRaw(item)
|
||||
data[key] = getRawData(toRaw(item))
|
||||
} else {
|
||||
data[key] = getRawData(item)
|
||||
}
|
||||
@@ -54,15 +55,6 @@ export const getRawData = (args: any): any => {
|
||||
return args
|
||||
}
|
||||
|
||||
export const showNotification = (title: string, body: string) => {
|
||||
const notification = new Notification(title, {
|
||||
body
|
||||
})
|
||||
notification.onclick = () => {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const openFile = (fileName: string) => {
|
||||
sendToMain(PICGO_OPEN_FILE, fileName)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import { v4 as uuid } from 'uuid'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
import { getRawData } from './common'
|
||||
|
||||
export function saveConfig (_config: IObj | string, value?: any) {
|
||||
export async function saveConfig (_config: IObj | string, value?: any) {
|
||||
let config
|
||||
if (typeof _config === 'string') {
|
||||
config = {
|
||||
[_config]: value
|
||||
[_config]: getRawData(value)
|
||||
}
|
||||
} else {
|
||||
config = getRawData(_config)
|
||||
}
|
||||
ipcRenderer.send(PICGO_SAVE_CONFIG, config)
|
||||
await ipcRenderer.invoke(PICGO_SAVE_CONFIG, config)
|
||||
}
|
||||
|
||||
export function getConfig<T> (key?: string): Promise<T | undefined> {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { sendRPC } from './dataSender'
|
||||
import { PICGO_NOTIFICATION_CLICKED } from '~/universal/events/constants'
|
||||
import { IRPCActionType } from '~/universal/types/enum'
|
||||
|
||||
const notificationCallbacks = new Map<string, () => void>()
|
||||
const MAX_CALLBACK_LIMIT = 10
|
||||
|
||||
const handleNotificationClick = (_event: Electron.IpcRendererEvent, id: string) => {
|
||||
const callback = notificationCallbacks.get(id)
|
||||
if (!callback) return
|
||||
try {
|
||||
callback()
|
||||
} finally {
|
||||
notificationCallbacks.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
// HMR Protection: Remove existing listener before adding a new one
|
||||
ipcRenderer.removeAllListeners(PICGO_NOTIFICATION_CLICKED)
|
||||
ipcRenderer.on(PICGO_NOTIFICATION_CLICKED, handleNotificationClick)
|
||||
|
||||
interface NotificationOptions {
|
||||
title: string
|
||||
body: string
|
||||
callback?: () => void
|
||||
}
|
||||
|
||||
export const showNotification = (options: NotificationOptions) => {
|
||||
const id = uuid()
|
||||
|
||||
if (options.callback) {
|
||||
if (notificationCallbacks.size >= MAX_CALLBACK_LIMIT) {
|
||||
const oldestId = notificationCallbacks.keys().next().value
|
||||
if (oldestId) {
|
||||
notificationCallbacks.delete(oldestId)
|
||||
}
|
||||
}
|
||||
notificationCallbacks.set(id, options.callback)
|
||||
}
|
||||
|
||||
sendRPC(IRPCActionType.SHOW_NOTIFICATION, options.title, options.body, id)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export const SHOW_INPUT_BOX = 'SHOW_INPUT_BOX'
|
||||
export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE'
|
||||
export const LOG_INVALID_URL_LINES = 'LOG_INVALID_URL_LINES'
|
||||
export const TOGGLE_SHORTKEY_MODIFIED_MODE = 'TOGGLE_SHORTKEY_MODIFIED_MODE'
|
||||
export const TALKING_DATA_APPID = '7E6832BCE3F1438696579E541DFEBFDA'
|
||||
export const TALKING_DATA_EVENT = 'TALKING_DATA_EVENT'
|
||||
@@ -37,6 +38,7 @@ export const FORCE_UPDATE = 'FORCE_UPDATE'
|
||||
export const OPEN_WINDOW = 'OPEN_WINDOW'
|
||||
export const GET_PICBEDS = 'GET_PICBEDS'
|
||||
export const RPC_ACTIONS = 'RPC_ACTIONS'
|
||||
export const PICGO_NOTIFICATION_CLICKED = 'PICGO_NOTIFICATION_CLICKED'
|
||||
export const GET_PICBED_CONFIG = 'GET_PICBED_CONFIG'
|
||||
export const REGISTER_DEVICE_ID = 'REGISTER_DEVICE_ID'
|
||||
// i18n
|
||||
|
||||
@@ -74,6 +74,7 @@ export enum IRPCActionType {
|
||||
COPY_TEXT = 'COPY_TEXT',
|
||||
SHOW_DOCK_ICON = 'SHOW_DOCK_ICON',
|
||||
SHOW_MENUBAR_ICON = 'SHOW_MENUBAR_ICON',
|
||||
SHOW_NOTIFICATION = 'SHOW_NOTIFICATION',
|
||||
|
||||
// gallery and toolbox rpc
|
||||
UPDATE_GALLERY = 'UPDATE_GALLERY',
|
||||
|
||||
Vendored
+55
-5
@@ -63,6 +63,7 @@ interface ILocales {
|
||||
SETTINGS_CLICK_TO_SET: string
|
||||
SETTINGS_CLICK_TO_CHECK: string
|
||||
SETTINGS_SET_SHORTCUT: string
|
||||
SETTINGS_URL_REWRITE: string
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: string
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: string
|
||||
SETTINGS_SET_SERVER: string
|
||||
@@ -75,6 +76,7 @@ interface ILocales {
|
||||
SETTINGS_RENAME_BEFORE_UPLOAD: string
|
||||
SETTINGS_TIMESTAMP_RENAME: string
|
||||
SETTINGS_OPEN_UPLOAD_TIPS: string
|
||||
SETTINGS_NOTIFICATION_SOUND: string
|
||||
SETTINGS_MINI_WINDOW_ON_TOP: string
|
||||
SETTINGS_AUTO_COPY_URL_AFTER_UPLOAD: string
|
||||
SETTINGS_TIPS_PLACEHOLDER_URL: string
|
||||
@@ -112,6 +114,43 @@ interface ILocales {
|
||||
SETTINGS_CHOOSE_LANGUAGE: string
|
||||
BUILTIN_CLIPBOARD_TIPS: string
|
||||
UPLOADER_CONFIG_NAME: string
|
||||
URL_REWRITE_HELP: string
|
||||
URL_REWRITE_ADD_RULE: string
|
||||
URL_REWRITE_EDIT_RULE: string
|
||||
URL_REWRITE_EMPTY: string
|
||||
URL_REWRITE_ORDER: string
|
||||
URL_REWRITE_MATCH: string
|
||||
URL_REWRITE_REPLACE: string
|
||||
URL_REWRITE_FLAGS: string
|
||||
URL_REWRITE_ENABLED: string
|
||||
URL_REWRITE_ACTIONS: string
|
||||
URL_REWRITE_MOVE_UP: string
|
||||
URL_REWRITE_MOVE_DOWN: string
|
||||
URL_REWRITE_EDIT: string
|
||||
URL_REWRITE_DELETE: string
|
||||
URL_REWRITE_DELETE_CONFIRM: string
|
||||
URL_REWRITE_MATCH_TIPS: string
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: string
|
||||
URL_REWRITE_REPLACE_TIPS: string
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: string
|
||||
URL_REWRITE_OPTIONS: string
|
||||
URL_REWRITE_RULE_ENABLED: string
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: string
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: string
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: string
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: string
|
||||
URL_REWRITE_MATCH_REQUIRED: string
|
||||
URL_REWRITE_REPLACE_REQUIRED: string
|
||||
URL_REWRITE_INVALID_REGEX: string
|
||||
URL_REWRITE_PREVIEW_TITLE: string
|
||||
URL_REWRITE_PREVIEW_TIPS: string
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: string
|
||||
URL_REWRITE_PREVIEW_RUN: string
|
||||
URL_REWRITE_PREVIEW_OUTPUT: string
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: string
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: string
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: string
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: string
|
||||
UPLOADER_CONFIG_PLACEHOLDER: string
|
||||
SELECTED_SETTING_HINT: string
|
||||
SETTINGS_ENCODE_OUTPUT_URL: string
|
||||
@@ -133,11 +172,19 @@ interface ILocales {
|
||||
SHORTCUT_DISABLE: string
|
||||
SHORTCUT_EDIT: string
|
||||
SHORTCUT_CHANGE_UPLOAD: string
|
||||
CHANGE_IMAGE_URL_HOST: string
|
||||
NEW_IMAGE_URL_HOST: string
|
||||
SELECTED_IMAGE_URL_HOST: string
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: string
|
||||
CHANGE_IMAGE_URL_HOST_WARN: string
|
||||
GALLERY_URL_REWRITE_TITLE: string
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: string
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: string
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: string
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: string
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: string
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: string
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: string
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: string
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: string
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: string
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: string
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: string
|
||||
WAIT_TO_UPLOAD: string
|
||||
ALREADY_UPLOAD: string
|
||||
PICTURE_UPLOAD: string
|
||||
@@ -206,6 +253,9 @@ interface ILocales {
|
||||
TIPS_NOTICE: string
|
||||
TIPS_WARNING: string
|
||||
TIPS_ERROR: string
|
||||
TIPS_SKIPPED_INVALID_URLS: string
|
||||
TIPS_TOO_MANY_URLS_CONFIRM: string
|
||||
TIPS_NO_VALID_URLS: string
|
||||
TIPS_INSTALL_NODE_AND_RELOAD_PICGO: string
|
||||
TIPS_PLUGIN_REMOVE_GALLERY_ITEM: string
|
||||
TIPS_PLUGIN_OVERWRITE_GALLERY: string
|
||||
|
||||
Vendored
+1
@@ -10,6 +10,7 @@ type IOpenFileArgs = [filePath: string]
|
||||
type ICopyTextArgs = [text: string]
|
||||
type IShowDockIconArgs = [visible: boolean]
|
||||
type IShowMenubarIconArgs = [visible: boolean]
|
||||
type IShowNotificationArgs = [title: string, body: string, id?: string]
|
||||
type IGetGalleryMenuListArgs = [selectedList: IGalleryItem[]]
|
||||
|
||||
interface IRPCServer {
|
||||
|
||||
Vendored
+16
-3
@@ -55,6 +55,7 @@ interface ImgInfo {
|
||||
imgUrl?: string
|
||||
id?: string
|
||||
type?: string
|
||||
originImgUrl?: string
|
||||
[propName: string]: any
|
||||
}
|
||||
|
||||
@@ -183,6 +184,10 @@ interface IPicGoPluginShowConfigDialogOption {
|
||||
tips?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
/**
|
||||
* default to 500
|
||||
*/
|
||||
width?: number
|
||||
}
|
||||
|
||||
interface IPicGoPluginOriginConfig {
|
||||
@@ -247,6 +252,11 @@ interface IShowInputBoxOption {
|
||||
value?: string
|
||||
title: string
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'textarea'
|
||||
/**
|
||||
* default to 400
|
||||
*/
|
||||
width?: number
|
||||
}
|
||||
|
||||
type IShowFileExplorerOption = IObj
|
||||
@@ -256,7 +266,10 @@ type IUploadOption = string[] | ImgInfo[]
|
||||
interface IShowNotificationOption {
|
||||
title: string
|
||||
body: string
|
||||
// icon?: string | import('electron').NativeImage
|
||||
/**
|
||||
* will log text
|
||||
*/
|
||||
text?: string
|
||||
}
|
||||
|
||||
interface IPrivateShowNotificationOption extends IShowNotificationOption{
|
||||
@@ -265,7 +278,7 @@ interface IPrivateShowNotificationOption extends IShowNotificationOption{
|
||||
*/
|
||||
clickToCopy?: boolean
|
||||
copyContent?: string // something to copy
|
||||
clickFn?: () => void
|
||||
callback?: () => void
|
||||
}
|
||||
|
||||
interface IShowMessageBoxOption {
|
||||
@@ -356,7 +369,7 @@ type ILoggerType = string | Error | boolean | number | undefined
|
||||
interface IAppNotification {
|
||||
title: string
|
||||
body: string
|
||||
icon?: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
interface ITalkingDataOptions {
|
||||
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ interface ISettingForm {
|
||||
rename: boolean
|
||||
autoRename: boolean
|
||||
uploadNotification: boolean
|
||||
notificationSound: boolean
|
||||
miniWindowOnTop: boolean
|
||||
logLevel: string[]
|
||||
autoCopyUrl: boolean
|
||||
|
||||
@@ -1,4 +1,92 @@
|
||||
export const isUrl = (url: string): boolean => (url.startsWith('http://') || url.startsWith('https://'))
|
||||
|
||||
export interface IParseNewlineSeparatedUrlsResult {
|
||||
urls: string[]
|
||||
invalidLines: string[]
|
||||
}
|
||||
|
||||
export interface IParseNewlineSeparatedUrlsOptions {
|
||||
source?: 'plain' | 'uri-list'
|
||||
}
|
||||
|
||||
export const parseNewlineSeparatedUrls = (
|
||||
input: string,
|
||||
options: IParseNewlineSeparatedUrlsOptions = {}
|
||||
): IParseNewlineSeparatedUrlsResult => {
|
||||
const source = options.source || 'plain'
|
||||
const urls: string[] = []
|
||||
const invalidLines: string[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
const splitConcatenatedUrls = (line: string): string[] => {
|
||||
const indexes: number[] = []
|
||||
const re = /https?:\/\//g
|
||||
let match: RegExpExecArray | null = null
|
||||
while ((match = re.exec(line))) {
|
||||
const index = match.index
|
||||
if (index === 0) {
|
||||
indexes.push(index)
|
||||
continue
|
||||
}
|
||||
const prevChar = line[index - 1]
|
||||
if (prevChar === '=' || prevChar === '?' || prevChar === '&' || prevChar === '#') continue
|
||||
indexes.push(index)
|
||||
}
|
||||
|
||||
if (indexes.length <= 1) return [line]
|
||||
|
||||
return indexes.map((startIndex, i) => {
|
||||
const endIndex = indexes[i + 1] || line.length
|
||||
return line.slice(startIndex, endIndex)
|
||||
})
|
||||
}
|
||||
|
||||
const normalizedInput = input
|
||||
.split('\u0000').join('\n')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
|
||||
normalizedInput.split('\n').forEach((rawLine) => {
|
||||
const line = rawLine.trim()
|
||||
if (!line) return
|
||||
if (source === 'uri-list' && line.startsWith('#')) return
|
||||
|
||||
if (!isUrl(line)) {
|
||||
invalidLines.push(rawLine)
|
||||
return
|
||||
}
|
||||
|
||||
const parts = splitConcatenatedUrls(line)
|
||||
parts.forEach((part) => {
|
||||
if (seen.has(part)) return
|
||||
urls.push(part)
|
||||
seen.add(part)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
urls,
|
||||
invalidLines
|
||||
}
|
||||
}
|
||||
|
||||
export const extractHttpUrlsFromText = (text: string): string[] => {
|
||||
const urls: string[] = []
|
||||
const seen = new Set<string>()
|
||||
const matches = text.match(/https?:\/\/[^\s<>"']+/g) || []
|
||||
|
||||
matches.forEach((match) => {
|
||||
const maybeUrl = match.replace(/[)\]}>.,;:!?]+$/, '')
|
||||
|
||||
if (seen.has(maybeUrl)) return
|
||||
if (!isUrl(maybeUrl)) return
|
||||
|
||||
urls.push(maybeUrl)
|
||||
seen.add(maybeUrl)
|
||||
})
|
||||
|
||||
return urls
|
||||
}
|
||||
export const isUrlEncode = (url: string): boolean => {
|
||||
url = url || ''
|
||||
try {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"globals": {
|
||||
"assert": true,
|
||||
"expect": true,
|
||||
"should": true,
|
||||
"__static": true
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
// Set BABEL_ENV to use proper env config
|
||||
process.env.BABEL_ENV = 'test'
|
||||
|
||||
// Enable use of ES6+ on required files
|
||||
require('babel-register')({
|
||||
ignore: /node_modules/
|
||||
})
|
||||
|
||||
// Attach Chai APIs to global scope
|
||||
const { expect, should, assert } = require('chai')
|
||||
global.expect = expect
|
||||
global.should = should
|
||||
global.assert = assert
|
||||
|
||||
// Require all JS files in `./specs` for Mocha to consume
|
||||
require('require-dir')('./specs')
|
||||
@@ -1,13 +0,0 @@
|
||||
import utils from '../utils'
|
||||
|
||||
describe('Launch', function () {
|
||||
beforeEach(utils.beforeEach)
|
||||
afterEach(utils.afterEach)
|
||||
|
||||
it('shows the proper application title', function () {
|
||||
return this.app.client.getTitle()
|
||||
.then(title => {
|
||||
expect(title).to.equal('picgo')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import electron from 'electron'
|
||||
import { Application } from 'spectron'
|
||||
|
||||
export default {
|
||||
afterEach () {
|
||||
this.timeout(10000)
|
||||
|
||||
if (this.app && this.app.isRunning()) {
|
||||
return this.app.stop()
|
||||
}
|
||||
},
|
||||
beforeEach () {
|
||||
this.timeout(10000)
|
||||
this.app = new Application({
|
||||
path: electron,
|
||||
args: ['dist/electron/main.js'],
|
||||
startTimeout: 10000,
|
||||
waitTimeout: 10000
|
||||
})
|
||||
|
||||
return this.app.start()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
Vue.config.devtools = false
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// require all test files (files that ends with .spec.js)
|
||||
const testsContext = require.context('./specs', true, /\.spec$/)
|
||||
testsContext.keys().forEach(testsContext)
|
||||
|
||||
// require all src files except main.js for coverage.
|
||||
// you can also change this to match only the subset of files that
|
||||
// you want coverage for.
|
||||
const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/)
|
||||
srcContext.keys().forEach(srcContext)
|
||||
@@ -1,62 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const baseConfig = require('../../.electron-vue/webpack.renderer.config')
|
||||
const projectRoot = path.resolve(__dirname, '../../src/renderer')
|
||||
|
||||
// Set BABEL_ENV to use proper preset config
|
||||
process.env.BABEL_ENV = 'test'
|
||||
|
||||
let webpackConfig = merge(baseConfig, {
|
||||
devtool: '#inline-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"testing"'
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
// don't treat dependencies as externals
|
||||
delete webpackConfig.entry
|
||||
delete webpackConfig.externals
|
||||
delete webpackConfig.output.libraryTarget
|
||||
|
||||
// apply vue option to apply isparta-loader on js
|
||||
webpackConfig.module.rules
|
||||
.find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader'
|
||||
|
||||
module.exports = config => {
|
||||
config.set({
|
||||
browsers: ['visibleElectron'],
|
||||
client: {
|
||||
useIframe: false
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: './coverage',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
customLaunchers: {
|
||||
'visibleElectron': {
|
||||
base: 'Electron',
|
||||
flags: ['--show']
|
||||
}
|
||||
},
|
||||
frameworks: ['mocha', 'chai'],
|
||||
files: ['./index.js'],
|
||||
preprocessors: {
|
||||
'./index.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
reporters: ['spec', 'coverage'],
|
||||
singleRun: true,
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
noInfo: true
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import LandingPage from '@/components/LandingPage'
|
||||
|
||||
describe('LandingPage.vue', () => {
|
||||
it('should render correct contents', () => {
|
||||
const vm = new Vue({
|
||||
el: document.createElement('div'),
|
||||
render: h => h(LandingPage)
|
||||
}).$mount()
|
||||
|
||||
expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user