mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-29 22:30:57 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f1b7ca55 | ||
|
|
8aec8ca11a | ||
|
|
c235d4f727 | ||
|
|
78e29a9a69 | ||
|
|
c71032ff51 | ||
|
|
77cfa697fc | ||
|
|
89fc8a7280 | ||
|
|
809885e8b5 | ||
|
|
424ee3bc2c | ||
|
|
278a6de8a6 | ||
|
|
142e464f54 | ||
|
|
fa0b82c004 | ||
|
|
0d1311557a | ||
|
|
4105e178bd | ||
|
|
4d4a4874d7 | ||
|
|
40f555edc7 | ||
|
|
7bef302f90 | ||
|
|
e115443811 | ||
|
|
c2261d5b83 | ||
|
|
1459f79b23 | ||
|
|
1ee1ccbe8d | ||
|
|
4653eb2a04 | ||
|
|
940617cd63 | ||
|
|
9b287cb5af | ||
|
|
6b96ee79e1 | ||
|
|
7d9480e3cf | ||
|
|
45943ba00d | ||
|
|
d86fc5cdd0 | ||
|
|
ed142203aa | ||
|
|
fd7d1f1e95 | ||
|
|
a7d98e765b | ||
|
|
192af25f6f | ||
|
|
7176908274 | ||
|
|
29154babf8 | ||
|
|
001ff14799 | ||
|
|
57ab919d1b | ||
|
|
4e722278d8 | ||
|
|
b8313c3f15 | ||
|
|
c100babd9f | ||
|
|
3999d5d39b | ||
|
|
14d1c2b618 | ||
|
|
da18bd0dd5 | ||
|
|
9b0eb90d06 | ||
|
|
e4bb07e852 | ||
|
|
e1117bf1a1 | ||
|
|
c21e2d657d | ||
|
|
25fb6883c1 | ||
|
|
cf6c56c9d2 | ||
|
|
b0a9d045fd | ||
|
|
1e783a5e3c | ||
|
|
6517ef560f | ||
|
|
d061e2e866 | ||
|
|
ee56f3e2bc | ||
|
|
702ecd4118 | ||
|
|
c14734d25e | ||
|
|
36f3b45e66 | ||
|
|
efb062034d | ||
|
|
6598630984 | ||
|
|
98c4b1c359 | ||
|
|
525b382803 | ||
|
|
f770614c4e | ||
|
|
97ee43c875 | ||
|
|
a72c2c4ba8 | ||
|
|
29d3d64a32 | ||
|
|
b0bfc10a67 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -101,7 +101,7 @@ jobs:
|
||||
suffix: _portable
|
||||
file-end: .exe
|
||||
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
||||
8
.github/workflows/ruff.yaml
vendored
8
.github/workflows/ruff.yaml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Execute Ruff format
|
||||
uses: chartboost/ruff-action@v1
|
||||
uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
version: 0.8.1
|
||||
version: 0.11.0
|
||||
args: format --check
|
||||
|
||||
ruff-check:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Execute Ruff check
|
||||
uses: chartboost/ruff-action@v1
|
||||
uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
version: 0.8.1
|
||||
version: 0.11.8
|
||||
args: check
|
||||
|
||||
800
CHANGELOG.md
800
CHANGELOG.md
@@ -5,6 +5,805 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [9.5.2] - 2025-03-31
|
||||
|
||||
### Added
|
||||
|
||||
#### Search
|
||||
|
||||
- feat(ui): add setting to not display full filepaths by [@HermanKassler](https://github.com/HermanKassler) in [#841](https://github.com/TagStudioDev/TagStudio/pull/841)
|
||||
- feat: add filename and path sorting by [@Computerdores](https://github.com/Computerdores) in [#842](https://github.com/TagStudioDev/TagStudio/pull/842)
|
||||
|
||||
#### Settings
|
||||
|
||||
- feat: new settings menu + settings backend by [@Computerdores](https://github.com/Computerdores) in [#859](https://github.com/TagStudioDev/TagStudio/pull/859)
|
||||
|
||||
#### UI
|
||||
|
||||
- feat(ui): merge media controls by [@csponge](https://github.com/csponge) in [#805](https://github.com/TagStudioDev/TagStudio/pull/805)
|
||||
- fix: Remove border from video preview top and left by [@zfbx](https://github.com/zfbx) in [#900](https://github.com/TagStudioDev/TagStudio/pull/900)
|
||||
- feat(ui): add more default icons and file type equivalencies by [@CyanVoxel](https://github.com/CyanVoxel) in [#882](https://github.com/TagStudioDev/TagStudio/pull/882)
|
||||
- ui: recent libraries list improvements by [@CyanVoxel](https://github.com/CyanVoxel) in [#881](https://github.com/TagStudioDev/TagStudio/pull/881)
|
||||
|
||||
#### Misc
|
||||
|
||||
- feat: provide a .desktop file by [@xarvex](https://github.com/xarvex) in [#870](https://github.com/TagStudioDev/TagStudio/pull/870)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix: catch NotImplementedError for Float16 JPEG-XL files by [@CyanVoxel](https://github.com/CyanVoxel) in [#849](https://github.com/TagStudioDev/TagStudio/pull/849)
|
||||
- fix(nix/package): account for GTK platform by [@xarvex](https://github.com/xarvex) in [#868](https://github.com/TagStudioDev/TagStudio/pull/868)
|
||||
- fix: do not set palette for Linux-like systems that offer theming by [@xarvex](https://github.com/xarvex) in [#869](https://github.com/TagStudioDev/TagStudio/pull/869)
|
||||
- fix(flake): remove pinned input, only consume in Nix shell by [@xarvex](https://github.com/xarvex) in [#872](https://github.com/TagStudioDev/TagStudio/pull/872)
|
||||
- fix: stop ffmpeg cmd windows, refactor ffmpeg_checker by [@CyanVoxel](https://github.com/CyanVoxel) in [#855](https://github.com/TagStudioDev/TagStudio/pull/855)
|
||||
- fix: hide mnemonics on macOS by [@CyanVoxel](https://github.com/CyanVoxel) in [#856](https://github.com/TagStudioDev/TagStudio/pull/856)
|
||||
- fix: use UNION instead of UNION ALL by [@CyanVoxel](https://github.com/CyanVoxel) in [#877](https://github.com/TagStudioDev/TagStudio/pull/877)
|
||||
- fix: remove unescaped ampersand from "about.description" by [@CyanVoxel](https://github.com/CyanVoxel) in [#885](https://github.com/TagStudioDev/TagStudio/pull/885)
|
||||
- fix(ui): display 0 frame webp files in preview panel by [@CyanVoxel](https://github.com/CyanVoxel) in [64dc88a](https://github.com/TagStudioDev/TagStudio/commit/64dc88afa90bb11f3c9b74a2522f947370ce21db)
|
||||
- fix: close pdf file object in thumb renderer by [@Computerdores](https://github.com/Computerdores) in [#893](https://github.com/TagStudioDev/TagStudio/pull/893)
|
||||
- perf: improve responsiveness of GIF entries by [@Computerdores](https://github.com/Computerdores) in [#894](https://github.com/TagStudioDev/TagStudio/pull/894)
|
||||
- fix(ui): seamlessly loop videos by [@CyanVoxel](https://github.com/CyanVoxel) in [#902](https://github.com/TagStudioDev/TagStudio/pull/902)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- refactor!: change layout; import and build change by [@xarvex](https://github.com/xarvex) and [@CyanVoxel](https://github.com/CyanVoxel) in [#844](https://github.com/TagStudioDev/TagStudio/pull/844)
|
||||
- fix: log all problems in translation test by [@Computerdores](https://github.com/Computerdores) in [#839](https://github.com/TagStudioDev/TagStudio/pull/839)
|
||||
- refactor: split translation keys for about screen by [@CyanVoxel](https://github.com/CyanVoxel) in [#845](https://github.com/TagStudioDev/TagStudio/pull/845)
|
||||
- feat(ci): development tooling refresh and split documentation by [@xarvex](https://github.com/xarvex) in [#867](https://github.com/TagStudioDev/TagStudio/pull/867)
|
||||
- refactor: type hints and improvements in file_opener.py by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#876](https://github.com/TagStudioDev/TagStudio/pull/876)
|
||||
- build: update spec file to use proper pathex and datas paths by [@Leonard2](https://github.com/Leonard2) in [#895](https://github.com/TagStudioDev/TagStudio/pull/895)
|
||||
- refactor: fix various missing and broken type hints@VasigaranAndAngel in [#901](https://github.com/TagStudioDev/TagStudio/pull/901)
|
||||
- refactor: fix type hints and overrides in flowlayout.py by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#880](https://github.com/TagStudioDev/TagStudio/pull/880)
|
||||
|
||||
### Documentation
|
||||
|
||||
- docs: fix typos and grammar by [@Gawidev](https://github.com/Gawidev) in [#879](https://github.com/TagStudioDev/TagStudio/pull/879)
|
||||
- docs: update `ThumbRenderer` source by [@emmanuel-ferdman](https://github.com/emmanuel-ferdman) in [#896](https://github.com/TagStudioDev/TagStudio/pull/896)
|
||||
|
||||
### Translations
|
||||
|
||||
- Added Japanese (50%)
|
||||
- [@needledetector](https://github.com/needledetector)
|
||||
- Updated Turkish (93%)
|
||||
- [@Nyghl](https://github.com/Nyghl)
|
||||
- Updated Filipino (57%)
|
||||
- [@searinminecraft](https://github.com/searinminecraft)
|
||||
- Updated Tamil (92%)
|
||||
- [@TamilNeram](https://github.com/TamilNeram)
|
||||
- Updated Portuguese (Brazil) (83%)
|
||||
- [@viniciushelder](https://github.com/viniciushelder)
|
||||
- Updated German (95%)
|
||||
- [@DontBlameMe99](https://github.com/DontBlameMe99), [@Computerdores](https://github.com/Computerdores)
|
||||
- Updated Russian (85%)
|
||||
- werdi, [@Dott-rus](https://github.com/Dott-rus)
|
||||
- Updated Hungarian (100%)
|
||||
- Szíjártó Levente Pál
|
||||
- Updated Spanish (96%)
|
||||
- Joan, [@Nginearing](https://github.com/Nginearing)
|
||||
- Updated French (100%)
|
||||
- [@kitsumed](https://github.com/kitsumed)
|
||||
- Updated Toki Pona (80%)
|
||||
- [@Math-Bee](https://github.com/Math-Bee)
|
||||
|
||||
## [9.5.1] - 2025-03-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed translations crashing the program and preventing it from being reopened ([#827](https://github.com/TagStudioDev/TagStudio/issues/827))
|
||||
- fix: restore `translate_formatted()` method as `format()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#830](https://github.com/TagStudioDev/TagStudio/pull/830)
|
||||
- tests: add tests for translations by [@Computerdores](https://github.com/Computerdores) in [#833](https://github.com/TagStudioDev/TagStudio/pull/833)
|
||||
- fix(translations): fix invalid placeholders by [@CyanVoxel](https://github.com/CyanVoxel) in [#835](https://github.com/TagStudioDev/TagStudio/pull/835)
|
||||
- Removed empty parentheses from the "About" screen title
|
||||
- fix: separate about screen title from translations by [@CyanVoxel](https://github.com/CyanVoxel) in [#836](https://github.com/TagStudioDev/TagStudio/pull/836)
|
||||
|
||||
### Translations
|
||||
|
||||
- Updated French (99%)
|
||||
- [@alessdangelo](https://github.com/alessdangelo), [@Bamowen](https://github.com/Bamowen), [@kitsumed](https://github.com/kitsumed)
|
||||
- Updated German (98%)
|
||||
- [@Thesacraft](https://github.com/Thesacraft)
|
||||
- Updated Portuguese (Brazil) (88%)
|
||||
- [@viniciushelder](https://github.com/viniciushelder)
|
||||
- Updated Russian (73%)
|
||||
- werdei
|
||||
- Updated Spanish (95%)
|
||||
- [@JCC1998](https://github.com/JCC1998)
|
||||
|
||||
### Documentation
|
||||
|
||||
- docs: fix category typo by [@salem404](https://github.com/salem404) in [#834](https://github.com/TagStudioDev/TagStudio/pull/834)
|
||||
|
||||
## [9.5.0] - 2025-03-03
|
||||
|
||||
### Added
|
||||
|
||||
#### Overhauled Search Engine
|
||||
|
||||
##### Boolean Operators
|
||||
|
||||
- feat: implement query language by [@Computerdores](https://github.com/Computerdores) in [#606](https://github.com/TagStudioDev/TagStudio/pull/606)
|
||||
- feat: optimize AND queries by [@Computerdores](https://github.com/Computerdores) in [#679](https://github.com/TagStudioDev/TagStudio/pull/679)
|
||||
|
||||
##### Filetype, Mediatype, and Glob Path + Smartcase Searches
|
||||
|
||||
- fix: remove wildcard requirement for tags by [@Tyrannicodin](https://github.com/Tyrannicodin) in [#481](https://github.com/TagStudioDev/TagStudio/pull/481)
|
||||
- feat: add filetype and mediatype searches by [@python357-1](https://github.com/python357-1) in [#575](https://github.com/TagStudioDev/TagStudio/pull/575)
|
||||
- feat: make path search use globs by [@python357-1](https://github.com/python357-1) in [#582](https://github.com/TagStudioDev/TagStudio/pull/582)
|
||||
- feat: implement search equivalence of "jpg" and "jpeg" filetypes by [@Computerdores](https://github.com/Computerdores) in [#649](https://github.com/TagStudioDev/TagStudio/pull/649)
|
||||
- feat: add smartcase and globless path searches by [@CyanVoxel](https://github.com/CyanVoxel) in [#743](https://github.com/TagStudioDev/TagStudio/pull/743)
|
||||
|
||||
##### Sortable Results
|
||||
|
||||
- feat: sort by "date added" in library by [@Computerdores](https://github.com/Computerdores) in [#674](https://github.com/TagStudioDev/TagStudio/pull/674)
|
||||
|
||||
##### Autocomplete
|
||||
|
||||
- feat: add autocomplete for search engine by [@python357-1](https://github.com/python357-1) in [#586](https://github.com/TagStudioDev/TagStudio/pull/586)
|
||||
|
||||
#### Replaced "Tag Fields" with Tag Categories
|
||||
|
||||
Instead of tags needing to be added to a tag field type such as "Meta Tags", "Content Tags", or just the "Tags" field, tags are now added directly to file entries with no intermediary step. While tag field types offered a way to further organize tags, it was cumbersome, inflexible, and simply not fully fleshed out. Tag Categories offer all of the previous (intentional) functionality while greatly increasing the ease of use and customization.
|
||||
|
||||
- feat!: tag categories by [@CyanVoxel](https://github.com/CyanVoxel) in [#655](https://github.com/TagStudioDev/TagStudio/pull/655)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY4ZWEzOWRkZjkwOGZmYjZmZDUzMjU1MjJhNDNkNzYzZmM4YjZkMTUyNWIzMjNhMGY1NWNhYmU4ODNiNzlhMzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.cE_WO9AHsigusAbtaQV0QtN4FjYJz0lyHLLwFFDBO-0)
|
||||
|
||||
#### Thumbnails and File Previews
|
||||
|
||||
##### New Thumbnail Support
|
||||
|
||||
- feat: add svg thumbnail support (port [#442](https://github.com/TagStudioDev/TagStudio/pull/442)) by [@Tyrannicodin](https://github.com/Tyrannicodin) and [@CyanVoxel](https://github.com/CyanVoxel) in [#540](https://github.com/TagStudioDev/TagStudio/pull/540)
|
||||
- feat: add pdf thumbnail support (port [#378](https://github.com/TagStudioDev/TagStudio/pull/378)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#543](https://github.com/TagStudioDev/TagStudio/pull/543)
|
||||
- feat: add ePub thumbnail support (port [#387](https://github.com/TagStudioDev/TagStudio/pull/387)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#539](https://github.com/TagStudioDev/TagStudio/pull/539)
|
||||
- feat: add OpenDocument thumbnail support (port [#366](https://github.com/TagStudioDev/TagStudio/pull/366)) by [@Joshua-Beatty](https://github.com/Joshua-Beatty) and [@CyanVoxel](https://github.com/CyanVoxel) in [#545](https://github.com/TagStudioDev/TagStudio/pull/545)
|
||||
- feat: add JXL thumbnail and animated APNG + WEBP support (port [#344](https://github.com/TagStudioDev/TagStudio/pull/344) and partially port [#357](https://github.com/TagStudioDev/TagStudio/pull/357)) by [@BPplays](https://github.com/BPplays) and [@CyanVoxel](https://github.com/CyanVoxel) in [#549](https://github.com/TagStudioDev/TagStudio/pull/549)
|
||||
- fix: catch ImportError for pillow_jxl module by [@CyanVoxel](https://github.com/CyanVoxel) in [a2f9685](https://github.com/TagStudioDev/TagStudio/commit/a2f9685bc0d744ea6f5334c6d2926aad3f6d375a)
|
||||
|
||||
##### Audio Playback
|
||||
|
||||
- feat: audio playback by [@csponge](https://github.com/csponge) in [#576](https://github.com/TagStudioDev/TagStudio/pull/576)
|
||||
- feat(ui): add audio volume slider by [@SkeleyM](https://github.com/SkeleyM) in [#691](https://github.com/TagStudioDev/TagStudio/pull/691)
|
||||
|
||||
##### Thumbnail Caching
|
||||
|
||||
- feat(ui): add thumbnail caching by [@CyanVoxel](https://github.com/CyanVoxel) in [#694](https://github.com/TagStudioDev/TagStudio/pull/694)
|
||||
|
||||
#### Tags
|
||||
|
||||
##### Delete Tags _(Finally!)_
|
||||
|
||||
- feat: remove and create tags from tag database panel by [@DandyDev01](https://github.com/DandyDev01) in [#569](https://github.com/TagStudioDev/TagStudio/pull/569)
|
||||
|
||||
##### Custom User-Created Tag Colors
|
||||
|
||||
Create your own custom tag colors via the new Tag Color Manager! Tag colors are assigned a namespace (group) and include a name, primary color, and optional secondary color. By default the secondary color is used for the tag text color, but this can also be toggled to apply to the border color as well!
|
||||
|
||||
- feat(ui)!: user-created tag colors@CyanVoxel in [#801](https://github.com/TagStudioDev/TagStudio/pull/801)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjM4NzczYTFhYmFhNjgwMWJlYjgwNTkzYjA3ZWFlNTkwNzBiYTlhNTAzM2Y0MWM1MWQ0MzY1YmEyNmE4NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.EOEVWLDMx5CT-Gg5UhBmdMIYT49IZPKrrA9VL7N-pBQ) [](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJkNTUxODJiMDZhN2I2MDAxYzZlNzIyOTAzYTgwZDg3ZDFlYWM3ODM1YWY0Mzg5MDJjNDY0NzJhYjU4ZTAzMmEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.rqM3YOwrYdBCiwbBbEvalj7Tsfl-XWqgD1K9PeE46tI)
|
||||
|
||||
##### New Tag Colors + UI
|
||||
|
||||
- feat: expanded tag color system by [@CyanVoxel](https://github.com/CyanVoxel) in [#709](https://github.com/TagStudioDev/TagStudio/pull/709)
|
||||
- fix(ui): use correct pink tag color by [@CyanVoxel](https://github.com/CyanVoxel) in [431efe4](https://github.com/TagStudioDev/TagStudio/commit/431efe4fe93213141c763e59ca9887215766fd42)
|
||||
- fix(ui): use consistent tag outline colors by [@CyanVoxel](https://github.com/CyanVoxel) in [020a73d](https://github.com/TagStudioDev/TagStudio/commit/020a73d095c74283d6c80426d3c3db8874409952)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTU5ODIsIm5iZiI6MTc0NjY1NTY4MiwicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIyMDgwMlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk1OWNhZGNkOTRiZGJhNGQxNGU1MjJhYTViYTc0OTNiNTA4NDUxODA4OTYxZDUwNzYxZDhmYWZkNzM4NTE3N2QmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.HLnwHyp3BYg8vXo3BtvqBoOqtpQTI1eykqa-L3chLUk)
|
||||
|
||||
##### New Tag Alias UI
|
||||
|
||||
- fix: preview panel aliases not staying up to date with database by [@DandyDev01](https://github.com/DandyDev01) in [#641](https://github.com/TagStudioDev/TagStudio/pull/641)
|
||||
- fix: subtags/parent tags & aliases update the UI for building a tag by [@DandyDev01](https://github.com/DandyDev01) in [#534](https://github.com/TagStudioDev/TagStudio/pull/534)
|
||||
|
||||
#### Translations
|
||||
|
||||
TagStudio now has official translation support! Head to the new settings panel and select from one of the initial languages included. Note that many languages currently have incomplete translations.
|
||||
|
||||
Translation hosting generously provided by [Weblate](https://weblate.org/en/). Check out our [project page](https://hosted.weblate.org/projects/tagstudio/) to help translate TagStudio! Thank you to everyone who's helped contribute to the translations so far!
|
||||
|
||||
- translations: add string tokens for en.json by [@Bamowen](https://github.com/Bamowen) in [#507](https://github.com/TagStudioDev/TagStudio/pull/507)
|
||||
- feat: translations by [@Computerdores](https://github.com/Computerdores) in [#662](https://github.com/TagStudioDev/TagStudio/pull/662)
|
||||
- feat(ui): add language setting by [@CyanVoxel](https://github.com/CyanVoxel) in [#803](https://github.com/TagStudioDev/TagStudio/pull/803)
|
||||
|
||||
Initial Languages:
|
||||
|
||||
- Chinese (Traditional) (68%)
|
||||
- [@brisu](https://github.com/brisu)
|
||||
- Dutch (35%)
|
||||
- [@Pheubel](https://github.com/Pheubel)
|
||||
- Filipino (43%)
|
||||
- [@searinminecraft](https://github.com/searinminecraft)
|
||||
- French (100%)
|
||||
- [@Bamowen](https://github.com/Bamowen), [@alessdangelo](https://github.com/alessdangelo), [@kitsumed](https://github.com/kitsumed), Obscaeris
|
||||
- German (98%)
|
||||
- [@Ryussei](https://github.com/Ryussei), [@Computerdores](https://github.com/Computerdores), Aaron M, [@JoeJoeTV](https://github.com/JoeJoeTV), [@Kurty00](https://github.com/Kurty00)
|
||||
- Hungarian (100%)
|
||||
- [@smileyhead](https://github.com/smileyhead)
|
||||
- Norwegian Bokmål (16%)
|
||||
- [@comradekingu](https://github.com/comradekingu)
|
||||
- Polish (97%)
|
||||
- Anonymous
|
||||
- Portuguese (Brazil) (64%)
|
||||
- [@LoboMetalurgico](https://github.com/LoboMetalurgico), [@SpaceFox1](https://github.com/SpaceFox1), [@DaviMarquezeli](https://github.com/DaviMarquezeli), [@viniciushelder](https://github.com/viniciushelder), Alexander Lennart Formiga Johnsson
|
||||
- Russian (22%)
|
||||
- [@The-Stolas](https://github.com/The-Stolas)
|
||||
- Spanish (57%)
|
||||
- [@gallegonovato](https://github.com/gallegonovato), [@Nginearing](https://github.com/Nginearing), [@noceno](https://github.com/noceno)
|
||||
- Swedish (24%)
|
||||
- [@adampawelec](https://github.com/adampawelec), [@mashed5894](https://github.com/mashed5894)
|
||||
- Tamil (22%)
|
||||
- [@VasigaranAndAngel](https://github.com/VasigaranAndAngel)
|
||||
- Toki Pona (32%)
|
||||
- [@goldstargloww](https://github.com/goldstargloww)
|
||||
- Turkish (22%)
|
||||
- [@Nyghl](https://github.com/Nyghl)
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
- feat: about section by [@mashed5894](https://github.com/mashed5894) in [#712](https://github.com/TagStudioDev/TagStudio/pull/712)
|
||||
- feat(ui): add configurable splash screens by [@CyanVoxel](https://github.com/CyanVoxel) in [#703](https://github.com/TagStudioDev/TagStudio/pull/703)
|
||||
- feat(ui): show filenames in thumbnail grid by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
|
||||
- feat(about): clickable links to docs/discord/etc in about modal by [@SkeleyM](https://github.com/SkeleyM) in [#799](https://github.com/TagStudioDev/TagStudio/pull/799)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix(ui): display all tags in panel during empty search by [@samuellieberman](https://github.com/samuellieberman) in [#328](https://github.com/TagStudioDev/TagStudio/pull/328)
|
||||
- fix: avoid `KeyError` in `add_folders_to_tree()` (fix [#346](https://github.com/TagStudioDev/TagStudio/issues/346)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#347](https://github.com/TagStudioDev/TagStudio/pull/347)
|
||||
- fix: error on closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#484](https://github.com/TagStudioDev/TagStudio/pull/484)
|
||||
- fix: resolution info [#550](https://github.com/TagStudioDev/TagStudio/issues/550) by [@Roc25](https://github.com/Roc25) in [#551](https://github.com/TagStudioDev/TagStudio/pull/551)
|
||||
- fix: remove queued thumnail jobs when closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#583](https://github.com/TagStudioDev/TagStudio/pull/583)
|
||||
- fix: use absolute ffprobe path on macos (Fix [#511](https://github.com/TagStudioDev/TagStudio/issues/511)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#629](https://github.com/TagStudioDev/TagStudio/pull/629)
|
||||
- fix(ui): prevent duplicate parent tags in UI by [@SkeleyM](https://github.com/SkeleyM) in [#665](https://github.com/TagStudioDev/TagStudio/pull/665)
|
||||
- fix: fix -o flag not working if path has whitespace around it by [@python357-1](https://github.com/python357-1) in [#670](https://github.com/TagStudioDev/TagStudio/pull/670)
|
||||
- fix: better file opening compatibility with non-ascii filenames by [@SkeleyM](https://github.com/SkeleyM) in [#667](https://github.com/TagStudioDev/TagStudio/pull/667)
|
||||
- fix: restore environment before launching external programs by [@mashed5894](https://github.com/mashed5894) in [#707](https://github.com/TagStudioDev/TagStudio/pull/707)
|
||||
- fix: have pydub use known ffmpeg + ffprobe locations by [@CyanVoxel](https://github.com/CyanVoxel) in [#724](https://github.com/TagStudioDev/TagStudio/pull/724)
|
||||
- fix: add ".DS_Store" to `GLOBAL_IGNORE_SET` by [@CyanVoxel](https://github.com/CyanVoxel) in [b72a2f2](https://github.com/TagStudioDev/TagStudio/commit/b72a2f233141db4db6aa6be8796b626ebd3f0756)
|
||||
- fix: don't add "._" files to libraries by [@CyanVoxel](https://github.com/CyanVoxel) in [eb1f634](https://github.com/TagStudioDev/TagStudio/commit/eb1f634d386cd8a5ecee1e6ff6a0b7d8811550fa)
|
||||
|
||||
### Changed
|
||||
|
||||
#### SQLite Save File Format
|
||||
|
||||
This was the main focus of this update, and where the majority of development time and resources have been spent since v9.4. These changes include everything that was done to migrate from the JSON format to SQLite starting from the initial SQLite PR, while re-implementing every feature from v9.4 as the initial SQLite PR was based on v9.3.x at the time.
|
||||
|
||||
- refactor!: use SQLite and SQLAlchemy for database backend by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#332](https://github.com/TagStudioDev/TagStudio/pull/332)
|
||||
- feat: make search results more ergonomic by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#498](https://github.com/TagStudioDev/TagStudio/pull/498)
|
||||
- feat: store `Entry` suffix separately by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#503](https://github.com/TagStudioDev/TagStudio/pull/503)
|
||||
- feat: port thumbnail ([#390](https://github.com/TagStudioDev/TagStudio/pull/390)) and related features to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#522](https://github.com/TagStudioDev/TagStudio/pull/522)
|
||||
- fix: don't check db version with new library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#536](https://github.com/TagStudioDev/TagStudio/pull/536)
|
||||
- fix(ui): update ui when removing fields by [@DandyDev01](https://github.com/DandyDev01) in [#560](https://github.com/TagStudioDev/TagStudio/pull/560)
|
||||
- feat(parity): backend for aliases and parent tags by [@DandyDev01](https://github.com/DandyDev01) in [#596](https://github.com/TagStudioDev/TagStudio/pull/596)
|
||||
- fix: "open in explorer" opens correct folder by [@KirilBourakov](https://github.com/KirilBourakov) in [#603](https://github.com/TagStudioDev/TagStudio/pull/603)
|
||||
- fix: ui/ux parity fixes for thumbnails and files by [@CyanVoxel](https://github.com/CyanVoxel) in [#608](https://github.com/TagStudioDev/TagStudio/pull/608)
|
||||
- feat(parity): migrate json libraries to sqlite by [@CyanVoxel](https://github.com/CyanVoxel) in [#604](https://github.com/TagStudioDev/TagStudio/pull/604)
|
||||
- fix: clear all setting values when opening a library by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#622](https://github.com/TagStudioDev/TagStudio/pull/622)
|
||||
- fix: remove/rework windows path tests by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#625](https://github.com/TagStudioDev/TagStudio/pull/625)
|
||||
- fix: add check to see if library is loaded in filter_items by [@Roc25](https://github.com/Roc25) in [#547](https://github.com/TagStudioDev/TagStudio/pull/547)
|
||||
- fix: multiple macro errors by [@Computerdores](https://github.com/Computerdores) in [#612](https://github.com/TagStudioDev/TagStudio/pull/612)
|
||||
- fix: don't allow blank tag alias values in db by [@CyanVoxel](https://github.com/CyanVoxel) in [#628](https://github.com/TagStudioDev/TagStudio/pull/628)
|
||||
- feat: Reimplement drag drop files on sql migration by [@seakrueger](https://github.com/seakrueger) in [#528](https://github.com/TagStudioDev/TagStudio/pull/528)
|
||||
- fix: stop sqlite db from being updated while running tests by [@python357-1](https://github.com/python357-1) in [#648](https://github.com/TagStudioDev/TagStudio/pull/648)
|
||||
- fix: enter/return adds top result tag by [@SkeleyM](https://github.com/SkeleyM) in [#651](https://github.com/TagStudioDev/TagStudio/pull/651)
|
||||
- fix: show correct unlinked files count by [@SkeleyM](https://github.com/SkeleyM) in [#653](https://github.com/TagStudioDev/TagStudio/pull/653)
|
||||
- feat: implement parent tag search by [@Computerdores](https://github.com/Computerdores) in [#673](https://github.com/TagStudioDev/TagStudio/pull/673)
|
||||
- fix: only close add tag menu with no search by [@SkeleyM](https://github.com/SkeleyM) in [#685](https://github.com/TagStudioDev/TagStudio/pull/685)
|
||||
- fix: drag and drop no longer resets by [@SkeleyM](https://github.com/SkeleyM) in [#710](https://github.com/TagStudioDev/TagStudio/pull/710)
|
||||
- feat(ui): port "create and add tag" to main branch by [@SkeleyM](https://github.com/SkeleyM) in [#711](https://github.com/TagStudioDev/TagStudio/pull/711)
|
||||
- fix: don't add default title field, use proper phrasing for adding files by [@CyanVoxel](https://github.com/CyanVoxel) in [#701](https://github.com/TagStudioDev/TagStudio/pull/701)
|
||||
- fix: preview panel + main window fixes and optimizations by [@CyanVoxel](https://github.com/CyanVoxel) in [#700](https://github.com/TagStudioDev/TagStudio/pull/700)
|
||||
- fix: sort tag results by [@mashed5894](https://github.com/mashed5894) in [#721](https://github.com/TagStudioDev/TagStudio/pull/721)
|
||||
- fix: restore opening last library on startup by [@SkeleyM](https://github.com/SkeleyM) in [#729](https://github.com/TagStudioDev/TagStudio/pull/729)
|
||||
- fix(ui): don't always create tag on enter by [@SkeleyM](https://github.com/SkeleyM) in [#731](https://github.com/TagStudioDev/TagStudio/pull/731)
|
||||
- fix: use tag aliases in tag search by [@CyanVoxel](https://github.com/CyanVoxel) in [#726](https://github.com/TagStudioDev/TagStudio/pull/726)
|
||||
- fix: keep initial id order in `get_entries_full()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#736](https://github.com/TagStudioDev/TagStudio/pull/736)
|
||||
- fix: always catch db mismatch by [@CyanVoxel](https://github.com/CyanVoxel) in [#738](https://github.com/TagStudioDev/TagStudio/pull/738)
|
||||
- fix: relink unlinked entry to existing entry without sql error by [@mashed5894](https://github.com/mashed5894) in [#730](https://github.com/TagStudioDev/TagStudio/issues/730)
|
||||
- fix: refactor and fix bugs with missing_files.py by [@CyanVoxel](https://github.com/CyanVoxel) in [#739](https://github.com/TagStudioDev/TagStudio/pull/739)
|
||||
- fix: dragging files references correct entry IDs [@CyanVoxel](https://github.com/CyanVoxel) in [44ff17c](https://github.com/TagStudioDev/TagStudio/commit/44ff17c0b3f05570e356c112f005dbc14c7cc05d)
|
||||
- ui: port splash screen from Alpha-v9.4 by [@CyanVoxel](https://github.com/CyanVoxel) in [af760ee](https://github.com/TagStudioDev/TagStudio/commit/af760ee61a523c84bab0fb03a68d7465866d0e05)
|
||||
- fix: tags created from tag database now add aliases by [@CyanVoxel](https://github.com/CyanVoxel) in [2903dd2](https://github.com/TagStudioDev/TagStudio/commit/2903dd22c45c02498687073d075bb88886de6b62)
|
||||
- fix: check for tag name parity during JSON migration by [@CyanVoxel](https://github.com/CyanVoxel) in [#748](https://github.com/TagStudioDev/TagStudio/pull/748)
|
||||
- feat(ui): re-implement tag display names on sql by [@CyanVoxel](https://github.com/CyanVoxel) in [#747](https://github.com/TagStudioDev/TagStudio/pull/747)
|
||||
- fix(ui): restore Windows accent color on PySide 6.8.0.1 by [@CyanVoxel](https://github.com/CyanVoxel) in [#755](https://github.com/TagStudioDev/TagStudio/pull/755)
|
||||
- fix(ui): (mostly) fix right-click search option on tags by [@CyanVoxel](https://github.com/CyanVoxel) in [#756](https://github.com/TagStudioDev/TagStudio/pull/756)
|
||||
- feat: copy/paste fields and tags by [@mashed5894](https://github.com/mashed5894) in [#722](https://github.com/TagStudioDev/TagStudio/pull/722)
|
||||
- perf: optimize query methods and reduce preview panel updates by [@CyanVoxel](https://github.com/CyanVoxel) in [#794](https://github.com/TagStudioDev/TagStudio/pull/794)
|
||||
- feat: port file trashing ([#409](https://github.com/TagStudioDev/TagStudio/pull/409)) to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#792](https://github.com/TagStudioDev/TagStudio/pull/792)
|
||||
- fix: prevent future library versions from being opened by [@CyanVoxel](https://github.com/CyanVoxel) in [bcf3b2f](https://github.com/TagStudioDev/TagStudio/commit/bcf3b2f96bc8b876ca4b0c1d1882ce14a190f249)
|
||||
|
||||
#### UI/UX
|
||||
|
||||
- feat(ui): pre-select default tag name in `BuildTagPanel` by [@Cool-Game-Dev](https://github.com/Cool-Game-Dev) in [#592](https://github.com/TagStudioDev/TagStudio/pull/592)
|
||||
- feat(ui): keyboard navigation for editing tags by [@Computerdores](https://github.com/Computerdores) in [#407](https://github.com/TagStudioDev/TagStudio/pull/407)
|
||||
- feat(ui): use tag query as default new tag name by [@CyanVoxel](https://github.com/CyanVoxel) in [29c0dfd](https://github.com/TagStudioDev/TagStudio/commit/29c0dfdb2d88e8f473e27c7f1fe7ede6e5bd0feb)
|
||||
- feat(ui): shortcut to add tags to selected entries; change click behavior of tags to edit by [@CyanVoxel](https://github.com/CyanVoxel) in [#749](https://github.com/TagStudioDev/TagStudio/pull/749)
|
||||
- fix(ui): use consistent dark mode colors for all systems by [@CyanVoxel](https://github.com/CyanVoxel) in [#752](https://github.com/TagStudioDev/TagStudio/pull/752)
|
||||
- fix(ui): use camera white balance for raw images by [@CyanVoxel](https://github.com/CyanVoxel) in [6ee5304](https://github.com/TagStudioDev/TagStudio/commit/6ee5304b52f217af0f5df543fcb389649203d6b2)
|
||||
- Mixed field editing has been limited due to various bugs in both the JSON and SQL implementations. This will be re-implemented in a future release.
|
||||
- fix(ui): improve tagging ux by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
|
||||
- fix(ui): hide library actions when no library is open by [@CyanVoxel](https://github.com/CyanVoxel) in [#787](https://github.com/TagStudioDev/TagStudio/pull/787)
|
||||
- refactor(ui): recycle tag list in TagSearchPanel by [@CyanVoxel](https://github.com/CyanVoxel) in [#788](https://github.com/TagStudioDev/TagStudio/pull/788)
|
||||
- feat(ui): add tag view limit dropdown
|
||||
- fix(ui): expand usage of esc and enter for modals by [@CyanVoxel](https://github.com/CyanVoxel) in [#793](https://github.com/TagStudioDev/TagStudio/pull/793)
|
||||
|
||||
#### Performance
|
||||
|
||||
- feat: improve performance of "Delete Missing Entries" by [@Toby222](https://github.com/Toby222) and [@Computerdores](https://github.com/Computerdores) in [#696](https://github.com/TagStudioDev/TagStudio/pull/696)
|
||||
|
||||
#### Internal Changes
|
||||
|
||||
- refactor: combine open launch args by [@UnusualEgg](https://github.com/UnusualEgg) in [#364](https://github.com/TagStudioDev/TagStudio/pull/364)
|
||||
- feat: add date_created, date_modified, and date_added columns to entries table by [@CyanVoxel](https://github.com/CyanVoxel) in [#740](https://github.com/TagStudioDev/TagStudio/pull/740)
|
||||
|
||||
## [9.5.0 Pre-Release 4] - 2025-02-17
|
||||
|
||||
### Added
|
||||
|
||||
#### Custom User-Created Tag Colors ([@CyanVoxel](https://github.com/CyanVoxel) in [#801](https://github.com/TagStudioDev/TagStudio/pull/801))
|
||||
|
||||
Create your own custom tag colors via the new Tag Color Manager! Tag colors are assigned a namespace (group) and include a name, primary color, and optional secondary color. By default the secondary color is used for the tag text color, but this can also be toggled to apply to the border color as well!
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/413668576-b591f1fe-1c44-4d82-b6e5-d166590aeab1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg1NzYtYjU5MWYxZmUtMWM0NC00ZDgyLWI2ZTUtZDE2NjU5MGFlYWIxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUwZmExZjRlMWI4YzJjZTZmNDRiMjFiN2ZlZjg4ZjE3MWM4NzBkNmJlZWNjMzg2OWU5YTE2OWVmZTA2YTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nEs6bk1euKTEkIqDipNJBrXHbHegb3PHWoW3tKI02_8)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/413668612-96e81b08-6993-4a5e-96d0-3b05b50fbe44.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTM2Njg2MTItOTZlODFiMDgtNjk5My00YTVlLTk2ZDAtM2IwNWI1MGZiZTQ0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTlhOGU2NjA2ZjRhMjNjZGYxZDE1ZWYzZmVjN2RjM2Q0YzA1NTcwZGE5OGFkMjc2MDIzMTk1YTFlYjY2NTQxNmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.VD2trGcVKQVKUpzVog1UhZUM0JRcEHUhwGCiHpZ8zF0)
|
||||
|
||||
#### Translations
|
||||
|
||||
TagStudio now has official translation support! Head to the new settings panel and select from one of the initial languages included. Note that many languages currently have incomplete translations.
|
||||
|
||||
Translation hosting generously provided by [Weblate](https://weblate.org/en/). Check out our [project page](https://hosted.weblate.org/projects/tagstudio/) to help translate TagStudio! Thank you to everyone who's helped contribute to the translations so far!
|
||||
|
||||
- translations: add string tokens for en.json by [@Bamowen](https://github.com/Bamowen) in [#507](https://github.com/TagStudioDev/TagStudio/pull/507)
|
||||
- feat: translations by [@Computerdores](https://github.com/Computerdores) in [#662](https://github.com/TagStudioDev/TagStudio/pull/662)
|
||||
- feat(ui): add language setting by [@CyanVoxel](https://github.com/CyanVoxel) in [#803](https://github.com/TagStudioDev/TagStudio/pull/803)
|
||||
|
||||
Initial Languages:
|
||||
|
||||
- Chinese (Traditional) (68%)
|
||||
- [@brisu](https://github.com/brisu)
|
||||
- Dutch (35%)
|
||||
- [@Pheubel](https://github.com/Pheubel)
|
||||
- Filipino (15%)
|
||||
- [@searinminecraft](https://github.com/searinminecraft)
|
||||
- French (89%)
|
||||
- [@Bamowen](https://github.com/Bamowen), [@alessdangelo](https://github.com/alessdangelo), [@kitsumed](https://github.com/kitsumed), Obscaeris
|
||||
- German (73%)
|
||||
- [@Ryussei](https://github.com/Ryussei), [@Computerdores](https://github.com/Computerdores), Aaron M
|
||||
- Hungarian (89%)
|
||||
- [@smileyhead](https://github.com/smileyhead)
|
||||
- Norwegian Bokmål (16%)
|
||||
- [@comradekingu](https://github.com/comradekingu)
|
||||
- Polish (76%)
|
||||
- Anonymous
|
||||
- Portuguese (Brazil) (22%)
|
||||
- [@LoboMetalurgico](https://github.com/LoboMetalurgico), [@SpaceFox1](https://github.com/SpaceFox1)
|
||||
- Russian (22%)
|
||||
- [@The-Stolas](https://github.com/The-Stolas)
|
||||
- Spanish (46%)
|
||||
- [@gallegonovato](https://github.com/gallegonovato), [@Nginearing](https://github.com/Nginearing), [@noceno](https://github.com/noceno)
|
||||
- Swedish (24%)
|
||||
- [@adampawelec](https://github.com/adampawelec), [@mashed5894](https://github.com/mashed5894)
|
||||
- Tamil (22%)
|
||||
- [@VasigaranAndAngel](https://github.com/VasigaranAndAngel)
|
||||
- Toki Pona (32%)
|
||||
- [@goldstargloww](https://github.com/goldstargloww)
|
||||
- Turkish (22%)
|
||||
- [@Nyghl](https://github.com/Nyghl)
|
||||
|
||||
### Fixed
|
||||
|
||||
- feat(about): clickable links to docs/discord/etc in about modal by [@SkeleyM](https://github.com/SkeleyM) in [#799](https://github.com/TagStudioDev/TagStudio/pull/799)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
This release increases the internal `DB_VERSION` to 8. Libraries created with this version of TagStudio can still be opened in earlier v9.5.0 pre-release versions, however the behavior of custom color borders will not be identical to the behavior in this PR. Otherwise it should still be possible to use any custom colors created in this version in these earlier pre-releases (but not really recommended).
|
||||
|
||||
## [9.5.0 Pre-Release 3] - 2025-02-10
|
||||
|
||||
### Added
|
||||
|
||||
##### [#743](https://github.com/TagStudioDev/TagStudio/pull/743) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
Added "Smartcase" and Globless Path Search
|
||||
|
||||
- `path: temp`: Returns all paths that have "temp" **(Case insensitive)** somewhere in the name.
|
||||
|
||||
- `path: Temp`: Returns all paths that have "Temp" **(Case sensitive)** somewhere in the name.
|
||||
|
||||
|
||||
Glob Patterns w/ Smartcase
|
||||
|
||||
- `path: *temp*`: Returns all paths that have "temp" **(Case insensitive)** somewhere in the name.
|
||||
|
||||
- `path: *Temp*`: Returns all paths that have "Temp" **(Case sensitive)** somewhere in the name.
|
||||
|
||||
- `path: temp*`: Returns all paths that start with "temp" **(Case insensitive)** somewhere in the name.
|
||||
|
||||
- `path: Temp*`: Returns all paths that start with "Temp" **(Case sensitive)** somewhere in the name.
|
||||
|
||||
- `path: *temp`: Returns all paths that end with "temp" **(Case insensitive)** somewhere in the name.
|
||||
|
||||
- `path: *TEmP`: Returns all paths that end with "TEmP" **(Case sensitive)** somewhere in the name.
|
||||
|
||||
|
||||
##### [#788](https://github.com/TagStudioDev/TagStudio/pull/788) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
- Added a "View Limit" dropdown to tag search boxes to limit the number of on-screen tags. Previously this limit was hardcoded to 100, but now options range from 25 to unlimited.
|
||||
[](https://private-user-images.githubusercontent.com/46939827/411701461-7f7da065-888d-4fe5-a4e7-f99447bcce98.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTQ3MTYsIm5iZiI6MTc0NjY1NDQxNiwicGF0aCI6Ii80NjkzOTgyNy80MTE3MDE0NjEtN2Y3ZGEwNjUtODg4ZC00ZmU1LWE0ZTctZjk5NDQ3YmNjZTk4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNDY1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWVlZjVhZjYyNWQwY2FmMjhmMWI1MzI5ZDdjYWMxMmM0N2M0Nzc4MmY1YjE0NWY4MjVhZmIyMDI1NzQzY2M0YmQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.a3xUaH5r3HsDgOb6-lo3T-xSRRSy7dDOrln5i62KFP8)
|
||||
|
||||
### Changed
|
||||
|
||||
- fix(ui): expand usage of esc and enter for modals by [@CyanVoxel](https://github.com/CyanVoxel) in [#793](https://github.com/TagStudioDev/TagStudio/pull/793)
|
||||
- perf: optimize query methods and reduce preview panel updates by [@CyanVoxel](https://github.com/CyanVoxel) in [#794](https://github.com/TagStudioDev/TagStudio/pull/794)
|
||||
|
||||
##### [#788](https://github.com/TagStudioDev/TagStudio/pull/788) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
- Improved performance of tag search boxes, including the tag manager
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix(ui): hide library actions when no library is open by [@CyanVoxel](https://github.com/CyanVoxel) in [#787](https://github.com/TagStudioDev/TagStudio/pull/787)
|
||||
- feat: port file trashing ([#409](https://github.com/TagStudioDev/TagStudio/pull/409)) to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#792](https://github.com/TagStudioDev/TagStudio/pull/792)
|
||||
|
||||
### Docs
|
||||
|
||||
- Added references to alternative POSIX shells, as well as pyenv to CONTRIBUTING.md by [@ChloeZamorano](https://github.com/ChloeZamorano) in [#791](https://github.com/TagStudioDev/TagStudio/pull/791)
|
||||
## [9.5.0 Pre-Release 2] - 2025-02-03
|
||||
|
||||
### Added
|
||||
|
||||
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
- Add Ctrl+M shortcut to open the "Tag Manager"
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix: don't wrap field names too early by [@CyanVoxel](https://github.com/CyanVoxel) in [2215403](https://github.com/TagStudioDev/TagStudio/commit/2215403201e3b416a43ead0a322688180af6d71b) and [90a826d](https://github.com/TagStudioDev/TagStudio/commit/90a826d12804b3386a0b9003abb20f23f88ab3be)
|
||||
- fix: save all tag attributes from "Create & Add" modal by [@SkeleyM](https://github.com/SkeleyM) in [#762](https://github.com/TagStudioDev/TagStudio/pull/762)
|
||||
- fix: allow tag names with colons in search by [@SkeleyM](https://github.com/SkeleyM) in [#765](https://github.com/TagStudioDev/TagStudio/pull/765)
|
||||
- fix: catch `ParsingError` by [@CyanVoxel](https://github.com/CyanVoxel) in [#779](https://github.com/TagStudioDev/TagStudio/pull/779)
|
||||
- fix: patch incorrect description type & invalid disambiguation_id refs by [@CyanVoxel](https://github.com/CyanVoxel) in [#782](https://github.com/TagStudioDev/TagStudio/pull/782)
|
||||
|
||||
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
- Reset tag search box and focus each time a tag search panel is opened
|
||||
- Include tag parents in tag search results (v9.4 parity)
|
||||
- Lowercase tag names now get properly sorted with uppercase ones
|
||||
- Don't include tag display names in "closeness" factor when searching
|
||||
- Escape "&" characters inside tag names so Qt doesn't treat them as mnemonics
|
||||
- Set minimum tag width
|
||||
- Fix "Add Tags" panel missing its window title when accessing from the keyboard shortcut
|
||||
|
||||
### Changed
|
||||
|
||||
##### [#784](https://github.com/TagStudioDev/TagStudio/pull/784) by [@CyanVoxel](https://github.com/CyanVoxel)
|
||||
|
||||
- The "use for disambiguation" button has been moved to the right-hand side of parent tags in order to prevent accidental clicks involving the left-hand "remove tag" button
|
||||
- Add "Create & Add" button to the bottom of all non-whitespace searches, even if they return some tags
|
||||
- The awkward "+" button next to tags in the "Add Tags" panel has been removed in favor of clicking on tags themselves
|
||||
- Improved visual feedback for highlighting, keyboard focusing, and clicking tags
|
||||
- The clickable area of the "-" button on tags has been increased and has visual feedback when you hover and click it
|
||||
- You can now tab into the tag search list and add tags with a spacebar press (previously possible but very janky)
|
||||
- In tag search panels, pressing the Esc key will return your focus to the search bar and highlight your previous query. If the search box is already highlighted, pressing Esc will close the modal
|
||||
- In modals such as the "Add Tag" and "Edit Tag" panels, pressing Esc will cancel the operation and close the modal
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- refactor: wrap migration_iterator lambda in a try/except block by [@CyanVoxel](https://github.com/CyanVoxel) in [#773](https://github.com/TagStudioDev/TagStudio/pull/773)
|
||||
|
||||
### Docs
|
||||
|
||||
- docs: update field and library pages by [@CyanVoxel](https://github.com/CyanVoxel) in [f5ff4d7](https://github.com/TagStudioDev/TagStudio/commit/f5ff4d78c1ad53134e9c64698886aee68c0f1dc1)
|
||||
- docs: add information about "tag manager" by [@CyanVoxel](https://github.com/CyanVoxel) in [9bdbafa](https://github.com/TagStudioDev/TagStudio/commit/9bdbafa40c4274922f6533b5b5fcee9a4fe43030)
|
||||
- docs: add note about glob searching in the readme by [@CyanVoxel](https://github.com/CyanVoxel) in [6e402ac](https://github.com/TagStudioDev/TagStudio/commit/6e402ac34d2d60e71fbd36ad234fe3914d5eb8e0)
|
||||
- docs: add library_search page by [@CyanVoxel](https://github.com/CyanVoxel) in [5be7dfc](https://github.com/TagStudioDev/TagStudio/commit/5be7dfc314b21042c18b2f08893f2b452d12394a)
|
||||
- docs: docs: add more links to index.md by [@CyanVoxel](https://github.com/CyanVoxel) in [d795889](https://github.com/TagStudioDev/TagStudio/commit/d7958892b7762586837204d686a6a2a993e3c26e)
|
||||
- docs: fix typo for "category" in usage.md by [@pinheadtf2](https://github.com/pinheadtf2) in [#760](https://github.com/TagStudioDev/TagStudio/pull/760)
|
||||
- fix(docs): fix screenshot sometimes not rendering by [@SkeleyM](https://github.com/SkeleyM) in [#775](https://github.com/TagStudioDev/TagStudio/pull/775)
|
||||
## [9.5.0 Pre-Release 1] - 2025-01-31
|
||||
|
||||
### Added
|
||||
|
||||
#### Overhauled Search Engine
|
||||
|
||||
##### Boolean Operators
|
||||
|
||||
- feat: implement query language by [@Computerdores](https://github.com/Computerdores) in [#606](https://github.com/TagStudioDev/TagStudio/pull/606)
|
||||
- feat: optimize AND queries by [@Computerdores](https://github.com/Computerdores) in [#679](https://github.com/TagStudioDev/TagStudio/pull/679)
|
||||
|
||||
##### Filetype, Mediatype, and Glob Path Searches
|
||||
|
||||
- fix: remove wildcard requirement for tags by [@Tyrannicodin](https://github.com/Tyrannicodin) in [#481](https://github.com/TagStudioDev/TagStudio/pull/481)
|
||||
- feat: add filetype and mediatype searches by [@python357-1](https://github.com/python357-1) in [#575](https://github.com/TagStudioDev/TagStudio/pull/575)
|
||||
- feat: make path search use globs by [@python357-1](https://github.com/python357-1) in [#582](https://github.com/TagStudioDev/TagStudio/pull/582)
|
||||
- feat: implement search equivalence of "jpg" and "jpeg" filetypes by [@Computerdores](https://github.com/Computerdores) in [#649](https://github.com/TagStudioDev/TagStudio/pull/649)
|
||||
|
||||
##### Sortable Results
|
||||
|
||||
- feat: sort by "date added" in library by [@Computerdores](https://github.com/Computerdores) in [#674](https://github.com/TagStudioDev/TagStudio/pull/674)
|
||||
|
||||
##### Autocomplete
|
||||
|
||||
- feat: add autocomplete for search engine by [@python357-1](https://github.com/python357-1) in [#586](https://github.com/TagStudioDev/TagStudio/pull/586)
|
||||
|
||||
#### Replaced "Tag Fields" with Tag Categories
|
||||
|
||||
Instead of tags needing to be added to a tag field type such as "Meta Tags", "Content Tags", or just the "Tags" field, tags are now added directly to file entries with no intermediary step. While tag field types offered a way to further organize tags, it was cumbersome, inflexible, and simply not fully fleshed out. Tag Categories offer all of the previous (intentional) functionality while greatly increasing the ease of use and customization.
|
||||
|
||||
- feat!: tag categories by [@CyanVoxel](https://github.com/CyanVoxel) in [#655](https://github.com/TagStudioDev/TagStudio/pull/655)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/400138597-0b92eca5-db8f-4e3e-954b-1b4f3795f073.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDAxMzg1OTctMGI5MmVjYTUtZGI4Zi00ZTNlLTk1NGItMWI0ZjM3OTVmMDczLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTM1M2EwNGFjYmJiM2QwZjg2YzNmNTVjMGYwZDJkZGJkZTg5NjZjNDFhNTYyNDE0OGNlOWFhNzRkMzE3MjBkNzEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.B5NRZmHygdHlMy2ZnZtHjfOs83jjEliwfxoe3eMBnEQ)
|
||||
|
||||
#### Thumbnails and File Previews
|
||||
|
||||
##### New Thumbnail Support
|
||||
|
||||
- feat: add svg thumbnail support (port [#442](https://github.com/TagStudioDev/TagStudio/pull/442)) by [@Tyrannicodin](https://github.com/Tyrannicodin) and [@CyanVoxel](https://github.com/CyanVoxel) in [#540](https://github.com/TagStudioDev/TagStudio/pull/540)
|
||||
- feat: add pdf thumbnail support (port [#378](https://github.com/TagStudioDev/TagStudio/pull/378)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#543](https://github.com/TagStudioDev/TagStudio/pull/543)
|
||||
- feat: add ePub thumbnail support (port [#387](https://github.com/TagStudioDev/TagStudio/pull/387)) by [@Heiholf](https://github.com/Heiholf) and [@CyanVoxel](https://github.com/CyanVoxel) in [#539](https://github.com/TagStudioDev/TagStudio/pull/539)
|
||||
- feat: add OpenDocument thumbnail support (port [#366](https://github.com/TagStudioDev/TagStudio/pull/366)) by [@Joshua-Beatty](https://github.com/Joshua-Beatty) and [@CyanVoxel](https://github.com/CyanVoxel) in [#545](https://github.com/TagStudioDev/TagStudio/pull/545)
|
||||
- feat: add JXL thumbnail and animated APNG + WEBP support (port [#344](https://github.com/TagStudioDev/TagStudio/pull/344) and partially port [#357](https://github.com/TagStudioDev/TagStudio/pull/357)) by [@BPplays](https://github.com/BPplays) and [@CyanVoxel](https://github.com/CyanVoxel) in [#549](https://github.com/TagStudioDev/TagStudio/pull/549)
|
||||
- fix: catch ImportError for pillow_jxl module by [@CyanVoxel](https://github.com/CyanVoxel) in [a2f9685](https://github.com/TagStudioDev/TagStudio/commit/a2f9685bc0d744ea6f5334c6d2926aad3f6d375a)
|
||||
|
||||
##### Audio Playback
|
||||
|
||||
- feat: audio playback by [@csponge](https://github.com/csponge) in [#576](https://github.com/TagStudioDev/TagStudio/pull/576)
|
||||
- feat(ui): add audio volume slider by [@SkeleyM](https://github.com/SkeleyM) in [#691](https://github.com/TagStudioDev/TagStudio/pull/691)
|
||||
|
||||
##### Thumbnail Caching
|
||||
|
||||
- feat(ui): add thumbnail caching by [@CyanVoxel](https://github.com/CyanVoxel) in [#694](https://github.com/TagStudioDev/TagStudio/pull/694)
|
||||
|
||||
#### Tags
|
||||
|
||||
##### Delete Tags _(Finally!)_
|
||||
|
||||
- feat: remove and create tags from tag database panel by [@DandyDev01](https://github.com/DandyDev01) in [#569](https://github.com/TagStudioDev/TagStudio/pull/569)
|
||||
|
||||
##### New Tag Colors + UI
|
||||
|
||||
- feat: expanded tag color system by [@CyanVoxel](https://github.com/CyanVoxel) in [#709](https://github.com/TagStudioDev/TagStudio/pull/709)
|
||||
- fix(ui): use correct pink tag color by [@CyanVoxel](https://github.com/CyanVoxel) in [431efe4](https://github.com/TagStudioDev/TagStudio/commit/431efe4fe93213141c763e59ca9887215766fd42)
|
||||
- fix(ui): use consistent tag outline colors by [@CyanVoxel](https://github.com/CyanVoxel) in [020a73d](https://github.com/TagStudioDev/TagStudio/commit/020a73d095c74283d6c80426d3c3db8874409952)
|
||||
|
||||
[](https://private-user-images.githubusercontent.com/46939827/408753168-c8f82d89-ad7e-4be6-830e-b91cdc58e4c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDY2NTUwODcsIm5iZiI6MTc0NjY1NDc4NywicGF0aCI6Ii80NjkzOTgyNy80MDg3NTMxNjgtYzhmODJkODktYWQ3ZS00YmU2LTgzMGUtYjkxY2RjNThlNGM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA1MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNTA3VDIxNTMwN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI3ZDk2MDg5ODVjYzA2ZGU2Njc1ODE5M2U4OWU4ZGMwY2MxNzUzM2M2YmM2ZmRiMTdlOTkyNGM3MjlhMWNiOWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0RYUWgVW8VFvu2unzdWHtDCE4USYM77OcMvWeZkd8Hs)
|
||||
|
||||
##### New Tag Alias UI
|
||||
|
||||
- fix: preview panel aliases not staying up to date with database by [@DandyDev01](https://github.com/DandyDev01) in [#641](https://github.com/TagStudioDev/TagStudio/pull/641)
|
||||
- fix: subtags/parent tags & aliases update the UI for building a tag by [@DandyDev01](https://github.com/DandyDev01) in [#534](https://github.com/TagStudioDev/TagStudio/pull/534)
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
- feat: about section by [@mashed5894](https://github.com/mashed5894) in [#712](https://github.com/TagStudioDev/TagStudio/pull/712)
|
||||
- feat(ui): add configurable splash screens by [@CyanVoxel](https://github.com/CyanVoxel) in [#703](https://github.com/TagStudioDev/TagStudio/pull/703)
|
||||
- feat(ui): show filenames in thumbnail grid by [@CyanVoxel](https://github.com/CyanVoxel) in [#633](https://github.com/TagStudioDev/TagStudio/pull/633)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix(ui): display all tags in panel during empty search by [@samuellieberman](https://github.com/samuellieberman) in [#328](https://github.com/TagStudioDev/TagStudio/pull/328)
|
||||
- fix: avoid `KeyError` in `add_folders_to_tree()` (fix [#346](https://github.com/TagStudioDev/TagStudio/issues/346)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#347](https://github.com/TagStudioDev/TagStudio/pull/347)
|
||||
- fix: error on closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#484](https://github.com/TagStudioDev/TagStudio/pull/484)
|
||||
- fix: resolution info [#550](https://github.com/TagStudioDev/TagStudio/issues/550) by [@Roc25](https://github.com/Roc25) in [#551](https://github.com/TagStudioDev/TagStudio/pull/551)
|
||||
- fix: remove queued thumnail jobs when closing library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#583](https://github.com/TagStudioDev/TagStudio/pull/583)
|
||||
- fix: use absolute ffprobe path on macos (Fix [#511](https://github.com/TagStudioDev/TagStudio/issues/511)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#629](https://github.com/TagStudioDev/TagStudio/pull/629)
|
||||
- fix(ui): prevent duplicate parent tags in UI by [@SkeleyM](https://github.com/SkeleyM) in [#665](https://github.com/TagStudioDev/TagStudio/pull/665)
|
||||
- fix: fix -o flag not working if path has whitespace around it by [@python357-1](https://github.com/python357-1) in [#670](https://github.com/TagStudioDev/TagStudio/pull/670)
|
||||
- fix: better file opening compatibility with non-ascii filenames by [@SkeleyM](https://github.com/SkeleyM) in [#667](https://github.com/TagStudioDev/TagStudio/pull/667)
|
||||
- fix: restore environment before launching external programs by [@mashed5894](https://github.com/mashed5894) in [#707](https://github.com/TagStudioDev/TagStudio/pull/707)
|
||||
- fix: have pydub use known ffmpeg + ffprobe locations by [@CyanVoxel](https://github.com/CyanVoxel) in [#724](https://github.com/TagStudioDev/TagStudio/pull/724)
|
||||
- fix: add ".DS_Store" to `GLOBAL_IGNORE_SET` by [@CyanVoxel](https://github.com/CyanVoxel) in [b72a2f2](https://github.com/TagStudioDev/TagStudio/commit/b72a2f233141db4db6aa6be8796b626ebd3f0756)
|
||||
- fix: don't add "._" files to libraries by [@CyanVoxel](https://github.com/CyanVoxel) in [eb1f634](https://github.com/TagStudioDev/TagStudio/commit/eb1f634d386cd8a5ecee1e6ff6a0b7d8811550fa)
|
||||
|
||||
### Changed
|
||||
|
||||
#### SQLite Save File Format
|
||||
|
||||
This was the main focus of this update, and where the majority of development time and resources have been spent since v9.4. These changes include everything that was done to migrate from the JSON format to SQLite starting from the initial SQLite PR, while re-implementing every feature from v9.4 as the initial SQLite PR was based on v9.3.x at the time.
|
||||
|
||||
- refactor!: use SQLite and SQLAlchemy for database backend by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#332](https://github.com/TagStudioDev/TagStudio/pull/332)
|
||||
- feat: make search results more ergonomic by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#498](https://github.com/TagStudioDev/TagStudio/pull/498)
|
||||
- feat: store `Entry` suffix separately by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#503](https://github.com/TagStudioDev/TagStudio/pull/503)
|
||||
- feat: port thumbnail ([#390](https://github.com/TagStudioDev/TagStudio/pull/390)) and related features to v9.5 by [@CyanVoxel](https://github.com/CyanVoxel) in [#522](https://github.com/TagStudioDev/TagStudio/pull/522)
|
||||
- fix: don't check db version with new library by [@yedpodtrzitko](https://github.com/yedpodtrzitko) in [#536](https://github.com/TagStudioDev/TagStudio/pull/536)
|
||||
- fix(ui): update ui when removing fields by [@DandyDev01](https://github.com/DandyDev01) in [#560](https://github.com/TagStudioDev/TagStudio/pull/560)
|
||||
- feat(parity): backend for aliases and parent tags by [@DandyDev01](https://github.com/DandyDev01) in [#596](https://github.com/TagStudioDev/TagStudio/pull/596)
|
||||
- fix: "open in explorer" opens correct folder by [@KirilBourakov](https://github.com/KirilBourakov) in [#603](https://github.com/TagStudioDev/TagStudio/pull/603)
|
||||
- fix: ui/ux parity fixes for thumbnails and files by [@CyanVoxel](https://github.com/CyanVoxel) in [#608](https://github.com/TagStudioDev/TagStudio/pull/608)
|
||||
- feat(parity): migrate json libraries to sqlite by [@CyanVoxel](https://github.com/CyanVoxel) in [#604](https://github.com/TagStudioDev/TagStudio/pull/604)
|
||||
- fix: clear all setting values when opening a library by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#622](https://github.com/TagStudioDev/TagStudio/pull/622)
|
||||
- fix: remove/rework windows path tests by [@VasigaranAndAngel](https://github.com/VasigaranAndAngel) in [#625](https://github.com/TagStudioDev/TagStudio/pull/625)
|
||||
- fix: add check to see if library is loaded in filter_items by [@Roc25](https://github.com/Roc25) in [#547](https://github.com/TagStudioDev/TagStudio/pull/547)
|
||||
- fix: multiple macro errors by [@Computerdores](https://github.com/Computerdores) in [#612](https://github.com/TagStudioDev/TagStudio/pull/612)
|
||||
- fix: don't allow blank tag alias values in db by [@CyanVoxel](https://github.com/CyanVoxel) in [#628](https://github.com/TagStudioDev/TagStudio/pull/628)
|
||||
- feat: Reimplement drag drop files on sql migration by [@seakrueger](https://github.com/seakrueger) in [#528](https://github.com/TagStudioDev/TagStudio/pull/528)
|
||||
- fix: stop sqlite db from being updated while running tests by [@python357-1](https://github.com/python357-1) in [#648](https://github.com/TagStudioDev/TagStudio/pull/648)
|
||||
- fix: enter/return adds top result tag by [@SkeleyM](https://github.com/SkeleyM) in [#651](https://github.com/TagStudioDev/TagStudio/pull/651)
|
||||
- fix: show correct unlinked files count by [@SkeleyM](https://github.com/SkeleyM) in [#653](https://github.com/TagStudioDev/TagStudio/pull/653)
|
||||
- feat: implement parent tag search by [@Computerdores](https://github.com/Computerdores) in [#673](https://github.com/TagStudioDev/TagStudio/pull/673)
|
||||
- fix: only close add tag menu with no search by [@SkeleyM](https://github.com/SkeleyM) in [#685](https://github.com/TagStudioDev/TagStudio/pull/685)
|
||||
- fix: drag and drop no longer resets by [@SkeleyM](https://github.com/SkeleyM) in [#710](https://github.com/TagStudioDev/TagStudio/pull/710)
|
||||
- feat(ui): port "create and add tag" to main branch by [@SkeleyM](https://github.com/SkeleyM) in [#711](https://github.com/TagStudioDev/TagStudio/pull/711)
|
||||
- fix: don't add default title field, use proper phrasing for adding files by [@CyanVoxel](https://github.com/CyanVoxel) in [#701](https://github.com/TagStudioDev/TagStudio/pull/701)
|
||||
- fix: preview panel + main window fixes and optimizations by [@CyanVoxel](https://github.com/CyanVoxel) in [#700](https://github.com/TagStudioDev/TagStudio/pull/700)
|
||||
- fix: sort tag results by [@mashed5894](https://github.com/mashed5894) in [#721](https://github.com/TagStudioDev/TagStudio/pull/721)
|
||||
- fix: restore opening last library on startup by [@SkeleyM](https://github.com/SkeleyM) in [#729](https://github.com/TagStudioDev/TagStudio/pull/729)
|
||||
- fix(ui): don't always create tag on enter by [@SkeleyM](https://github.com/SkeleyM) in [#731](https://github.com/TagStudioDev/TagStudio/pull/731)
|
||||
- fix: use tag aliases in tag search by [@CyanVoxel](https://github.com/CyanVoxel) in [#726](https://github.com/TagStudioDev/TagStudio/pull/726)
|
||||
- fix: keep initial id order in `get_entries_full()` by [@CyanVoxel](https://github.com/CyanVoxel) in [#736](https://github.com/TagStudioDev/TagStudio/pull/736)
|
||||
- fix: always catch db mismatch by [@CyanVoxel](https://github.com/CyanVoxel) in [#738](https://github.com/TagStudioDev/TagStudio/pull/738)
|
||||
- fix: relink unlinked entry to existing entry without sql error by [@mashed5894](https://github.com/mashed5894) in [#730](https://github.com/TagStudioDev/TagStudio/issues/730)
|
||||
- fix: refactor and fix bugs with missing_files.py by [@CyanVoxel](https://github.com/CyanVoxel) in [#739](https://github.com/TagStudioDev/TagStudio/pull/739)
|
||||
- fix: dragging files references correct entry IDs [@CyanVoxel](https://github.com/CyanVoxel) in [44ff17c](https://github.com/TagStudioDev/TagStudio/commit/44ff17c0b3f05570e356c112f005dbc14c7cc05d)
|
||||
- ui: port splash screen from Alpha-v9.4 by [@CyanVoxel](https://github.com/CyanVoxel) in [af760ee](https://github.com/TagStudioDev/TagStudio/commit/af760ee61a523c84bab0fb03a68d7465866d0e05)
|
||||
- fix: tags created from tag database now add aliases by [@CyanVoxel](https://github.com/CyanVoxel) in [2903dd2](https://github.com/TagStudioDev/TagStudio/commit/2903dd22c45c02498687073d075bb88886de6b62)
|
||||
- fix: check for tag name parity during JSON migration by [@CyanVoxel](https://github.com/CyanVoxel) in [#748](https://github.com/TagStudioDev/TagStudio/pull/748)
|
||||
- feat(ui): re-implement tag display names on sql by [@CyanVoxel](https://github.com/CyanVoxel) in [#747](https://github.com/TagStudioDev/TagStudio/pull/747)
|
||||
- fix(ui): restore Windows accent color on PySide 6.8.0.1 by [@CyanVoxel](https://github.com/CyanVoxel) in [#755](https://github.com/TagStudioDev/TagStudio/pull/755)
|
||||
- fix(ui): (mostly) fix right-click search option on tags by [@CyanVoxel](https://github.com/CyanVoxel) in [#756](https://github.com/TagStudioDev/TagStudio/pull/756)
|
||||
- feat: copy/paste fields and tags by [@mashed5894](https://github.com/mashed5894) in [#722](https://github.com/TagStudioDev/TagStudio/pull/722)
|
||||
|
||||
#### UI/UX
|
||||
|
||||
- feat(ui): pre-select default tag name in `BuildTagPanel` by [@Cool-Game-Dev](https://github.com/Cool-Game-Dev) in [#592](https://github.com/TagStudioDev/TagStudio/pull/592)
|
||||
- feat(ui): keyboard navigation for editing tags by [@Computerdores](https://github.com/Computerdores) in [#407](https://github.com/TagStudioDev/TagStudio/pull/407)
|
||||
- feat(ui): use tag query as default new tag name by [@CyanVoxel](https://github.com/CyanVoxel) in [29c0dfd](https://github.com/TagStudioDev/TagStudio/commit/29c0dfdb2d88e8f473e27c7f1fe7ede6e5bd0feb)
|
||||
- feat(ui): shortcut to add tags to selected entries; change click behavior of tags to edit by [@CyanVoxel](https://github.com/CyanVoxel) in [#749](https://github.com/TagStudioDev/TagStudio/pull/749)
|
||||
- fix(ui): use consistent dark mode colors for all systems by [@CyanVoxel](https://github.com/CyanVoxel) in [#752](https://github.com/TagStudioDev/TagStudio/pull/752)
|
||||
- fix(ui): use camera white balance for raw images by [@CyanVoxel](https://github.com/CyanVoxel) in [6ee5304](https://github.com/TagStudioDev/TagStudio/commit/6ee5304b52f217af0f5df543fcb389649203d6b2)
|
||||
- Mixed field editing has been limited due to various bugs in both the JSON and SQL implementations. This will be re-implemented in a future release.
|
||||
|
||||
#### Performance
|
||||
|
||||
- feat: improve performance of "Delete Missing Entries" by [@Toby222](https://github.com/Toby222) and [@Computerdores](https://github.com/Computerdores) in [#696](https://github.com/TagStudioDev/TagStudio/pull/696)
|
||||
|
||||
#### Internal Changes
|
||||
|
||||
- refactor: combine open launch args by [@UnusualEgg](https://github.com/UnusualEgg) in [#364](https://github.com/TagStudioDev/TagStudio/pull/364)
|
||||
- feat: add date_created, date_modified, and date_added columns to entries table by [@CyanVoxel](https://github.com/CyanVoxel) in [#740](https://github.com/TagStudioDev/TagStudio/pull/740)
|
||||
|
||||
## [9.4.2] - 2024-12-01
|
||||
|
||||
### Added/Fixed
|
||||
|
||||
- Create auto-backup of library for use in save failures (Fix [#343](https://github.com/TagStudioDev/TagStudio/issues/343)) by [@CyanVoxel](https://github.com/CyanVoxel) in [#554](https://github.com/TagStudioDev/TagStudio/pull/554)
|
||||
|
||||
## [9.4.1] - 2024-09-13
|
||||
|
||||
### Added
|
||||
|
||||
- Warn user if FFmpeg is not installed
|
||||
- Support for `.raf` and `.orf` raw image thumbnails and previews
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use `birthtime` for file creation time on Mac & Windows
|
||||
- Use audio icon fallback when FFmpeg is not detected
|
||||
- Retain search query upon directory refresh
|
||||
|
||||
### Changed
|
||||
|
||||
- Significantly improve file re-scanning performance
|
||||
|
||||
## [9.4.0] - 2024-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- Copy and paste fields
|
||||
- Add multiple fields at once
|
||||
- Drag and drop files in/out of the program
|
||||
- Files can be shared by dragging them from the thumbnail grid to other programs
|
||||
- Files can be added to library folder by dragging them into the program
|
||||
- Manage Python virtual environment in Nix flake
|
||||
- Ability to create tag when adding tags
|
||||
- Blender preview thumbnail support
|
||||
- File deletion/trashing
|
||||
- Added right-click option on thumbnails and preview panel to delete files
|
||||
- Added Edit Menu option for deleting files
|
||||
- Added <kbd>Delete</kbd> key shortcut for deleting files
|
||||
- Font preview thumbnail support
|
||||
- Short "Aa" previews for thumbnails
|
||||
- Full alphabet preview for the preview pane
|
||||
- Sort tags by alphabetical/color
|
||||
- File explorer action follows OS naming
|
||||
- Preview Source Engine files
|
||||
- Expanded thumbnail and preview features
|
||||
- Add album cover art thumbnails
|
||||
- Add audio waveform thumbnails for audio files without embedded cover art
|
||||
- Add new default file thumbnails, both for generic and specific file types
|
||||
- Change the unlinked file icon to better convey its meaning
|
||||
- Add dropdown for different thumbnail sizes
|
||||
- Show File Creation and Modified dates; Restyle file path label
|
||||
|
||||
### Fixed
|
||||
|
||||
- Backslashes in f-string on file dupe widget
|
||||
- Tags not shown when none searched
|
||||
- Avoid error from eagerly grabbing data values
|
||||
- Correct behavior for tag search options
|
||||
- Load Gallery-DL sidecar files correctly
|
||||
- Correct duplicate file matching
|
||||
- GPU hardware acceleration in Nix flake
|
||||
- Suppress command prompt windows for FFmpeg in builds
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Move type constants to media classes
|
||||
- Combine open launch arguments
|
||||
- Revamp Nix flake with devenv/direnv in cb4798b
|
||||
- Remove impurity of Nix flake when used with direnv in bc38e56
|
||||
|
||||
## [9.3.2] - 2024-07-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix signal log warning
|
||||
- Fix "Folders to Tags" feature
|
||||
- Fix search ignoring case of extension list
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Add tests into CI by
|
||||
- Create testing library files ad-hoc
|
||||
- Refactoring: centralize field IDs
|
||||
- Update to pyside6 version 6.7.1
|
||||
|
||||
## [9.3.1] - 2024-06-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Separately pin QT nixpkg version
|
||||
- Bugfix for #252, don't attempt to read video file if invalid or 0 frames long
|
||||
- Toggle Mouse Event Transparency on ItemThumbs
|
||||
- Refactor `video_player.py`
|
||||
|
||||
## [9.3.0] - 2024-06-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added playback previews for video files
|
||||
- Added Boolean "and/or" search mode selection
|
||||
- Added ability to scan and fix duplicate entries (not to be confused with duplicate files) from the "Fix Unlinked Entries" menu
|
||||
- Added “Select All” (<kbd>Ctrl</kbd>+<kbd>A</kbd> / <kbd>⌘ Command</kbd>+<kbd>A</kbd>) hotkey for the library grid view
|
||||
- Added "Clear Selection" hotkey (<kbd>Esc</kbd>) for the library grid view
|
||||
- Added the ability to invert the file extension inclusion list into an exclusion list
|
||||
- Added default landing page when no library is open
|
||||
|
||||
### Fixed
|
||||
|
||||
- TagStudio will no longer attempt to or allow you to reopen a library from a missing location
|
||||
- Fixed `PermissionError` when attempting to access files with a higher permission level upon scanning the library directory
|
||||
- Fixed RAW image previews sometimes not loadingand
|
||||
- Fixed most non-UTF-8 encoded text files from not being able to be previewed
|
||||
- Fixed "Refresh Directories"/"Fix Unlinked Entries" creating duplicate entries
|
||||
- Other miscellaneous fixes
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed "Subtags" to "Parent Tags" to help better describe their function
|
||||
- Increased number of tags shown by default in the "Add Tag" modal from 29 to 100
|
||||
- Documentation is now split into individual linked files and updated to include future features
|
||||
- Replaced use of `os.path` with `pathlib`
|
||||
- `.cr2` files are now included in the list of RAW image file types
|
||||
- Minimum supported macOS version raised to 12.0
|
||||
|
||||
## [9.2.1] - 2024-05-23
|
||||
|
||||
### Added
|
||||
|
||||
- Basic thumbnail/preview support for RAW images (currently `.raw`, `.dng`, `.rw2`, `.nef`, `.arw`, `.crw`, `.cr3`)
|
||||
- NOTE: These previews are currently slow to load given the nature of rendering them. In the future once thumbnail caching is added, this process should only happen once.
|
||||
- Thumbnail/preview support for HEIF images
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed sidebar not expanding horizontally
|
||||
- Fixed "Recent Library" list not updating when creating a new library
|
||||
- Fixed palletized images not loading with alpha channels
|
||||
- Low resolution images (such as pixel art) now render with crisp edges in thumbnails and previews
|
||||
- Fixed visual bug where the edit icon would show for incorrect fields
|
||||
|
||||
## [9.2.0] - 2024-05-14
|
||||
|
||||
### Added
|
||||
@@ -48,6 +847,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- New application icons
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Using and editing multiple entry fields of the same type may result in incorrect field(s) being updated
|
||||
- Adding Favorite or Archived tags via the thumbnail badges may apply the tag(s) to incorrect fields
|
||||
- Searching for tag names with spaces does not currently function as intended
|
||||
|
||||
@@ -43,13 +43,13 @@ If you know what you're doing and have developed for Python projects in the past
|
||||
4. If using a virtual environment instead of a dependency manager, install an editable version of the program and development dependencies with the following PIP command:
|
||||
|
||||
```
|
||||
pip install -e .[dev]
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
Otherwise, modify the command above for use with your dependency manager of choice. For example if using uv, you may use this:
|
||||
|
||||
```
|
||||
uv pip install -e .[dev]
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Workflow Checks
|
||||
@@ -93,19 +93,7 @@ Mypy is also available as a VS Code [extension](https://marketplace.visualstudio
|
||||
- Run all tests by running `pytest tests/` in the repository root.
|
||||
|
||||
## Code Style
|
||||
|
||||
Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._
|
||||
|
||||
- Do your best to write clear, concise, and modular code.
|
||||
- Keep a maximum column with of no more than **100** characters.
|
||||
- Code comments should be used to help describe sections of code that can't speak for themselves.
|
||||
- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add.
|
||||
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
|
||||
- Imports should be ordered alphabetically.
|
||||
- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
|
||||
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/src/tagstudio/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
|
||||
- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
|
||||
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
|
||||
See the [Style Guide](/STYLE.md)
|
||||
|
||||
### Modules & Implementations
|
||||
|
||||
|
||||
84
STYLE.md
Normal file
84
STYLE.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Code Style
|
||||
|
||||
Most of the style guidelines can be checked, fixed, and enforced via Ruff. Older code may not be adhering to all of these guidelines, in which case _"do as I say, not as I do"..._
|
||||
|
||||
- Do your best to write clear, concise, and modular code.
|
||||
- This should include making methods private by default (e.g. `__method()`)
|
||||
- Methods should only be protected (e.g. `_method()`) or public (e.g. `method()`) when needed and warranted
|
||||
- Keep a maximum column width of no more than **100** characters.
|
||||
- Code comments should be used to help describe sections of code that can't speak for themselves.
|
||||
- Use [Google style](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) docstrings for any classes and functions you add.
|
||||
- If you're modifying an existing function that does _not_ have docstrings, you don't _have_ to add docstrings to it... but it would be pretty cool if you did ;)
|
||||
- Imports should be ordered alphabetically.
|
||||
- Lists of values should be ordered using their [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order).
|
||||
- Some files have their methods ordered alphabetically as well (i.e. [`thumb_renderer`](https://github.com/TagStudioDev/TagStudio/blob/main/src/tagstudio/qt/widgets/thumb_renderer.py)). If you're working in a file and notice this, please try and keep to the pattern.
|
||||
- When writing text for window titles or form titles, use "[Title Case](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case)" capitalization. Your IDE may have a command to format this for you automatically, although some may incorrectly capitalize short prepositions. In a pinch you can use a website such as [capitalizemytitle.com](https://capitalizemytitle.com/) to check.
|
||||
- If it wasn't mentioned above, then stick to [**PEP-8**](https://peps.python.org/pep-0008/)!
|
||||
|
||||
## QT
|
||||
As of writing this section, the QT part of the code base is quite unstructured and the View and Controller parts are completely intermixed[^1]. This makes maintenance, fixes and general understanding of the code base quite challenging, because the interesting parts you are looking for are entangled in a bunch of repetitive UI setup code. To address this we are aiming to more strictly separate the view and controller aspects of the QT frontend.
|
||||
|
||||
The general structure of the QT code base should look like this:
|
||||
```
|
||||
qt
|
||||
├── controller
|
||||
│ ├── widgets
|
||||
│ │ └── preview_panel_controller.py
|
||||
│ └── main_window_controller.py
|
||||
├── view
|
||||
│ ├── widgets
|
||||
│ │ └── preview_panel_view.py
|
||||
│ └── main_window_view.py
|
||||
├── ts_qt.py
|
||||
└── mixed.py
|
||||
```
|
||||
|
||||
In this structure there are the `view` and `controller` sub-directories. They have the exact same structure and for every `<component>_view.py` there is a `<component>_controller.py` at the same location in the other subdirectory and vice versa.
|
||||
|
||||
Typically the classes should look like this:
|
||||
```py
|
||||
# my_cool_widget_view.py
|
||||
class MyCoolWidgetView(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__button = QPushButton()
|
||||
self.__color_dropdown = QComboBox()
|
||||
# ...
|
||||
self.__connect_callbacks()
|
||||
|
||||
def __connect_callbacks(self):
|
||||
self.__button.clicked.connect(self._button_click_callback)
|
||||
self.__color_dropdown.currentIndexChanged.connect(
|
||||
lambda idx: self._color_dropdown_callback(self.__color_dropdown.itemData(idx))
|
||||
)
|
||||
|
||||
def _button_click_callback(self):
|
||||
raise NotImplementedError()
|
||||
```
|
||||
```py
|
||||
# my_cool_widget_controller.py
|
||||
class MyCoolWidget(MyCoolWidgetView):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def _button_click_callback(self):
|
||||
print("Button was clicked!")
|
||||
|
||||
def _color_dropdown_callback(self, color: Color):
|
||||
print(f"The selected color is now: {color}")
|
||||
```
|
||||
|
||||
Observe the following key aspects of this example:
|
||||
- The Controller is just called `MyCoolWidget` instead of `MyCoolWidgetController` as it will be directly used by other code
|
||||
- The UI elements are in private variables
|
||||
- This enforces that the controller shouldn't directly access UI elements
|
||||
- Instead the view should provide a protected API (e.g. `_get_color()`) for things like setting/getting the value of a dropdown, etc.
|
||||
- Instead of `_get_color()` there could also be a `_color` method marked with `@property`
|
||||
- The callback methods are already defined as protected methods with NotImplementedErrors
|
||||
- Defines the interface the callbacks
|
||||
- Enforces that UI events be handled
|
||||
|
||||
> [!TIP]
|
||||
> A good (non-exhaustive) rule of thumb is: If it requires a non-UI import, then it doesn't belong in the `*_view.py` file.
|
||||
|
||||
[^1]: For an explanation of the Model-View-Controller (MVC) Model, checkout this article: [MVC Framework Introduction](https://www.geeksforgeeks.org/mvc-framework-introduction/).
|
||||
10
docs/assets/icon_mono.svg
Normal file
10
docs/assets/icon_mono.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 739 739" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.986683,0,0,0.986683,-136.081,-136.081)">
|
||||
<path d="M535.939,863.161C515.931,843.153 203.505,529.713 183.497,509.705C169.086,495.294 161.469,476.645 160.649,457.754C160.046,443.845 138.078,230.102 137.923,217.139C137.681,196.785 145.323,176.356 160.839,160.839C177.115,144.564 198.795,136.951 220.125,138.016C232.439,138.63 447.036,159.52 461.817,160.931C479.3,162.6 496.329,170.12 509.705,183.497C523.113,196.904 849.753,522.531 863.161,535.939C893.716,566.494 893.716,616.108 863.161,646.663L646.663,863.161C616.108,893.716 566.494,893.716 535.939,863.161ZM321.355,223.613C296.045,198.303 254.947,198.303 229.636,223.613C204.326,248.924 204.326,290.022 229.636,315.332C254.947,340.643 296.045,340.643 321.355,315.332C346.666,290.022 346.666,248.924 321.355,223.613ZM362.109,606.786C409.476,654.152 424.103,598.401 454.027,606.786C468.584,610.865 453.72,642.505 443.028,673.551C425.086,725.641 484.094,757.817 516.601,720.743C545.603,687.667 503.579,655.692 520.581,632.527C537.795,609.074 563.542,633.319 565.542,665.527C568.921,719.955 535.825,735.585 543.999,774.591C553.59,820.348 624.181,827.565 638,774.591C647.736,737.269 603.102,705.31 628.352,644.476C636.209,625.545 662.786,619.154 669.759,644.476C673.976,659.791 660.264,670.152 666.759,693.55C674.41,721.114 725.088,732.96 740.374,693.55C746.873,676.793 734.853,651.273 731.597,640.406C714.283,582.611 826.807,582.426 762.374,517.789C703.034,458.263 493.6,249.017 493.6,249.017C479.164,234.58 457.464,234.58 443.028,249.017L249.017,443.028C234.58,457.464 234.58,479.164 249.017,493.6C249.017,493.6 340.149,584.825 362.109,606.786Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.986683,0,0,0.986683,-136.081,-136.081)">
|
||||
<path d="M733.962,560.164C740.987,553.139 740.987,541.733 733.962,534.708L482.173,282.92C475.148,275.895 463.742,275.895 456.717,282.92L431.261,308.376C424.237,315.4 424.237,326.807 431.261,333.831L683.05,585.62C690.075,592.645 701.481,592.645 708.506,585.62L733.962,560.164ZM439.639,559.207C446.664,552.182 446.664,540.776 439.639,533.751L335.491,429.602C328.466,422.578 317.059,422.578 310.035,429.602L284.579,455.058C277.554,462.083 277.554,473.489 284.579,480.514L388.728,584.663C395.752,591.688 407.159,591.688 414.184,584.663L439.639,559.207ZM584.306,556.624C591.331,549.599 591.331,538.192 584.306,531.168L409.115,355.977C402.091,348.953 390.684,348.953 383.66,355.977L358.204,381.433C351.179,388.458 351.179,399.864 358.204,406.889L533.394,582.079C540.419,589.104 551.825,589.104 558.85,582.079L584.306,556.624ZM298.425,246.543C311.081,259.198 311.081,279.747 298.425,292.402C285.77,305.058 265.221,305.058 252.566,292.402C239.911,279.747 239.911,259.198 252.566,246.543C265.221,233.888 285.77,233.888 298.425,246.543Z" style="fill:white;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -6,7 +6,7 @@ If you wish to develop for TagStudio, you'll need to create a development enviro
|
||||
!!! tip "Contributing"
|
||||
If you wish to contribute to TagStudio's development, please read our [CONTRIBUTING.md](https://github.com/TagStudioDev/TagStudio/blob/main/CONTRIBUTING.md)!
|
||||
|
||||
## Install Python
|
||||
## Installing Python
|
||||
|
||||
Python [3.12](https://www.python.org/downloads) is required to develop for TagStudio. Any version matching "Python 3.12.x" should work, with "x" being any number. Alternatively you can use a tool such as [pyenv](https://github.com/pyenv/pyenv) to install this version of Python without affecting any existing Python installations on your system. Tools such as [uv](#installing-with-uv) can also install Python versions.
|
||||
|
||||
@@ -22,7 +22,7 @@ python --version
|
||||
|
||||
---
|
||||
|
||||
#### Installing with pyenv
|
||||
### Installing with pyenv
|
||||
|
||||
If you choose to install Python using pyenv, please refer to the following instructions:
|
||||
|
||||
@@ -32,23 +32,31 @@ If you choose to install Python using pyenv, please refer to the following instr
|
||||
|
||||
---
|
||||
|
||||
### Installing Dependencies
|
||||
## Cloning from GitHub
|
||||
|
||||
The repository can be cloned/downloaded via `git` in your terminal, or by downloading the zip file from the "Code" button on the [repository page](https://github.com/TagStudioDev/TagStudio).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/TagStudioDev/TagStudio.git
|
||||
```
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
To install the required dependencies, you can use a dependency manager such as [uv](https://docs.astral.sh/uv) or [Poetry 2.0](https://python-poetry.org). Alternatively you can create a virtual environment and manually install the dependencies yourself.
|
||||
|
||||
#### Installing with uv
|
||||
### Installing with uv
|
||||
|
||||
If using [uv](https://docs.astral.sh/uv), you can install the dependencies for TagStudio with the following command:
|
||||
|
||||
```sh
|
||||
uv pip install -e .[dev]
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
A reference `.envrc` is provided for use with [direnv](#direnv), see [`contrib/.envrc-uv`](https://github.com/TagStudioDev/TagStudio/blob/main/contrib/.envrc-uv).
|
||||
|
||||
---
|
||||
|
||||
#### Installing with Poetry
|
||||
### Installing with Poetry
|
||||
|
||||
If using [Poetry](https://python-poetry.org), you can install the dependencies for TagStudio with the following command:
|
||||
|
||||
@@ -58,7 +66,7 @@ poetry install --with dev
|
||||
|
||||
---
|
||||
|
||||
#### Manual Installation
|
||||
### Manual Installation
|
||||
|
||||
If you choose to manually set up a virtual environment and install dependencies instead of using a dependency manager, please refer to the following instructions:
|
||||
|
||||
@@ -92,7 +100,7 @@ If you choose to manually set up a virtual environment and install dependencies
|
||||
3. Use the following PIP command to create an editable installation and install the required development dependencies:
|
||||
|
||||
```sh
|
||||
pip install -e .[dev]
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Nix(OS)
|
||||
|
||||
@@ -46,7 +46,7 @@ pip install .
|
||||
!!! note "Developer Dependencies"
|
||||
If you wish to create an editable install with the additional dependencies required for developing TagStudio, use this modified PIP command instead:
|
||||
```sh
|
||||
pip install -e .[dev]
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
_See more under "[Developing](./develop.md)"_
|
||||
|
||||
|
||||
@@ -1,3 +1,99 @@
|
||||
th, td {
|
||||
/* Dark Theme */
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-default-bg-color: #060617;
|
||||
--md-default-fg-color: #eae1ff;
|
||||
--md-default-fg-color--light: #b898ff;
|
||||
--md-code-fg-color: #eae1ffcc;
|
||||
--md-code-hl-string-color: rgb(92, 255, 228);
|
||||
--md-code-hl-keyword-color: rgb(61, 155, 255);
|
||||
--md-code-hl-constant-color: rgb(205, 78, 255);
|
||||
--md-footer-bg-color--dark: #03030c;
|
||||
--md-code-bg-color: #090a26;
|
||||
}
|
||||
|
||||
/* Light Theme */
|
||||
[data-md-color-scheme="default"] {
|
||||
--md-default-fg-color--light: #090a26;
|
||||
}
|
||||
|
||||
.md-header {
|
||||
background: linear-gradient(
|
||||
60deg,
|
||||
rgb(205, 78, 255) 10%,
|
||||
rgb(116, 123, 255) 50%,
|
||||
rgb(72, 145, 255) 75%,
|
||||
rgb(98, 242, 255) 100%
|
||||
);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5em 1em 0.5em 1em !important;
|
||||
}
|
||||
|
||||
.md-header {
|
||||
border-style: solid;
|
||||
border-width: 0 0 2px 0;
|
||||
border-color: #ffffff33;
|
||||
}
|
||||
|
||||
.md-header__title {
|
||||
font-family: "Bai Jamjuree", Roboto, sans-serif;
|
||||
font-weight: 500 !important;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: -0.05rem !important;
|
||||
}
|
||||
|
||||
.md-nav__title,
|
||||
h1,
|
||||
h2,
|
||||
.md-nav__item--section > .md-nav__link {
|
||||
font-family: "Bai Jamjuree", Roboto, sans-serif;
|
||||
font-weight: 500 !important;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: -0.05rem !important;
|
||||
}
|
||||
|
||||
/* Add padding to stop text from cutting off early due to negative letter spacing */
|
||||
.md-nav__item--section > .md-nav__link > .md-ellipsis {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.md-header__title .md-header__topic .md-ellipsis {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.md-header__button.md-logo {
|
||||
padding-right: 0;
|
||||
padding-bottom: 0.3rem;
|
||||
margin-right: -0.8rem;
|
||||
}
|
||||
|
||||
.md-search__form {
|
||||
height: 1.5rem;
|
||||
background-color: #00004444;
|
||||
border-radius: 0.3rem !important;
|
||||
border-style: solid;
|
||||
border-width: 2px 1px 0 1px;
|
||||
border-color: #00004422;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.md-search__form > .md-icon {
|
||||
margin-top: -0.2rem;
|
||||
}
|
||||
|
||||
.twemoji {
|
||||
margin-top: 0.05rem;
|
||||
}
|
||||
|
||||
/* Matches the palette used by mkdocs-material */
|
||||
.priority-high {
|
||||
color: #f1185a;
|
||||
}
|
||||
|
||||
.priority-med {
|
||||
color: #7c4eff;
|
||||
}
|
||||
|
||||
.priority-low {
|
||||
color: #28afff;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,17 @@ These version milestones are rough estimations for when the previous core featur
|
||||
|
||||
#### UI
|
||||
|
||||
- [ ] Translations _(Any applicable)_ [MEDIUM]
|
||||
- [x] Translations _(Any applicable)_ [MEDIUM]
|
||||
- [x] Unified Media Player [HIGH]
|
||||
- [x] Auto-hiding player controls
|
||||
- [x] Play/Pause [HIGH]
|
||||
- [x] Loop [HIGH]
|
||||
- [x] Toggle Autoplay [MEDIUM]
|
||||
- [x] Volume Control [HIGH]
|
||||
- [x] Toggle Mute [HIGH]
|
||||
- [x] Timeline scrubber [HIGH]
|
||||
- [ ] Fullscreen [MEDIUM]
|
||||
- [x] Configurable page size [HIGH]
|
||||
|
||||
#### Performance
|
||||
|
||||
@@ -163,17 +173,7 @@ These version milestones are rough estimations for when the previous core featur
|
||||
- [ ] Toggle File Extension Label [MEDIUM]
|
||||
- [ ] Toggle Duration Label [MEDIUM]
|
||||
- [ ] Custom Tag Badges [LOW]
|
||||
- [ ] Unified Media Player [HIGH]
|
||||
- [ ] Auto-hiding player controls
|
||||
- [x] Play/Pause [HIGH]
|
||||
- [x] Loop [HIGH]
|
||||
- [x] Toggle Autoplay [MEDIUM]
|
||||
- [ ] Volume Control [HIGH]
|
||||
- [x] Toggle Mute [HIGH]
|
||||
- [ ] Timeline scrubber [HIGH]
|
||||
- [ ] Fullscreen [MEDIUM]
|
||||
- [ ] Library list view [HIGH]
|
||||
- [ ] Configurable page size [HIGH]
|
||||
|
||||
### v9.8
|
||||
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725024810,
|
||||
"narHash": "sha256-ODYRm8zHfLTH3soTFWE452ydPYz2iTvr9T8ftDMUQ3E=",
|
||||
"lastModified": 1743550720,
|
||||
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "af510d4a62d071ea13925ce41c95e3dec816c01d",
|
||||
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741173522,
|
||||
"narHash": "sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0=",
|
||||
"lastModified": 1744932701,
|
||||
"narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d69ab0d71b22fa1ce3dbeff666e6deb4917db049",
|
||||
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
33
flake.nix
33
flake.nix
@@ -30,19 +30,40 @@
|
||||
{
|
||||
packages =
|
||||
let
|
||||
pythonPackages = pkgs.python312Packages;
|
||||
python3Packages = pkgs.python312Packages;
|
||||
|
||||
pillow-jxl-plugin = pythonPackages.callPackage ./nix/package/pillow-jxl-plugin.nix {
|
||||
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
|
||||
inherit (pkgs) cmake;
|
||||
inherit pyexiv2;
|
||||
inherit (pkgs) rustPlatform;
|
||||
};
|
||||
pyexiv2 = pythonPackages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
|
||||
vtf2img = pythonPackages.callPackage ./nix/package/vtf2img.nix { };
|
||||
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
|
||||
vtf2img = python3Packages.callPackage ./nix/package/vtf2img.nix { };
|
||||
in
|
||||
rec {
|
||||
default = tagstudio;
|
||||
tagstudio = pythonPackages.callPackage ./nix/package {
|
||||
tagstudio = pkgs.callPackage ./nix/package {
|
||||
# HACK: Remove when PySide6 is bumped to 6.9.x.
|
||||
# Sourced from https://github.com/NixOS/nixpkgs/commit/2f9c1ad5e19a6154d541f878774a9aacc27381b7.
|
||||
pyside6 =
|
||||
if lib.versionAtLeast python3Packages.pyside6.version "6.9.0" then
|
||||
(python3Packages.pyside6.override {
|
||||
shiboken6 = python3Packages.shiboken6.overrideAttrs {
|
||||
version = "6.8.0.2";
|
||||
|
||||
src = pkgs.fetchurl {
|
||||
url = "mirror://qt/official_releases/QtForPython/shiboken6/PySide6-6.8.0.2-src/pyside-setup-everywhere-src-6.8.0.tar.xz";
|
||||
hash = "sha256-Ghohmo8yfjQNJYJ1+tOp8mG48EvFcEF0fnPdatJStOE=";
|
||||
};
|
||||
|
||||
sourceRoot = "pyside-setup-everywhere-src-6.8.0/sources/shiboken6";
|
||||
|
||||
patches = [ ./nix/package/shiboken6-fix-include-qt-headers.patch ];
|
||||
};
|
||||
}).overrideAttrs
|
||||
{ sourceRoot = "pyside-setup-everywhere-src-6.8.0/sources/pyside6"; }
|
||||
else
|
||||
python3Packages.pyside6;
|
||||
|
||||
inherit pillow-jxl-plugin vtf2img;
|
||||
};
|
||||
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
|
||||
|
||||
19
mkdocs.yml
19
mkdocs.yml
@@ -53,31 +53,33 @@ nav:
|
||||
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: overrides
|
||||
palette:
|
||||
# Palette toggle for automatic mode
|
||||
- media: "(prefers-color-scheme)"
|
||||
toggle:
|
||||
icon: material/brightness-auto
|
||||
name: Switch to light mode
|
||||
icon: material/lightbulb-auto-outline
|
||||
name: Switch to Light Mode
|
||||
# Palette toggle for light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: deep purple
|
||||
accent: deep purple
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
icon: material/lightbulb
|
||||
name: Switch to Dark Mode
|
||||
# Palette toggle for dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: deep purple
|
||||
accent: deep purple
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: SSwitch to system preference
|
||||
logo: assets/icon.png
|
||||
icon: material/lightbulb-night-outline
|
||||
name: Switch to System Preference
|
||||
logo: assets/icon_mono.svg
|
||||
favicon: assets/icon.ico
|
||||
font: false # use system fonts
|
||||
font:
|
||||
code: Jetbrains Mono
|
||||
language: en
|
||||
features:
|
||||
- navigation.instant
|
||||
@@ -138,3 +140,4 @@ plugins:
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
- https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
{
|
||||
buildPythonApplication,
|
||||
chardet,
|
||||
ffmpeg-headless,
|
||||
ffmpeg-python,
|
||||
hatchling,
|
||||
humanfriendly,
|
||||
lib,
|
||||
mutagen,
|
||||
numpy,
|
||||
opencv-python,
|
||||
pillow,
|
||||
pillow-heif,
|
||||
pillow-jxl-plugin,
|
||||
pipewire,
|
||||
pydantic,
|
||||
pydub,
|
||||
pyside6,
|
||||
pytest-qt,
|
||||
pytest-xdist,
|
||||
pytestCheckHook,
|
||||
pythonRelaxDepsHook,
|
||||
python3Packages,
|
||||
qt6,
|
||||
rawpy,
|
||||
send2trash,
|
||||
sqlalchemy,
|
||||
stdenv,
|
||||
structlog,
|
||||
syrupy,
|
||||
toml,
|
||||
ujson,
|
||||
vtf2img,
|
||||
wrapGAppsHook,
|
||||
|
||||
pillow-jxl-plugin,
|
||||
pyside6,
|
||||
vtf2img,
|
||||
|
||||
withJXLSupport ? false,
|
||||
}:
|
||||
|
||||
let
|
||||
pyproject = (lib.importTOML ../../pyproject.toml).project;
|
||||
in
|
||||
buildPythonApplication {
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = pyproject.name;
|
||||
inherit (pyproject) version;
|
||||
pyproject = true;
|
||||
@@ -46,7 +25,7 @@ buildPythonApplication {
|
||||
src = ../../.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pythonRelaxDepsHook
|
||||
python3Packages.pythonRelaxDepsHook
|
||||
qt6.wrapQtAppsHook
|
||||
|
||||
# INFO: Should be unnecessary once PR is pulled.
|
||||
@@ -59,7 +38,7 @@ buildPythonApplication {
|
||||
qt6.qtmultimedia
|
||||
];
|
||||
|
||||
nativeCheckInputs = [
|
||||
nativeCheckInputs = with python3Packages; [
|
||||
pytest-qt
|
||||
pytest-xdist
|
||||
pytestCheckHook
|
||||
@@ -80,30 +59,41 @@ buildPythonApplication {
|
||||
lib.makeLibraryPath [ pipewire ]
|
||||
}";
|
||||
|
||||
pythonRemoveDeps = true;
|
||||
pythonRemoveDeps = lib.optional (!withJXLSupport) [ "pillow_jxl" ];
|
||||
pythonRelaxDeps = [
|
||||
"numpy"
|
||||
"pillow"
|
||||
"pillow-heif"
|
||||
"pillow-jxl-plugin"
|
||||
"structlog"
|
||||
"typing-extensions"
|
||||
];
|
||||
pythonImportsCheck = [ "tagstudio" ];
|
||||
|
||||
build-system = [ hatchling ];
|
||||
dependencies = [
|
||||
chardet
|
||||
ffmpeg-python
|
||||
humanfriendly
|
||||
mutagen
|
||||
numpy
|
||||
opencv-python
|
||||
pillow
|
||||
pillow-heif
|
||||
pydantic
|
||||
pydub
|
||||
pyside6
|
||||
rawpy
|
||||
send2trash
|
||||
sqlalchemy
|
||||
structlog
|
||||
toml
|
||||
ujson
|
||||
vtf2img
|
||||
] ++ lib.optional withJXLSupport pillow-jxl-plugin;
|
||||
build-system = with python3Packages; [ hatchling ];
|
||||
dependencies =
|
||||
with python3Packages;
|
||||
[
|
||||
chardet
|
||||
ffmpeg-python
|
||||
humanfriendly
|
||||
mutagen
|
||||
numpy
|
||||
opencv-python
|
||||
pillow
|
||||
pillow-heif
|
||||
pydantic
|
||||
pydub
|
||||
pyside6
|
||||
rawpy
|
||||
send2trash
|
||||
sqlalchemy
|
||||
structlog
|
||||
toml
|
||||
ujson
|
||||
vtf2img
|
||||
]
|
||||
++ lib.optional withJXLSupport pillow-jxl-plugin;
|
||||
|
||||
disabledTests = [
|
||||
# INFO: These tests require modifications to a library, which does not work
|
||||
@@ -120,6 +110,17 @@ buildPythonApplication {
|
||||
"test_json_migration"
|
||||
"test_library_migrations"
|
||||
|
||||
"test_add_same_tag_to_selection_single"
|
||||
"test_add_tag_to_selection_multiple"
|
||||
"test_add_tag_to_selection_single"
|
||||
"test_custom_tag_category"
|
||||
"test_file_path_display"
|
||||
"test_meta_tag_category"
|
||||
"test_update_selection_empty"
|
||||
"test_update_selection_empty"
|
||||
"test_update_selection_multiple"
|
||||
"test_update_selection_single"
|
||||
|
||||
# INFO: This test requires modification of a configuration file.
|
||||
"test_filepath_setting"
|
||||
];
|
||||
|
||||
81
nix/package/shiboken6-fix-include-qt-headers.patch
Normal file
81
nix/package/shiboken6-fix-include-qt-headers.patch
Normal file
@@ -0,0 +1,81 @@
|
||||
Sourced from https://github.com/NixOS/nixpkgs/blob/5ba0f1ea90b0afa2abc23a43edb63af51d932e6d/pkgs/development/python-modules/shiboken6/fix-include-qt-headers.patch.
|
||||
--- a/ApiExtractor/clangparser/compilersupport.cpp
|
||||
+++ b/ApiExtractor/clangparser/compilersupport.cpp
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QVersionNumber>
|
||||
+#include <QtCore/QRegularExpression>
|
||||
|
||||
#include <clang-c/Index.h>
|
||||
|
||||
@@ -341,6 +342,13 @@ QByteArrayList emulatedCompilerOptions()
|
||||
{
|
||||
QByteArrayList result;
|
||||
HeaderPaths headerPaths;
|
||||
+
|
||||
+ bool isNixDebug = qgetenv("NIX_DEBUG").toInt() > 0;
|
||||
+ // examples:
|
||||
+ // /nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-qtsensors-6.4.2-dev/include
|
||||
+ // /nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-qtbase-6.4.2-dev/include
|
||||
+ QRegularExpression qtHeaderRegex(uR"(/[0-9a-z]{32}-qt[a-z0-9]+-)"_s);
|
||||
+
|
||||
switch (compiler()) {
|
||||
case Compiler::Msvc:
|
||||
result.append(QByteArrayLiteral("-fms-compatibility-version=19.26.28806"));
|
||||
@@ -352,9 +360,30 @@ QByteArrayList emulatedCompilerOptions()
|
||||
appendClangBuiltinIncludes(&headerPaths);
|
||||
break;
|
||||
case Compiler::Clang:
|
||||
- headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s)));
|
||||
+ // fix: error: cannot jump from switch statement to this case label: case Compiler::Gpp
|
||||
+ // note: jump bypasses variable initialization: const HeaderPaths clangPaths =
|
||||
+ {
|
||||
+ //headerPaths.append(gppInternalIncludePaths(compilerFromCMake(u"clang++"_s)));
|
||||
+ // fix: qt.shiboken: x is specified in typesystem, but not defined. This could potentially lead to compilation errors.
|
||||
+ // PySide requires that Qt headers are not -isystem
|
||||
+ // https://bugreports.qt.io/browse/PYSIDE-787
|
||||
+ const HeaderPaths clangPaths = gppInternalIncludePaths(compilerFromCMake(u"clang++"_qs));
|
||||
+ for (const HeaderPath &h : clangPaths) {
|
||||
+ auto match = qtHeaderRegex.match(QString::fromUtf8(h.path));
|
||||
+ if (!match.hasMatch()) {
|
||||
+ if (isNixDebug)
|
||||
+ qDebug() << "shiboken compilersupport.cpp: found non-qt header: " << h.path;
|
||||
+ // add using -isystem
|
||||
+ headerPaths.append(h);
|
||||
+ } else {
|
||||
+ if (isNixDebug)
|
||||
+ qDebug() << "shiboken compilersupport.cpp: found qt header: " << h.path;
|
||||
+ headerPaths.append({h.path, HeaderType::Standard});
|
||||
+ }
|
||||
+ }
|
||||
result.append(noStandardIncludeOption());
|
||||
break;
|
||||
+ }
|
||||
case Compiler::Gpp:
|
||||
if (needsClangBuiltinIncludes())
|
||||
appendClangBuiltinIncludes(&headerPaths);
|
||||
@@ -363,8 +392,20 @@ QByteArrayList emulatedCompilerOptions()
|
||||
// <type_traits> etc (g++ 11.3).
|
||||
const HeaderPaths gppPaths = gppInternalIncludePaths(compilerFromCMake(u"g++"_qs));
|
||||
for (const HeaderPath &h : gppPaths) {
|
||||
- if (h.path.contains("c++") || h.path.contains("sysroot"))
|
||||
+ // fix: qt.shiboken: x is specified in typesystem, but not defined. This could potentially lead to compilation errors.
|
||||
+ // PySide requires that Qt headers are not -isystem
|
||||
+ // https://bugreports.qt.io/browse/PYSIDE-787
|
||||
+ auto match = qtHeaderRegex.match(QString::fromUtf8(h.path));
|
||||
+ if (!match.hasMatch()) {
|
||||
+ if (isNixDebug)
|
||||
+ qDebug() << "shiboken compilersupport.cpp: found non-qt header: " << h.path;
|
||||
+ // add using -isystem
|
||||
headerPaths.append(h);
|
||||
+ } else {
|
||||
+ if (isNixDebug)
|
||||
+ qDebug() << "shiboken compilersupport.cpp: found qt header: " << h.path;
|
||||
+ headerPaths.append({h.path, HeaderType::Standard});
|
||||
+ }
|
||||
}
|
||||
break;
|
||||
}
|
||||
--
|
||||
2.39.0
|
||||
122
overrides/partials/header.html
Normal file
122
overrides/partials/header.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!--
|
||||
Copyright (c) 2016-2025 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!-- Determine classes -->
|
||||
{% set class = "md-header" %}
|
||||
{% if "navigation.tabs.sticky" in features %}
|
||||
{% set class = class ~ " md-header--shadow md-header--lifted" %}
|
||||
{% elif "navigation.tabs" not in features %}
|
||||
{% set class = class ~ " md-header--shadow" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Header -->
|
||||
<header class="{{ class }}" data-md-component="header">
|
||||
<nav
|
||||
class="md-header__inner md-grid"
|
||||
aria-label="{{ lang.t('header') }}"
|
||||
>
|
||||
|
||||
<!-- Link to home -->
|
||||
<a
|
||||
href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
|
||||
title="{{ config.site_name | e }}"
|
||||
class="md-header__button md-logo"
|
||||
aria-label="{{ config.site_name }}"
|
||||
data-md-component="logo"
|
||||
>
|
||||
{% include "partials/logo.html" %}
|
||||
</a>
|
||||
|
||||
<!-- Button to open drawer -->
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
{% set icon = config.theme.icon.menu or "material/menu" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
|
||||
<!-- Header title -->
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
<a style="font-weight: 900;">Tag</a><a style="font-weight: 500;"><i>Studio</i></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
{% if page.meta and page.meta.title %}
|
||||
{{ page.meta.title }}
|
||||
{% else %}
|
||||
{{ page.title }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color palette toggle -->
|
||||
{% if config.theme.palette %}
|
||||
{% if not config.theme.palette is mapping %}
|
||||
{% include "partials/palette.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- User preference: color palette -->
|
||||
{% if not config.theme.palette is mapping %}
|
||||
{% include "partials/javascripts/palette.html" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Site language selector -->
|
||||
{% if config.extra.alternate %}
|
||||
{% include "partials/alternate.html" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Button to open search modal -->
|
||||
{% if "material/search" in config.plugins %}
|
||||
{% set search = config.plugins["material/search"] | attr("config") %}
|
||||
|
||||
<!-- Check if search is actually enabled - see https://t.ly/DT_0V -->
|
||||
{% if search.enabled %}
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
{% set icon = config.theme.icon.search or "material/magnify" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</label>
|
||||
|
||||
<!-- Search interface -->
|
||||
{% include "partials/search.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Repository information -->
|
||||
{% if config.repo_url %}
|
||||
<div class="md-header__source">
|
||||
{% include "partials/source.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<!-- Navigation tabs (sticky) -->
|
||||
{% if "navigation.tabs.sticky" in features %}
|
||||
{% if "navigation.tabs" in features %}
|
||||
{% include "partials/tabs.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</header>
|
||||
69
overrides/partials/nav.html
Normal file
69
overrides/partials/nav.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!--
|
||||
Copyright (c) 2016-2025 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
{% import "partials/nav-item.html" as item with context %}
|
||||
|
||||
<!-- Determine classes -->
|
||||
{% set class = "md-nav md-nav--primary" %}
|
||||
{% if "navigation.tabs" in features %}
|
||||
{% set class = class ~ " md-nav--lifted" %}
|
||||
{% endif %}
|
||||
{% if "toc.integrate" in features %}
|
||||
{% set class = class ~ " md-nav--integrated" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav
|
||||
class="{{ class }}"
|
||||
aria-label="{{ lang.t('nav') }}"
|
||||
data-md-level="0"
|
||||
>
|
||||
|
||||
<!-- Site title -->
|
||||
<!-- <label class="md-nav__title" for="__drawer">
|
||||
<a
|
||||
href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
|
||||
title="{{ config.site_name | e }}"
|
||||
class="md-nav__button md-logo"
|
||||
aria-label="{{ config.site_name }}"
|
||||
data-md-component="logo"
|
||||
>
|
||||
{% include "partials/logo.html" %}
|
||||
</a>
|
||||
{{ config.site_name }}
|
||||
</label> -->
|
||||
|
||||
<!-- Repository information -->
|
||||
{% if config.repo_url %}
|
||||
<div class="md-nav__source">
|
||||
{% include "partials/source.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Navigation list -->
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
{% for nav_item in nav %}
|
||||
{% set path = "__nav_" ~ loop.index %}
|
||||
{{ item.render(nav_item, path, 1) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -5,45 +5,46 @@ build-backend = "hatchling.build"
|
||||
[project]
|
||||
name = "TagStudio"
|
||||
description = "A User-Focused Photo & File Management System."
|
||||
version = "9.5.2"
|
||||
version = "9.5.3"
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
"chardet==5.2.0",
|
||||
"ffmpeg-python==0.2.0",
|
||||
"humanfriendly==10.0",
|
||||
"mutagen==1.47.0",
|
||||
"numpy==2.1.0",
|
||||
"opencv_python==4.10.0.84",
|
||||
"Pillow==10.3.0",
|
||||
"pillow-heif==0.16.0",
|
||||
"pillow-jxl-plugin==1.3.0",
|
||||
"pydub==0.25.1",
|
||||
"PySide6==6.8.0.1",
|
||||
"rawpy==0.22.0",
|
||||
"Send2Trash==1.8.3",
|
||||
"SQLAlchemy==2.0.34",
|
||||
"structlog==24.4.0",
|
||||
"typing_extensions>=3.10.0.0,<4.11.0",
|
||||
"ujson>=5.8.0,<5.9.0",
|
||||
"vtf2img==0.1.0",
|
||||
"toml==0.10.2",
|
||||
"pydantic==2.9.2",
|
||||
"chardet~=5.2",
|
||||
"ffmpeg-python~=0.2",
|
||||
"humanfriendly==10.*",
|
||||
"mutagen~=1.47",
|
||||
"numpy~=2.2",
|
||||
"opencv_python~=4.11",
|
||||
"Pillow>=10.2,<=12.0",
|
||||
"pillow-heif~=0.22",
|
||||
"pillow-jxl-plugin~=1.3",
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
"rawpy~=0.24",
|
||||
"Send2Trash~=1.8",
|
||||
"SQLAlchemy~=2.0",
|
||||
"srctools~=2.6",
|
||||
"structlog~=25.3",
|
||||
"toml~=0.10",
|
||||
"typing_extensions~=4.13",
|
||||
"ujson~=5.10",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["tagstudio[mkdocs,mypy,pre-commit,pyinstaller,pytest,ruff]"]
|
||||
mkdocs = ["mkdocs-material[imaging]==9.*"]
|
||||
mypy = ["mypy==1.11.2", "mypy-extensions==1.*", "types-ujson>=5.8.0,<5.9.0"]
|
||||
pre-commit = ["pre-commit==3.7.0"]
|
||||
pyinstaller = ["Pyinstaller==6.6.0"]
|
||||
mkdocs = ["mkdocs-material[imaging]>=9.6.14"]
|
||||
mypy = ["mypy==1.15.0", "mypy-extensions==1.*", "types-ujson~=5.10"]
|
||||
pre-commit = ["pre-commit~=4.2"]
|
||||
pyinstaller = ["Pyinstaller~=6.13"]
|
||||
pytest = [
|
||||
"pytest==8.2.0",
|
||||
"pytest-cov==5.0.0",
|
||||
"pytest==8.3.5",
|
||||
"pytest-cov==6.1.1",
|
||||
"pytest-qt==4.4.0",
|
||||
"syrupy==4.7.1",
|
||||
"syrupy==4.9.1",
|
||||
]
|
||||
ruff = ["ruff==0.8.1"]
|
||||
ruff = ["ruff==0.11.8"]
|
||||
|
||||
[project.gui-scripts]
|
||||
tagstudio = "tagstudio.main:main"
|
||||
@@ -83,7 +84,8 @@ qt_api = "pyside6"
|
||||
|
||||
[tool.pyright]
|
||||
ignore = [".venv/**"]
|
||||
include = ["src/tagstudio/**"]
|
||||
include = ["src/tagstudio", "tests"]
|
||||
extraPaths = ["src/tagstudio", "tests"]
|
||||
reportAny = false
|
||||
reportIgnoreCommentWithoutRule = false
|
||||
reportImplicitStringConcatenation = false
|
||||
@@ -96,7 +98,7 @@ reportUnknownMemberType = false
|
||||
reportUnusedCallResult = false
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["main_window.py", "home_ui.py", "resources.py", "resources_rc.py"]
|
||||
exclude = ["home_ui.py", "resources.py", "resources_rc.py"]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
VERSION: str = "9.5.2" # Major.Minor.Patch
|
||||
VERSION: str = "9.5.3" # Major.Minor.Patch
|
||||
VERSION_BRANCH: str = "" # Usually "" or "Pre-Release"
|
||||
|
||||
# The folder & file names where TagStudio keeps its data relative to a library.
|
||||
|
||||
@@ -24,6 +24,15 @@ class ShowFilepathOption(int, enum.Enum):
|
||||
DEFAULT = SHOW_RELATIVE_PATHS
|
||||
|
||||
|
||||
class TagClickActionOption(int, enum.Enum):
|
||||
"""Values representing the options for the "tag_click_action" setting."""
|
||||
|
||||
OPEN_EDIT = 0
|
||||
SET_SEARCH = 1
|
||||
ADD_TO_SEARCH = 2
|
||||
DEFAULT = OPEN_EDIT
|
||||
|
||||
|
||||
class Theme(str, enum.Enum):
|
||||
COLOR_BG_DARK = "#65000000"
|
||||
COLOR_BG_LIGHT = "#22000000"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
@@ -10,7 +11,7 @@ import structlog
|
||||
import toml
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
|
||||
if platform.system() == "Windows":
|
||||
DEFAULT_GLOBAL_SETTINGS_PATH = (
|
||||
@@ -49,6 +50,13 @@ class GlobalSettings(BaseModel):
|
||||
page_size: int = Field(default=100)
|
||||
show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT)
|
||||
theme: Theme = Field(default=Theme.SYSTEM)
|
||||
tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT)
|
||||
|
||||
date_format: str = Field(default="%x")
|
||||
hour_format: bool = Field(default=True)
|
||||
zero_padding: bool = Field(default=True)
|
||||
|
||||
loaded_from: Path = Field(default=DEFAULT_GLOBAL_SETTINGS_PATH, exclude=True)
|
||||
|
||||
@staticmethod
|
||||
def read_settings(path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> "GlobalSettings":
|
||||
@@ -58,14 +66,37 @@ class GlobalSettings(BaseModel):
|
||||
if len(filecontents.strip()) != 0:
|
||||
logger.info("[Settings] Reading Global Settings File", path=path)
|
||||
settings_data = toml.loads(filecontents)
|
||||
settings = GlobalSettings(**settings_data)
|
||||
settings = GlobalSettings(**settings_data, loaded_from=path)
|
||||
return settings
|
||||
|
||||
return GlobalSettings()
|
||||
return GlobalSettings(loaded_from=path)
|
||||
|
||||
def save(self, path: Path = DEFAULT_GLOBAL_SETTINGS_PATH) -> None:
|
||||
def save(self, path: Path | None = None) -> None:
|
||||
if path is None:
|
||||
path = self.loaded_from
|
||||
if not path.parent.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, "w") as f:
|
||||
toml.dump(dict(self), f, encoder=TomlEnumEncoder())
|
||||
toml.dump(self.model_dump(), f, encoder=TomlEnumEncoder())
|
||||
|
||||
@property
|
||||
def datetime_format(self) -> str:
|
||||
date_format = self.date_format
|
||||
is_24h = self.hour_format
|
||||
hour_format = "%H:%M:%S" if is_24h else "%I:%M:%S %p"
|
||||
zero_padding = self.zero_padding
|
||||
zero_padding_symbol = ""
|
||||
|
||||
if not zero_padding:
|
||||
zero_padding_symbol = "#" if platform.system() == "Windows" else "-"
|
||||
date_format = date_format.replace("%d", f"%{zero_padding_symbol}d").replace(
|
||||
"%m", f"%{zero_padding_symbol}m"
|
||||
)
|
||||
hour_format = hour_format.replace("%H", f"%{zero_padding_symbol}H").replace(
|
||||
"%I", f"%{zero_padding_symbol}I"
|
||||
)
|
||||
return f"{date_format}, {hour_format}"
|
||||
|
||||
def format_datetime(self, dt: datetime) -> str:
|
||||
return datetime.strftime(dt, self.datetime_format)
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.query_lang.ast import AST, Constraint, ConstraintType
|
||||
from tagstudio.core.query_lang.ast import AST
|
||||
from tagstudio.core.query_lang.parser import Parser
|
||||
|
||||
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
|
||||
@@ -72,61 +72,62 @@ class SortingModeEnum(enum.Enum):
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilterState:
|
||||
class BrowsingState:
|
||||
"""Represent a state of the Library grid view."""
|
||||
|
||||
# these should remain
|
||||
page_size: int
|
||||
page_index: int = 0
|
||||
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
|
||||
ascending: bool = True
|
||||
|
||||
# these should be erased on update
|
||||
query: str | None = None
|
||||
|
||||
# Abstract Syntax Tree Of the current Search Query
|
||||
ast: AST | None = None
|
||||
|
||||
@property
|
||||
def limit(self):
|
||||
return self.page_size
|
||||
|
||||
@property
|
||||
def offset(self):
|
||||
return self.page_size * self.page_index
|
||||
def ast(self) -> AST | None:
|
||||
if self.query is None:
|
||||
return None
|
||||
return Parser(self.query).parse()
|
||||
|
||||
@classmethod
|
||||
def show_all(cls, page_size: int) -> "FilterState":
|
||||
return FilterState(page_size=page_size)
|
||||
def show_all(cls) -> "BrowsingState":
|
||||
return BrowsingState()
|
||||
|
||||
@classmethod
|
||||
def from_search_query(cls, search_query: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Parser(search_query).parse(), page_size=page_size)
|
||||
def from_search_query(cls, search_query: str) -> "BrowsingState":
|
||||
return cls(query=search_query)
|
||||
|
||||
@classmethod
|
||||
def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size)
|
||||
def from_tag_id(cls, tag_id: int | str) -> "BrowsingState":
|
||||
return cls(query=f"tag_id:{str(tag_id)}")
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path | str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size)
|
||||
def from_path(cls, path: Path | str) -> "BrowsingState":
|
||||
return cls(query=f'path:"{str(path).strip()}"')
|
||||
|
||||
@classmethod
|
||||
def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size)
|
||||
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
|
||||
return cls(query=f"mediatype:{mediatype}")
|
||||
|
||||
@classmethod
|
||||
def from_filetype(cls, filetype: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size)
|
||||
def from_filetype(cls, filetype: str) -> "BrowsingState":
|
||||
return cls(query=f"filetype:{filetype}")
|
||||
|
||||
@classmethod
|
||||
def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size)
|
||||
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
|
||||
return cls(query=f'tag:"{tag_name}"')
|
||||
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
|
||||
def with_page_index(self, index: int) -> "BrowsingState":
|
||||
return replace(self, page_index=index)
|
||||
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
|
||||
return replace(self, sorting_mode=mode)
|
||||
|
||||
def with_sorting_direction(self, ascending: bool) -> "FilterState":
|
||||
def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
|
||||
return replace(self, ascending=ascending)
|
||||
|
||||
def with_search_query(self, search_query: str) -> "BrowsingState":
|
||||
return replace(self, query=search_query)
|
||||
|
||||
|
||||
class FieldTypeEnum(enum.Enum):
|
||||
TEXT_LINE = "Text Line"
|
||||
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
import shutil
|
||||
import time
|
||||
import unicodedata
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Iterable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from os import makedirs
|
||||
@@ -61,8 +61,8 @@ from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.db import make_tables
|
||||
from tagstudio.core.library.alchemy.enums import (
|
||||
MAX_SQL_VARIABLES,
|
||||
BrowsingState,
|
||||
FieldTypeEnum,
|
||||
FilterState,
|
||||
SortingModeEnum,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
@@ -170,26 +170,26 @@ class SearchResult:
|
||||
|
||||
Attributes:
|
||||
total_count(int): total number of items for given query, might be different than len(items).
|
||||
items(list[Entry]): for current page (size matches filter.page_size).
|
||||
ids(list[int]): for current page (size matches filter.page_size).
|
||||
"""
|
||||
|
||||
total_count: int
|
||||
items: list[Entry]
|
||||
ids: list[int]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Boolean evaluation for the wrapper.
|
||||
|
||||
:return: True if there are items in the result.
|
||||
:return: True if there are ids in the result.
|
||||
"""
|
||||
return self.total_count > 0
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the total number of items in the result."""
|
||||
return len(self.items)
|
||||
"""Return the total number of ids in the result."""
|
||||
return len(self.ids)
|
||||
|
||||
def __getitem__(self, index: int) -> Entry:
|
||||
"""Allow to access items via index directly on the wrapper."""
|
||||
return self.items[index]
|
||||
def __getitem__(self, index: int) -> int:
|
||||
"""Allow to access ids via index directly on the wrapper."""
|
||||
return self.ids[index]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -611,7 +611,7 @@ class Library:
|
||||
|
||||
def apply_db9_filename_population(self, session: Session):
|
||||
"""Populate the filename column introduced in DB_VERSION 9."""
|
||||
for entry in self.get_entries():
|
||||
for entry in self.all_entries():
|
||||
session.merge(entry).filename = entry.path.name
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Populated filename column in entries table")
|
||||
@@ -674,7 +674,7 @@ class Library:
|
||||
start_time = time.time()
|
||||
entry = session.scalar(entry_stmt)
|
||||
if with_tags:
|
||||
tags = set(session.scalars(tag_stmt)) # pyright: ignore [reportPossiblyUnboundVariable]
|
||||
tags = set(session.scalars(tag_stmt)) # pyright: ignore[reportPossiblyUnboundVariable]
|
||||
end_time = time.time()
|
||||
logger.info(
|
||||
f"[Library] Time it took to get entry: "
|
||||
@@ -692,6 +692,12 @@ class Library:
|
||||
entry.tags = tags
|
||||
return entry
|
||||
|
||||
def get_entries(self, entry_ids: Iterable[int]) -> list[Entry]:
|
||||
with Session(self.engine) as session:
|
||||
statement = select(Entry).where(Entry.id.in_(entry_ids))
|
||||
entries = dict((e.id, e) for e in session.scalars(statement))
|
||||
return [entries[id] for id in entry_ids]
|
||||
|
||||
def get_entries_full(self, entry_ids: list[int] | set[int]) -> Iterator[Entry]:
|
||||
"""Load entry and join with all joins and all tags."""
|
||||
with Session(self.engine) as session:
|
||||
@@ -746,12 +752,25 @@ class Library:
|
||||
make_transient(entry)
|
||||
return entry
|
||||
|
||||
def get_tag_entries(
|
||||
self, tag_ids: Iterable[int], entry_ids: Iterable[int]
|
||||
) -> dict[int, set[int]]:
|
||||
"""Returns a dict of tag_id->(entry_ids with tag_id)."""
|
||||
tag_entries: dict[int, set[int]] = dict((id, set()) for id in tag_ids)
|
||||
with Session(self.engine) as session:
|
||||
statement = select(TagEntry).where(
|
||||
and_(TagEntry.tag_id.in_(tag_ids), TagEntry.entry_id.in_(entry_ids))
|
||||
)
|
||||
for tag_entry in session.scalars(statement).fetchall():
|
||||
tag_entries[tag_entry.tag_id].add(tag_entry.entry_id)
|
||||
return tag_entries
|
||||
|
||||
@property
|
||||
def entries_count(self) -> int:
|
||||
with Session(self.engine) as session:
|
||||
return session.scalar(select(func.count(Entry.id)))
|
||||
|
||||
def get_entries(self, with_joins: bool = False) -> Iterator[Entry]:
|
||||
def all_entries(self, with_joins: bool = False) -> Iterator[Entry]:
|
||||
"""Load entries without joins."""
|
||||
with Session(self.engine) as session:
|
||||
stmt = select(Entry)
|
||||
@@ -857,17 +876,18 @@ class Library:
|
||||
|
||||
def search_library(
|
||||
self,
|
||||
search: FilterState,
|
||||
search: BrowsingState,
|
||||
page_size: int | None,
|
||||
) -> SearchResult:
|
||||
"""Filter library by search query.
|
||||
|
||||
:return: number of entries matching the query and one page of results.
|
||||
"""
|
||||
assert isinstance(search, FilterState)
|
||||
assert isinstance(search, BrowsingState)
|
||||
assert self.engine
|
||||
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
statement = select(Entry)
|
||||
statement = select(Entry.id, func.count().over())
|
||||
|
||||
if search.ast:
|
||||
start_time = time.time()
|
||||
@@ -885,13 +905,6 @@ class Library:
|
||||
elif extensions:
|
||||
statement = statement.where(Entry.suffix.in_(extensions))
|
||||
|
||||
statement = statement.distinct(Entry.id)
|
||||
start_time = time.time()
|
||||
query_count = select(func.count()).select_from(statement.alias("entries"))
|
||||
count_all: int = session.execute(query_count).scalar() or 0
|
||||
end_time = time.time()
|
||||
logger.info(f"finished counting ({format_timespan(end_time - start_time)})")
|
||||
|
||||
sort_on: ColumnExpressionArgument = Entry.id
|
||||
match search.sorting_mode:
|
||||
case SortingModeEnum.DATE_ADDED:
|
||||
@@ -902,7 +915,8 @@ class Library:
|
||||
sort_on = func.lower(Entry.path)
|
||||
|
||||
statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on))
|
||||
statement = statement.limit(search.limit).offset(search.offset)
|
||||
if page_size is not None:
|
||||
statement = statement.limit(page_size).offset(search.page_index * page_size)
|
||||
|
||||
logger.info(
|
||||
"searching library",
|
||||
@@ -911,13 +925,18 @@ class Library:
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
items = session.scalars(statement).fetchall()
|
||||
rows = session.execute(statement).fetchall()
|
||||
ids = []
|
||||
count = 0
|
||||
for row in rows:
|
||||
id, count = row._tuple()
|
||||
ids.append(id)
|
||||
end_time = time.time()
|
||||
logger.info(f"SQL Execution finished ({format_timespan(end_time - start_time)})")
|
||||
|
||||
res = SearchResult(
|
||||
total_count=count_all,
|
||||
items=list(items),
|
||||
total_count=count,
|
||||
ids=ids,
|
||||
)
|
||||
|
||||
session.expunge_all()
|
||||
@@ -1424,12 +1443,14 @@ class Library:
|
||||
)
|
||||
tag = session.scalar(tags_query.where(Tag.id == tag_id))
|
||||
|
||||
session.expunge(tag)
|
||||
for parent in tag.parent_tags:
|
||||
session.expunge(parent)
|
||||
if tag is not None:
|
||||
session.expunge(tag)
|
||||
|
||||
for alias in tag.aliases:
|
||||
session.expunge(alias)
|
||||
for parent in tag.parent_tags:
|
||||
session.expunge(parent)
|
||||
|
||||
for alias in tag.aliases:
|
||||
session.expunge(alias)
|
||||
|
||||
return tag
|
||||
|
||||
@@ -1437,10 +1458,23 @@ class Library:
|
||||
with Session(self.engine) as session:
|
||||
statement = (
|
||||
select(Tag)
|
||||
.options(selectinload(Tag.parent_tags), selectinload(Tag.aliases))
|
||||
.outerjoin(TagAlias)
|
||||
.where(or_(Tag.name == tag_name, TagAlias.name == tag_name))
|
||||
)
|
||||
return session.scalar(statement)
|
||||
|
||||
tag = session.scalar(statement)
|
||||
|
||||
if tag is not None:
|
||||
session.expunge(tag)
|
||||
|
||||
for parent in tag.parent_tags:
|
||||
session.expunge(parent)
|
||||
|
||||
for alias in tag.aliases:
|
||||
session.expunge(alias)
|
||||
|
||||
return tag
|
||||
|
||||
def get_alias(self, tag_id: int, alias_id: int) -> TagAlias | None:
|
||||
with Session(self.engine) as session:
|
||||
|
||||
@@ -147,7 +147,7 @@ class SQLBoolExpressionBuilder(BaseVisitor[ColumnElement[bool]]):
|
||||
# raise exception if Constraint stays unhandled
|
||||
raise NotImplementedError("This type of constraint is not implemented yet")
|
||||
|
||||
def visit_property(self, node: Property) -> None:
|
||||
def visit_property(self, node: Property) -> ColumnElement[bool]:
|
||||
raise NotImplementedError("This should never be reached!")
|
||||
|
||||
def visit_not(self, node: Not) -> ColumnElement[bool]:
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
FILETYPE_EQUIVALENTS = [
|
||||
set(["aif", "aiff", "aifc"]),
|
||||
@@ -80,6 +81,21 @@ class MediaCategory:
|
||||
name: str
|
||||
is_iana: bool = False
|
||||
|
||||
def contains(self, ext: str, mime_fallback: bool = False) -> bool:
|
||||
"""Check if an extension is a member of this MediaCategory.
|
||||
|
||||
Args:
|
||||
ext (str): File extension with a leading "." and in all lowercase.
|
||||
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
|
||||
"""
|
||||
if ext in self.extensions:
|
||||
return True
|
||||
elif mime_fallback and self.is_iana:
|
||||
mime_type: str | None = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
|
||||
if mime_type is not None and mime_type.startswith(self.media_type.value):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MediaCategories:
|
||||
"""Contain pre-made MediaCategory objects as well as methods to interact with them."""
|
||||
@@ -93,6 +109,7 @@ class MediaCategories:
|
||||
".psd",
|
||||
}
|
||||
_AFFINITY_PHOTO_SET: set[str] = {".afphoto"}
|
||||
_KRITA_SET: set[str] = {".kra", ".krz"}
|
||||
_ARCHIVE_SET: set[str] = {
|
||||
".7z",
|
||||
".gz",
|
||||
@@ -326,6 +343,7 @@ class MediaCategories:
|
||||
".odp",
|
||||
".ods",
|
||||
".odt",
|
||||
".ora",
|
||||
}
|
||||
_PACKAGE_SET: set[str] = {
|
||||
".aab",
|
||||
@@ -590,6 +608,12 @@ class MediaCategories:
|
||||
is_iana=True,
|
||||
name="video",
|
||||
)
|
||||
KRITA_TYPES = MediaCategory(
|
||||
media_type=MediaType.IMAGE,
|
||||
extensions=_KRITA_SET,
|
||||
is_iana=False,
|
||||
name="krita",
|
||||
)
|
||||
|
||||
ALL_CATEGORIES = [
|
||||
ADOBE_PHOTOSHOP_TYPES,
|
||||
@@ -624,6 +648,7 @@ class MediaCategories:
|
||||
SPREADSHEET_TYPES,
|
||||
TEXT_TYPES,
|
||||
VIDEO_TYPES,
|
||||
KRITA_TYPES,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@@ -635,16 +660,11 @@ class MediaCategories:
|
||||
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
|
||||
"""
|
||||
media_types: set[MediaType] = set()
|
||||
# mime_guess: bool = False
|
||||
|
||||
for cat in MediaCategories.ALL_CATEGORIES:
|
||||
if ext in cat.extensions:
|
||||
if cat.contains(ext, mime_fallback):
|
||||
media_types.add(cat.media_type)
|
||||
elif mime_fallback and cat.is_iana:
|
||||
mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
|
||||
if mime_type and mime_type.startswith(cat.media_type.value):
|
||||
media_types.add(cat.media_type)
|
||||
# mime_guess = True
|
||||
|
||||
return media_types
|
||||
|
||||
@staticmethod
|
||||
@@ -656,10 +676,4 @@ class MediaCategories:
|
||||
media_cat (MediaCategory): The MediaCategory to to check for extension membership.
|
||||
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
|
||||
"""
|
||||
if ext in media_cat.extensions:
|
||||
return True
|
||||
elif mime_fallback and media_cat.is_iana:
|
||||
mime_type: str = mimetypes.guess_type(Path("x" + ext), strict=False)[0]
|
||||
if mime_type and mime_type.startswith(media_cat.media_type.value):
|
||||
return True
|
||||
return False
|
||||
return media_cat.contains(ext, mime_fallback)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Generic, TypeVar
|
||||
from typing import Generic, TypeVar, Union
|
||||
|
||||
|
||||
class ConstraintType(Enum):
|
||||
@@ -12,7 +12,7 @@ class ConstraintType(Enum):
|
||||
Special = 5
|
||||
|
||||
@staticmethod
|
||||
def from_string(text: str) -> "ConstraintType":
|
||||
def from_string(text: str) -> Union["ConstraintType", None]:
|
||||
return {
|
||||
"tag": ConstraintType.Tag,
|
||||
"tag_id": ConstraintType.TagID,
|
||||
@@ -24,7 +24,7 @@ class ConstraintType(Enum):
|
||||
|
||||
|
||||
class AST:
|
||||
parent: "AST" = None
|
||||
parent: Union["AST", None] = None
|
||||
|
||||
def __str__(self):
|
||||
class_name = self.__class__.__name__
|
||||
|
||||
@@ -26,19 +26,19 @@ class Token:
|
||||
start: int
|
||||
end: int
|
||||
|
||||
def __init__(self, type: TokenType, value: Any, start: int = None, end: int = None) -> None:
|
||||
def __init__(self, type: TokenType, value: Any, start: int, end: int) -> None:
|
||||
self.type = type
|
||||
self.value = value
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
@staticmethod
|
||||
def from_type(type: TokenType, pos: int = None) -> "Token":
|
||||
def from_type(type: TokenType, pos: int) -> "Token":
|
||||
return Token(type, None, pos, pos)
|
||||
|
||||
@staticmethod
|
||||
def EOF() -> "Token": # noqa: N802
|
||||
return Token.from_type(TokenType.EOF)
|
||||
def EOF(pos: int) -> "Token": # noqa: N802
|
||||
return Token.from_type(TokenType.EOF, pos)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Token({self.type}, {self.value}, {self.start}, {self.end})" # pragma: nocover
|
||||
@@ -50,7 +50,7 @@ class Token:
|
||||
class Tokenizer:
|
||||
text: str
|
||||
pos: int
|
||||
current_char: str
|
||||
current_char: str | None
|
||||
|
||||
ESCAPABLE_CHARS = ["\\", '"', '"']
|
||||
NOT_IN_ULITERAL = [":", " ", "[", "]", "(", ")", "=", ","]
|
||||
@@ -63,7 +63,7 @@ class Tokenizer:
|
||||
def get_next_token(self) -> Token:
|
||||
self.__skip_whitespace()
|
||||
if self.current_char is None:
|
||||
return Token.EOF()
|
||||
return Token.EOF(self.pos)
|
||||
|
||||
if self.current_char in ("'", '"'):
|
||||
return self.__quoted_string()
|
||||
@@ -119,6 +119,8 @@ class Tokenizer:
|
||||
out = ""
|
||||
|
||||
while escape or self.current_char != quote:
|
||||
if self.current_char is None:
|
||||
raise ParsingError(start, self.pos, "Unterminated quoted string")
|
||||
if escape:
|
||||
escape = False
|
||||
if self.current_char not in Tokenizer.ESCAPABLE_CHARS:
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
|
||||
@@ -52,14 +52,15 @@ class DupeRegistry:
|
||||
continue
|
||||
|
||||
results = self.library.search_library(
|
||||
FilterState.from_path(path_relative, page_size=500),
|
||||
BrowsingState.from_path(path_relative), 500
|
||||
)
|
||||
entries = self.library.get_entries(results.ids)
|
||||
|
||||
if not results:
|
||||
# file not in library
|
||||
continue
|
||||
|
||||
files.append(results[0])
|
||||
files.append(entries[0])
|
||||
|
||||
if not len(files) > 1:
|
||||
# only one file in the group, nothing to do
|
||||
@@ -79,7 +80,7 @@ class DupeRegistry:
|
||||
)
|
||||
|
||||
for i, entries in enumerate(self.groups):
|
||||
remove_ids = [x.id for x in entries[1:]]
|
||||
remove_ids = entries[1:]
|
||||
logger.info("Removing entries group", ids=remove_ids)
|
||||
self.library.remove_entries(remove_ids)
|
||||
yield i - 1 # The -1 waits for the next step to finish
|
||||
|
||||
@@ -27,7 +27,7 @@ class MissingRegistry:
|
||||
"""Track the number of entries that point to an invalid filepath."""
|
||||
logger.info("[refresh_missing_files] Refreshing missing files...")
|
||||
self.missing_file_entries = []
|
||||
for i, entry in enumerate(self.library.get_entries()):
|
||||
for i, entry in enumerate(self.library.all_entries()):
|
||||
full_path = self.library.library_dir / entry.path
|
||||
if not full_path.exists() or not full_path.is_file():
|
||||
self.missing_file_entries.append(entry)
|
||||
|
||||
@@ -7,16 +7,13 @@
|
||||
"""TagStudio launcher."""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
structlog.configure(
|
||||
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
|
||||
)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -67,7 +64,7 @@ def main():
|
||||
driver.start()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
logging.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
|
||||
logger.info(f"\nTagStudio Frontend ({ui_name}) Crashed! Press Enter to Continue...")
|
||||
input()
|
||||
|
||||
|
||||
|
||||
@@ -141,8 +141,7 @@ class CacheManager(metaclass=Singleton):
|
||||
self.last_lib_path = self.lib.library_dir
|
||||
CacheManager.folder_dict.clear()
|
||||
|
||||
# NOTE: The /dev/null check is a workaround for current test assumptions.
|
||||
if self.last_lib_path and self.last_lib_path != Path("/dev/null"):
|
||||
if self.last_lib_path:
|
||||
# Ensure thumbnail cache path exists.
|
||||
self.cache_dir().mkdir(exist_ok=True)
|
||||
# Registers any existing folders and counts the capacity of the most recent one.
|
||||
|
||||
93
src/tagstudio/qt/controller/components/tag_box_controller.py
Normal file
93
src/tagstudio/qt/controller/components/tag_box_controller.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.view.components.tag_box_view import TagBoxWidgetView
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagBoxWidget(TagBoxWidgetView):
|
||||
on_update = Signal()
|
||||
|
||||
__entries: list[int] = []
|
||||
|
||||
def __init__(self, title: str, driver: "QtDriver"):
|
||||
super().__init__(title, driver)
|
||||
self.__driver = driver
|
||||
|
||||
def set_entries(self, entries: list[int]) -> None:
|
||||
self.__entries = entries
|
||||
|
||||
@override
|
||||
def _on_click(self, tag: Tag) -> None: # type: ignore[misc]
|
||||
match self.__driver.settings.tag_click_action:
|
||||
case TagClickActionOption.OPEN_EDIT:
|
||||
self._on_edit(tag)
|
||||
case TagClickActionOption.SET_SEARCH:
|
||||
self.__driver.update_browsing_state(BrowsingState.from_tag_id(tag.id))
|
||||
case TagClickActionOption.ADD_TO_SEARCH:
|
||||
# NOTE: modifying the ast and then setting that would be nicer
|
||||
# than this string manipulation, but also much more complex,
|
||||
# due to needing to implement a visitor that turns an AST to a string
|
||||
# So if that exists when you read this, change the following accordingly.
|
||||
current = self.__driver.browsing_history.current
|
||||
suffix = BrowsingState.from_tag_id(tag.id).query
|
||||
assert suffix is not None
|
||||
self.__driver.update_browsing_state(
|
||||
current.with_search_query(
|
||||
f"{current.query} {suffix}" if current.query else suffix
|
||||
)
|
||||
)
|
||||
|
||||
@override
|
||||
def _on_remove(self, tag: Tag) -> None: # type: ignore[misc]
|
||||
logger.info(
|
||||
"[TagBoxWidget] remove_tag",
|
||||
selected=self.__entries,
|
||||
)
|
||||
|
||||
for entry_id in self.__entries:
|
||||
self.__driver.lib.remove_tags_from_entries(entry_id, tag.id)
|
||||
|
||||
self.on_update.emit()
|
||||
|
||||
@override
|
||||
def _on_edit(self, tag: Tag) -> None: # type: ignore[misc]
|
||||
build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag)
|
||||
|
||||
edit_modal = PanelModal(
|
||||
build_tag_panel,
|
||||
self.__driver.lib.tag_display_name(tag.id),
|
||||
"Edit Tag",
|
||||
done_callback=self.on_update.emit,
|
||||
has_save=True,
|
||||
)
|
||||
# TODO - this was update_tag()
|
||||
edit_modal.saved.connect(
|
||||
lambda: self.__driver.lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
alias_names=set(build_tag_panel.alias_names),
|
||||
alias_ids=set(build_tag_panel.alias_ids),
|
||||
)
|
||||
)
|
||||
edit_modal.show()
|
||||
|
||||
@override
|
||||
def _on_search(self, tag: Tag) -> None: # type: ignore[misc]
|
||||
self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}")
|
||||
self.__driver.update_browsing_state(BrowsingState.from_tag_id(tag.id))
|
||||
@@ -0,0 +1,155 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import cv2
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import QSize
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.qt.helpers.file_opener import open_file
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
from tagstudio.qt.view.widgets.preview.preview_thumb_view import PreviewThumbView
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
class PreviewThumb(PreviewThumbView):
|
||||
__current_file: Path
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__(library, driver)
|
||||
|
||||
self.__driver: QtDriver = driver
|
||||
|
||||
def __get_image_stats(self, filepath: Path) -> FileAttributeData:
|
||||
"""Get width and height of an image as dict."""
|
||||
stats = FileAttributeData()
|
||||
ext = filepath.suffix.lower()
|
||||
|
||||
if MediaCategories.IMAGE_RAW_TYPES.contains(ext, mime_fallback=True):
|
||||
try:
|
||||
with rawpy.imread(str(filepath)) as raw:
|
||||
rgb = raw.postprocess()
|
||||
image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
|
||||
stats.width = image.width
|
||||
stats.height = image.height
|
||||
except (
|
||||
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
FileNotFoundError,
|
||||
):
|
||||
pass
|
||||
elif MediaCategories.IMAGE_RASTER_TYPES.contains(ext, mime_fallback=True):
|
||||
try:
|
||||
image = Image.open(str(filepath))
|
||||
stats.width = image.width
|
||||
stats.height = image.height
|
||||
except (
|
||||
DecompressionBombError,
|
||||
FileNotFoundError,
|
||||
NotImplementedError,
|
||||
UnidentifiedImageError,
|
||||
) as e:
|
||||
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
|
||||
elif MediaCategories.IMAGE_VECTOR_TYPES.contains(ext, mime_fallback=True):
|
||||
pass # TODO
|
||||
|
||||
return stats
|
||||
|
||||
def __get_gif_data(self, filepath: Path) -> tuple[bytes, tuple[int, int]] | None:
|
||||
"""Loads an animated image and returns gif data and size, if successful."""
|
||||
ext = filepath.suffix.lower()
|
||||
|
||||
try:
|
||||
image: Image.Image = Image.open(filepath)
|
||||
|
||||
if ext == ".apng":
|
||||
image_bytes_io = io.BytesIO()
|
||||
image.save(
|
||||
image_bytes_io,
|
||||
"GIF",
|
||||
lossless=True,
|
||||
save_all=True,
|
||||
loop=0,
|
||||
disposal=2,
|
||||
)
|
||||
image.close()
|
||||
image_bytes_io.seek(0)
|
||||
return (image_bytes_io.read(), (image.width, image.height))
|
||||
else:
|
||||
image.close()
|
||||
with open(filepath, "rb") as f:
|
||||
return (f.read(), (image.width, image.height))
|
||||
|
||||
except (UnidentifiedImageError, FileNotFoundError) as e:
|
||||
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
|
||||
return None
|
||||
|
||||
def __get_video_res(self, filepath: str) -> tuple[bool, QSize]:
|
||||
video = cv2.VideoCapture(filepath, cv2.CAP_FFMPEG)
|
||||
success, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
image = Image.fromarray(frame)
|
||||
return (success, QSize(image.width, image.height))
|
||||
|
||||
def display_file(self, filepath: Path) -> FileAttributeData:
|
||||
"""Render a single file preview."""
|
||||
self.__current_file = filepath
|
||||
|
||||
ext = filepath.suffix.lower()
|
||||
|
||||
# Video
|
||||
if MediaCategories.VIDEO_TYPES.contains(ext, mime_fallback=True) and is_readable_video(
|
||||
filepath
|
||||
):
|
||||
size: QSize | None = None
|
||||
try:
|
||||
success, size = self.__get_video_res(str(filepath))
|
||||
if not success:
|
||||
size = None
|
||||
except cv2.error as e:
|
||||
logger.error("[PreviewThumb] Could not play video", filepath=filepath, error=e)
|
||||
|
||||
return self._display_video(filepath, size)
|
||||
# Audio
|
||||
elif MediaCategories.AUDIO_TYPES.contains(ext, mime_fallback=True):
|
||||
return self._display_audio(filepath)
|
||||
# Animated Images
|
||||
elif MediaCategories.IMAGE_ANIMATED_TYPES.contains(ext, mime_fallback=True):
|
||||
if (ret := self.__get_gif_data(filepath)) and (
|
||||
stats := self._display_gif(ret[0], ret[1])
|
||||
) is not None:
|
||||
return stats
|
||||
else:
|
||||
self._display_image(filepath)
|
||||
return self.__get_image_stats(filepath)
|
||||
# Other Types (Including Images)
|
||||
else:
|
||||
self._display_image(filepath)
|
||||
return self.__get_image_stats(filepath)
|
||||
|
||||
def _open_file_action_callback(self):
|
||||
open_file(self.__current_file)
|
||||
|
||||
def _open_explorer_action_callback(self):
|
||||
open_file(self.__current_file, file_manager=True)
|
||||
|
||||
def _delete_action_callback(self):
|
||||
if bool(self.__current_file):
|
||||
self.__driver.delete_files_callback(self.__current_file)
|
||||
|
||||
def _button_wrapper_callback(self):
|
||||
open_file(self.__current_file)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import typing
|
||||
from warnings import catch_warnings
|
||||
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.modals.add_field import AddFieldModal
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal
|
||||
from tagstudio.qt.view.widgets.preview_panel_view import PreviewPanelView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
class PreviewPanel(PreviewPanelView):
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__(library, driver)
|
||||
|
||||
self.__add_field_modal = AddFieldModal(self.lib)
|
||||
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
|
||||
|
||||
def _add_field_button_callback(self):
|
||||
self.__add_field_modal.show()
|
||||
|
||||
def _add_tag_button_callback(self):
|
||||
self.__add_tag_modal.show()
|
||||
|
||||
def _set_selection_callback(self):
|
||||
with catch_warnings(record=True):
|
||||
self.__add_field_modal.done.disconnect()
|
||||
self.__add_tag_modal.tsp.tag_chosen.disconnect()
|
||||
|
||||
self.__add_field_modal.done.connect(self._add_field_to_selected)
|
||||
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
|
||||
|
||||
def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
|
||||
self._fields.add_field_to_selected(field_list)
|
||||
if len(self._selected) == 1:
|
||||
self._fields.update_from_entry(self._selected[0])
|
||||
|
||||
def _add_tag_to_selected(self, tag_id: int):
|
||||
self._fields.add_tags_to_selected(tag_id)
|
||||
if len(self._selected) == 1:
|
||||
self._fields.update_from_entry(self._selected[0])
|
||||
@@ -3,12 +3,12 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from send2trash import send2trash
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def delete_file(path: str | Path) -> bool:
|
||||
@@ -19,13 +19,13 @@ def delete_file(path: str | Path) -> bool:
|
||||
"""
|
||||
_path = Path(path)
|
||||
try:
|
||||
logging.info(f"[delete_file] Sending to Trash: {_path}")
|
||||
logger.info(f"[delete_file] Sending to Trash: {_path}")
|
||||
send2trash(_path)
|
||||
return True
|
||||
except PermissionError as e:
|
||||
logging.error(f"[delete_file][ERROR] PermissionError: {e}")
|
||||
logger.error(f"[delete_file][ERROR] PermissionError: {e}")
|
||||
except FileNotFoundError:
|
||||
logging.error(f"[delete_file][ERROR] File Not Found: {_path}")
|
||||
logger.error(f"[delete_file][ERROR] File Not Found: {_path}")
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logger.error(e)
|
||||
return False
|
||||
|
||||
@@ -13,6 +13,8 @@ class QPushButtonWrapper(QPushButton):
|
||||
the warning that is triggered by disconnecting a signal that is not currently connected.
|
||||
"""
|
||||
|
||||
is_connected: bool
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_connected = False
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import QSlider, QStyle, QStyleOptionSlider
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class QClickSlider(QSlider):
|
||||
"""Custom QSlider wrapper.
|
||||
@@ -35,9 +38,14 @@ class QClickSlider(QSlider):
|
||||
was_slider_clicked = handle_rect.contains(int(ev.position().x()), int(ev.position().y()))
|
||||
|
||||
if not was_slider_clicked:
|
||||
self.setSliderDown(True)
|
||||
self.setValue(
|
||||
QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), ev.x(), self.width())
|
||||
)
|
||||
self.mouse_pressed = True
|
||||
|
||||
super().mousePressEvent(ev)
|
||||
|
||||
@override
|
||||
def mouseReleaseEvent(self, ev: QMouseEvent) -> None:
|
||||
self.setSliderDown(False)
|
||||
return super().mouseReleaseEvent(ev)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from shutil import which
|
||||
@@ -15,7 +16,13 @@ from tagstudio.qt.helpers.silent_popen import silent_Popen, silent_run
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
FFMPEG_MACOS_LOCATIONS: list[str] = ["", "/opt/homebrew/bin/", "/usr/local/bin/"]
|
||||
user = os.environ.get("USER", None)
|
||||
FFMPEG_MACOS_LOCATIONS: list[str] = [
|
||||
"",
|
||||
"/opt/homebrew/bin/",
|
||||
"/usr/local/bin/",
|
||||
f"/etc/profiles/per-user/{user}/bin",
|
||||
]
|
||||
|
||||
|
||||
def _get_ffprobe_location() -> str:
|
||||
@@ -58,7 +65,7 @@ def probe(filename, cmd=FFPROBE_CMD, timeout=None, **kwargs):
|
||||
``stderr`` property of the exception.
|
||||
"""
|
||||
args = [cmd, "-show_format", "-show_streams", "-of", "json"]
|
||||
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs)
|
||||
args += ffmpeg._utils.convert_kwargs_to_cmd_line_args(kwargs) # pyright: ignore[reportAttributeAccessIssue]
|
||||
args += [filename]
|
||||
|
||||
# PATCHED
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import logging
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QMetaObject, QRect, QSize, QStringListModel, Qt
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt
|
||||
from PySide6.QtGui import QAction, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QCompleter,
|
||||
@@ -16,6 +21,8 @@ from PySide6.QtWidgets import (
|
||||
QLayout,
|
||||
QLineEdit,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMenuBar,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
@@ -26,7 +33,14 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption
|
||||
from tagstudio.core.library.alchemy.enums import SortingModeEnum
|
||||
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
|
||||
from tagstudio.qt.pagination import Pagination
|
||||
from tagstudio.qt.platform_strings import trash_term
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.landing import LandingWidget
|
||||
|
||||
@@ -34,15 +48,437 @@ from tagstudio.qt.widgets.landing import LandingWidget
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class Ui_MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self, driver: "QtDriver", parent=None) -> None:
|
||||
class MainMenuBar(QMenuBar):
|
||||
file_menu: QMenu
|
||||
open_library_action: QAction
|
||||
open_recent_library_menu: QMenu
|
||||
save_library_backup_action: QAction
|
||||
settings_action: QAction
|
||||
open_on_start_action: QAction
|
||||
refresh_dir_action: QAction
|
||||
close_library_action: QAction
|
||||
|
||||
edit_menu: QMenu
|
||||
new_tag_action: QAction
|
||||
select_all_action: QAction
|
||||
select_inverse_action: QAction
|
||||
clear_select_action: QAction
|
||||
copy_fields_action: QAction
|
||||
paste_fields_action: QAction
|
||||
add_tag_to_selected_action: QAction
|
||||
delete_file_action: QAction
|
||||
manage_file_ext_action: QAction
|
||||
tag_manager_action: QAction
|
||||
color_manager_action: QAction
|
||||
|
||||
view_menu: QMenu
|
||||
show_filenames_action: QAction
|
||||
|
||||
tools_menu: QMenu
|
||||
fix_unlinked_entries_action: QAction
|
||||
fix_dupe_files_action: QAction
|
||||
clear_thumb_cache_action: QAction
|
||||
|
||||
macros_menu: QMenu
|
||||
folders_to_tags_action: QAction
|
||||
|
||||
help_menu: QMenu
|
||||
about_action: QAction
|
||||
|
||||
def __init__(self, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self.driver: "QtDriver" = driver
|
||||
self.setupUi(self)
|
||||
|
||||
self.setup_file_menu()
|
||||
self.setup_edit_menu()
|
||||
self.setup_view_menu()
|
||||
self.setup_tools_menu()
|
||||
self.setup_macros_menu()
|
||||
self.setup_help_menu()
|
||||
|
||||
def setup_file_menu(self):
|
||||
self.file_menu = QMenu(Translations["menu.file"], self)
|
||||
|
||||
# Open/Create Library
|
||||
self.open_library_action = QAction(Translations["menu.file.open_create_library"], self)
|
||||
self.open_library_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_O,
|
||||
)
|
||||
)
|
||||
self.open_library_action.setToolTip("Ctrl+O")
|
||||
self.file_menu.addAction(self.open_library_action)
|
||||
|
||||
# Open Recent
|
||||
self.open_recent_library_menu = QMenu(Translations["menu.file.open_recent_library"], self)
|
||||
self.file_menu.addMenu(self.open_recent_library_menu)
|
||||
|
||||
# Save Library Backup
|
||||
self.save_library_backup_action = QAction(Translations["menu.file.save_backup"], self)
|
||||
self.save_library_backup_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||||
| QtCore.Qt.KeyboardModifier.ShiftModifier
|
||||
),
|
||||
QtCore.Qt.Key.Key_S,
|
||||
)
|
||||
)
|
||||
self.save_library_backup_action.setStatusTip("Ctrl+Shift+S")
|
||||
self.save_library_backup_action.setEnabled(False)
|
||||
self.file_menu.addAction(self.save_library_backup_action)
|
||||
|
||||
self.file_menu.addSeparator()
|
||||
|
||||
# Settings...
|
||||
self.settings_action = QAction(Translations["menu.settings"], self)
|
||||
self.file_menu.addAction(self.settings_action)
|
||||
|
||||
# Open Library on Start
|
||||
self.open_on_start_action = QAction(Translations["settings.open_library_on_start"], self)
|
||||
self.open_on_start_action.setCheckable(True)
|
||||
self.file_menu.addAction(self.open_on_start_action)
|
||||
|
||||
self.file_menu.addSeparator()
|
||||
|
||||
# Refresh Directories
|
||||
self.refresh_dir_action = QAction(Translations["menu.file.refresh_directories"], self)
|
||||
self.refresh_dir_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_R,
|
||||
)
|
||||
)
|
||||
self.refresh_dir_action.setStatusTip("Ctrl+R")
|
||||
self.refresh_dir_action.setEnabled(False)
|
||||
self.file_menu.addAction(self.refresh_dir_action)
|
||||
|
||||
self.file_menu.addSeparator()
|
||||
|
||||
# Close Library
|
||||
self.close_library_action = QAction(Translations["menu.file.close_library"], self)
|
||||
self.close_library_action.setEnabled(False)
|
||||
self.file_menu.addAction(self.close_library_action)
|
||||
|
||||
self.file_menu.addSeparator()
|
||||
|
||||
self.addMenu(self.file_menu)
|
||||
|
||||
def setup_edit_menu(self):
|
||||
self.edit_menu = QMenu(Translations["generic.edit_alt"], self)
|
||||
|
||||
# New Tag
|
||||
self.new_tag_action = QAction(Translations["menu.edit.new_tag"], self)
|
||||
self.new_tag_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_T,
|
||||
)
|
||||
)
|
||||
self.new_tag_action.setToolTip("Ctrl+T")
|
||||
self.new_tag_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.new_tag_action)
|
||||
|
||||
self.edit_menu.addSeparator()
|
||||
|
||||
# Select All
|
||||
self.select_all_action = QAction(Translations["select.all"], self)
|
||||
self.select_all_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_A,
|
||||
)
|
||||
)
|
||||
self.select_all_action.setToolTip("Ctrl+A")
|
||||
self.select_all_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.select_all_action)
|
||||
|
||||
# Invert Selection
|
||||
self.select_inverse_action = QAction(Translations["select.inverse"], self)
|
||||
self.select_inverse_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||||
^ QtCore.Qt.KeyboardModifier.ShiftModifier
|
||||
),
|
||||
QtCore.Qt.Key.Key_I,
|
||||
)
|
||||
)
|
||||
self.select_inverse_action.setToolTip("Ctrl+Shift+I")
|
||||
self.select_inverse_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.select_inverse_action)
|
||||
|
||||
# Clear Selection
|
||||
self.clear_select_action = QAction(Translations["select.clear"], self)
|
||||
self.clear_select_action.setShortcut(QtCore.Qt.Key.Key_Escape)
|
||||
self.clear_select_action.setToolTip("Esc")
|
||||
self.clear_select_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.clear_select_action)
|
||||
|
||||
# Copy Fields
|
||||
self.copy_fields_action = QAction(Translations["edit.copy_fields"], self)
|
||||
self.copy_fields_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_C,
|
||||
)
|
||||
)
|
||||
self.copy_fields_action.setToolTip("Ctrl+C")
|
||||
self.copy_fields_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.copy_fields_action)
|
||||
|
||||
# Paste Fields
|
||||
self.paste_fields_action = QAction(Translations["edit.paste_fields"], self)
|
||||
self.paste_fields_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_V,
|
||||
)
|
||||
)
|
||||
self.paste_fields_action.setToolTip("Ctrl+V")
|
||||
self.paste_fields_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.paste_fields_action)
|
||||
|
||||
# Add Tag to Selected
|
||||
self.add_tag_to_selected_action = QAction(Translations["select.add_tag_to_selected"], self)
|
||||
self.add_tag_to_selected_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||||
^ QtCore.Qt.KeyboardModifier.ShiftModifier
|
||||
),
|
||||
QtCore.Qt.Key.Key_T,
|
||||
)
|
||||
)
|
||||
self.add_tag_to_selected_action.setToolTip("Ctrl+Shift+T")
|
||||
self.add_tag_to_selected_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.add_tag_to_selected_action)
|
||||
|
||||
self.edit_menu.addSeparator()
|
||||
|
||||
# Move Files to trash
|
||||
self.delete_file_action = QAction(
|
||||
Translations.format("menu.delete_selected_files_ambiguous", trash_term=trash_term()),
|
||||
self,
|
||||
)
|
||||
self.delete_file_action.setShortcut(QtCore.Qt.Key.Key_Delete)
|
||||
self.delete_file_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.delete_file_action)
|
||||
|
||||
self.edit_menu.addSeparator()
|
||||
|
||||
# Manage File Extensions
|
||||
self.manage_file_ext_action = QAction(
|
||||
Translations["menu.edit.manage_file_extensions"], self
|
||||
)
|
||||
self.manage_file_ext_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.manage_file_ext_action)
|
||||
|
||||
# Manage Tags
|
||||
self.tag_manager_action = QAction(Translations["menu.edit.manage_tags"], self)
|
||||
self.tag_manager_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_M,
|
||||
)
|
||||
)
|
||||
self.tag_manager_action.setEnabled(False)
|
||||
self.tag_manager_action.setToolTip("Ctrl+M")
|
||||
self.edit_menu.addAction(self.tag_manager_action)
|
||||
|
||||
# Color Manager
|
||||
self.color_manager_action = QAction(Translations["edit.color_manager"], self)
|
||||
self.color_manager_action.setEnabled(False)
|
||||
self.edit_menu.addAction(self.color_manager_action)
|
||||
|
||||
self.addMenu(self.edit_menu)
|
||||
|
||||
def setup_view_menu(self):
|
||||
self.view_menu = QMenu(Translations["menu.view"], self)
|
||||
|
||||
# show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar)
|
||||
# show_libs_list_action.setCheckable(True)
|
||||
# show_libs_list_action.setChecked(self.settings.show_library_list)
|
||||
|
||||
self.show_filenames_action = QAction(Translations["settings.show_filenames_in_grid"], self)
|
||||
self.show_filenames_action.setCheckable(True)
|
||||
self.view_menu.addAction(self.show_filenames_action)
|
||||
|
||||
self.view_menu.addSeparator()
|
||||
|
||||
self.increase_thumbnail_size_action = QAction(
|
||||
Translations["menu.view.increase_thumbnail_size"], self
|
||||
)
|
||||
self.increase_thumbnail_size_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_Plus,
|
||||
)
|
||||
)
|
||||
self.increase_thumbnail_size_action.setToolTip("Ctrl++")
|
||||
self.view_menu.addAction(self.increase_thumbnail_size_action)
|
||||
|
||||
self.decrease_thumbnail_size_action = QAction(
|
||||
Translations["menu.view.decrease_thumbnail_size"], self
|
||||
)
|
||||
self.decrease_thumbnail_size_action.setShortcut(
|
||||
QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier(QtCore.Qt.KeyboardModifier.ControlModifier),
|
||||
QtCore.Qt.Key.Key_Minus,
|
||||
)
|
||||
)
|
||||
self.decrease_thumbnail_size_action.setToolTip("Ctrl+-")
|
||||
self.view_menu.addAction(self.decrease_thumbnail_size_action)
|
||||
|
||||
self.view_menu.addSeparator()
|
||||
|
||||
self.addMenu(self.view_menu)
|
||||
|
||||
def setup_tools_menu(self):
|
||||
self.tools_menu = QMenu(Translations["menu.tools"], self)
|
||||
|
||||
# Fix Unlinked Entries
|
||||
self.fix_unlinked_entries_action = QAction(
|
||||
Translations["menu.tools.fix_unlinked_entries"], self
|
||||
)
|
||||
self.fix_unlinked_entries_action.setEnabled(False)
|
||||
self.tools_menu.addAction(self.fix_unlinked_entries_action)
|
||||
|
||||
# Fix Duplicate Files
|
||||
self.fix_dupe_files_action = QAction(Translations["menu.tools.fix_duplicate_files"], self)
|
||||
self.fix_dupe_files_action.setEnabled(False)
|
||||
self.tools_menu.addAction(self.fix_dupe_files_action)
|
||||
|
||||
self.tools_menu.addSeparator()
|
||||
|
||||
# Clear Thumbnail Cache
|
||||
self.clear_thumb_cache_action = QAction(
|
||||
Translations["settings.clear_thumb_cache.title"], self
|
||||
)
|
||||
self.clear_thumb_cache_action.setEnabled(False)
|
||||
self.tools_menu.addAction(self.clear_thumb_cache_action)
|
||||
|
||||
self.addMenu(self.tools_menu)
|
||||
|
||||
def setup_macros_menu(self):
|
||||
self.macros_menu = QMenu(Translations["menu.macros"], self)
|
||||
|
||||
self.folders_to_tags_action = QAction(Translations["menu.macros.folders_to_tags"], self)
|
||||
self.folders_to_tags_action.setEnabled(False)
|
||||
self.macros_menu.addAction(self.folders_to_tags_action)
|
||||
|
||||
self.addMenu(self.macros_menu)
|
||||
|
||||
def setup_help_menu(self):
|
||||
self.help_menu = QMenu(Translations["menu.help"], self)
|
||||
|
||||
self.about_action = QAction(Translations["menu.help.about"], self)
|
||||
self.help_menu.addAction(self.about_action)
|
||||
|
||||
self.addMenu(self.help_menu)
|
||||
|
||||
def rebuild_open_recent_library_menu(
|
||||
self,
|
||||
libraries: list[Path],
|
||||
show_filepath: ShowFilepathOption,
|
||||
open_library_callback: Callable[[Path], None],
|
||||
clear_libraries_callback: Callable[[], None],
|
||||
):
|
||||
actions: list[QAction] = []
|
||||
for path in libraries:
|
||||
action = QAction(self.open_recent_library_menu)
|
||||
if show_filepath == ShowFilepathOption.SHOW_FULL_PATHS:
|
||||
action.setText(str(path))
|
||||
else:
|
||||
action.setText(str(path.name))
|
||||
action.triggered.connect(lambda checked=False, p=path: open_library_callback(p))
|
||||
actions.append(action)
|
||||
|
||||
clear_recent_action = QAction(
|
||||
Translations["menu.file.clear_recent_libraries"], self.open_recent_library_menu
|
||||
)
|
||||
clear_recent_action.triggered.connect(clear_libraries_callback)
|
||||
actions.append(clear_recent_action)
|
||||
|
||||
# Clear previous actions
|
||||
for action in self.open_recent_library_menu.actions():
|
||||
self.open_recent_library_menu.removeAction(action)
|
||||
|
||||
# Add new actions
|
||||
for action in actions:
|
||||
self.open_recent_library_menu.addAction(action)
|
||||
|
||||
# Only enable add "clear recent" if there are still recent libraries.
|
||||
if len(actions) > 1:
|
||||
self.open_recent_library_menu.setDisabled(False)
|
||||
self.open_recent_library_menu.addSeparator()
|
||||
self.open_recent_library_menu.addAction(clear_recent_action)
|
||||
else:
|
||||
self.open_recent_library_menu.setDisabled(True)
|
||||
|
||||
|
||||
# View Component
|
||||
class MainWindow(QMainWindow):
|
||||
THUMB_SIZES: list[tuple[str, int]] = [
|
||||
(Translations["home.thumbnail_size.extra_large"], 256),
|
||||
(Translations["home.thumbnail_size.large"], 192),
|
||||
(Translations["home.thumbnail_size.medium"], 128),
|
||||
(Translations["home.thumbnail_size.small"], 96),
|
||||
(Translations["home.thumbnail_size.mini"], 76),
|
||||
]
|
||||
|
||||
def __init__(self, driver: "QtDriver", parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.rm = ResourceManager()
|
||||
|
||||
# region Type declarations for variables that will be initialized in methods
|
||||
# initialized in setup_search_bar
|
||||
self.search_bar_layout: QHBoxLayout
|
||||
self.back_button: QPushButton
|
||||
self.forward_button: QPushButton
|
||||
self.search_field: QLineEdit
|
||||
self.search_field_completion_list: QStringListModel
|
||||
self.search_field_completer: QCompleter
|
||||
self.search_button: QPushButton
|
||||
|
||||
# initialized in setup_extra_input_bar
|
||||
self.extra_input_layout: QHBoxLayout
|
||||
self.sorting_mode_combobox: QComboBox
|
||||
self.sorting_direction_combobox: QComboBox
|
||||
self.thumb_size_combobox: QComboBox
|
||||
|
||||
# initialized in setup_content
|
||||
self.content_layout: QHBoxLayout
|
||||
self.content_splitter: QSplitter
|
||||
|
||||
# initialized in setup_entry_list
|
||||
self.entry_list_container: QWidget
|
||||
self.entry_list_layout: QVBoxLayout
|
||||
self.entry_scroll_area: QScrollArea
|
||||
self.thumb_grid: QWidget
|
||||
self.thumb_layout: FlowLayout
|
||||
self.landing_widget: LandingWidget
|
||||
self.pagination: Pagination
|
||||
|
||||
# initialized in setup_preview_panel
|
||||
self.preview_panel: PreviewPanel
|
||||
# endregion
|
||||
|
||||
if not self.objectName():
|
||||
self.setObjectName("MainWindow")
|
||||
self.resize(1300, 720)
|
||||
|
||||
self.setup_menu_bar()
|
||||
|
||||
self.setup_central_widget(driver)
|
||||
|
||||
self.setup_status_bar()
|
||||
|
||||
QMetaObject.connectSlotsByName(self)
|
||||
|
||||
# NOTE: These are old attempts to allow for a translucent/acrylic
|
||||
# window effect. This may be attempted again in the future.
|
||||
@@ -54,155 +490,211 @@ class Ui_MainWindow(QMainWindow):
|
||||
# self.windowFX = WindowEffect()
|
||||
# self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False)
|
||||
|
||||
# region UI Setup Methods
|
||||
|
||||
def setupUi(self, MainWindow):
|
||||
if not MainWindow.objectName():
|
||||
MainWindow.setObjectName(u"MainWindow")
|
||||
MainWindow.resize(1300, 720)
|
||||
|
||||
self.centralwidget = QWidget(MainWindow)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.gridLayout = QGridLayout(self.centralwidget)
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
|
||||
# ComboBox group for search type and thumbnail size
|
||||
self.horizontalLayout_3 = QHBoxLayout()
|
||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||
|
||||
# left side spacer
|
||||
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self.horizontalLayout_3.addItem(spacerItem)
|
||||
|
||||
# Sorting Dropdowns
|
||||
self.sorting_mode_combobox = QComboBox(self.centralwidget)
|
||||
self.sorting_mode_combobox.setObjectName(u"sortingModeComboBox")
|
||||
self.horizontalLayout_3.addWidget(self.sorting_mode_combobox)
|
||||
|
||||
self.sorting_direction_combobox = QComboBox(self.centralwidget)
|
||||
self.sorting_direction_combobox.setObjectName(u"sortingDirectionCombobox")
|
||||
self.horizontalLayout_3.addWidget(self.sorting_direction_combobox)
|
||||
# region Menu Bar
|
||||
|
||||
# Thumbnail Size placeholder
|
||||
self.thumb_size_combobox = QComboBox(self.centralwidget)
|
||||
self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox")
|
||||
def setup_menu_bar(self):
|
||||
self.menu_bar = MainMenuBar(self)
|
||||
|
||||
self.setMenuBar(self.menu_bar)
|
||||
self.menu_bar.setNativeMenuBar(True)
|
||||
|
||||
# endregion
|
||||
|
||||
def setup_central_widget(self, driver: "QtDriver"):
|
||||
self.central_widget = QWidget(self)
|
||||
self.central_widget.setObjectName("central_widget")
|
||||
self.central_layout = QGridLayout(self.central_widget)
|
||||
self.central_layout.setObjectName("central_layout")
|
||||
|
||||
self.setup_search_bar()
|
||||
|
||||
self.setup_extra_input_bar()
|
||||
|
||||
self.setup_content(driver)
|
||||
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
def setup_search_bar(self):
|
||||
"""Sets up Nav Buttons, Search Field, Search Button."""
|
||||
self.search_bar_layout = QHBoxLayout()
|
||||
self.search_bar_layout.setObjectName("search_bar_layout")
|
||||
self.search_bar_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
|
||||
|
||||
self.back_button = QPushButton(self.central_widget)
|
||||
back_icon: Image.Image = self.rm.get("bxs-left-arrow") # pyright: ignore[reportAssignmentType]
|
||||
back_icon = theme_fg_overlay(back_icon, use_alpha=False)
|
||||
self.back_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(back_icon)))
|
||||
self.back_button.setObjectName("back_button")
|
||||
self.back_button.setMinimumSize(QSize(32, 32))
|
||||
self.back_button.setMaximumSize(QSize(32, 16777215))
|
||||
self.search_bar_layout.addWidget(self.back_button)
|
||||
|
||||
self.forward_button = QPushButton(self.central_widget)
|
||||
forward_icon: Image.Image = self.rm.get("bxs-right-arrow") # pyright: ignore[reportAssignmentType]
|
||||
forward_icon = theme_fg_overlay(forward_icon, use_alpha=False)
|
||||
self.forward_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(forward_icon)))
|
||||
self.forward_button.setIconSize(QSize(16, 16))
|
||||
self.forward_button.setObjectName("forward_button")
|
||||
self.forward_button.setMinimumSize(QSize(32, 32))
|
||||
self.forward_button.setMaximumSize(QSize(32, 16777215))
|
||||
self.search_bar_layout.addWidget(self.forward_button)
|
||||
|
||||
self.search_field = QLineEdit(self.central_widget)
|
||||
self.search_field.setPlaceholderText(Translations["home.search_entries"])
|
||||
self.search_field.setObjectName("search_field")
|
||||
self.search_field.setMinimumSize(QSize(0, 32))
|
||||
self.search_field_completion_list = QStringListModel()
|
||||
self.search_field_completer = QCompleter(
|
||||
self.search_field_completion_list, self.search_field
|
||||
)
|
||||
self.search_field_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
self.search_field.setCompleter(self.search_field_completer)
|
||||
self.search_bar_layout.addWidget(self.search_field)
|
||||
|
||||
self.search_button = QPushButton(Translations["home.search"], self.central_widget)
|
||||
self.search_button.setObjectName("search_button")
|
||||
self.search_button.setMinimumSize(QSize(0, 32))
|
||||
self.search_bar_layout.addWidget(self.search_button)
|
||||
|
||||
self.central_layout.addLayout(self.search_bar_layout, 3, 0, 1, 1)
|
||||
|
||||
def setup_extra_input_bar(self):
|
||||
"""Sets up inputs for sorting settings and thumbnail size."""
|
||||
self.extra_input_layout = QHBoxLayout()
|
||||
self.extra_input_layout.setObjectName("extra_input_layout")
|
||||
|
||||
## left side spacer
|
||||
self.extra_input_layout.addItem(
|
||||
QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
)
|
||||
|
||||
## Sorting Mode Dropdown
|
||||
self.sorting_mode_combobox = QComboBox(self.central_widget)
|
||||
self.sorting_mode_combobox.setObjectName("sorting_mode_combobox")
|
||||
for sort_mode in SortingModeEnum:
|
||||
self.sorting_mode_combobox.addItem(Translations[sort_mode.value], sort_mode)
|
||||
self.extra_input_layout.addWidget(self.sorting_mode_combobox)
|
||||
|
||||
## Sorting Direction Dropdown
|
||||
self.sorting_direction_combobox = QComboBox(self.central_widget)
|
||||
self.sorting_direction_combobox.setObjectName("sorting_direction_combobox")
|
||||
self.sorting_direction_combobox.addItem(
|
||||
Translations["sorting.direction.ascending"], userData=True
|
||||
)
|
||||
self.sorting_direction_combobox.addItem(
|
||||
Translations["sorting.direction.descending"], userData=False
|
||||
)
|
||||
self.sorting_direction_combobox.setCurrentIndex(1) # Default: Descending
|
||||
self.extra_input_layout.addWidget(self.sorting_direction_combobox)
|
||||
|
||||
## Thumbnail Size placeholder
|
||||
self.thumb_size_combobox = QComboBox(self.central_widget)
|
||||
self.thumb_size_combobox.setObjectName("thumb_size_combobox")
|
||||
self.thumb_size_combobox.setPlaceholderText(Translations["home.thumbnail_size"])
|
||||
self.thumb_size_combobox.setCurrentText("")
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.thumb_size_combobox.sizePolicy().hasHeightForWidth())
|
||||
self.thumb_size_combobox.setSizePolicy(sizePolicy)
|
||||
size_policy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||
size_policy.setHorizontalStretch(0)
|
||||
size_policy.setVerticalStretch(0)
|
||||
size_policy.setHeightForWidth(self.thumb_size_combobox.sizePolicy().hasHeightForWidth())
|
||||
self.thumb_size_combobox.setSizePolicy(size_policy)
|
||||
self.thumb_size_combobox.setMinimumWidth(128)
|
||||
self.thumb_size_combobox.setMaximumWidth(352)
|
||||
self.horizontalLayout_3.addWidget(self.thumb_size_combobox)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1)
|
||||
self.extra_input_layout.addWidget(self.thumb_size_combobox)
|
||||
for size in MainWindow.THUMB_SIZES:
|
||||
self.thumb_size_combobox.addItem(size[0], size[1])
|
||||
self.thumb_size_combobox.setCurrentIndex(2) # Default: Medium
|
||||
|
||||
self.splitter = QSplitter()
|
||||
self.splitter.setObjectName(u"splitter")
|
||||
self.splitter.setHandleWidth(12)
|
||||
self.central_layout.addLayout(self.extra_input_layout, 5, 0, 1, 1)
|
||||
|
||||
self.frame_container = QWidget()
|
||||
self.frame_layout = QVBoxLayout(self.frame_container)
|
||||
self.frame_layout.setSpacing(0)
|
||||
def setup_content(self, driver: "QtDriver"):
|
||||
self.content_layout = QHBoxLayout()
|
||||
self.content_layout.setObjectName("content_layout")
|
||||
|
||||
self.scrollArea = QScrollArea()
|
||||
self.scrollArea.setObjectName(u"scrollArea")
|
||||
self.scrollArea.setFocusPolicy(Qt.WheelFocus)
|
||||
self.scrollArea.setFrameShape(QFrame.NoFrame)
|
||||
self.scrollArea.setFrameShadow(QFrame.Plain)
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollAreaWidgetContents = QWidget()
|
||||
self.scrollAreaWidgetContents.setObjectName(
|
||||
u"scrollAreaWidgetContents")
|
||||
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590))
|
||||
self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents)
|
||||
self.gridLayout_2.setSpacing(8)
|
||||
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 8)
|
||||
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
|
||||
self.frame_layout.addWidget(self.scrollArea)
|
||||
|
||||
self.landing_widget: LandingWidget = LandingWidget(self.driver, self.devicePixelRatio())
|
||||
self.frame_layout.addWidget(self.landing_widget)
|
||||
self.content_splitter = QSplitter()
|
||||
self.content_splitter.setObjectName("content_splitter")
|
||||
self.content_splitter.setHandleWidth(12)
|
||||
|
||||
self.setup_entry_list(driver)
|
||||
|
||||
self.setup_preview_panel(driver)
|
||||
|
||||
self.content_splitter.setStretchFactor(0, 1)
|
||||
self.content_layout.addWidget(self.content_splitter)
|
||||
|
||||
self.central_layout.addLayout(self.content_layout, 10, 0, 1, 1)
|
||||
|
||||
def setup_entry_list(self, driver: "QtDriver"):
|
||||
self.entry_list_container = QWidget()
|
||||
self.entry_list_layout = QVBoxLayout(self.entry_list_container)
|
||||
self.entry_list_layout.setSpacing(0)
|
||||
|
||||
self.entry_scroll_area = QScrollArea()
|
||||
self.entry_scroll_area.setObjectName("entry_scroll_area")
|
||||
self.entry_scroll_area.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
||||
self.entry_scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.entry_scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.entry_scroll_area.setWidgetResizable(True)
|
||||
self.entry_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
self.thumb_grid = QWidget()
|
||||
self.thumb_grid.setObjectName("thumb_grid")
|
||||
self.thumb_layout = FlowLayout()
|
||||
self.thumb_layout.enable_grid_optimizations(value=True)
|
||||
self.thumb_layout.setSpacing(min(self.thumb_size // 10, 12))
|
||||
self.thumb_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.thumb_grid.setLayout(self.thumb_layout)
|
||||
self.entry_scroll_area.setWidget(self.thumb_grid)
|
||||
|
||||
self.entry_list_layout.addWidget(self.entry_scroll_area)
|
||||
|
||||
self.landing_widget = LandingWidget(driver, self.devicePixelRatio())
|
||||
self.entry_list_layout.addWidget(self.landing_widget)
|
||||
|
||||
self.pagination = Pagination()
|
||||
self.frame_layout.addWidget(self.pagination)
|
||||
self.entry_list_layout.addWidget(self.pagination)
|
||||
self.content_splitter.addWidget(self.entry_list_container)
|
||||
|
||||
self.horizontalLayout.addWidget(self.splitter)
|
||||
self.splitter.addWidget(self.frame_container)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1)
|
||||
def setup_preview_panel(self, driver: "QtDriver"):
|
||||
self.preview_panel = PreviewPanel(driver.lib, driver)
|
||||
self.content_splitter.addWidget(self.preview_panel)
|
||||
|
||||
nav_button_style = "font-size:14;font-weight:bold;"
|
||||
self.horizontalLayout_2 = QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize)
|
||||
self.backButton = QPushButton("<", self.centralwidget)
|
||||
self.backButton.setObjectName(u"backButton")
|
||||
self.backButton.setMinimumSize(QSize(0, 32))
|
||||
self.backButton.setMaximumSize(QSize(32, 16777215))
|
||||
self.backButton.setStyleSheet(nav_button_style)
|
||||
self.horizontalLayout_2.addWidget(self.backButton)
|
||||
def setup_status_bar(self):
|
||||
self.status_bar = QStatusBar(self)
|
||||
self.status_bar.setObjectName("status_bar")
|
||||
status_bar_size_policy = QSizePolicy(
|
||||
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum
|
||||
)
|
||||
status_bar_size_policy.setHorizontalStretch(0)
|
||||
status_bar_size_policy.setVerticalStretch(0)
|
||||
status_bar_size_policy.setHeightForWidth(self.status_bar.sizePolicy().hasHeightForWidth())
|
||||
self.status_bar.setSizePolicy(status_bar_size_policy)
|
||||
self.status_bar.setSizeGripEnabled(False)
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
||||
self.forwardButton = QPushButton(">", self.centralwidget)
|
||||
self.forwardButton.setObjectName(u"forwardButton")
|
||||
self.forwardButton.setMinimumSize(QSize(0, 32))
|
||||
self.forwardButton.setMaximumSize(QSize(32, 16777215))
|
||||
self.forwardButton.setStyleSheet(nav_button_style)
|
||||
self.horizontalLayout_2.addWidget(self.forwardButton)
|
||||
|
||||
self.searchField = QLineEdit(self.centralwidget)
|
||||
self.searchField.setPlaceholderText(Translations["home.search_entries"])
|
||||
self.searchField.setObjectName(u"searchField")
|
||||
self.searchField.setMinimumSize(QSize(0, 32))
|
||||
|
||||
self.searchFieldCompletionList = QStringListModel()
|
||||
self.searchFieldCompleter = QCompleter(self.searchFieldCompletionList, self.searchField)
|
||||
self.searchFieldCompleter.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
self.searchField.setCompleter(self.searchFieldCompleter)
|
||||
self.horizontalLayout_2.addWidget(self.searchField)
|
||||
|
||||
self.searchButton = QPushButton(Translations["home.search"], self.centralwidget)
|
||||
self.searchButton.setObjectName(u"searchButton")
|
||||
self.searchButton.setMinimumSize(QSize(0, 32))
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.searchButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
||||
self.gridLayout_2.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
MainWindow.setCentralWidget(self.centralwidget)
|
||||
self.statusbar = QStatusBar(MainWindow)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
|
||||
sizePolicy1.setHorizontalStretch(0)
|
||||
sizePolicy1.setVerticalStretch(0)
|
||||
sizePolicy1.setHeightForWidth(
|
||||
self.statusbar.sizePolicy().hasHeightForWidth())
|
||||
self.statusbar.setSizePolicy(sizePolicy1)
|
||||
self.statusbar.setSizeGripEnabled(False)
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
|
||||
QMetaObject.connectSlotsByName(MainWindow)
|
||||
# setupUi
|
||||
|
||||
def moveEvent(self, event) -> None:
|
||||
# time.sleep(0.02) # sleep for 20ms
|
||||
pass
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
# time.sleep(0.02) # sleep for 20ms
|
||||
pass
|
||||
# endregion
|
||||
|
||||
def toggle_landing_page(self, enabled: bool):
|
||||
if enabled:
|
||||
self.scrollArea.setHidden(True)
|
||||
self.entry_scroll_area.setHidden(True)
|
||||
self.landing_widget.setHidden(False)
|
||||
self.landing_widget.animate_logo_in()
|
||||
else:
|
||||
self.landing_widget.setHidden(True)
|
||||
self.landing_widget.set_status_label("")
|
||||
self.scrollArea.setHidden(False)
|
||||
|
||||
self.entry_scroll_area.setHidden(False)
|
||||
|
||||
@property
|
||||
def sorting_mode(self) -> SortingModeEnum:
|
||||
"""What to sort by."""
|
||||
return self.sorting_mode_combobox.currentData()
|
||||
|
||||
@property
|
||||
def sorting_direction(self) -> bool:
|
||||
"""Whether to Sort the results in ascending order."""
|
||||
return self.sorting_direction_combobox.currentData()
|
||||
|
||||
@property
|
||||
def thumb_size(self) -> int:
|
||||
return self.thumb_size_combobox.currentData()
|
||||
|
||||
@@ -87,6 +87,8 @@ class AddFieldModal(QWidget):
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.cancel_button.click()
|
||||
elif event.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return):
|
||||
self.save_button.click()
|
||||
else: # Other key presses
|
||||
pass
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
@@ -30,7 +30,7 @@ from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
|
||||
from tagstudio.qt.modals.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.modals.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.modals.tag_search import TagSearchModal
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.tag import (
|
||||
@@ -166,11 +166,8 @@ class BuildTagPanel(PanelWidget):
|
||||
if tag is not None:
|
||||
exclude_ids.append(tag.id)
|
||||
|
||||
tsp = TagSearchPanel(self.lib, exclude_ids)
|
||||
tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
|
||||
self.add_tag_modal = PanelModal(tsp)
|
||||
self.add_tag_modal.setTitle(Translations["tag.parent_tags.add"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.parent_tags.add"])
|
||||
self.add_tag_modal = TagSearchModal(self.lib, exclude_ids)
|
||||
self.add_tag_modal.tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
|
||||
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
|
||||
|
||||
# Color ----------------------------------------------------------------
|
||||
|
||||
@@ -41,7 +41,7 @@ class FfmpegChecker(QMessageBox):
|
||||
|
||||
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
|
||||
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
|
||||
missing = f"<span style='color:{red}'>{Translations["generic.missing"]}</span>"
|
||||
missing = f"<span style='color:{red}'>{Translations['generic.missing']}</span>"
|
||||
found = f"<span style='color:{green}'>{Translations['about.module.found']}</span>"
|
||||
status = Translations.format(
|
||||
"ffmpeg.missing.status",
|
||||
@@ -50,4 +50,4 @@ class FfmpegChecker(QMessageBox):
|
||||
ffprobe=ffprobe,
|
||||
ffprobe_status=found if which(FFPROBE_CMD) else missing,
|
||||
)
|
||||
self.setText(f"{Translations["ffmpeg.missing.description"]}<br><br>{status}")
|
||||
self.setText(f"{Translations['ffmpeg.missing.description']}<br><br>{status}")
|
||||
|
||||
@@ -63,7 +63,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.relink_class.done.connect(
|
||||
# refresh the grid
|
||||
lambda: (
|
||||
self.driver.filter_items(),
|
||||
self.driver.update_browsing_state(),
|
||||
self.refresh_missing_files(),
|
||||
)
|
||||
)
|
||||
@@ -78,7 +78,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
lambda: (
|
||||
self.set_missing_count(),
|
||||
# refresh the grid
|
||||
self.driver.filter_items(),
|
||||
self.driver.update_browsing_state(),
|
||||
)
|
||||
)
|
||||
self.delete_button.clicked.connect(self.delete_modal.show)
|
||||
|
||||
@@ -44,14 +44,14 @@ class BranchData:
|
||||
|
||||
def add_folders_to_tree(library: Library, tree: BranchData, items: tuple[str, ...]) -> BranchData:
|
||||
branch = tree
|
||||
parent_tag = None
|
||||
for folder in items:
|
||||
if folder not in branch.dirs:
|
||||
# TODO: Reimplement parent tags
|
||||
new_tag = Tag(name=folder)
|
||||
new_tag = Tag(name=folder, parent_tags=({parent_tag} if parent_tag else None))
|
||||
library.add_tag(new_tag)
|
||||
branch.dirs[folder] = BranchData(tag=new_tag)
|
||||
branch.tag = new_tag
|
||||
branch = branch.dirs[folder]
|
||||
parent_tag = branch.tag
|
||||
return branch
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ def folders_to_tags(library: Library):
|
||||
reversed_tag = reverse_tag(library, tag, None)
|
||||
add_tag_to_tree(reversed_tag)
|
||||
|
||||
for entry in library.get_entries():
|
||||
folders = entry.path.parts[1:-1]
|
||||
for entry in library.all_entries():
|
||||
folders = entry.path.parts[0:-1]
|
||||
if not folders:
|
||||
continue
|
||||
|
||||
@@ -90,9 +90,11 @@ def reverse_tag(library: Library, tag: Tag, items: list[Tag] | None) -> list[Tag
|
||||
items.reverse()
|
||||
return items
|
||||
|
||||
for subtag_id in tag.parent_ids:
|
||||
subtag = library.get_tag(subtag_id)
|
||||
return reverse_tag(library, subtag, items)
|
||||
parent_tag = None # to avoid subtag unbound error
|
||||
for parent_tag_id in tag.parent_ids:
|
||||
parent_tag = library.get_tag(parent_tag_id)
|
||||
assert parent_tag is not None
|
||||
return reverse_tag(library, parent_tag, items)
|
||||
|
||||
|
||||
# =========== UI ===========
|
||||
@@ -123,8 +125,8 @@ def generate_preview_data(library: Library) -> BranchData:
|
||||
reversed_tag = reverse_tag(library, tag, None)
|
||||
add_tag_to_tree(reversed_tag)
|
||||
|
||||
for entry in library.get_entries():
|
||||
folders = entry.path.parts[1:-1]
|
||||
for entry in library.all_entries():
|
||||
folders = entry.path.parts[0:-1]
|
||||
if not folders:
|
||||
continue
|
||||
|
||||
@@ -132,7 +134,7 @@ def generate_preview_data(library: Library) -> BranchData:
|
||||
if branch:
|
||||
has_tag = False
|
||||
for tag in entry.tags:
|
||||
if tag.name == branch.tag.name:
|
||||
if branch.tag and tag.name == branch.tag.name:
|
||||
has_tag = True
|
||||
break
|
||||
if not has_tag:
|
||||
@@ -216,20 +218,21 @@ class FoldersToTagsModal(QWidget):
|
||||
self.apply_button.setMinimumWidth(100)
|
||||
self.apply_button.clicked.connect(self.on_apply)
|
||||
|
||||
self.showEvent = self.on_open # type: ignore
|
||||
|
||||
self.root_layout.addWidget(self.title_widget)
|
||||
self.root_layout.addWidget(self.desc_widget)
|
||||
self.root_layout.addWidget(self.open_close_button_w)
|
||||
self.root_layout.addWidget(self.scroll_area)
|
||||
self.root_layout.addWidget(self.apply_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def on_apply(self, event):
|
||||
def on_apply(self):
|
||||
folders_to_tags(self.library)
|
||||
self.close()
|
||||
self.driver.preview_panel.update_widgets(update_preview=False)
|
||||
self.driver.main_window.preview_panel.set_selection(
|
||||
self.driver.selected, update_preview=False
|
||||
)
|
||||
|
||||
def on_open(self, event):
|
||||
@override
|
||||
def showEvent(self, event: QtGui.QShowEvent):
|
||||
for i in reversed(range(self.scroll_layout.count())):
|
||||
self.scroll_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
@@ -271,6 +274,7 @@ class TreeItem(QWidget):
|
||||
|
||||
self.label = QLabel()
|
||||
self.tag_layout.addWidget(self.label)
|
||||
assert data.tag is not None and parent_tag is not None
|
||||
self.tag_widget = ModifiedTagWidget(data.tag, parent_tag)
|
||||
self.tag_widget.bg_button.clicked.connect(lambda: self.hide_show())
|
||||
self.tag_layout.addWidget(self.tag_widget)
|
||||
@@ -323,10 +327,7 @@ class ModifiedTagWidget(QWidget):
|
||||
|
||||
self.bg_button = QPushButton(self)
|
||||
self.bg_button.setFlat(True)
|
||||
if parent_tag is not None:
|
||||
text = f"{tag.name} ({parent_tag.name})".replace("&", "&&")
|
||||
else:
|
||||
text = tag.name.replace("&", "&&")
|
||||
text = f"{tag.name} ({parent_tag.name})".replace("&", "&&")
|
||||
self.bg_button.setText(text)
|
||||
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class MirrorEntriesModal(QWidget):
|
||||
pw.from_iterable_function(
|
||||
self.mirror_entries_runnable,
|
||||
displayed_text,
|
||||
self.driver.preview_panel.update_widgets,
|
||||
lambda s=self.driver.selected: self.driver.main_window.preview_panel.set_selection(s),
|
||||
self.done.emit,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
from tagstudio.core.global_settings import Theme
|
||||
from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
@@ -25,16 +25,28 @@ from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = {
|
||||
ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"],
|
||||
ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations["settings.filepath.option.relative"],
|
||||
ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"],
|
||||
}
|
||||
FILEPATH_OPTION_MAP: dict[ShowFilepathOption, str] = {}
|
||||
|
||||
THEME_MAP: dict[Theme, str] = {
|
||||
Theme.DARK: Translations["settings.theme.dark"],
|
||||
Theme.LIGHT: Translations["settings.theme.light"],
|
||||
Theme.SYSTEM: Translations["settings.theme.system"],
|
||||
THEME_MAP: dict[Theme, str] = {}
|
||||
|
||||
TAG_CLICK_ACTION_MAP: dict[TagClickActionOption, str] = {}
|
||||
|
||||
DATE_FORMAT_MAP: dict[str, str] = {
|
||||
"%d/%m/%y": "21/08/24",
|
||||
"%d/%m/%Y": "21/08/2024",
|
||||
"%d.%m.%y": "21.08.24",
|
||||
"%d.%m.%Y": "21.08.2024",
|
||||
"%d-%m-%y": "21-08-24",
|
||||
"%d-%m-%Y": "21-08-2024",
|
||||
"%x": "08/21/24",
|
||||
"%m/%d/%Y": "08/21/2024",
|
||||
"%m-%d-%y": "08-21-24",
|
||||
"%m-%d-%Y": "08-21-2024",
|
||||
"%m.%d.%y": "08.21.24",
|
||||
"%m.%d.%Y": "08.21.2024",
|
||||
"%Y/%m/%d": "2024/08/21",
|
||||
"%Y-%m-%d": "2024-08-21",
|
||||
"%Y.%m.%d": "2024.08.21",
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +55,29 @@ class SettingsPanel(PanelWidget):
|
||||
|
||||
def __init__(self, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
# set these "constants" because language will be loaded from config shortly after startup
|
||||
# and we want to use the current language for the dropdowns
|
||||
global FILEPATH_OPTION_MAP, THEME_MAP, TAG_CLICK_ACTION_MAP
|
||||
FILEPATH_OPTION_MAP = {
|
||||
ShowFilepathOption.SHOW_FULL_PATHS: Translations["settings.filepath.option.full"],
|
||||
ShowFilepathOption.SHOW_RELATIVE_PATHS: Translations[
|
||||
"settings.filepath.option.relative"
|
||||
],
|
||||
ShowFilepathOption.SHOW_FILENAMES_ONLY: Translations["settings.filepath.option.name"],
|
||||
}
|
||||
THEME_MAP = {
|
||||
Theme.DARK: Translations["settings.theme.dark"],
|
||||
Theme.LIGHT: Translations["settings.theme.light"],
|
||||
Theme.SYSTEM: Translations["settings.theme.system"],
|
||||
}
|
||||
TAG_CLICK_ACTION_MAP = {
|
||||
TagClickActionOption.OPEN_EDIT: Translations["settings.tag_click_action.open_edit"],
|
||||
TagClickActionOption.SET_SEARCH: Translations["settings.tag_click_action.set_search"],
|
||||
TagClickActionOption.ADD_TO_SEARCH: Translations[
|
||||
"settings.tag_click_action.add_to_search"
|
||||
],
|
||||
}
|
||||
|
||||
self.driver = driver
|
||||
self.setMinimumSize(400, 300)
|
||||
|
||||
@@ -140,13 +175,48 @@ class SettingsPanel(PanelWidget):
|
||||
self.theme_combobox = QComboBox()
|
||||
for k in THEME_MAP:
|
||||
self.theme_combobox.addItem(THEME_MAP[k], k)
|
||||
theme: Theme = self.driver.settings.theme
|
||||
theme = self.driver.settings.theme
|
||||
if theme not in THEME_MAP:
|
||||
theme = Theme.DEFAULT
|
||||
self.theme_combobox.setCurrentIndex(list(THEME_MAP.keys()).index(theme))
|
||||
self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label)
|
||||
form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox)
|
||||
|
||||
# Tag Click Action
|
||||
self.tag_click_action_combobox = QComboBox()
|
||||
for k in TAG_CLICK_ACTION_MAP:
|
||||
self.tag_click_action_combobox.addItem(TAG_CLICK_ACTION_MAP[k], k)
|
||||
tag_click_action = self.driver.settings.tag_click_action
|
||||
if tag_click_action not in TAG_CLICK_ACTION_MAP:
|
||||
tag_click_action = TagClickActionOption.DEFAULT
|
||||
self.tag_click_action_combobox.setCurrentIndex(
|
||||
list(TAG_CLICK_ACTION_MAP.keys()).index(tag_click_action)
|
||||
)
|
||||
form_layout.addRow(
|
||||
Translations["settings.tag_click_action.label"], self.tag_click_action_combobox
|
||||
)
|
||||
|
||||
# Date Format
|
||||
self.dateformat_combobox = QComboBox()
|
||||
for k in DATE_FORMAT_MAP:
|
||||
self.dateformat_combobox.addItem(DATE_FORMAT_MAP[k], k)
|
||||
dateformat: str = self.driver.settings.date_format
|
||||
if dateformat not in DATE_FORMAT_MAP:
|
||||
dateformat = "%x"
|
||||
self.dateformat_combobox.setCurrentIndex(list(DATE_FORMAT_MAP.keys()).index(dateformat))
|
||||
self.dateformat_combobox.currentIndexChanged.connect(self.__update_restart_label)
|
||||
form_layout.addRow(Translations["settings.dateformat.label"], self.dateformat_combobox)
|
||||
|
||||
# 24-Hour Format
|
||||
self.hourformat_checkbox = QCheckBox()
|
||||
self.hourformat_checkbox.setChecked(self.driver.settings.hour_format)
|
||||
form_layout.addRow(Translations["settings.hourformat.label"], self.hourformat_checkbox)
|
||||
|
||||
# Zero-padding
|
||||
self.zeropadding_checkbox = QCheckBox()
|
||||
self.zeropadding_checkbox.setChecked(self.driver.settings.zero_padding)
|
||||
form_layout.addRow(Translations["settings.zeropadding.label"], self.zeropadding_checkbox)
|
||||
|
||||
def __build_library_settings(self):
|
||||
self.library_settings_container = QWidget()
|
||||
form_layout = QFormLayout(self.library_settings_container)
|
||||
@@ -167,6 +237,10 @@ class SettingsPanel(PanelWidget):
|
||||
"page_size": int(self.page_size_line_edit.text()),
|
||||
"show_filepath": self.filepath_combobox.currentData(),
|
||||
"theme": self.theme_combobox.currentData(),
|
||||
"tag_click_action": self.tag_click_action_combobox.currentData(),
|
||||
"date_format": self.dateformat_combobox.currentData(),
|
||||
"hour_format": self.hourformat_checkbox.isChecked(),
|
||||
"zero_padding": self.zeropadding_checkbox.isChecked(),
|
||||
}
|
||||
|
||||
def update_settings(self, driver: "QtDriver"):
|
||||
@@ -179,13 +253,17 @@ class SettingsPanel(PanelWidget):
|
||||
driver.settings.page_size = settings["page_size"]
|
||||
driver.settings.show_filepath = settings["show_filepath"]
|
||||
driver.settings.theme = settings["theme"]
|
||||
driver.settings.tag_click_action = settings["tag_click_action"]
|
||||
driver.settings.date_format = settings["date_format"]
|
||||
driver.settings.hour_format = settings["hour_format"]
|
||||
driver.settings.zero_padding = settings["zero_padding"]
|
||||
|
||||
driver.settings.save()
|
||||
|
||||
# Apply changes
|
||||
# Show File Path
|
||||
driver.update_recent_lib_menu()
|
||||
driver.preview_panel.update_widgets()
|
||||
driver.main_window.preview_panel.set_selection(self.driver.selected)
|
||||
library_directory = driver.lib.library_dir
|
||||
if settings["show_filepath"] == ShowFilepathOption.SHOW_FULL_PATHS:
|
||||
display_path = library_directory or ""
|
||||
@@ -201,10 +279,10 @@ class SettingsPanel(PanelWidget):
|
||||
|
||||
modal = PanelModal(
|
||||
widget=settings_panel,
|
||||
window_title=Translations["settings.title"],
|
||||
done_callback=lambda: settings_panel.update_settings(driver),
|
||||
has_save=True,
|
||||
)
|
||||
modal.title_widget.setVisible(False)
|
||||
modal.setWindowTitle(Translations["settings.title"])
|
||||
|
||||
return modal
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, override
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
@@ -123,7 +124,7 @@ class TagColorManager(QWidget):
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.preview_panel.fields.update_from_entry(
|
||||
else self.driver.main_window.preview_panel.field_containers_widget.update_from_entry( # noqa: E501
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
)
|
||||
@@ -140,7 +141,7 @@ class TagColorManager(QWidget):
|
||||
self.setup_color_groups(),
|
||||
()
|
||||
if len(self.driver.selected) < 1
|
||||
else self.driver.preview_panel.fields.update_from_entry(
|
||||
else self.driver.main_window.preview_panel.field_containers_widget.update_from_entry( # noqa: E501
|
||||
self.driver.selected[0], update_badges=False
|
||||
),
|
||||
),
|
||||
@@ -175,7 +176,6 @@ class TagColorManager(QWidget):
|
||||
self.create_namespace_modal = PanelModal(
|
||||
build_namespace_panel,
|
||||
Translations["namespace.create.title"],
|
||||
Translations["namespace.create.title"],
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,10 +35,9 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
panel = BuildTagPanel(self.lib)
|
||||
self.modal = PanelModal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
has_save=True,
|
||||
)
|
||||
self.modal.setTitle(Translations["tag.new"])
|
||||
self.modal.setWindowTitle(Translations["tag.new"])
|
||||
if name.strip():
|
||||
panel.name_field.setText(name)
|
||||
|
||||
@@ -51,7 +50,7 @@ class TagDatabasePanel(TagSearchPanel):
|
||||
alias_ids=panel.alias_ids,
|
||||
),
|
||||
self.modal.hide(),
|
||||
self.update_tags(),
|
||||
self.update_tags(self.search_field.text()),
|
||||
)
|
||||
)
|
||||
self.modal.show()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
@@ -24,11 +24,10 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import FilterState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.modals import build_tag
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
@@ -37,14 +36,35 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
class TagSearchModal(PanelModal):
|
||||
tsp: "TagSearchPanel"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
done_callback=None,
|
||||
save_callback=None,
|
||||
has_save=False,
|
||||
):
|
||||
self.tsp = TagSearchPanel(library, exclude, is_tag_chooser)
|
||||
super().__init__(
|
||||
self.tsp,
|
||||
Translations["tag.add.plural"],
|
||||
done_callback=done_callback,
|
||||
save_callback=save_callback,
|
||||
has_save=has_save,
|
||||
)
|
||||
|
||||
|
||||
class TagSearchPanel(PanelWidget):
|
||||
tag_chosen = Signal(int)
|
||||
lib: Library
|
||||
driver: "QtDriver"
|
||||
driver: Union["QtDriver", None]
|
||||
is_initialized: bool = False
|
||||
first_tag_id: int | None = None
|
||||
is_tag_chooser: bool
|
||||
@@ -58,7 +78,7 @@ class TagSearchPanel(PanelWidget):
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] = None,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
@@ -177,10 +197,12 @@ class TagSearchPanel(PanelWidget):
|
||||
self.search_field.setFocus()
|
||||
self.update_tags()
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = build_tag.BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(self.build_tag_modal, has_save=True)
|
||||
self.add_tag_modal.setTitle(Translations["tag.new"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.add"])
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
self.build_tag_modal: BuildTagPanel = BuildTagPanel(self.lib)
|
||||
self.add_tag_modal: PanelModal = PanelModal(
|
||||
self.build_tag_modal, Translations["tag.new"], Translations["tag.add"], has_save=True
|
||||
)
|
||||
|
||||
self.build_tag_modal.name_field.setText(name)
|
||||
self.add_tag_modal.saved.connect(on_tag_modal_saved)
|
||||
@@ -194,6 +216,7 @@ class TagSearchPanel(PanelWidget):
|
||||
create_button: QPushButton | None = None
|
||||
if self.create_button_in_layout and self.scroll_layout.count():
|
||||
create_button = self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget() # type: ignore
|
||||
assert create_button is not None
|
||||
create_button.deleteLater()
|
||||
self.create_button_in_layout = False
|
||||
|
||||
@@ -264,7 +287,8 @@ class TagSearchPanel(PanelWidget):
|
||||
self.scroll_layout.addWidget(new_tw)
|
||||
|
||||
# Assign the tag to the widget at the given index.
|
||||
tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget()
|
||||
tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget() # pyright: ignore[reportAssignmentType]
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
tag_widget.set_tag(tag)
|
||||
|
||||
# Set tag widget viability and potentially return early
|
||||
@@ -288,13 +312,11 @@ class TagSearchPanel(PanelWidget):
|
||||
tag_widget.on_remove.connect(lambda t=tag: self.delete_tag(t))
|
||||
tag_widget.bg_button.clicked.connect(lambda: self.tag_chosen.emit(tag_id))
|
||||
|
||||
if self.driver:
|
||||
if self.driver is not None:
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=tag.id: (
|
||||
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
|
||||
self.driver.filter_items(
|
||||
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
|
||||
),
|
||||
lambda checked=False, tag_id=tag.id, driver=self.driver: (
|
||||
driver.main_window.search_field.setText(f"tag_id:{tag_id}"),
|
||||
driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
|
||||
)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
@@ -354,21 +376,23 @@ class TagSearchPanel(PanelWidget):
|
||||
pass
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
def callback(btp: build_tag.BuildTagPanel):
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
|
||||
def callback(btp: BuildTagPanel):
|
||||
self.lib.update_tag(
|
||||
btp.build_tag(), set(btp.parent_ids), set(btp.alias_names), set(btp.alias_ids)
|
||||
)
|
||||
self.update_tags(self.search_field.text())
|
||||
|
||||
build_tag_panel = build_tag.BuildTagPanel(self.lib, tag=tag)
|
||||
build_tag_panel = BuildTagPanel(self.lib, tag=tag)
|
||||
|
||||
self.edit_modal = PanelModal(
|
||||
build_tag_panel,
|
||||
self.lib.tag_display_name(tag.id),
|
||||
Translations["tag.edit"],
|
||||
done_callback=(self.update_tags(self.search_field.text())),
|
||||
has_save=True,
|
||||
)
|
||||
self.edit_modal.setWindowTitle(Translations["tag.edit"])
|
||||
|
||||
self.edit_modal.saved.connect(lambda: callback(build_tag_panel))
|
||||
self.edit_modal.show()
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
"""A pagination widget created for TagStudio."""
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QObject, QSize, Signal
|
||||
from PySide6.QtGui import QIntValidator
|
||||
from PySide6.QtGui import QIntValidator, QPixmap
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QWidget
|
||||
|
||||
from tagstudio.qt.helpers.color_overlay import theme_fg_overlay
|
||||
from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
|
||||
|
||||
class Pagination(QWidget, QObject):
|
||||
@@ -19,6 +22,7 @@ class Pagination(QWidget, QObject):
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.rm = ResourceManager()
|
||||
self.page_count: int = 0
|
||||
self.current_page_index: int = 0
|
||||
self.buffer_page_count: int = 4
|
||||
@@ -39,7 +43,10 @@ class Pagination(QWidget, QObject):
|
||||
|
||||
# [<] ----------------------------------
|
||||
self.prev_button = QPushButtonWrapper()
|
||||
self.prev_button.setText("<")
|
||||
prev_icon: Image.Image = self.rm.get("bxs-left-arrow") # pyright: ignore[reportAssignmentType]
|
||||
prev_icon = theme_fg_overlay(prev_icon, use_alpha=False)
|
||||
self.prev_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(prev_icon)))
|
||||
self.prev_button.setIconSize(QSize(12, 12))
|
||||
self.prev_button.setMinimumSize(self.button_size)
|
||||
self.prev_button.setMaximumSize(self.button_size)
|
||||
|
||||
@@ -89,7 +96,10 @@ class Pagination(QWidget, QObject):
|
||||
|
||||
# ---------------------------------- [>]
|
||||
self.next_button = QPushButtonWrapper()
|
||||
self.next_button.setText(">")
|
||||
next_icon: Image.Image = self.rm.get("bxs-right-arrow") # pyright: ignore[reportAssignmentType]
|
||||
next_icon = theme_fg_overlay(next_icon, use_alpha=False)
|
||||
self.next_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(next_icon)))
|
||||
self.next_button.setIconSize(QSize(12, 12))
|
||||
self.next_button.setMinimumSize(self.button_size)
|
||||
self.next_button.setMaximumSize(self.button_size)
|
||||
|
||||
|
||||
@@ -4,34 +4,44 @@
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
import structlog
|
||||
import ujson
|
||||
from PIL import Image, ImageQt
|
||||
from PIL import Image, ImageFile
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TResourceJsonAttrDict(TypedDict):
|
||||
path: str
|
||||
mode: Literal["qpixmap", "pil", "rb", "r"]
|
||||
|
||||
|
||||
TData = bytes | str | ImageFile.ImageFile | QPixmap
|
||||
|
||||
RESOURCE_FOLDER: Path = Path(__file__).parents[1]
|
||||
|
||||
|
||||
class ResourceManager:
|
||||
"""A resource manager for retrieving resources."""
|
||||
|
||||
_map: dict = {}
|
||||
_cache: dict[str, Any] = {}
|
||||
_initialized: bool = False
|
||||
_res_folder: Path = Path(__file__).parents[1]
|
||||
_map: dict[str, TResourceJsonAttrDict] = {}
|
||||
_cache: dict[str, TData] = {}
|
||||
_instance: "ResourceManager | None" = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Load JSON resource map
|
||||
if not ResourceManager._initialized:
|
||||
def __new__(cls):
|
||||
if ResourceManager._instance is None:
|
||||
ResourceManager._instance = super().__new__(cls)
|
||||
# Load JSON resource map
|
||||
with open(Path(__file__).parent / "resources.json", encoding="utf-8") as f:
|
||||
ResourceManager._map = ujson.load(f)
|
||||
logger.info(
|
||||
"[ResourceManager] Resources Registered:",
|
||||
count=len(ResourceManager._map.items()),
|
||||
)
|
||||
ResourceManager._initialized = True
|
||||
return ResourceManager._instance
|
||||
|
||||
@staticmethod
|
||||
def get_path(id: str) -> Path | None:
|
||||
@@ -43,57 +53,61 @@ class ResourceManager:
|
||||
Returns:
|
||||
Path: The resource path if found, else None.
|
||||
"""
|
||||
res: dict = ResourceManager._map.get(id)
|
||||
if res:
|
||||
return ResourceManager._res_folder / "resources" / res.get("path")
|
||||
res: TResourceJsonAttrDict | None = ResourceManager._map.get(id)
|
||||
if res is not None:
|
||||
return RESOURCE_FOLDER / "resources" / res.get("path")
|
||||
return None
|
||||
|
||||
def get(self, id: str) -> Any:
|
||||
def get(self, id: str) -> TData | None:
|
||||
"""Get a resource from the ResourceManager.
|
||||
|
||||
This can include resources inside and outside of QResources, and will return
|
||||
theme-respecting variations of resources if available.
|
||||
|
||||
Args:
|
||||
id (str): The name of the resource.
|
||||
|
||||
Returns:
|
||||
Any: The resource if found, else None.
|
||||
bytes: When the data is in byte format.
|
||||
str: When the data is in str format.
|
||||
ImageFile: When the data is in PIL.ImageFile.ImageFile format.
|
||||
QPixmap: When the data is in PySide6.QtGui.QPixmap format.
|
||||
None: If resource couldn't load.
|
||||
"""
|
||||
cached_res = ResourceManager._cache.get(id)
|
||||
if cached_res:
|
||||
cached_res: TData | None = ResourceManager._cache.get(id)
|
||||
if cached_res is not None:
|
||||
return cached_res
|
||||
|
||||
else:
|
||||
res: dict = ResourceManager._map.get(id)
|
||||
if not res:
|
||||
return None
|
||||
try:
|
||||
if res.get("mode") in ["r", "rb"]:
|
||||
with open(
|
||||
(ResourceManager._res_folder / "resources" / res.get("path")),
|
||||
res.get("mode"),
|
||||
) as f:
|
||||
data = f.read()
|
||||
if res.get("mode") == "rb":
|
||||
data = bytes(data)
|
||||
ResourceManager._cache[id] = data
|
||||
return data
|
||||
elif res and res.get("mode") == "pil":
|
||||
data = Image.open(ResourceManager._res_folder / "resources" / res.get("path"))
|
||||
return data
|
||||
elif res.get("mode") in ["qpixmap"]:
|
||||
data = Image.open(ResourceManager._res_folder / "resources" / res.get("path"))
|
||||
qim = ImageQt.ImageQt(data)
|
||||
pixmap = QPixmap.fromImage(qim)
|
||||
ResourceManager._cache[id] = pixmap
|
||||
return pixmap
|
||||
except FileNotFoundError:
|
||||
path: Path = ResourceManager._res_folder / "resources" / res.get("path")
|
||||
logger.error("[ResourceManager][ERROR]: Could not find resource: ", path=path)
|
||||
res: TResourceJsonAttrDict | None = ResourceManager._map.get(id)
|
||||
if res is None:
|
||||
return None
|
||||
|
||||
def __getattr__(self, __name: str) -> Any:
|
||||
file_path: Path = RESOURCE_FOLDER / "resources" / res.get("path")
|
||||
mode = res.get("mode")
|
||||
|
||||
data: TData | None = None
|
||||
try:
|
||||
match mode:
|
||||
case "r":
|
||||
data = file_path.read_text()
|
||||
|
||||
case "rb":
|
||||
data = file_path.read_bytes()
|
||||
|
||||
case "pil":
|
||||
data = Image.open(file_path)
|
||||
data.load()
|
||||
|
||||
case "qpixmap":
|
||||
data = QPixmap(file_path.as_posix())
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error("[ResourceManager][ERROR]: Could not find resource: ", path=file_path)
|
||||
|
||||
if data is not None:
|
||||
ResourceManager._cache[id] = data
|
||||
return data
|
||||
|
||||
def __getattr__(self, __name: str) -> TData:
|
||||
attr = self.get(__name)
|
||||
if attr:
|
||||
if attr is not None:
|
||||
return attr
|
||||
raise AttributeError(f"Attribute {id} not found")
|
||||
|
||||
@@ -123,8 +123,12 @@
|
||||
"path": "qt/images/thumb_loading.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"placeholder_mp4": {
|
||||
"path": "qt/videos/placeholder.mp4",
|
||||
"mode": "rb"
|
||||
"bxs-left-arrow": {
|
||||
"path": "qt/images/bxs-left-arrow.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"bxs-right-arrow": {
|
||||
"path": "qt/images/bxs-right-arrow.png",
|
||||
"mode": "pil"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ DEFAULT_TRANSLATION = "en"
|
||||
|
||||
LANGUAGES = {
|
||||
# "Cantonese (Traditional)": "yue_Hant", # Empty
|
||||
"Chinese (Simplified)": "zh_Hans",
|
||||
"Chinese (Traditional)": "zh_Hant",
|
||||
# "Czech": "cs", # Minimal
|
||||
# "Danish": "da", # Minimal
|
||||
@@ -23,7 +24,7 @@ LANGUAGES = {
|
||||
"Hungarian": "hu",
|
||||
# "Italian": "it", # Minimal
|
||||
"Japanese": "ja",
|
||||
"Norwegian Bokmål": "nb_NO", # Minimal
|
||||
"Norwegian Bokmål": "nb_NO",
|
||||
"Polish": "pl",
|
||||
"Portuguese (Brazil)": "pt_BR",
|
||||
# "Portuguese (Portugal)": "pt", # Empty
|
||||
@@ -33,7 +34,7 @@ LANGUAGES = {
|
||||
"Tamil": "ta",
|
||||
"Toki Pona": "tok",
|
||||
"Turkish": "tr",
|
||||
# "Viossa": "qpv", # Minimal
|
||||
"Viossa": "qpv",
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
65
src/tagstudio/qt/view/components/tag_box_view.py
Normal file
65
src/tagstudio/qt/view/components/tag_box_view.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.widgets.fields import FieldWidget
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagBoxWidgetView(FieldWidget):
|
||||
__lib: Library
|
||||
|
||||
def __init__(self, title: str, driver: "QtDriver") -> None:
|
||||
super().__init__(title)
|
||||
self.__lib = driver.lib
|
||||
|
||||
self.__root_layout = FlowLayout()
|
||||
self.__root_layout.enable_grid_optimizations(value=False)
|
||||
self.__root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.__root_layout)
|
||||
|
||||
def set_tags(self, tags: Iterable[Tag]) -> None:
|
||||
tags_ = sorted(list(tags), key=lambda tag: self.__lib.tag_display_name(tag.id))
|
||||
logger.info("[TagBoxWidget] Tags:", tags=tags)
|
||||
while self.__root_layout.itemAt(0):
|
||||
self.__root_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
for tag in tags_:
|
||||
tag_widget = TagWidget(tag, library=self.__lib, has_edit=True, has_remove=True)
|
||||
|
||||
tag_widget.on_click.connect(lambda t=tag: self._on_click(t))
|
||||
|
||||
tag_widget.on_remove.connect(lambda t=tag: self._on_remove(t))
|
||||
|
||||
tag_widget.on_edit.connect(lambda t=tag: self._on_edit(t))
|
||||
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, t=tag: self._on_search(t)
|
||||
)
|
||||
|
||||
self.__root_layout.addWidget(tag_widget)
|
||||
|
||||
def _on_click(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_remove(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_edit(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_search(self, tag: Tag) -> None:
|
||||
raise NotImplementedError
|
||||
312
src/tagstudio/qt/view/widgets/preview/preview_thumb_view.py
Normal file
312
src/tagstudio/qt/view/widgets/preview/preview_thumb_view.py
Normal file
@@ -0,0 +1,312 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt
|
||||
from PySide6.QtGui import QAction, QMovie, QPixmap, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QStackedLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaType
|
||||
from tagstudio.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle
|
||||
from tagstudio.qt.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.media_player import MediaPlayer
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
THUMB_SIZE_FACTOR = 2
|
||||
|
||||
|
||||
class PreviewThumbView(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
__img_button_size: tuple[int, int]
|
||||
__image_ratio: float
|
||||
|
||||
__filepath: Path | None
|
||||
__rendered_res: tuple[int, int]
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver") -> None:
|
||||
super().__init__()
|
||||
|
||||
self.__img_button_size = (266, 266)
|
||||
self.__image_ratio = 1.0
|
||||
|
||||
self.__image_layout = QStackedLayout(self)
|
||||
self.__image_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.__image_layout.setStackingMode(QStackedLayout.StackingMode.StackAll)
|
||||
self.__image_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
open_file_action = QAction(Translations["file.open_file"], self)
|
||||
open_file_action.triggered.connect(self._open_file_action_callback)
|
||||
open_explorer_action = QAction(open_file_str(), self)
|
||||
open_explorer_action.triggered.connect(self._open_explorer_action_callback)
|
||||
delete_action = QAction(
|
||||
Translations.format("trash.context.singular", trash_term=trash_term()),
|
||||
self,
|
||||
)
|
||||
delete_action.triggered.connect(self._delete_action_callback)
|
||||
|
||||
self.__button_wrapper = QPushButton()
|
||||
self.__button_wrapper.setMinimumSize(*self.__img_button_size)
|
||||
self.__button_wrapper.setFlat(True)
|
||||
self.__button_wrapper.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.__button_wrapper.addAction(open_file_action)
|
||||
self.__button_wrapper.addAction(open_explorer_action)
|
||||
self.__button_wrapper.addAction(delete_action)
|
||||
self.__button_wrapper.clicked.connect(self._button_wrapper_callback)
|
||||
|
||||
# In testing, it didn't seem possible to center the widgets directly
|
||||
# on the QStackedLayout. Adding sublayouts allows us to center the widgets.
|
||||
self.__preview_img_page = QWidget()
|
||||
self.__stacked_page_setup(self.__preview_img_page, self.__button_wrapper)
|
||||
|
||||
self.__preview_gif = QLabel()
|
||||
self.__preview_gif.setMinimumSize(*self.__img_button_size)
|
||||
self.__preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.__preview_gif.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.__preview_gif.addAction(open_file_action)
|
||||
self.__preview_gif.addAction(open_explorer_action)
|
||||
self.__preview_gif.addAction(delete_action)
|
||||
self.__gif_buffer: QBuffer = QBuffer()
|
||||
|
||||
self.__preview_gif_page = QWidget()
|
||||
self.__stacked_page_setup(self.__preview_gif_page, self.__preview_gif)
|
||||
|
||||
self.__media_player = MediaPlayer(driver)
|
||||
self.__media_player.addAction(open_file_action)
|
||||
self.__media_player.addAction(open_explorer_action)
|
||||
self.__media_player.addAction(delete_action)
|
||||
|
||||
# Need to watch for this to resize the player appropriately.
|
||||
self.__media_player.player.hasVideoChanged.connect(
|
||||
self.__media_player_video_changed_callback
|
||||
)
|
||||
|
||||
self.__media_player_page = QWidget()
|
||||
self.__stacked_page_setup(self.__media_player_page, self.__media_player)
|
||||
|
||||
self.__thumb_renderer = ThumbRenderer(library)
|
||||
self.__thumb_renderer.updated.connect(self.__thumb_renderer_updated_callback)
|
||||
self.__thumb_renderer.updated_ratio.connect(self.__thumb_renderer_updated_ratio_callback)
|
||||
|
||||
self.__image_layout.addWidget(self.__preview_img_page)
|
||||
self.__image_layout.addWidget(self.__preview_gif_page)
|
||||
self.__image_layout.addWidget(self.__media_player_page)
|
||||
|
||||
self.setMinimumSize(*self.__img_button_size)
|
||||
|
||||
self.hide_preview()
|
||||
|
||||
def _open_file_action_callback(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _open_explorer_action_callback(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _delete_action_callback(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _button_wrapper_callback(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __media_player_video_changed_callback(self, video: bool) -> None:
|
||||
self.__update_image_size((self.size().width(), self.size().height()))
|
||||
|
||||
def __thumb_renderer_updated_callback(
|
||||
self, _timestamp: float, img: QPixmap, _size: QSize, _path: Path
|
||||
) -> None:
|
||||
self.__button_wrapper.setIcon(img)
|
||||
|
||||
def __thumb_renderer_updated_ratio_callback(self, ratio: float) -> None:
|
||||
self.__image_ratio = ratio
|
||||
self.__update_image_size(
|
||||
(
|
||||
self.size().width(),
|
||||
self.size().height(),
|
||||
)
|
||||
)
|
||||
|
||||
def __stacked_page_setup(self, page: QWidget, widget: QWidget) -> None:
|
||||
layout = QHBoxLayout(page)
|
||||
layout.addWidget(widget)
|
||||
layout.setAlignment(widget, Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
page.setLayout(layout)
|
||||
|
||||
def __update_image_size(self, size: tuple[int, int]) -> None:
|
||||
adj_width: float = size[0]
|
||||
adj_height: float = size[1]
|
||||
# Landscape
|
||||
if self.__image_ratio > 1:
|
||||
adj_height = size[0] * (1 / self.__image_ratio)
|
||||
# Portrait
|
||||
elif self.__image_ratio <= 1:
|
||||
adj_width = size[1] * self.__image_ratio
|
||||
|
||||
if adj_width > size[0]:
|
||||
adj_height = adj_height * (size[0] / adj_width)
|
||||
adj_width = size[0]
|
||||
elif adj_height > size[1]:
|
||||
adj_width = adj_width * (size[1] / adj_height)
|
||||
adj_height = size[1]
|
||||
|
||||
adj_size = QSize(int(adj_width), int(adj_height))
|
||||
|
||||
self.__img_button_size = (int(adj_width), int(adj_height))
|
||||
self.__button_wrapper.setMaximumSize(adj_size)
|
||||
self.__button_wrapper.setIconSize(adj_size)
|
||||
self.__preview_gif.setMaximumSize(adj_size)
|
||||
self.__preview_gif.setMinimumSize(adj_size)
|
||||
|
||||
self.__media_player.setMaximumSize(adj_size)
|
||||
self.__media_player.setMinimumSize(adj_size)
|
||||
|
||||
proxy_style = RoundedPixmapStyle(radius=8)
|
||||
self.__preview_gif.setStyle(proxy_style)
|
||||
self.__media_player.setStyle(proxy_style)
|
||||
m = self.__preview_gif.movie()
|
||||
if m:
|
||||
m.setScaledSize(adj_size)
|
||||
|
||||
def __switch_preview(self, preview: MediaType | None) -> None:
|
||||
if preview in [MediaType.AUDIO, MediaType.VIDEO]:
|
||||
self.__media_player.show()
|
||||
self.__image_layout.setCurrentWidget(self.__media_player_page)
|
||||
else:
|
||||
self.__media_player.stop()
|
||||
self.__media_player.hide()
|
||||
|
||||
if preview in [MediaType.IMAGE, MediaType.AUDIO]:
|
||||
self.__button_wrapper.show()
|
||||
self.__image_layout.setCurrentWidget(
|
||||
self.__preview_img_page if preview == MediaType.IMAGE else self.__media_player_page
|
||||
)
|
||||
else:
|
||||
self.__button_wrapper.hide()
|
||||
|
||||
if preview == MediaType.IMAGE_ANIMATED:
|
||||
self.__preview_gif.show()
|
||||
self.__image_layout.setCurrentWidget(self.__preview_gif_page)
|
||||
else:
|
||||
if self.__preview_gif.movie():
|
||||
self.__preview_gif.movie().stop()
|
||||
self.__gif_buffer.close()
|
||||
self.__preview_gif.hide()
|
||||
|
||||
def __render_thumb(self, filepath: Path) -> None:
|
||||
self.__filepath = filepath
|
||||
self.__rendered_res = (
|
||||
math.ceil(self.__img_button_size[0] * THUMB_SIZE_FACTOR),
|
||||
math.ceil(self.__img_button_size[1] * THUMB_SIZE_FACTOR),
|
||||
)
|
||||
|
||||
self.__thumb_renderer.render(
|
||||
time.time(),
|
||||
filepath,
|
||||
self.__rendered_res,
|
||||
self.devicePixelRatio(),
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
|
||||
def __update_media_player(self, filepath: Path) -> int:
|
||||
"""Display either audio or video.
|
||||
|
||||
Returns the duration of the audio / video.
|
||||
"""
|
||||
self.__media_player.play(filepath)
|
||||
return self.__media_player.player.duration() * 1000
|
||||
|
||||
def _display_video(self, filepath: Path, size: QSize | None) -> FileAttributeData:
|
||||
self.__switch_preview(MediaType.VIDEO)
|
||||
stats = FileAttributeData(duration=self.__update_media_player(filepath))
|
||||
|
||||
if size is not None:
|
||||
stats.width = size.width()
|
||||
stats.height = size.height()
|
||||
|
||||
self.__image_ratio = stats.width / stats.height
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(stats.width, stats.height),
|
||||
QSize(stats.width, stats.height),
|
||||
)
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
def _display_audio(self, filepath: Path) -> FileAttributeData:
|
||||
self.__switch_preview(MediaType.AUDIO)
|
||||
self.__render_thumb(filepath)
|
||||
return FileAttributeData(duration=self.__update_media_player(filepath))
|
||||
|
||||
def _display_gif(self, gif_data: bytes, size: tuple[int, int]) -> FileAttributeData | None:
|
||||
"""Update the animated image preview from a filepath."""
|
||||
stats = FileAttributeData()
|
||||
|
||||
# Ensure that any movie and buffer from previous animations are cleared.
|
||||
if self.__preview_gif.movie():
|
||||
self.__preview_gif.movie().stop()
|
||||
self.__gif_buffer.close()
|
||||
|
||||
stats.width = size[0]
|
||||
stats.height = size[1]
|
||||
|
||||
self.__image_ratio = stats.width / stats.height
|
||||
|
||||
self.__gif_buffer.setData(gif_data)
|
||||
movie = QMovie(self.__gif_buffer, QByteArray())
|
||||
self.__preview_gif.setMovie(movie)
|
||||
|
||||
# If the animation only has 1 frame, it isn't animated and shouldn't be treated as such
|
||||
if movie.frameCount() <= 1:
|
||||
return None
|
||||
|
||||
# The animation has more than 1 frame, continue displaying it as an animation
|
||||
self.__switch_preview(MediaType.IMAGE_ANIMATED)
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(stats.width, stats.height),
|
||||
QSize(stats.width, stats.height),
|
||||
)
|
||||
)
|
||||
movie.start()
|
||||
|
||||
stats.duration = movie.frameCount() // 60
|
||||
|
||||
return stats
|
||||
|
||||
def _display_image(self, filepath: Path):
|
||||
"""Renders the given file as an image, no matter its media type."""
|
||||
self.__switch_preview(MediaType.IMAGE)
|
||||
self.__render_thumb(filepath)
|
||||
|
||||
def hide_preview(self) -> None:
|
||||
"""Completely hide the file preview."""
|
||||
self.__switch_preview(None)
|
||||
self.__filepath = None
|
||||
|
||||
@override
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
self.__update_image_size((self.size().width(), self.size().height()))
|
||||
|
||||
if self.__filepath is not None and self.__rendered_res < self.__img_button_size:
|
||||
self.__render_thumb(self.__filepath)
|
||||
|
||||
return super().resizeEvent(event)
|
||||
|
||||
@property
|
||||
def media_player(self) -> MediaPlayer:
|
||||
return self.__media_player
|
||||
212
src/tagstudio/qt/view/widgets/preview_panel_view.py
Normal file
212
src/tagstudio/qt/view/widgets/preview_panel_view.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import traceback
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.controller.widgets.preview.preview_thumb_controller import PreviewThumb
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.preview.field_containers import FieldContainers
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributeData, FileAttributes
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
BUTTON_STYLE = f"""
|
||||
QPushButton{{
|
||||
background-color: {Theme.COLOR_BG.value};
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}}
|
||||
QPushButton::hover{{
|
||||
background-color: {Theme.COLOR_HOVER.value};
|
||||
border-color: {get_ui_color(ColorType.BORDER, UiColor.THEME_DARK)};
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
QPushButton::pressed{{
|
||||
background-color: {Theme.COLOR_PRESSED.value};
|
||||
border-color: {get_ui_color(ColorType.LIGHT_ACCENT, UiColor.THEME_DARK)};
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}}
|
||||
QPushButton::disabled{{
|
||||
background-color: {Theme.COLOR_DISABLED_BG.value};
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class PreviewPanelView(QWidget):
|
||||
lib: Library
|
||||
|
||||
_selected: list[int]
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
|
||||
self.__thumb = PreviewThumb(self.lib, driver)
|
||||
self.__file_attrs = FileAttributes(self.lib, driver)
|
||||
self._fields = FieldContainers(
|
||||
self.lib, driver
|
||||
) # TODO: this should be name mangled, but is still needed on the controller side atm
|
||||
|
||||
preview_section = QWidget()
|
||||
preview_layout = QVBoxLayout(preview_section)
|
||||
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
preview_layout.setSpacing(6)
|
||||
|
||||
info_section = QWidget()
|
||||
info_layout = QVBoxLayout(info_section)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
info_layout.setSpacing(6)
|
||||
|
||||
splitter = QSplitter()
|
||||
splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
splitter.setHandleWidth(12)
|
||||
|
||||
add_buttons_container = QWidget()
|
||||
add_buttons_layout = QHBoxLayout(add_buttons_container)
|
||||
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
add_buttons_layout.setSpacing(6)
|
||||
|
||||
self.__add_tag_button = QPushButton(Translations["tag.add"])
|
||||
self.__add_tag_button.setEnabled(False)
|
||||
self.__add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.__add_tag_button.setMinimumHeight(28)
|
||||
self.__add_tag_button.setStyleSheet(BUTTON_STYLE)
|
||||
|
||||
self.__add_field_button = QPushButton(Translations["library.field.add"])
|
||||
self.__add_field_button.setEnabled(False)
|
||||
self.__add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.__add_field_button.setMinimumHeight(28)
|
||||
self.__add_field_button.setStyleSheet(BUTTON_STYLE)
|
||||
|
||||
add_buttons_layout.addWidget(self.__add_tag_button)
|
||||
add_buttons_layout.addWidget(self.__add_field_button)
|
||||
|
||||
preview_layout.addWidget(self.__thumb)
|
||||
info_layout.addWidget(self.__file_attrs)
|
||||
info_layout.addWidget(self._fields)
|
||||
|
||||
splitter.addWidget(preview_section)
|
||||
splitter.addWidget(info_section)
|
||||
splitter.setStretchFactor(1, 2)
|
||||
|
||||
root_layout = QVBoxLayout(self)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.addWidget(splitter)
|
||||
root_layout.addWidget(add_buttons_container)
|
||||
|
||||
self.__connect_callbacks()
|
||||
|
||||
def __connect_callbacks(self):
|
||||
self.__add_field_button.clicked.connect(self._add_field_button_callback)
|
||||
self.__add_tag_button.clicked.connect(self._add_tag_button_callback)
|
||||
|
||||
def _add_field_button_callback(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _add_tag_button_callback(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_selection_callback(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_selection(self, selected: list[int], update_preview: bool = True):
|
||||
"""Render the panel widgets with the newest data from the Library.
|
||||
|
||||
Args:
|
||||
selected (list[int]): List of the IDs of the selected entries.
|
||||
update_preview (bool): Should the file preview be updated?
|
||||
(Only works with one or more items selected)
|
||||
"""
|
||||
self._selected = selected
|
||||
try:
|
||||
# No Items Selected
|
||||
if len(selected) == 0:
|
||||
self.__thumb.hide_preview()
|
||||
self.__file_attrs.update_stats()
|
||||
self.__file_attrs.update_date_label()
|
||||
self._fields.hide_containers()
|
||||
|
||||
self.add_buttons_enabled = False
|
||||
|
||||
# One Item Selected
|
||||
elif len(selected) == 1:
|
||||
entry_id = selected[0]
|
||||
entry: Entry | None = self.lib.get_entry(entry_id)
|
||||
assert entry is not None
|
||||
|
||||
assert self.lib.library_dir is not None
|
||||
filepath: Path = self.lib.library_dir / entry.path
|
||||
|
||||
if update_preview:
|
||||
stats: FileAttributeData = self.__thumb.display_file(filepath)
|
||||
self.__file_attrs.update_stats(filepath, stats)
|
||||
self.__file_attrs.update_date_label(filepath)
|
||||
self._fields.update_from_entry(entry_id)
|
||||
|
||||
self._set_selection_callback()
|
||||
|
||||
self.add_buttons_enabled = True
|
||||
|
||||
# Multiple Selected Items
|
||||
elif len(selected) > 1:
|
||||
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
|
||||
self.__thumb.hide_preview() # TODO: Render mixed selection
|
||||
self.__file_attrs.update_multi_selection(len(selected))
|
||||
self.__file_attrs.update_date_label()
|
||||
self._fields.hide_containers() # TODO: Allow for mixed editing
|
||||
|
||||
self._set_selection_callback()
|
||||
|
||||
self.add_buttons_enabled = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("[Preview Panel] Error updating selection", error=e)
|
||||
traceback.print_exc()
|
||||
|
||||
@property
|
||||
def add_buttons_enabled(self) -> bool: # needed for the tests
|
||||
field = self.__add_field_button.isEnabled()
|
||||
tag = self.__add_tag_button.isEnabled()
|
||||
assert field == tag
|
||||
return field
|
||||
|
||||
@add_buttons_enabled.setter
|
||||
def add_buttons_enabled(self, enabled: bool):
|
||||
self.__add_field_button.setEnabled(enabled)
|
||||
self.__add_tag_button.setEnabled(enabled)
|
||||
|
||||
@property
|
||||
def _file_attributes_widget(self) -> FileAttributes: # needed for the tests
|
||||
"""Getter for the file attributes widget."""
|
||||
return self.__file_attrs
|
||||
|
||||
@property
|
||||
def field_containers_widget(self) -> FieldContainers: # needed for the tests
|
||||
"""Getter for the field containers widget."""
|
||||
return self._fields
|
||||
|
||||
@property
|
||||
def preview_thumb(self) -> PreviewThumb:
|
||||
return self.__thumb
|
||||
@@ -29,14 +29,16 @@ class CollageIconRenderer(QObject):
|
||||
|
||||
def render(
|
||||
self,
|
||||
entry_id,
|
||||
entry_id: int,
|
||||
size: tuple[int, int],
|
||||
data_tint_mode,
|
||||
data_only_mode,
|
||||
keep_aspect,
|
||||
data_tint_mode: bool,
|
||||
data_only_mode: bool,
|
||||
keep_aspect: bool,
|
||||
):
|
||||
entry = self.lib.get_entry(entry_id)
|
||||
filepath = self.lib.library_dir / entry.path
|
||||
lib_dir = self.lib.library_dir
|
||||
assert lib_dir is not None and entry is not None
|
||||
filepath = lib_dir / entry.path
|
||||
color: str = ""
|
||||
|
||||
try:
|
||||
@@ -57,7 +59,7 @@ class CollageIconRenderer(QObject):
|
||||
ext: str = filepath.suffix.lower()
|
||||
if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES):
|
||||
try:
|
||||
with Image.open(str(self.lib.library_dir / entry.path)) as pic:
|
||||
with Image.open(filepath) as pic:
|
||||
if keep_aspect:
|
||||
pic.thumbnail(size)
|
||||
else:
|
||||
|
||||
@@ -134,7 +134,6 @@ class ColorBoxWidget(FieldWidget):
|
||||
self.edit_modal = PanelModal(
|
||||
build_color_panel,
|
||||
"Edit Color",
|
||||
"Edit Color",
|
||||
has_save=True,
|
||||
)
|
||||
|
||||
|
||||
82
src/tagstudio/qt/widgets/datetime_picker.py
Normal file
82
src/tagstudio/qt/widgets/datetime_picker.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime as dt
|
||||
from typing import cast
|
||||
|
||||
from PySide6.QtCore import QDateTime
|
||||
from PySide6.QtWidgets import QDateTimeEdit, QVBoxLayout
|
||||
|
||||
from tagstudio.qt.widgets.panel import PanelWidget
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
QDTF2DTF = {
|
||||
"%d": "dd",
|
||||
"%m": "MM",
|
||||
"%y": "yy",
|
||||
"%H": "HH",
|
||||
"%M": "mm",
|
||||
"%S": "ss",
|
||||
"%Y": "yyyy",
|
||||
"%I": "hh",
|
||||
"%p": "AP",
|
||||
"%x": "MM/dd/yy",
|
||||
}
|
||||
|
||||
|
||||
def qdtf2dtf(dtf: str) -> str:
|
||||
out = dtf
|
||||
for old, new in QDTF2DTF.items():
|
||||
out = out.replace(old, new)
|
||||
return out
|
||||
|
||||
|
||||
class DatetimePicker(PanelWidget):
|
||||
def __init__(self, driver: "QtDriver", datetime: dt | str):
|
||||
super().__init__()
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
|
||||
if isinstance(datetime, str):
|
||||
datetime = DatetimePicker.string2dt(datetime)
|
||||
self.datetime_edit = QDateTimeEdit()
|
||||
self.datetime_edit.setCalendarPopup(True)
|
||||
self.datetime_edit.setDateTime(DatetimePicker.dt2qdt(datetime))
|
||||
# sketchy way to show seconds without showing the day of the week;
|
||||
# while also still having localisation
|
||||
self.datetime_edit.setDisplayFormat(qdtf2dtf(driver.settings.datetime_format))
|
||||
|
||||
self.initial_value = datetime
|
||||
self.root_layout.addWidget(self.datetime_edit)
|
||||
|
||||
def get_content(self):
|
||||
return DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime()))
|
||||
|
||||
def reset(self):
|
||||
self.datetime_edit.setDateTime(DatetimePicker.dt2qdt(self.initial_value))
|
||||
|
||||
def add_callback(self, callback: Callable, event: str = "returnPressed"):
|
||||
if event == "returnPressed":
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"unknown event type: {event}")
|
||||
|
||||
@staticmethod
|
||||
def qdt2dt(qdt: QDateTime) -> dt:
|
||||
return cast(dt, qdt.toPython())
|
||||
|
||||
@staticmethod
|
||||
def dt2qdt(datetime: dt) -> QDateTime:
|
||||
return QDateTime.fromSecsSinceEpoch(int(datetime.timestamp()))
|
||||
|
||||
@staticmethod
|
||||
def string2dt(datetime_str: str) -> dt:
|
||||
return dt.strptime(datetime_str, DATETIME_FORMAT)
|
||||
|
||||
@staticmethod
|
||||
def dt2string(datetime: dt) -> str:
|
||||
return dt.strftime(datetime, DATETIME_FORMAT)
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Callable, override
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
|
||||
|
||||
import time
|
||||
import typing
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from warnings import catch_warnings
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QEvent, QMimeData, QSize, Qt, QUrl
|
||||
from PySide6.QtGui import QAction, QDrag, QEnterEvent, QPixmap
|
||||
from PySide6.QtGui import QAction, QDrag, QEnterEvent, QGuiApplication, QMouseEvent, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QBoxLayout,
|
||||
QCheckBox,
|
||||
@@ -126,7 +124,7 @@ class ItemThumb(FlowWidget):
|
||||
self.lib = library
|
||||
self.mode: ItemType | None = mode
|
||||
self.driver = driver
|
||||
self.item_id: int | None = None
|
||||
self.item_id: int = -1
|
||||
self.thumb_size: tuple[int, int] = thumb_size
|
||||
self.show_filename_label: bool = show_filename_label
|
||||
self.label_height = 12
|
||||
@@ -205,11 +203,11 @@ class ItemThumb(FlowWidget):
|
||||
self.thumb_button = ThumbButton(self.thumb_container, thumb_size)
|
||||
self.renderer = ThumbRenderer(self.lib)
|
||||
self.renderer.updated.connect(
|
||||
lambda timestamp, image, size, filename, ext: (
|
||||
self.update_thumb(timestamp, image=image),
|
||||
self.update_size(timestamp, size=size),
|
||||
self.set_filename_text(filename),
|
||||
self.set_extension(ext), # type: ignore
|
||||
lambda timestamp, image, size, filename: (
|
||||
self.update_thumb(image, timestamp),
|
||||
self.update_size(size, timestamp),
|
||||
self.set_filename_text(filename, timestamp),
|
||||
self.set_extension(filename, timestamp),
|
||||
)
|
||||
)
|
||||
self.thumb_button.setFlat(True)
|
||||
@@ -321,7 +319,16 @@ class ItemThumb(FlowWidget):
|
||||
|
||||
self.base_layout.addWidget(self.thumb_container)
|
||||
self.base_layout.addWidget(self.file_label)
|
||||
|
||||
# NOTE: self.item_id seems to act as a reference here and does not need to be updated inside
|
||||
# QtDriver.update_thumbs() while item_thumb.delete_action does.
|
||||
# If this behavior ever changes, move this method back to QtDriver.update_thumbs().
|
||||
self.thumb_button.clicked.connect(
|
||||
lambda: self.driver.toggle_item_selection(
|
||||
self.item_id,
|
||||
append=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier),
|
||||
bridge=(QGuiApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier),
|
||||
)
|
||||
)
|
||||
self.set_mode(mode)
|
||||
|
||||
@property
|
||||
@@ -365,13 +372,16 @@ class ItemThumb(FlowWidget):
|
||||
self.item_type_badge.setHidden(False)
|
||||
self.mode = mode
|
||||
|
||||
def set_extension(self, ext: str) -> None:
|
||||
def set_extension(self, filename: Path, timestamp: float | None = None) -> None:
|
||||
if timestamp and timestamp < ItemThumb.update_cutoff:
|
||||
return
|
||||
|
||||
ext = filename.suffix.lower()
|
||||
if ext and ext.startswith(".") is False:
|
||||
ext = "." + ext
|
||||
media_types: set[MediaType] = MediaCategories.get_types(ext)
|
||||
if (
|
||||
ext
|
||||
and not MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES)
|
||||
not MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_TYPES)
|
||||
or MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES)
|
||||
or MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_VECTOR_TYPES)
|
||||
or MediaCategories.is_ext_in_category(ext, MediaCategories.ADOBE_PHOTOSHOP_TYPES)
|
||||
@@ -385,12 +395,13 @@ class ItemThumb(FlowWidget):
|
||||
".webp",
|
||||
]
|
||||
):
|
||||
self.ext_badge.setHidden(False)
|
||||
self.ext_badge.setText(ext.upper()[1:])
|
||||
self.ext_badge.setText(ext.upper()[1:] or filename.stem.upper())
|
||||
if ext or filename.stem:
|
||||
self.ext_badge.setHidden(False)
|
||||
if MediaType.VIDEO in media_types or MediaType.AUDIO in media_types:
|
||||
self.count_badge.setHidden(False)
|
||||
else:
|
||||
if self.mode == ItemType.ENTRY:
|
||||
if self.mode == ItemType.ENTRY or self.mode is None:
|
||||
self.ext_badge.setHidden(True)
|
||||
self.count_badge.setHidden(True)
|
||||
|
||||
@@ -403,7 +414,10 @@ class ItemThumb(FlowWidget):
|
||||
self.ext_badge.setHidden(True)
|
||||
self.count_badge.setHidden(True)
|
||||
|
||||
def set_filename_text(self, filename: Path | None):
|
||||
def set_filename_text(self, filename: Path, timestamp: float | None = None):
|
||||
if timestamp and timestamp < ItemThumb.update_cutoff:
|
||||
return
|
||||
|
||||
self.set_item_path(filename)
|
||||
self.file_label.setText(str(filename.name))
|
||||
|
||||
@@ -422,12 +436,14 @@ class ItemThumb(FlowWidget):
|
||||
self.setFixedHeight(self.thumb_size[1])
|
||||
self.show_filename_label = set_visible
|
||||
|
||||
def update_thumb(self, timestamp: float, image: QPixmap | None = None):
|
||||
def update_thumb(self, image: QPixmap | None = None, timestamp: float | None = None):
|
||||
"""Update attributes of a thumbnail element."""
|
||||
if timestamp > ItemThumb.update_cutoff:
|
||||
self.thumb_button.setIcon(image if image else QPixmap())
|
||||
if timestamp and timestamp < ItemThumb.update_cutoff:
|
||||
return
|
||||
|
||||
def update_size(self, timestamp: float, size: QSize):
|
||||
self.thumb_button.setIcon(image if image else QPixmap())
|
||||
|
||||
def update_size(self, size: QSize, timestamp: float | None = None):
|
||||
"""Updates attributes of a thumbnail element.
|
||||
|
||||
Args:
|
||||
@@ -436,23 +452,18 @@ class ItemThumb(FlowWidget):
|
||||
|
||||
size (QSize): The new thumbnail size to set.
|
||||
"""
|
||||
if timestamp > ItemThumb.update_cutoff:
|
||||
self.thumb_size = size.toTuple() # type: ignore
|
||||
self.thumb_button.setIconSize(size)
|
||||
self.thumb_button.setMinimumSize(size)
|
||||
self.thumb_button.setMaximumSize(size)
|
||||
if timestamp and timestamp < ItemThumb.update_cutoff:
|
||||
return
|
||||
|
||||
def update_clickable(self, clickable: typing.Callable):
|
||||
"""Updates attributes of a thumbnail element."""
|
||||
if clickable:
|
||||
with catch_warnings(record=True):
|
||||
self.thumb_button.clicked.disconnect()
|
||||
self.thumb_button.clicked.connect(clickable)
|
||||
self.thumb_size = size.width(), size.height()
|
||||
self.thumb_button.setIconSize(size)
|
||||
self.thumb_button.setMinimumSize(size)
|
||||
self.thumb_button.setMaximumSize(size)
|
||||
|
||||
def set_item_id(self, item_id: int):
|
||||
self.item_id = item_id
|
||||
|
||||
def set_item_path(self, path: Path | str | None):
|
||||
def set_item_path(self, path: Path | str):
|
||||
"""Set the absolute filepath for the item. Used for locating on disk."""
|
||||
self.opener.set_filepath(path)
|
||||
|
||||
@@ -474,11 +485,13 @@ class ItemThumb(FlowWidget):
|
||||
is_hidden = not (show or self.badge_active[badge_type])
|
||||
badge.setHidden(is_hidden)
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None: # type: ignore[misc]
|
||||
self.show_check_badges(show=True)
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None: # type: ignore[misc]
|
||||
self.show_check_badges(show=False)
|
||||
return super().leaveEvent(event)
|
||||
|
||||
@@ -490,6 +503,9 @@ class ItemThumb(FlowWidget):
|
||||
toggle_value = self.badges[badge_type].isChecked()
|
||||
self.badge_active[badge_type] = toggle_value
|
||||
badge_values: dict[BadgeType, bool] = {badge_type: toggle_value}
|
||||
# TODO: Ensure that self.item_id is always an integer. During tests, it is currently None.
|
||||
# This issue should be addressed by either fixing the test setup or modifying the
|
||||
# self.driver.update_badges() method.
|
||||
self.driver.update_badges(badge_values, self.item_id)
|
||||
|
||||
def toggle_item_tag(
|
||||
@@ -498,13 +514,16 @@ class ItemThumb(FlowWidget):
|
||||
toggle_value: bool,
|
||||
tag_id: int,
|
||||
):
|
||||
if entry_id in self.driver.selected and self.driver.preview_panel.is_open:
|
||||
if entry_id in self.driver.selected:
|
||||
if len(self.driver.selected) == 1:
|
||||
self.driver.preview_panel.fields.update_toggled_tag(tag_id, toggle_value)
|
||||
self.driver.main_window.preview_panel.field_containers_widget.update_toggled_tag(
|
||||
tag_id, toggle_value
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
def mouseMoveEvent(self, event): # noqa: N802
|
||||
@override
|
||||
def mouseMoveEvent(self, event: QMouseEvent) -> None: # type: ignore[misc]
|
||||
if event.buttons() is not Qt.MouseButton.LeftButton:
|
||||
return
|
||||
|
||||
@@ -519,6 +538,7 @@ class ItemThumb(FlowWidget):
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
assert self.lib.library_dir is not None
|
||||
url = QUrl.fromLocalFile(Path(self.lib.library_dir) / entry.path)
|
||||
paths.append(url)
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QEasingCurve, QPoint, QPropertyAnimation, Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
@@ -21,7 +21,7 @@ from tagstudio.qt.widgets.clickable_label import ClickableLabel
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class LandingWidget(QWidget):
|
||||
@@ -84,13 +84,12 @@ class LandingWidget(QWidget):
|
||||
self.landing_layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
self.landing_layout.addWidget(self.status_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def update_logo_color(self, style: str = "mono"):
|
||||
def update_logo_color(self, style: typing.Literal["mono", "gradient"] = "mono"):
|
||||
"""Update the color of the TagStudio logo.
|
||||
|
||||
Args:
|
||||
style (str): = The style of the logo. Either "mono" or "gradient".
|
||||
"""
|
||||
logo_im: Image.Image = None
|
||||
if style == "mono":
|
||||
logo_im = theme_fg_overlay(self.logo_raw)
|
||||
elif style == "gradient":
|
||||
@@ -154,7 +153,7 @@ class LandingWidget(QWidget):
|
||||
|
||||
# def animate_status(self):
|
||||
# # if self.status_label.y() > 50:
|
||||
# logging.info(f"{self.status_label.pos()}")
|
||||
# logger.info(f"{self.status_label.pos()}")
|
||||
# self.status_pos_anim.setStartValue(
|
||||
# QPoint(self.status_label.x(), self.status_label.y() + 50)
|
||||
# )
|
||||
|
||||
@@ -7,20 +7,31 @@ from pathlib import Path
|
||||
from time import gmtime
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageDraw
|
||||
from PySide6.QtCore import QEvent, QObject, QRectF, QSize, Qt, QUrl, QVariantAnimation
|
||||
from PySide6.QtGui import QAction, QBitmap, QBrush, QColor, QPen, QRegion, QResizeEvent
|
||||
from PySide6.QtGui import (
|
||||
QAction,
|
||||
QBitmap,
|
||||
QBrush,
|
||||
QColor,
|
||||
QLinearGradient,
|
||||
QMouseEvent,
|
||||
QPen,
|
||||
QRegion,
|
||||
QResizeEvent,
|
||||
)
|
||||
from PySide6.QtMultimedia import QAudioOutput, QMediaDevices, QMediaPlayer
|
||||
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
|
||||
from PySide6.QtSvgWidgets import QSvgWidget
|
||||
from PySide6.QtWidgets import (
|
||||
QGraphicsScene,
|
||||
QGraphicsView,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QSlider,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
@@ -30,6 +41,8 @@ from tagstudio.qt.translations import Translations
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class MediaPlayer(QGraphicsView):
|
||||
"""A basic media player widget.
|
||||
@@ -46,70 +59,41 @@ class MediaPlayer(QGraphicsView):
|
||||
slider_style = """
|
||||
QSlider {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
QSlider::groove:horizontal {
|
||||
border: 1px solid #999999;
|
||||
height: 2px;
|
||||
margin: 2px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: #6ea0ff;
|
||||
border: 1px solid #5c5c5c;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px 0;
|
||||
border-radius: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
QSlider::add-page:horizontal {
|
||||
background: #3f4144;
|
||||
height: 2px;
|
||||
margin: 2px 0;
|
||||
border-radius: 2px;
|
||||
background: #65000000;
|
||||
border-radius: 3px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #65444444;
|
||||
}
|
||||
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #6ea0ff;
|
||||
height: 2px;
|
||||
margin: 2px 0;
|
||||
border-radius: 2px;
|
||||
background: #88FFFFFF;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
QSlider::groove:vertical {
|
||||
border: 1px solid #999999;
|
||||
width: 2px;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
QSlider::groove:horizontal {
|
||||
background: transparent;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
QSlider::handle:vertical {
|
||||
background: #6ea0ff;
|
||||
border: 1px solid #5c5c5c;
|
||||
QSlider::handle:horizontal {
|
||||
background: #FFFFFF;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 -6px;
|
||||
margin: -3px 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QSlider::add-page:vertical {
|
||||
background: #6ea0ff;
|
||||
width: 2px;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QSlider::sup-page:vertical {
|
||||
background: #3f4144;
|
||||
width: 2px;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
"""
|
||||
|
||||
# setup the scene
|
||||
fixed_policy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
retain_policy = QSizePolicy()
|
||||
retain_policy.setRetainSizeWhenHidden(True)
|
||||
self.filepath: Path | None = None
|
||||
|
||||
# Graphics Scene
|
||||
self.installEventFilter(self)
|
||||
self.setScene(QGraphicsScene(self))
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
@@ -121,6 +105,7 @@ class MediaPlayer(QGraphicsView):
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
self.setObjectName("mediaPlayer")
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.video_preview = VideoPreview()
|
||||
@@ -129,67 +114,60 @@ class MediaPlayer(QGraphicsView):
|
||||
self.video_preview.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
||||
self.video_preview.installEventFilter(self)
|
||||
|
||||
# animation
|
||||
self.animation = QVariantAnimation(self)
|
||||
self.animation.valueChanged.connect(lambda value: self.set_tint_opacity(value))
|
||||
|
||||
# Set up the tint.
|
||||
self.tint = self.scene().addRect(
|
||||
0,
|
||||
0,
|
||||
self.size().width(),
|
||||
self.size().height(),
|
||||
12,
|
||||
QPen(QColor(0, 0, 0, 0)),
|
||||
QBrush(QColor(0, 0, 0, 0)),
|
||||
)
|
||||
|
||||
# setup the player
|
||||
self.filepath: Path | None = None
|
||||
# Player
|
||||
self.player = QMediaPlayer()
|
||||
self.player.setAudioOutput(QAudioOutput(QMediaDevices().defaultAudioOutput(), self.player))
|
||||
self.is_paused = (
|
||||
False # Q MediaPlayer.PlaybackState shows StoppedState when changing tracks
|
||||
)
|
||||
|
||||
# Used to keep track of play state.
|
||||
# It would be nice if we could use QMediaPlayer.PlaybackState,
|
||||
# but this will always show StoppedState when changing
|
||||
# tracks. Therefore, we wouldn't know if the previous
|
||||
# state was paused or playing
|
||||
self.is_paused = False
|
||||
|
||||
# Subscribe to player events from MediaPlayer
|
||||
self.player.positionChanged.connect(self.player_position_changed)
|
||||
self.player.mediaStatusChanged.connect(self.media_status_changed)
|
||||
self.player.playingChanged.connect(self.playing_changed)
|
||||
self.player.hasVideoChanged.connect(self.has_video_changed)
|
||||
self.player.audioOutput().mutedChanged.connect(self.muted_changed)
|
||||
|
||||
# Media controls
|
||||
self.master_controls = QWidget()
|
||||
master_layout = QGridLayout(self.master_controls)
|
||||
master_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.master_controls.setStyleSheet("background: transparent;")
|
||||
self.master_controls.setMinimumHeight(75)
|
||||
# Media Controls
|
||||
self.controls = QWidget()
|
||||
self.controls.setObjectName("controls")
|
||||
root_layout = QVBoxLayout(self.controls)
|
||||
root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
root_layout.setSpacing(6)
|
||||
self.controls.setStyleSheet("background: transparent;")
|
||||
self.controls.setMinimumHeight(48)
|
||||
|
||||
self.pslider = QClickSlider()
|
||||
self.pslider.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.pslider.setTickPosition(QSlider.TickPosition.NoTicks)
|
||||
self.pslider.setSingleStep(1)
|
||||
self.pslider.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.pslider.setStyleSheet(slider_style)
|
||||
self.pslider.sliderReleased.connect(self.slider_released)
|
||||
self.pslider.valueChanged.connect(self.slider_value_changed)
|
||||
self.pslider.hide()
|
||||
self.timeline_slider = QClickSlider()
|
||||
self.timeline_slider.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.timeline_slider.setTickPosition(QSlider.TickPosition.NoTicks)
|
||||
self.timeline_slider.setSingleStep(1)
|
||||
self.timeline_slider.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.timeline_slider.setStyleSheet(slider_style)
|
||||
self.timeline_slider.sliderReleased.connect(self.slider_released)
|
||||
self.timeline_slider.valueChanged.connect(self.slider_value_changed)
|
||||
self.timeline_slider.hide()
|
||||
self.timeline_slider.setFixedHeight(12)
|
||||
|
||||
master_layout.addWidget(self.pslider, 0, 0, 0, 2)
|
||||
master_layout.setAlignment(self.pslider, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
fixed_policy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
root_layout.addWidget(self.timeline_slider)
|
||||
root_layout.setAlignment(self.timeline_slider, Qt.AlignmentFlag.AlignBottom)
|
||||
|
||||
self.sub_controls = QWidget()
|
||||
self.sub_controls.setMouseTracking(True)
|
||||
self.sub_controls.installEventFilter(self)
|
||||
sub_layout = QHBoxLayout(self.sub_controls)
|
||||
sub_layout.setContentsMargins(0, 0, 0, 0)
|
||||
sub_layout.setContentsMargins(0, 0, 0, 6)
|
||||
self.sub_controls.setStyleSheet("background: transparent;")
|
||||
self.sub_controls.setMinimumHeight(16)
|
||||
|
||||
self.play_pause = QSvgWidget()
|
||||
self.play_pause.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
@@ -218,9 +196,6 @@ class MediaPlayer(QGraphicsView):
|
||||
sub_layout.addWidget(self.mute_unmute)
|
||||
sub_layout.setAlignment(self.mute_unmute, Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
retain_policy = QSizePolicy()
|
||||
retain_policy.setRetainSizeWhenHidden(True)
|
||||
|
||||
self.volume_slider = QClickSlider()
|
||||
self.volume_slider.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.volume_slider.setValue(int(self.player.audioOutput().volume() * 100))
|
||||
@@ -228,24 +203,20 @@ class MediaPlayer(QGraphicsView):
|
||||
self.volume_slider.setStyleSheet(slider_style)
|
||||
self.volume_slider.setSizePolicy(retain_policy)
|
||||
self.volume_slider.hide()
|
||||
self.volume_slider.setMinimumWidth(32)
|
||||
|
||||
sub_layout.addWidget(self.volume_slider)
|
||||
sub_layout.setAlignment(self.volume_slider, Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Adding a stretch here ensures the rest of the widgets
|
||||
# in the sub_layout will not stretch to fill the remaining
|
||||
# space.
|
||||
sub_layout.addStretch()
|
||||
|
||||
master_layout.addWidget(self.sub_controls, 1, 0)
|
||||
|
||||
self.position_label = QLabel("0:00")
|
||||
self.position_label.setStyleSheet("color: #ffffff;")
|
||||
master_layout.addWidget(self.position_label, 1, 1)
|
||||
master_layout.setAlignment(self.position_label, Qt.AlignmentFlag.AlignRight)
|
||||
self.position_label.setStyleSheet("color: white;")
|
||||
sub_layout.addWidget(self.position_label)
|
||||
root_layout.setAlignment(self.position_label, Qt.AlignmentFlag.AlignRight)
|
||||
self.position_label.hide()
|
||||
|
||||
self.scene().addWidget(self.master_controls)
|
||||
root_layout.addWidget(self.sub_controls)
|
||||
self.scene().addWidget(self.controls)
|
||||
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
autoplay_action = QAction(Translations["media_player.autoplay"], self)
|
||||
@@ -263,7 +234,6 @@ class MediaPlayer(QGraphicsView):
|
||||
self.loop = loop_action
|
||||
self.toggle_loop()
|
||||
|
||||
# start the player muted
|
||||
self.player.audioOutput().setMuted(True)
|
||||
|
||||
def set_video_output(self, video: QGraphicsVideoItem):
|
||||
@@ -277,7 +247,6 @@ class MediaPlayer(QGraphicsView):
|
||||
def toggle_loop(self) -> None:
|
||||
self.driver.settings.loop = self.loop.isChecked()
|
||||
self.driver.settings.save()
|
||||
|
||||
self.player.setLoops(-1 if self.driver.settings.loop else 1)
|
||||
|
||||
def apply_rounded_corners(self) -> None:
|
||||
@@ -307,26 +276,31 @@ class MediaPlayer(QGraphicsView):
|
||||
Args:
|
||||
opacity(int): The opacity value, from 0-255.
|
||||
"""
|
||||
self.tint.setBrush(QBrush(QColor(0, 0, 0, opacity)))
|
||||
gradient = QLinearGradient(0, 0, 0, self.height())
|
||||
gradient.setColorAt(0.8, QColor(0, 0, 0, 0))
|
||||
gradient.setColorAt(1, QColor(0, 0, 0, opacity))
|
||||
self.tint.setBrush(QBrush(gradient))
|
||||
|
||||
@override
|
||||
def underMouse(self) -> bool: # noqa: N802
|
||||
self.animation.setStartValue(self.tint.brush().color().alpha())
|
||||
self.animation.setEndValue(100)
|
||||
self.animation.setDuration(250)
|
||||
self.animation.setStartValue(0)
|
||||
self.animation.setEndValue(160)
|
||||
self.animation.setDuration(125)
|
||||
self.animation.start()
|
||||
self.pslider.show()
|
||||
self.timeline_slider.show()
|
||||
self.play_pause.show()
|
||||
self.mute_unmute.show()
|
||||
self.position_label.show()
|
||||
|
||||
return super().underMouse()
|
||||
|
||||
@override
|
||||
def releaseMouse(self) -> None: # noqa: N802
|
||||
self.animation.setStartValue(self.tint.brush().color().alpha())
|
||||
self.animation.setStartValue(160)
|
||||
self.animation.setEndValue(0)
|
||||
self.animation.setDuration(500)
|
||||
self.animation.setDuration(125)
|
||||
self.animation.start()
|
||||
self.pslider.hide()
|
||||
self.timeline_slider.hide()
|
||||
self.play_pause.hide()
|
||||
self.mute_unmute.hide()
|
||||
self.volume_slider.hide()
|
||||
@@ -334,6 +308,14 @@ class MediaPlayer(QGraphicsView):
|
||||
|
||||
return super().releaseMouse()
|
||||
|
||||
@override
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||
# Pause media if background is clicked, with buffer around controls
|
||||
buffer: int = 6
|
||||
if event.y() < (self.height() - self.controls.height() - buffer):
|
||||
self.toggle_play()
|
||||
return super().mousePressEvent(event)
|
||||
|
||||
@override
|
||||
def eventFilter(self, arg__1: QObject, arg__2: QEvent) -> bool:
|
||||
"""Manage events for the media player."""
|
||||
@@ -345,8 +327,6 @@ class MediaPlayer(QGraphicsView):
|
||||
self.toggle_play()
|
||||
elif arg__1 == self.mute_unmute:
|
||||
self.toggle_mute()
|
||||
else:
|
||||
self.toggle_play()
|
||||
elif arg__2.type() is QEvent.Type.Enter:
|
||||
if arg__1 == self or arg__1 == self.video_preview:
|
||||
self.underMouse()
|
||||
@@ -369,9 +349,7 @@ class MediaPlayer(QGraphicsView):
|
||||
ms: Time in ms
|
||||
|
||||
Returns:
|
||||
A formatted time:
|
||||
|
||||
"1:43"
|
||||
A formatted time: "1:43"
|
||||
|
||||
The formatted time will only include the hour if
|
||||
the provided time is at least 60 minutes.
|
||||
@@ -416,9 +394,9 @@ class MediaPlayer(QGraphicsView):
|
||||
self.scene().removeItem(self.video_preview)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Clear the filepath and stop the player."""
|
||||
"""Clear the filepath, stop the player and release the source."""
|
||||
self.filepath = None
|
||||
self.player.stop()
|
||||
self.player.setSource(QUrl())
|
||||
|
||||
def play(self, filepath: Path) -> None:
|
||||
"""Set the source of the QMediaPlayer and play."""
|
||||
@@ -441,23 +419,27 @@ class MediaPlayer(QGraphicsView):
|
||||
self.mute_unmute.load(icon)
|
||||
|
||||
def slider_value_changed(self, value: int) -> None:
|
||||
if self.timeline_slider.isSliderDown():
|
||||
self.player.setPosition(value)
|
||||
self.player.setPlaybackRate(0.0001)
|
||||
|
||||
current = self.format_time(value)
|
||||
duration = self.format_time(self.player.duration())
|
||||
self.position_label.setText(f"{current} / {duration}")
|
||||
|
||||
def slider_released(self) -> None:
|
||||
was_playing = self.player.isPlaying()
|
||||
self.player.setPosition(self.pslider.value())
|
||||
self.player.setPosition(self.timeline_slider.value())
|
||||
self.player.setPlaybackRate(1) # Restore from slider_value_changed()
|
||||
|
||||
# Setting position causes the player to start playing again.
|
||||
# We should reset back to initial state.
|
||||
# Setting position causes the player to start playing again
|
||||
if not was_playing:
|
||||
self.player.pause()
|
||||
|
||||
def player_position_changed(self, position: int) -> None:
|
||||
if not self.pslider.isSliderDown():
|
||||
# User isn't using the slider, so update position in widgets.
|
||||
self.pslider.setValue(position)
|
||||
if not self.timeline_slider.isSliderDown():
|
||||
# User isn't using the slider, so update position in widgets
|
||||
self.timeline_slider.setValue(position)
|
||||
current = self.format_time(self.player.position())
|
||||
duration = self.format_time(self.player.duration())
|
||||
self.position_label.setText(f"{current} / {duration}")
|
||||
@@ -465,8 +447,8 @@ class MediaPlayer(QGraphicsView):
|
||||
def media_status_changed(self, status: QMediaPlayer.MediaStatus) -> None:
|
||||
# We can only set the slider duration once we know the size of the media
|
||||
if status == QMediaPlayer.MediaStatus.LoadedMedia and self.filepath is not None:
|
||||
self.pslider.setMinimum(0)
|
||||
self.pslider.setMaximum(self.player.duration())
|
||||
self.timeline_slider.setMinimum(0)
|
||||
self.timeline_slider.setMaximum(self.player.duration())
|
||||
|
||||
current = self.format_time(self.player.position())
|
||||
duration = self.format_time(self.player.duration())
|
||||
@@ -475,24 +457,15 @@ class MediaPlayer(QGraphicsView):
|
||||
def _update_controls(self, size: QSize) -> None:
|
||||
self.scene().setSceneRect(0, 0, size.width(), size.height())
|
||||
|
||||
# occupy entire scene width
|
||||
self.master_controls.setMinimumWidth(size.width())
|
||||
self.master_controls.setMaximumWidth(size.width())
|
||||
# Occupy entire scene width
|
||||
self.controls.setMinimumWidth(size.width())
|
||||
self.controls.setMaximumWidth(size.width())
|
||||
|
||||
self.master_controls.move(0, int(self.scene().height() - self.master_controls.height()))
|
||||
self.controls.move(0, int(self.scene().height() - self.controls.height()))
|
||||
|
||||
ps_w = self.master_controls.width() - 5
|
||||
self.pslider.setMinimumWidth(ps_w)
|
||||
self.pslider.setMaximumWidth(ps_w)
|
||||
|
||||
# Changing the orientation of the volume slider to
|
||||
# make it easier to use in smaller sizes.
|
||||
orientation = self.volume_slider.orientation()
|
||||
if size.width() <= 175 and orientation is Qt.Orientation.Horizontal:
|
||||
self.volume_slider.setOrientation(Qt.Orientation.Vertical)
|
||||
self.volume_slider.setMaximumHeight(30)
|
||||
elif size.width() > 175 and orientation is Qt.Orientation.Vertical:
|
||||
self.volume_slider.setOrientation(Qt.Orientation.Horizontal)
|
||||
ps_w = self.controls.width() - 5
|
||||
self.timeline_slider.setMinimumWidth(ps_w)
|
||||
self.timeline_slider.setMaximumWidth(ps_w)
|
||||
|
||||
if self.video_preview:
|
||||
self.video_preview.setSize(self.size())
|
||||
@@ -514,12 +487,3 @@ class VideoPreview(QGraphicsVideoItem):
|
||||
@override
|
||||
def boundingRect(self):
|
||||
return QRectF(0, 0, self.size().width(), self.size().height())
|
||||
|
||||
@override
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
# painter.brush().setColor(QColor(0, 0, 0, 255))
|
||||
# You can set any shape you want here.
|
||||
# RoundedRect is the standard rectangle with rounded corners.
|
||||
# With 2nd and 3rd parameter you can tweak the curve until you get what you expect
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
@@ -616,7 +616,7 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
logger.info(
|
||||
"[Field Comparison]",
|
||||
fields="\n".join([str(x) for x in zip(json_fields, sql_fields)]),
|
||||
fields="\n".join([str(x) for x in zip(json_fields, sql_fields, strict=False)]),
|
||||
)
|
||||
|
||||
self.field_parity = True
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import Callable, override
|
||||
from collections.abc import Callable
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
@@ -24,9 +25,9 @@ class PanelModal(QWidget):
|
||||
self,
|
||||
widget: "PanelWidget",
|
||||
title: str = "",
|
||||
window_title: str = "",
|
||||
done_callback: Callable | None = None,
|
||||
save_callback: Callable | None = None,
|
||||
window_title: str | None = None,
|
||||
done_callback: Callable[[], None] | None = None,
|
||||
save_callback: Callable[[str], None] | None = None,
|
||||
has_save: bool = False,
|
||||
):
|
||||
# [Done]
|
||||
@@ -34,7 +35,7 @@ class PanelModal(QWidget):
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.widget = widget
|
||||
self.setWindowTitle(window_title)
|
||||
self.setWindowTitle(title if window_title is None else window_title)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0, 6, 6)
|
||||
@@ -43,7 +44,7 @@ class PanelModal(QWidget):
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px")
|
||||
self.setTitle(title)
|
||||
self.title_widget.setText(title)
|
||||
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.button_container = QWidget()
|
||||
@@ -88,7 +89,8 @@ class PanelModal(QWidget):
|
||||
# trigger save button actions when pressing enter in the widget
|
||||
self.widget.add_callback(lambda: self.save_button.click())
|
||||
|
||||
widget.done.connect(lambda: save_callback(widget.get_content()))
|
||||
if save_callback is not None:
|
||||
widget.done.connect(lambda: save_callback(widget.get_content()))
|
||||
|
||||
self.root_layout.addWidget(self.title_widget)
|
||||
self.root_layout.addWidget(widget)
|
||||
@@ -97,22 +99,20 @@ class PanelModal(QWidget):
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
widget.parent_post_init()
|
||||
|
||||
def closeEvent(self, event): # noqa: N802
|
||||
@override
|
||||
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
||||
if self.cancel_button:
|
||||
self.cancel_button.click()
|
||||
elif self.done_button:
|
||||
self.done_button.click()
|
||||
event.accept()
|
||||
|
||||
def setTitle(self, title: str): # noqa: N802
|
||||
self.title_widget.setText(title)
|
||||
|
||||
|
||||
class PanelWidget(QWidget):
|
||||
"""Used for widgets that go in a modal panel, ex. for editing or searching."""
|
||||
|
||||
done = Signal()
|
||||
parent_modal: PanelModal = None
|
||||
parent_modal: PanelModal | None = None
|
||||
panel_save_button: QPushButton | None = None
|
||||
panel_cancel_button: QPushButton | None = None
|
||||
panel_done_button: QPushButton | None = None
|
||||
@@ -121,19 +121,19 @@ class PanelWidget(QWidget):
|
||||
super().__init__()
|
||||
|
||||
def get_content(self) -> str:
|
||||
return ""
|
||||
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
def parent_post_init(self) -> None:
|
||||
pass
|
||||
|
||||
def parent_post_init(self):
|
||||
pass
|
||||
|
||||
def add_callback(self, callback: Callable, event: str = "returnPressed"):
|
||||
def add_callback(self, callback: Callable[[], None], event: str = "returnPressed"):
|
||||
logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}")
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if self.panel_cancel_button:
|
||||
self.panel_cancel_button.click()
|
||||
@@ -145,5 +145,4 @@ class PanelWidget(QWidget):
|
||||
elif self.panel_done_button:
|
||||
self.panel_done_button.click()
|
||||
else: # Other key presses
|
||||
pass
|
||||
return super().keyPressEvent(event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
@@ -32,10 +32,11 @@ from tagstudio.core.library.alchemy.fields import (
|
||||
)
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.qt.controller.components.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.widgets.fields import FieldContainer
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.tag_box import TagBoxWidget
|
||||
from tagstudio.qt.widgets.text import TextWidget
|
||||
from tagstudio.qt.widgets.text_box_edit import EditTextBox
|
||||
from tagstudio.qt.widgets.text_line_edit import EditTextLine
|
||||
@@ -109,8 +110,9 @@ class FieldContainers(QWidget):
|
||||
"""Update tags and fields from a single Entry source."""
|
||||
logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id)
|
||||
|
||||
self.cached_entries = [self.lib.get_entry_full(entry_id)]
|
||||
entry = self.cached_entries[0]
|
||||
entry = self.lib.get_entry_full(entry_id)
|
||||
assert entry is not None
|
||||
self.cached_entries = [entry]
|
||||
self.update_granular(entry.tags, entry.fields, update_badges)
|
||||
|
||||
def update_granular(
|
||||
@@ -177,6 +179,7 @@ class FieldContainers(QWidget):
|
||||
"""
|
||||
tag_obj = self.lib.get_tag(tag_id) # Get full object
|
||||
if p_ids is None:
|
||||
assert tag_obj is not None
|
||||
p_ids = tag_obj.parent_ids
|
||||
|
||||
for p_id in p_ids:
|
||||
@@ -186,6 +189,7 @@ class FieldContainers(QWidget):
|
||||
if tag_id not in cluster_map[p_id]:
|
||||
cluster_map[p_id].add(tag_id)
|
||||
p_tag = self.lib.get_tag(p_id) # Get full object
|
||||
assert p_tag is not None
|
||||
if p_tag.parent_ids:
|
||||
add_to_cluster(
|
||||
tag_id,
|
||||
@@ -201,7 +205,7 @@ class FieldContainers(QWidget):
|
||||
logger.info("[FieldContainers] Cluster Map", cluster_map=cluster_map)
|
||||
|
||||
# Initialize all categories from parents.
|
||||
tags_ = {self.lib.get_tag(x) for x in exhausted}
|
||||
tags_ = {t for tid in exhausted if (t := self.lib.get_tag(tid)) is not None}
|
||||
for tag in tags_:
|
||||
if tag.is_category:
|
||||
cats[tag] = set()
|
||||
@@ -218,20 +222,28 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
|
||||
if final_tags := cluster_map.get(key.id, set()).union([key.id]):
|
||||
cats[key] = {self.lib.get_tag(x) for x in final_tags if x in base_tag_ids}
|
||||
added_ids = added_ids.union({x for x in final_tags if x in base_tag_ids})
|
||||
cats[key] = {
|
||||
t
|
||||
for tid in final_tags
|
||||
if tid in base_tag_ids and (t := self.lib.get_tag(tid)) is not None
|
||||
}
|
||||
added_ids = added_ids.union({tid for tid in final_tags if tid in base_tag_ids})
|
||||
|
||||
# Add remaining tags to None key (general case).
|
||||
cats[None] = {self.lib.get_tag(x) for x in base_tag_ids if x not in added_ids}
|
||||
cats[None] = {
|
||||
t
|
||||
for tid in base_tag_ids
|
||||
if tid not in added_ids and (t := self.lib.get_tag(tid)) is not None
|
||||
}
|
||||
logger.info(
|
||||
f"[FieldContainers] [{key}] Key cluster: None, general case!",
|
||||
general_tags=cats[key],
|
||||
"[FieldContainers] Key cluster: None, general case!",
|
||||
general_tags=cats[None],
|
||||
added=added_ids,
|
||||
base_tag_ids=base_tag_ids,
|
||||
)
|
||||
|
||||
# Remove unused categories
|
||||
empty: list[Tag] = []
|
||||
empty: list[Tag | None] = []
|
||||
for k, v in list(cats.items()):
|
||||
if not v:
|
||||
empty.append(k)
|
||||
@@ -304,7 +316,7 @@ class FieldContainers(QWidget):
|
||||
|
||||
# Normalize line endings in any text content.
|
||||
if not is_mixed:
|
||||
assert isinstance(field.value, (str, type(None)))
|
||||
assert isinstance(field.value, str | type(None))
|
||||
text = field.value or ""
|
||||
else:
|
||||
text = "<i>Mixed Data</i>"
|
||||
@@ -326,7 +338,7 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
if "pytest" in sys.modules:
|
||||
# for better testability
|
||||
container.modal = modal
|
||||
container.modal = modal # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
container.set_edit_callback(modal.show)
|
||||
container.set_remove_callback(
|
||||
@@ -344,7 +356,7 @@ class FieldContainers(QWidget):
|
||||
container.set_inline(False)
|
||||
# Normalize line endings in any text content.
|
||||
if not is_mixed:
|
||||
assert isinstance(field.value, (str, type(None)))
|
||||
assert isinstance(field.value, str | type(None))
|
||||
text = (field.value or "").replace("\r", "\n")
|
||||
else:
|
||||
text = "<i>Mixed Data</i>"
|
||||
@@ -375,23 +387,36 @@ class FieldContainers(QWidget):
|
||||
)
|
||||
|
||||
elif field.type.type == FieldTypeEnum.DATETIME:
|
||||
logger.info("[FieldContainers][write_container] Datetime Field", field=field)
|
||||
if not is_mixed:
|
||||
try:
|
||||
container.set_title(field.type.name)
|
||||
container.set_inline(False)
|
||||
# TODO: Localize this and/or add preferences.
|
||||
date = dt.strptime(field.value, "%Y-%m-%d %H:%M:%S")
|
||||
title = f"{field.type.name} (Date)"
|
||||
inner_widget = TextWidget(title, date.strftime("%D - %r"))
|
||||
container.set_inner_widget(inner_widget)
|
||||
except Exception:
|
||||
container.set_title(field.type.name)
|
||||
container.set_inline(False)
|
||||
title = f"{field.type.name} (Date) (Unknown Format)"
|
||||
inner_widget = TextWidget(title, str(field.value))
|
||||
container.set_inner_widget(inner_widget)
|
||||
container.set_title(field.type.name)
|
||||
container.set_inline(False)
|
||||
|
||||
container.set_edit_callback()
|
||||
title = f"{field.type.name} (Date)"
|
||||
try:
|
||||
assert field.value is not None
|
||||
text = self.driver.settings.format_datetime(
|
||||
DatetimePicker.string2dt(field.value)
|
||||
)
|
||||
except (ValueError, AssertionError):
|
||||
title += " (Unknown Format)"
|
||||
text = str(field.value)
|
||||
|
||||
inner_widget = TextWidget(title, text)
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
modal = PanelModal(
|
||||
DatetimePicker(self.driver, field.value or dt.now()),
|
||||
title=f"Edit {field.type.name}",
|
||||
save_callback=(
|
||||
lambda content: (
|
||||
self.update_field(field, content), # type: ignore
|
||||
self.update_from_entry(self.cached_entries[0].id),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
container.set_edit_callback(modal.show)
|
||||
container.set_remove_callback(
|
||||
lambda: self.remove_message_box(
|
||||
prompt=self.remove_field_prompt(field.type.name),
|
||||
@@ -453,19 +478,19 @@ class FieldContainers(QWidget):
|
||||
inner_widget = container.get_inner_widget()
|
||||
|
||||
if isinstance(inner_widget, TagBoxWidget):
|
||||
inner_widget.set_tags(tags)
|
||||
with catch_warnings(record=True):
|
||||
inner_widget.updated.disconnect()
|
||||
inner_widget.on_update.disconnect()
|
||||
|
||||
else:
|
||||
inner_widget = TagBoxWidget(
|
||||
tags,
|
||||
"Tags",
|
||||
self.driver,
|
||||
)
|
||||
container.set_inner_widget(inner_widget)
|
||||
inner_widget.set_entries([e.id for e in self.cached_entries])
|
||||
inner_widget.set_tags(tags)
|
||||
|
||||
inner_widget.updated.connect(
|
||||
inner_widget.on_update.connect(
|
||||
lambda: (self.update_from_entry(self.cached_entries[0].id, update_badges=True))
|
||||
)
|
||||
else:
|
||||
@@ -491,7 +516,7 @@ class FieldContainers(QWidget):
|
||||
"""Update a field in all selected Entries, given a field object."""
|
||||
assert isinstance(
|
||||
field,
|
||||
(TextField, DatetimeField),
|
||||
TextField | DatetimeField,
|
||||
), f"instance: {type(field)}"
|
||||
|
||||
entry_ids = [e.id for e in self.cached_entries]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import os
|
||||
import platform
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime as dt
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
@@ -29,6 +30,13 @@ if typing.TYPE_CHECKING:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileAttributeData:
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
duration: int | None = None
|
||||
|
||||
|
||||
class FileAttributes(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
@@ -102,18 +110,19 @@ class FileAttributes(QWidget):
|
||||
def update_date_label(self, filepath: Path | None = None) -> None:
|
||||
"""Update the "Date Created" and "Date Modified" file property labels."""
|
||||
if filepath and filepath.is_file():
|
||||
created: dt = None
|
||||
created: dt
|
||||
if platform.system() == "Windows" or platform.system() == "Darwin":
|
||||
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
|
||||
else:
|
||||
created = dt.fromtimestamp(filepath.stat().st_ctime)
|
||||
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
|
||||
self.date_created_label.setText(
|
||||
f"<b>{Translations['file.date_created']}:</b> {dt.strftime(created, '%a, %x, %X')}"
|
||||
f"<b>{Translations['file.date_created']}:</b>"
|
||||
+ f" {self.driver.settings.format_datetime(created)}"
|
||||
)
|
||||
self.date_modified_label.setText(
|
||||
f"<b>{Translations['file.date_modified']}:</b> "
|
||||
f"{dt.strftime(modified, '%a, %x, %X')}"
|
||||
f"{self.driver.settings.format_datetime(modified)}"
|
||||
)
|
||||
self.date_created_label.setHidden(False)
|
||||
self.date_modified_label.setHidden(False)
|
||||
@@ -130,10 +139,10 @@ class FileAttributes(QWidget):
|
||||
self.date_created_label.setHidden(True)
|
||||
self.date_modified_label.setHidden(True)
|
||||
|
||||
def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict = None):
|
||||
def update_stats(self, filepath: Path | None = None, stats: FileAttributeData | None = None):
|
||||
"""Render the panel widgets with the newest data from the Library."""
|
||||
if not stats:
|
||||
stats = {}
|
||||
stats = FileAttributeData()
|
||||
|
||||
if not filepath:
|
||||
self.layout().setSpacing(0)
|
||||
@@ -144,11 +153,13 @@ class FileAttributes(QWidget):
|
||||
self.dimensions_label.setText("")
|
||||
self.dimensions_label.setHidden(True)
|
||||
else:
|
||||
ext = filepath.suffix.lower()
|
||||
self.library_path = self.library.library_dir
|
||||
display_path = filepath
|
||||
if self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FULL_PATHS:
|
||||
display_path = filepath
|
||||
elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_RELATIVE_PATHS:
|
||||
assert self.library_path is not None
|
||||
display_path = Path(filepath).relative_to(self.library_path)
|
||||
elif self.driver.settings.show_filepath == ShowFilepathOption.SHOW_FILENAMES_ONLY:
|
||||
display_path = Path(filepath.name)
|
||||
@@ -163,11 +174,11 @@ class FileAttributes(QWidget):
|
||||
for i, part in enumerate(display_path.parts):
|
||||
part_ = part.strip(os.path.sep)
|
||||
if i != len(display_path.parts) - 1:
|
||||
file_str += f"{"\u200b".join(part_)}{separator}</b>"
|
||||
file_str += f"{'\u200b'.join(part_)}{separator}</b>"
|
||||
else:
|
||||
if file_str != "":
|
||||
file_str += "<br>"
|
||||
file_str += f"<b>{"\u200b".join(part_)}</b>"
|
||||
file_str += f"<b>{'\u200b'.join(part_)}</b>"
|
||||
self.file_label.setText(file_str)
|
||||
self.file_label.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.opener = FileOpenerHelper(filepath)
|
||||
@@ -176,18 +187,10 @@ class FileAttributes(QWidget):
|
||||
stats_label_text = ""
|
||||
ext_display: str = ""
|
||||
file_size: str = ""
|
||||
width_px_text: str = ""
|
||||
height_px_text: str = ""
|
||||
duration_text: str = ""
|
||||
font_family: str = ""
|
||||
|
||||
# Attempt to populate the stat variables
|
||||
width_px_text = stats.get("width", "")
|
||||
height_px_text = stats.get("height", "")
|
||||
duration_text = stats.get("duration", "")
|
||||
font_family = stats.get("font_family", "")
|
||||
if ext:
|
||||
ext_display = ext.upper()[1:]
|
||||
ext_display = ext.upper()[1:] or filepath.stem.upper()
|
||||
if filepath:
|
||||
try:
|
||||
file_size = format_size(filepath.stat().st_size)
|
||||
@@ -215,14 +218,14 @@ class FileAttributes(QWidget):
|
||||
elif file_size:
|
||||
stats_label_text += file_size
|
||||
|
||||
if width_px_text and height_px_text:
|
||||
if stats.width is not None and stats.height is not None:
|
||||
stats_label_text = add_newline(stats_label_text)
|
||||
stats_label_text += f"{width_px_text} x {height_px_text} px"
|
||||
stats_label_text += f"{stats.width} x {stats.height} px"
|
||||
|
||||
if duration_text:
|
||||
if stats.duration is not None:
|
||||
stats_label_text = add_newline(stats_label_text)
|
||||
try:
|
||||
dur_str = str(timedelta(seconds=float(duration_text)))[:-7]
|
||||
dur_str = str(timedelta(seconds=float(stats.duration)))[:-7]
|
||||
if dur_str.startswith("0:"):
|
||||
dur_str = dur_str[2:]
|
||||
if dur_str.startswith("0"):
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import io
|
||||
import time
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from warnings import catch_warnings
|
||||
|
||||
import cv2
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt
|
||||
from PySide6.QtGui import QAction, QMovie, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QStackedLayout, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.media_types import MediaCategories, MediaType
|
||||
from tagstudio.qt.helpers.file_opener import FileOpenerHelper, open_file
|
||||
from tagstudio.qt.helpers.file_tester import is_readable_video
|
||||
from tagstudio.qt.helpers.qbutton_wrapper import QPushButtonWrapper
|
||||
from tagstudio.qt.helpers.rounded_pixmap_style import RoundedPixmapStyle
|
||||
from tagstudio.qt.platform_strings import open_file_str, trash_term
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.media_player import MediaPlayer
|
||||
from tagstudio.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
class PreviewThumb(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
|
||||
self.is_connected = False
|
||||
self.lib = library
|
||||
self.driver: QtDriver = driver
|
||||
|
||||
self.img_button_size: tuple[int, int] = (266, 266)
|
||||
self.image_ratio: float = 1.0
|
||||
|
||||
self.image_layout = QStackedLayout(self)
|
||||
self.image_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_layout.setStackingMode(QStackedLayout.StackingMode.StackAll)
|
||||
self.image_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.open_file_action = QAction(Translations["file.open_file"], self)
|
||||
self.open_explorer_action = QAction(open_file_str(), self)
|
||||
self.delete_action = QAction(
|
||||
Translations.format("trash.context.ambiguous", trash_term=trash_term()),
|
||||
self,
|
||||
)
|
||||
|
||||
self.preview_img = QPushButtonWrapper()
|
||||
self.preview_img.setMinimumSize(*self.img_button_size)
|
||||
self.preview_img.setFlat(True)
|
||||
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.preview_img.addAction(self.open_file_action)
|
||||
self.preview_img.addAction(self.open_explorer_action)
|
||||
self.preview_img.addAction(self.delete_action)
|
||||
|
||||
# In testing, it didn't seem possible to center the widgets directly
|
||||
# on the QStackedLayout. Adding sublayouts allows us to center the widgets.
|
||||
self.preview_img_page = QWidget()
|
||||
self._stacked_page_setup(self.preview_img_page, self.preview_img)
|
||||
|
||||
self.preview_gif = QLabel()
|
||||
self.preview_gif.setMinimumSize(*self.img_button_size)
|
||||
self.preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.preview_gif.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self.preview_gif.addAction(self.open_file_action)
|
||||
self.preview_gif.addAction(self.open_explorer_action)
|
||||
self.preview_gif.addAction(self.delete_action)
|
||||
self.gif_buffer: QBuffer = QBuffer()
|
||||
|
||||
self.preview_gif_page = QWidget()
|
||||
self._stacked_page_setup(self.preview_gif_page, self.preview_gif)
|
||||
|
||||
self.media_player = MediaPlayer(driver)
|
||||
self.media_player.addAction(self.open_file_action)
|
||||
self.media_player.addAction(self.open_explorer_action)
|
||||
self.media_player.addAction(self.delete_action)
|
||||
|
||||
# Need to watch for this to resize the player appropriately.
|
||||
self.media_player.player.hasVideoChanged.connect(self._has_video_changed)
|
||||
|
||||
self.mp_max_size = QSize(*self.img_button_size)
|
||||
|
||||
self.media_player_page = QWidget()
|
||||
self._stacked_page_setup(self.media_player_page, self.media_player)
|
||||
|
||||
self.thumb_renderer = ThumbRenderer(self.lib)
|
||||
self.thumb_renderer.updated.connect(
|
||||
lambda ts, i, s: (
|
||||
self.preview_img.setIcon(i),
|
||||
self._set_mp_max_size(i.size()),
|
||||
)
|
||||
)
|
||||
self.thumb_renderer.updated_ratio.connect(
|
||||
lambda ratio: (
|
||||
self.set_image_ratio(ratio),
|
||||
self.update_image_size(
|
||||
(
|
||||
self.size().width(),
|
||||
self.size().height(),
|
||||
),
|
||||
ratio,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.image_layout.addWidget(self.preview_img_page)
|
||||
self.image_layout.addWidget(self.preview_gif_page)
|
||||
self.image_layout.addWidget(self.media_player_page)
|
||||
|
||||
self.setMinimumSize(*self.img_button_size)
|
||||
|
||||
self.hide_preview()
|
||||
|
||||
def _set_mp_max_size(self, size: QSize) -> None:
|
||||
self.mp_max_size = size
|
||||
|
||||
def _has_video_changed(self, video: bool) -> None:
|
||||
self.update_image_size((self.size().width(), self.size().height()))
|
||||
|
||||
def _stacked_page_setup(self, page: QWidget, widget: QWidget):
|
||||
layout = QHBoxLayout(page)
|
||||
layout.addWidget(widget)
|
||||
layout.setAlignment(widget, Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
page.setLayout(layout)
|
||||
|
||||
def set_image_ratio(self, ratio: float):
|
||||
self.image_ratio = ratio
|
||||
|
||||
def update_image_size(self, size: tuple[int, int], ratio: float | None = None):
|
||||
if ratio:
|
||||
self.set_image_ratio(ratio)
|
||||
|
||||
adj_width: float = size[0]
|
||||
adj_height: float = size[1]
|
||||
# Landscape
|
||||
if self.image_ratio > 1:
|
||||
adj_height = size[0] * (1 / self.image_ratio)
|
||||
# Portrait
|
||||
elif self.image_ratio <= 1:
|
||||
adj_width = size[1] * self.image_ratio
|
||||
|
||||
if adj_width > size[0]:
|
||||
adj_height = adj_height * (size[0] / adj_width)
|
||||
adj_width = size[0]
|
||||
elif adj_height > size[1]:
|
||||
adj_width = adj_width * (size[1] / adj_height)
|
||||
adj_height = size[1]
|
||||
|
||||
adj_size = QSize(int(adj_width), int(adj_height))
|
||||
|
||||
self.img_button_size = (int(adj_width), int(adj_height))
|
||||
self.preview_img.setMaximumSize(adj_size)
|
||||
self.preview_img.setIconSize(adj_size)
|
||||
self.preview_gif.setMaximumSize(adj_size)
|
||||
self.preview_gif.setMinimumSize(adj_size)
|
||||
|
||||
if not self.media_player.player.hasVideo():
|
||||
# ensure we do not exceed the thumbnail size
|
||||
mp_width = (
|
||||
adj_size.width()
|
||||
if adj_size.width() < self.mp_max_size.width()
|
||||
else self.mp_max_size.width()
|
||||
)
|
||||
mp_height = (
|
||||
adj_size.height()
|
||||
if adj_size.height() < self.mp_max_size.height()
|
||||
else self.mp_max_size.height()
|
||||
)
|
||||
mp_size = QSize(mp_width, mp_height)
|
||||
self.media_player.setMinimumSize(mp_size)
|
||||
self.media_player.setMaximumSize(mp_size)
|
||||
else:
|
||||
# have video, so just resize as normal
|
||||
self.media_player.setMaximumSize(adj_size)
|
||||
self.media_player.setMinimumSize(adj_size)
|
||||
|
||||
proxy_style = RoundedPixmapStyle(radius=8)
|
||||
self.preview_gif.setStyle(proxy_style)
|
||||
self.media_player.setStyle(proxy_style)
|
||||
m = self.preview_gif.movie()
|
||||
if m:
|
||||
m.setScaledSize(adj_size)
|
||||
|
||||
def get_preview_size(self) -> tuple[int, int]:
|
||||
return (
|
||||
self.size().width(),
|
||||
self.size().height(),
|
||||
)
|
||||
|
||||
def switch_preview(self, preview: str):
|
||||
if preview in ["audio", "video"]:
|
||||
self.media_player.show()
|
||||
self.image_layout.setCurrentWidget(self.media_player_page)
|
||||
else:
|
||||
self.media_player.stop()
|
||||
self.media_player.hide()
|
||||
|
||||
if preview in ["image", "audio"]:
|
||||
self.preview_img.show()
|
||||
self.image_layout.setCurrentWidget(
|
||||
self.preview_img_page if preview == "image" else self.media_player_page
|
||||
)
|
||||
else:
|
||||
self.preview_img.hide()
|
||||
|
||||
if preview == "animated":
|
||||
self.preview_gif.show()
|
||||
self.image_layout.setCurrentWidget(self.preview_gif_page)
|
||||
else:
|
||||
if self.preview_gif.movie():
|
||||
self.preview_gif.movie().stop()
|
||||
self.gif_buffer.close()
|
||||
self.preview_gif.hide()
|
||||
|
||||
def _display_fallback_image(self, filepath: Path, ext: str) -> dict:
|
||||
"""Renders the given file as an image, no matter its media type.
|
||||
|
||||
Useful for fallback scenarios.
|
||||
"""
|
||||
self.switch_preview("image")
|
||||
self.thumb_renderer.render(
|
||||
time.time(),
|
||||
filepath,
|
||||
(512, 512),
|
||||
self.devicePixelRatio(),
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
return self._update_image(filepath, ext)
|
||||
|
||||
def _update_image(self, filepath: Path, ext: str) -> dict:
|
||||
"""Update the static image preview from a filepath."""
|
||||
stats: dict = {}
|
||||
self.switch_preview("image")
|
||||
|
||||
image: Image.Image | None = None
|
||||
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
|
||||
):
|
||||
try:
|
||||
with rawpy.imread(str(filepath)) as raw:
|
||||
rgb = raw.postprocess()
|
||||
image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
|
||||
stats["width"] = image.width
|
||||
stats["height"] = image.height
|
||||
except (
|
||||
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
FileNotFoundError,
|
||||
):
|
||||
pass
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_RASTER_TYPES, mime_fallback=True
|
||||
):
|
||||
try:
|
||||
image = Image.open(str(filepath))
|
||||
stats["width"] = image.width
|
||||
stats["height"] = image.height
|
||||
except (
|
||||
DecompressionBombError,
|
||||
FileNotFoundError,
|
||||
NotImplementedError,
|
||||
UnidentifiedImageError,
|
||||
) as e:
|
||||
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
|
||||
):
|
||||
pass
|
||||
|
||||
return stats
|
||||
|
||||
def _update_animation(self, filepath: Path, ext: str) -> dict:
|
||||
"""Update the animated image preview from a filepath."""
|
||||
stats: dict = {}
|
||||
|
||||
# Ensure that any movie and buffer from previous animations are cleared.
|
||||
if self.preview_gif.movie():
|
||||
self.preview_gif.movie().stop()
|
||||
self.gif_buffer.close()
|
||||
|
||||
try:
|
||||
image: Image.Image = Image.open(filepath)
|
||||
stats["width"] = image.width
|
||||
stats["height"] = image.height
|
||||
|
||||
self.update_image_size((image.width, image.height), image.width / image.height)
|
||||
if ext == ".apng":
|
||||
image_bytes_io = io.BytesIO()
|
||||
image.save(
|
||||
image_bytes_io,
|
||||
"GIF",
|
||||
lossless=True,
|
||||
save_all=True,
|
||||
loop=0,
|
||||
disposal=2,
|
||||
)
|
||||
image.close()
|
||||
image_bytes_io.seek(0)
|
||||
self.gif_buffer.setData(image_bytes_io.read())
|
||||
else:
|
||||
image.close()
|
||||
with open(filepath, "rb") as f:
|
||||
self.gif_buffer.setData(f.read())
|
||||
movie = QMovie(self.gif_buffer, QByteArray())
|
||||
self.preview_gif.setMovie(movie)
|
||||
|
||||
# If the animation only has 1 frame, display it like a normal image.
|
||||
if movie.frameCount() <= 1:
|
||||
self._display_fallback_image(filepath, ext)
|
||||
return stats
|
||||
|
||||
# The animation has more than 1 frame, continue displaying it as an animation
|
||||
self.switch_preview("animated")
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(stats["width"], stats["height"]),
|
||||
QSize(stats["width"], stats["height"]),
|
||||
)
|
||||
)
|
||||
movie.start()
|
||||
|
||||
stats["duration"] = movie.frameCount() // 60
|
||||
except (UnidentifiedImageError, FileNotFoundError) as e:
|
||||
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
|
||||
return self._display_fallback_image(filepath, ext)
|
||||
|
||||
return stats
|
||||
|
||||
def _get_video_res(self, filepath: str) -> tuple[bool, QSize]:
|
||||
video = cv2.VideoCapture(filepath, cv2.CAP_FFMPEG)
|
||||
success, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
image = Image.fromarray(frame)
|
||||
return (success, QSize(image.width, image.height))
|
||||
|
||||
def _update_media(self, filepath: Path, type: MediaType) -> dict:
|
||||
stats: dict = {}
|
||||
|
||||
self.media_player.play(filepath)
|
||||
|
||||
if type == MediaType.VIDEO:
|
||||
try:
|
||||
success, size = self._get_video_res(str(filepath))
|
||||
if success:
|
||||
self.update_image_size(
|
||||
(size.width(), size.height()), size.width() / size.height()
|
||||
)
|
||||
self.resizeEvent(
|
||||
QResizeEvent(
|
||||
QSize(size.width(), size.height()),
|
||||
QSize(size.width(), size.height()),
|
||||
)
|
||||
)
|
||||
|
||||
stats["width"] = size.width()
|
||||
stats["height"] = size.height()
|
||||
|
||||
except cv2.error as e:
|
||||
logger.error("[PreviewThumb] Could not play video", filepath=filepath, error=e)
|
||||
|
||||
self.switch_preview("video" if type == MediaType.VIDEO else "audio")
|
||||
stats["duration"] = self.media_player.player.duration() * 1000
|
||||
return stats
|
||||
|
||||
def update_preview(self, filepath: Path, ext: str) -> dict:
|
||||
"""Render a single file preview."""
|
||||
stats: dict = {}
|
||||
|
||||
# Video
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
|
||||
) and is_readable_video(filepath):
|
||||
stats = self._update_media(filepath, MediaType.VIDEO)
|
||||
|
||||
# Audio
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.AUDIO_TYPES, mime_fallback=True
|
||||
):
|
||||
self._update_image(filepath, ext)
|
||||
stats = self._update_media(filepath, MediaType.AUDIO)
|
||||
self.thumb_renderer.render(
|
||||
time.time(),
|
||||
filepath,
|
||||
(512, 512),
|
||||
self.devicePixelRatio(),
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
|
||||
# Animated Images
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
|
||||
):
|
||||
stats = self._update_animation(filepath, ext)
|
||||
|
||||
# Other Types (Including Images)
|
||||
else:
|
||||
# TODO: Get thumb renderer to return this stuff to pass on
|
||||
stats = self._update_image(filepath, ext)
|
||||
|
||||
self.thumb_renderer.render(
|
||||
time.time(),
|
||||
filepath,
|
||||
(512, 512),
|
||||
self.devicePixelRatio(),
|
||||
update_on_ratio_change=True,
|
||||
)
|
||||
|
||||
with catch_warnings(record=True):
|
||||
self.preview_img.clicked.disconnect()
|
||||
self.preview_img.clicked.connect(lambda checked=False, path=filepath: open_file(path))
|
||||
self.preview_img.is_connected = True
|
||||
|
||||
self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
self.opener = FileOpenerHelper(filepath)
|
||||
self.open_file_action.triggered.connect(self.opener.open_file)
|
||||
self.open_explorer_action.triggered.connect(self.opener.open_explorer)
|
||||
|
||||
with catch_warnings(record=True):
|
||||
self.delete_action.triggered.disconnect()
|
||||
|
||||
self.delete_action.setText(
|
||||
Translations.format("trash.context.singular", trash_term=trash_term())
|
||||
)
|
||||
self.delete_action.triggered.connect(
|
||||
lambda checked=False, f=filepath: self.driver.delete_files_callback(f)
|
||||
)
|
||||
self.delete_action.setEnabled(bool(filepath))
|
||||
|
||||
return stats
|
||||
|
||||
def hide_preview(self):
|
||||
"""Completely hide the file preview."""
|
||||
self.switch_preview("")
|
||||
|
||||
def stop_file_use(self):
|
||||
"""Stops the use of the currently previewed file. Used to release file permissions."""
|
||||
logger.info("[PreviewThumb] Stopping file use in video playback...")
|
||||
# This swaps the video out for a placeholder so the previous video's file
|
||||
# is no longer in use by this object.
|
||||
self.media_player.play(ResourceManager.get_path("placeholder_mp4"))
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
|
||||
self.update_image_size((self.size().width(), self.size().height()))
|
||||
return super().resizeEvent(event)
|
||||
@@ -1,205 +0,0 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import traceback
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QSplitter, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.modals.add_field import AddFieldModal
|
||||
from tagstudio.qt.modals.tag_search import TagSearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.preview.field_containers import FieldContainers
|
||||
from tagstudio.qt.widgets.preview.file_attributes import FileAttributes
|
||||
from tagstudio.qt.widgets.preview.preview_thumb import PreviewThumb
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class PreviewPanel(QWidget):
|
||||
"""The Preview Panel Widget."""
|
||||
|
||||
# TODO: There should be a global button theme somewhere.
|
||||
button_style = (
|
||||
f"QPushButton{{"
|
||||
f"background-color:{Theme.COLOR_BG.value};"
|
||||
"border-radius:6px;"
|
||||
"font-weight: 500;"
|
||||
"text-align: center;"
|
||||
f"}}"
|
||||
f"QPushButton::hover{{"
|
||||
f"background-color:{Theme.COLOR_HOVER.value};"
|
||||
f"border-color:{get_ui_color(ColorType.BORDER, UiColor.THEME_DARK)};"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"}}"
|
||||
f"QPushButton::pressed{{"
|
||||
f"background-color:{Theme.COLOR_PRESSED.value};"
|
||||
f"border-color:{get_ui_color(ColorType.LIGHT_ACCENT, UiColor.THEME_DARK)};"
|
||||
f"border-style:solid;"
|
||||
f"border-width: 2px;"
|
||||
f"}}"
|
||||
f"QPushButton::disabled{{"
|
||||
f"background-color:{Theme.COLOR_DISABLED_BG.value};"
|
||||
f"}}"
|
||||
)
|
||||
|
||||
def __init__(self, library: Library, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.driver: QtDriver = driver
|
||||
self.initialized = False
|
||||
self.is_open: bool = True
|
||||
|
||||
self.thumb = PreviewThumb(library, driver)
|
||||
self.file_attrs = FileAttributes(library, driver)
|
||||
self.fields = FieldContainers(library, driver)
|
||||
|
||||
self.tag_search_panel = TagSearchPanel(self.driver.lib, is_tag_chooser=True)
|
||||
self.add_tag_modal = PanelModal(self.tag_search_panel, Translations["tag.add.plural"])
|
||||
self.add_tag_modal.setWindowTitle(Translations["tag.add.plural"])
|
||||
|
||||
self.add_field_modal = AddFieldModal(self.lib)
|
||||
|
||||
preview_section = QWidget()
|
||||
preview_layout = QVBoxLayout(preview_section)
|
||||
preview_layout.setContentsMargins(0, 0, 0, 0)
|
||||
preview_layout.setSpacing(6)
|
||||
|
||||
info_section = QWidget()
|
||||
info_layout = QVBoxLayout(info_section)
|
||||
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||
info_layout.setSpacing(6)
|
||||
|
||||
splitter = QSplitter()
|
||||
splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
splitter.setHandleWidth(12)
|
||||
|
||||
add_buttons_container = QWidget()
|
||||
add_buttons_layout = QHBoxLayout(add_buttons_container)
|
||||
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
add_buttons_layout.setSpacing(6)
|
||||
|
||||
self.add_tag_button = QPushButton(Translations["tag.add"])
|
||||
self.add_tag_button.setEnabled(False)
|
||||
self.add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.add_tag_button.setMinimumHeight(28)
|
||||
self.add_tag_button.setStyleSheet(PreviewPanel.button_style)
|
||||
|
||||
self.add_field_button = QPushButton(Translations["library.field.add"])
|
||||
self.add_field_button.setEnabled(False)
|
||||
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.add_field_button.setMinimumHeight(28)
|
||||
self.add_field_button.setStyleSheet(PreviewPanel.button_style)
|
||||
|
||||
add_buttons_layout.addWidget(self.add_tag_button)
|
||||
add_buttons_layout.addWidget(self.add_field_button)
|
||||
|
||||
preview_layout.addWidget(self.thumb)
|
||||
info_layout.addWidget(self.file_attrs)
|
||||
info_layout.addWidget(self.fields)
|
||||
|
||||
splitter.addWidget(preview_section)
|
||||
splitter.addWidget(info_section)
|
||||
splitter.setStretchFactor(1, 2)
|
||||
|
||||
root_layout = QVBoxLayout(self)
|
||||
root_layout.setContentsMargins(0, 0, 0, 0)
|
||||
root_layout.addWidget(splitter)
|
||||
root_layout.addWidget(add_buttons_container)
|
||||
|
||||
def update_widgets(self, update_preview: bool = True) -> bool:
|
||||
"""Render the panel widgets with the newest data from the Library.
|
||||
|
||||
Args:
|
||||
update_preview(bool): Should the file preview be updated?
|
||||
(Only works with one or more items selected)
|
||||
"""
|
||||
# No Items Selected
|
||||
try:
|
||||
if len(self.driver.selected) == 0:
|
||||
self.thumb.hide_preview()
|
||||
self.file_attrs.update_stats()
|
||||
self.file_attrs.update_date_label()
|
||||
self.fields.hide_containers()
|
||||
|
||||
self.add_tag_button.setEnabled(False)
|
||||
self.add_field_button.setEnabled(False)
|
||||
|
||||
# One Item Selected
|
||||
elif len(self.driver.selected) == 1:
|
||||
entry: Entry = self.lib.get_entry(self.driver.selected[0])
|
||||
entry_id = self.driver.selected[0]
|
||||
filepath: Path = self.lib.library_dir / entry.path
|
||||
ext: str = filepath.suffix.lower()
|
||||
|
||||
if update_preview:
|
||||
stats: dict = self.thumb.update_preview(filepath, ext)
|
||||
self.file_attrs.update_stats(filepath, ext, stats)
|
||||
self.file_attrs.update_date_label(filepath)
|
||||
self.fields.update_from_entry(entry_id)
|
||||
self.update_add_tag_button(entry_id)
|
||||
self.update_add_field_button(entry_id)
|
||||
|
||||
self.add_tag_button.setEnabled(True)
|
||||
self.add_field_button.setEnabled(True)
|
||||
|
||||
# Multiple Selected Items
|
||||
elif len(self.driver.selected) > 1:
|
||||
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
|
||||
self.thumb.hide_preview() # TODO: Render mixed selection
|
||||
self.file_attrs.update_multi_selection(len(self.driver.selected))
|
||||
self.file_attrs.update_date_label()
|
||||
self.fields.hide_containers() # TODO: Allow for mixed editing
|
||||
self.update_add_tag_button()
|
||||
self.update_add_field_button()
|
||||
|
||||
self.add_tag_button.setEnabled(True)
|
||||
self.add_field_button.setEnabled(True)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("[Preview Panel] Error updating selection", error=e)
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def update_add_field_button(self, entry_id: int | None = None):
|
||||
with catch_warnings(record=True):
|
||||
self.add_field_modal.done.disconnect()
|
||||
self.add_field_button.clicked.disconnect()
|
||||
|
||||
self.add_field_modal.done.connect(
|
||||
lambda f: (
|
||||
self.fields.add_field_to_selected(f),
|
||||
(self.fields.update_from_entry(entry_id) if entry_id else ()),
|
||||
)
|
||||
)
|
||||
self.add_field_button.clicked.connect(self.add_field_modal.show)
|
||||
|
||||
def update_add_tag_button(self, entry_id: int = None):
|
||||
with catch_warnings(record=True):
|
||||
self.tag_search_panel.tag_chosen.disconnect()
|
||||
self.add_tag_button.clicked.disconnect()
|
||||
|
||||
self.tag_search_panel.tag_chosen.connect(
|
||||
lambda t: (
|
||||
self.fields.add_tags_to_selected(t),
|
||||
(self.fields.update_from_entry(entry_id) if entry_id else ()),
|
||||
)
|
||||
)
|
||||
|
||||
self.add_tag_button.clicked.connect(self.add_tag_modal.show)
|
||||
@@ -3,7 +3,7 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from PySide6.QtCore import Qt, QThreadPool
|
||||
from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget
|
||||
@@ -29,7 +29,7 @@ class ProgressWidget(QWidget):
|
||||
self.pb = QProgressDialog(
|
||||
labelText=label_text,
|
||||
minimum=minimum,
|
||||
cancelButtonText=cancel_button_text,
|
||||
cancelButtonText=cancel_button_text, # pyright: ignore[reportArgumentType]
|
||||
maximum=maximum,
|
||||
)
|
||||
self.root.addWidget(self.pb)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import typing
|
||||
from types import FunctionType
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import QEvent, Qt, Signal
|
||||
@@ -20,7 +20,7 @@ from tagstudio.qt.translations import Translations
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class TagAliasWidget(QWidget):
|
||||
self,
|
||||
id: int | None = 0,
|
||||
alias: str | None = None,
|
||||
on_remove_callback=None,
|
||||
on_remove_callback: Callable[[], None] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -84,11 +84,13 @@ class TagAliasWidget(QWidget):
|
||||
self.text_field.setMinimumWidth(text_width)
|
||||
self.text_field.adjustSize()
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
self.update()
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
self.update()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
@@ -98,15 +100,17 @@ class TagWidget(QWidget):
|
||||
on_click = Signal()
|
||||
on_edit = Signal()
|
||||
|
||||
tag: Tag | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tag: Tag | None,
|
||||
has_edit: bool,
|
||||
has_remove: bool,
|
||||
library: "Library | None" = None,
|
||||
on_remove_callback: FunctionType = None,
|
||||
on_click_callback: FunctionType = None,
|
||||
on_edit_callback: FunctionType = None,
|
||||
on_remove_callback: Callable[[], None] | None = None,
|
||||
on_click_callback: Callable[[], None] | None = None,
|
||||
on_edit_callback: Callable[[], None] | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.tag = tag
|
||||
@@ -123,10 +127,18 @@ class TagWidget(QWidget):
|
||||
self.bg_button = QPushButton(self)
|
||||
self.bg_button.setFlat(True)
|
||||
|
||||
# add callbacks
|
||||
if on_remove_callback is not None:
|
||||
self.on_remove.connect(on_remove_callback)
|
||||
if on_click_callback is not None:
|
||||
self.on_click.connect(on_click_callback)
|
||||
if on_edit_callback is not None:
|
||||
self.on_edit.connect(on_edit_callback)
|
||||
|
||||
# add edit action
|
||||
if has_edit:
|
||||
edit_action = QAction(self)
|
||||
edit_action.setText(Translations["generic.edit"])
|
||||
edit_action.triggered.connect(on_edit_callback)
|
||||
edit_action.triggered.connect(self.on_edit.emit)
|
||||
self.bg_button.addAction(edit_action)
|
||||
# if on_click_callback:
|
||||
@@ -261,13 +273,15 @@ class TagWidget(QWidget):
|
||||
def set_has_remove(self, has_remove: bool):
|
||||
self.has_remove = has_remove
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(False)
|
||||
self.update()
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
@override
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
if self.has_remove:
|
||||
self.remove_button.setHidden(True)
|
||||
self.update()
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.widgets.fields import FieldWidget
|
||||
from tagstudio.qt.widgets.panel import PanelModal
|
||||
from tagstudio.qt.widgets.tag import TagWidget
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagBoxWidget(FieldWidget):
|
||||
updated = Signal()
|
||||
error_occurred = Signal(Exception)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: set[Tag],
|
||||
title: str,
|
||||
driver: "QtDriver",
|
||||
) -> None:
|
||||
super().__init__(title)
|
||||
|
||||
self.tags: set[Tag] = tags
|
||||
self.driver = (
|
||||
driver # Used for creating tag click callbacks that search entries for that tag.
|
||||
)
|
||||
self.setObjectName("tagBox")
|
||||
self.base_layout = FlowLayout()
|
||||
self.base_layout.enable_grid_optimizations(value=False)
|
||||
self.base_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(self.base_layout)
|
||||
|
||||
self.set_tags(self.tags)
|
||||
|
||||
def set_tags(self, tags: typing.Iterable[Tag]):
|
||||
tags_ = sorted(list(tags), key=lambda tag: self.driver.lib.tag_display_name(tag.id))
|
||||
logger.info("[TagBoxWidget] Tags:", tags=tags)
|
||||
while self.base_layout.itemAt(0):
|
||||
self.base_layout.takeAt(0).widget().deleteLater()
|
||||
|
||||
for tag in tags_:
|
||||
tag_widget = TagWidget(tag, library=self.driver.lib, has_edit=True, has_remove=True)
|
||||
tag_widget.on_click.connect(lambda t=tag: self.edit_tag(t))
|
||||
|
||||
tag_widget.on_remove.connect(
|
||||
lambda tag_id=tag.id: (
|
||||
self.remove_tag(tag_id),
|
||||
self.driver.preview_panel.update_widgets(update_preview=False),
|
||||
)
|
||||
)
|
||||
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))
|
||||
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=tag.id: (
|
||||
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
|
||||
self.driver.filter_items(
|
||||
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.base_layout.addWidget(tag_widget)
|
||||
|
||||
def edit_tag(self, tag: Tag):
|
||||
assert isinstance(tag, Tag), f"tag is {type(tag)}"
|
||||
build_tag_panel = BuildTagPanel(self.driver.lib, tag=tag)
|
||||
|
||||
self.edit_modal = PanelModal(
|
||||
build_tag_panel,
|
||||
self.driver.lib.tag_display_name(tag.id),
|
||||
"Edit Tag",
|
||||
done_callback=lambda: self.driver.preview_panel.update_widgets(update_preview=False),
|
||||
has_save=True,
|
||||
)
|
||||
# TODO - this was update_tag()
|
||||
self.edit_modal.saved.connect(
|
||||
lambda: self.driver.lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
alias_names=set(build_tag_panel.alias_names),
|
||||
alias_ids=set(build_tag_panel.alias_ids),
|
||||
)
|
||||
)
|
||||
self.edit_modal.show()
|
||||
|
||||
def remove_tag(self, tag_id: int):
|
||||
logger.info(
|
||||
"[TagBoxWidget] remove_tag",
|
||||
selected=self.driver.selected,
|
||||
)
|
||||
|
||||
for entry_id in self.driver.selected:
|
||||
self.driver.lib.remove_tags_from_entries(entry_id, tag_id)
|
||||
|
||||
self.updated.emit()
|
||||
@@ -3,6 +3,8 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel
|
||||
|
||||
@@ -19,9 +21,25 @@ class TextWidget(FieldWidget):
|
||||
self.text_label = QLabel()
|
||||
self.text_label.setStyleSheet("font-size: 12px")
|
||||
self.text_label.setWordWrap(True)
|
||||
self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self.text_label.setTextFormat(Qt.TextFormat.MarkdownText)
|
||||
self.text_label.setOpenExternalLinks(True)
|
||||
self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.base_layout.addWidget(self.text_label)
|
||||
self.set_text(text)
|
||||
|
||||
def set_text(self, text: str):
|
||||
text = linkify(text)
|
||||
self.text_label.setText(text)
|
||||
|
||||
|
||||
# Regex from https://stackoverflow.com/a/6041965
|
||||
def linkify(text: str):
|
||||
url_pattern = (
|
||||
r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-*]*[\w@?^=%&\/~+#-*])"
|
||||
)
|
||||
return re.sub(
|
||||
url_pattern,
|
||||
lambda url: f'<a href="{url.group(0)}">{url.group(0)}</a>',
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from PySide6.QtWidgets import QLineEdit, QVBoxLayout
|
||||
|
||||
|
||||
@@ -136,5 +136,6 @@ class ThumbButton(QPushButtonWrapper):
|
||||
return super().leaveEvent(event)
|
||||
|
||||
def set_selected(self, value: bool) -> None: # noqa: N802
|
||||
self.selected = value
|
||||
self.repaint()
|
||||
if value != self.selected:
|
||||
self.selected = value
|
||||
self.repaint()
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
import contextlib
|
||||
import hashlib
|
||||
import math
|
||||
import struct
|
||||
import zipfile
|
||||
from copy import deepcopy
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from warnings import catch_warnings
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import rawpy
|
||||
import srctools
|
||||
import structlog
|
||||
from cv2.typing import MatLike
|
||||
from mutagen import MutagenError, flac, id3, mp4
|
||||
@@ -46,7 +47,6 @@ from PySide6.QtCore import (
|
||||
from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap
|
||||
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from vtf2img import Parser
|
||||
|
||||
from tagstudio.core.constants import (
|
||||
FONT_SAMPLE_SIZES,
|
||||
@@ -88,7 +88,7 @@ class ThumbRenderer(QObject):
|
||||
|
||||
rm: ResourceManager = ResourceManager()
|
||||
cache: CacheManager = CacheManager()
|
||||
updated = Signal(float, QPixmap, QSize, Path, str)
|
||||
updated = Signal(float, QPixmap, QSize, Path)
|
||||
updated_ratio = Signal(float)
|
||||
|
||||
cached_img_res: int = 256 # TODO: Pull this from config
|
||||
@@ -524,12 +524,12 @@ class ThumbRenderer(QObject):
|
||||
samples_per_bar: int = 3
|
||||
size_scaled: int = size * base_scale
|
||||
allow_small_min: bool = False
|
||||
im: Image.Image = None
|
||||
im: Image.Image | None = None
|
||||
|
||||
try:
|
||||
bar_count: int = min(math.floor((size // pixel_ratio) / 5), 64)
|
||||
audio: AudioSegment = AudioSegment.from_file(filepath, ext[1:])
|
||||
data = np.fromstring(audio._data, np.int16) # type: ignore
|
||||
audio = AudioSegment.from_file(filepath, ext[1:])
|
||||
data = np.frombuffer(buffer=audio._data, dtype=np.int16)
|
||||
data_indices = np.linspace(1, len(data), num=bar_count * samples_per_bar)
|
||||
bar_margin: float = ((size_scaled / (bar_count * 3)) * base_scale) / 2
|
||||
line_width: float = ((size_scaled - bar_margin) / (bar_count * 3)) * base_scale
|
||||
@@ -537,7 +537,7 @@ class ThumbRenderer(QObject):
|
||||
|
||||
count: int = 0
|
||||
maximum_item: int = 0
|
||||
max_array: list = []
|
||||
max_array: list[int] = []
|
||||
highest_line: int = 0
|
||||
|
||||
for i in range(-1, len(data_indices)):
|
||||
@@ -546,7 +546,7 @@ class ThumbRenderer(QObject):
|
||||
count = count + 1
|
||||
with catch_warnings(record=True):
|
||||
if abs(d) > maximum_item:
|
||||
maximum_item = abs(d)
|
||||
maximum_item = int(abs(d))
|
||||
else:
|
||||
max_array.append(maximum_item)
|
||||
|
||||
@@ -630,27 +630,22 @@ class ThumbRenderer(QObject):
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _source_engine(filepath: Path) -> Image.Image:
|
||||
"""This is a function to convert the VTF (Valve Texture Format) files to thumbnails.
|
||||
def _vtf_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for VTF (Valve Texture Format) images.
|
||||
|
||||
It works using the VTF2IMG library for PILLOW.
|
||||
Uses the srctools library for reading VTF files.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
parser = Parser(filepath)
|
||||
im: Image.Image = None
|
||||
im: Image.Image | None = None
|
||||
try:
|
||||
im = parser.get_image()
|
||||
with open(filepath, "rb") as f:
|
||||
vtf = srctools.VTF.read(f)
|
||||
im = vtf.get(frame=0).to_PIL()
|
||||
|
||||
except (
|
||||
AttributeError,
|
||||
UnidentifiedImageError,
|
||||
TypeError,
|
||||
struct.error,
|
||||
) as e:
|
||||
if str(e) == "expected string or buffer":
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
except ValueError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
@@ -676,6 +671,29 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _krita_thumb(filepath: Path) -> Image.Image:
|
||||
"""Extract and render a thumbnail for an Krita file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
file_path_within_zip = "preview.png"
|
||||
im: Image.Image = None
|
||||
with zipfile.ZipFile(filepath, "r") as zip_file:
|
||||
# Check if the file exists in the zip
|
||||
if file_path_within_zip in zip_file.namelist():
|
||||
# Read the specific file into memory
|
||||
file_data = zip_file.read(file_path_within_zip)
|
||||
thumb_im = Image.open(BytesIO(file_data))
|
||||
if thumb_im:
|
||||
im = Image.new("RGB", thumb_im.size, color="#1e1e1e")
|
||||
im.paste(thumb_im)
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath)
|
||||
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _powerpoint_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for a Microsoft PowerPoint file.
|
||||
@@ -754,8 +772,8 @@ class ThumbRenderer(QObject):
|
||||
data = np.asarray(raw.getchannel(0))
|
||||
|
||||
m, n = data.shape[:2]
|
||||
col: np.ndarray = data.any(0)
|
||||
row: np.ndarray = data.any(1)
|
||||
col: np.ndarray = cast(np.ndarray, data.any(0))
|
||||
row: np.ndarray = cast(np.ndarray, data.any(1))
|
||||
cropped_data = np.asarray(raw)[
|
||||
row.argmax() : m - row[::-1].argmax(),
|
||||
col.argmax() : n - col[::-1].argmax(),
|
||||
@@ -802,7 +820,7 @@ class ThumbRenderer(QObject):
|
||||
bg = Image.new("RGBA", (size, size), color="#00000000")
|
||||
draw = ImageDraw.Draw(bg)
|
||||
lines_of_padding = 2
|
||||
y_offset = 0
|
||||
y_offset = 0.0
|
||||
|
||||
for font_size in scaled_sizes:
|
||||
font = ImageFont.truetype(filepath, size=font_size)
|
||||
@@ -1299,7 +1317,6 @@ class ThumbRenderer(QObject):
|
||||
math.ceil(image.size[1] / pixel_ratio),
|
||||
),
|
||||
filepath,
|
||||
filepath.suffix.lower(),
|
||||
)
|
||||
else:
|
||||
self.updated.emit(
|
||||
@@ -1307,7 +1324,6 @@ class ThumbRenderer(QObject):
|
||||
QPixmap(),
|
||||
QSize(*base_size),
|
||||
filepath,
|
||||
filepath.suffix.lower(),
|
||||
)
|
||||
|
||||
def _render(
|
||||
@@ -1367,6 +1383,11 @@ class ThumbRenderer(QObject):
|
||||
# PowerPoint Slideshow
|
||||
elif ext in {".pptx"}:
|
||||
image = self._powerpoint_thumb(_filepath)
|
||||
# Krita ========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._krita_thumb(_filepath)
|
||||
# OpenDocument/OpenOffice ======================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True
|
||||
@@ -1419,7 +1440,7 @@ class ThumbRenderer(QObject):
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._source_engine(_filepath)
|
||||
image = self._vtf_thumb(_filepath)
|
||||
# No Rendered Thumbnail ========================================
|
||||
if not image:
|
||||
raise NoRendererError
|
||||
|
||||
BIN
src/tagstudio/resources/qt/images/bxs-left-arrow.png
Normal file
BIN
src/tagstudio/resources/qt/images/bxs-left-arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/tagstudio/resources/qt/images/bxs-right-arrow.png
Normal file
BIN
src/tagstudio/resources/qt/images/bxs-right-arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
@@ -3,7 +3,9 @@
|
||||
"about.description": "TagStudio ist eine Anwendung zum organisieren von Fotos und Dateien mit einem zugrunde liegendem Tag-basierten System, welches sich darauf konzentriert, dem Nutzer Freiraum und Flexibilität zu bieten. Keine proprietären Programme oder Formate, kein Meer an Hilfsdateien und keine komplette Umwälzung deiner Dateisystemstruktur.",
|
||||
"about.documentation": "Dokumentation",
|
||||
"about.license": "Lizenz",
|
||||
"about.module.found": "Gefunden",
|
||||
"about.title": "Über TagStudio",
|
||||
"about.website": "Webseite",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Bibliothek '{library_dir}'",
|
||||
@@ -21,6 +23,7 @@
|
||||
"color.secondary": "Sekundärfarbe",
|
||||
"color.title.no_color": "Keine Farbe",
|
||||
"color_manager.title": "Tag-Farben verwalten",
|
||||
"dependency.missing.title": "{dependency} nicht gefunden",
|
||||
"drop_import.description": "Die folgenden Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren",
|
||||
"drop_import.duplicates_choice.plural": "Die folgenden {count} Dateien passen zu Dateipfaden, welche bereits in der Bibliothek existieren.",
|
||||
"drop_import.duplicates_choice.singular": "Die folgende Datei passt zu einem Dateipfad, welcher bereits in der Bibliothek existiert.",
|
||||
@@ -60,13 +63,14 @@
|
||||
"entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...",
|
||||
"entries.unlinked.search_and_relink": "&Suchen && Neuverbinden",
|
||||
"entries.unlinked.title": "Unverknüpfte Einträge reparieren",
|
||||
"ffmpeg.missing.description": "FFmpeg und/oder FFprobe wurden nicht gefunden. FFmpeg ist für multimediale Wiedergabe und Thumbnails vonnöten.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Feld kopieren",
|
||||
"field.edit": "Feld bearbeiten",
|
||||
"field.paste": "Feld einfügen",
|
||||
"file.date_added": "Hinzufügungsdatum",
|
||||
"file.date_created": "Erstellungsdatum",
|
||||
"file.date_modified": "Datum geändert",
|
||||
"file.path": "Dateipfad",
|
||||
"file.dimensions": "Abmessungen",
|
||||
"file.duplicates.description": "TagStudio unterstützt das Importieren von DupeGuru-Ergebnissen um Dateiduplikate zu verwalten.",
|
||||
"file.duplicates.dupeguru.advice": "Nach dem Kopiervorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.",
|
||||
@@ -86,6 +90,7 @@
|
||||
"file.open_location.generic": "Datei im Explorer öffnen",
|
||||
"file.open_location.mac": "In Finder anzeigen",
|
||||
"file.open_location.windows": "Im Explorer anzeigen",
|
||||
"file.path": "Dateipfad",
|
||||
"folders_to_tags.close_all": "Alle schließen",
|
||||
"folders_to_tags.converting": "Wandele Ordner zu Tags um",
|
||||
"folders_to_tags.description": "Erstellt Tags basierend auf der Verzeichnisstruktur und wendet sie auf die Einträge an.\nDer folgende Verzeichnisbaum zeigt welche Tags erstellt werden würden und auf welche Einträge sie angewendet werden würden.",
|
||||
@@ -101,12 +106,13 @@
|
||||
"generic.copy": "Kopieren",
|
||||
"generic.cut": "Ausschneiden",
|
||||
"generic.delete": "Löschen",
|
||||
"generic.delete_alt": "Löschen",
|
||||
"generic.delete_alt": "&Löschen",
|
||||
"generic.done": "Fertig",
|
||||
"generic.done_alt": "Fertig",
|
||||
"generic.done_alt": "&Fertig",
|
||||
"generic.edit": "Bearbeiten",
|
||||
"generic.edit_alt": "B&earbeiten",
|
||||
"generic.filename": "Dateiname",
|
||||
"generic.missing": "Nicht vorhanden",
|
||||
"generic.navigation.back": "Zurück",
|
||||
"generic.navigation.next": "Weiter",
|
||||
"generic.none": "Kein(e)",
|
||||
@@ -115,11 +121,11 @@
|
||||
"generic.paste": "Einfügen",
|
||||
"generic.recent_libraries": "Aktuelle Bibliotheken",
|
||||
"generic.rename": "Umbenennen",
|
||||
"generic.rename_alt": "Umbenennen",
|
||||
"generic.rename_alt": "&Umbenennen",
|
||||
"generic.reset": "Zurücksetzen",
|
||||
"generic.save": "Speichern",
|
||||
"generic.skip": "Überspringen",
|
||||
"generic.skip_alt": "Über&springen",
|
||||
"generic.skip_alt": "&Überspringen",
|
||||
"home.search": "Suchen",
|
||||
"home.search_entries": "Nach Einträgen suchen",
|
||||
"home.search_library": "Bibliothek durchsuchen",
|
||||
@@ -181,6 +187,7 @@
|
||||
"macros.running.dialog.new_entries": "Führe konfigurierte Makros für {count}/{total} neue Dateieinträge aus...",
|
||||
"macros.running.dialog.title": "Ausführen von Makros bei neuen Einträgen",
|
||||
"media_player.autoplay": "Autoplay",
|
||||
"media_player.loop": "Schleife",
|
||||
"menu.delete_selected_files_ambiguous": "Datei(en) nach {trash_term} verschieben",
|
||||
"menu.delete_selected_files_plural": "Dateien nach {trash_term} verschieben",
|
||||
"menu.delete_selected_files_singular": "Datei nach {trash_term} verschieben",
|
||||
@@ -192,6 +199,8 @@
|
||||
"menu.file": "Datei",
|
||||
"menu.file.clear_recent_libraries": "Zuletzt verwendete löschen",
|
||||
"menu.file.close_library": "Bibliothek s&chließen",
|
||||
"menu.file.missing_library.message": "Der Pfad für die Bibliothek \"{library}\" kann nicht gefunden werden.",
|
||||
"menu.file.missing_library.title": "Nicht vorhandene Bibliothek",
|
||||
"menu.file.new_library": "Neue Bibliothek",
|
||||
"menu.file.open_create_library": "Bibli&othek öffnen/erstellen",
|
||||
"menu.file.open_library": "Bibliothek öffnen",
|
||||
@@ -220,8 +229,18 @@
|
||||
"select.add_tag_to_selected": "Tag zu Ausgewähltem hinzufügen",
|
||||
"select.all": "Alle auswählen",
|
||||
"select.clear": "Auswahl leeren",
|
||||
"select.inverse": "Auswahl invertieren",
|
||||
"settings.clear_thumb_cache.title": "Vorschaubild-Zwischenspeicher leeren",
|
||||
"settings.dateformat.english": "Englisch",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "Datumsformat",
|
||||
"settings.dateformat.system": "System",
|
||||
"settings.filepath.label": "Sichtbarer Dateipfad",
|
||||
"settings.filepath.option.full": "Zeige vollständigen Pfad",
|
||||
"settings.filepath.option.name": "Nur Dateinamen anzeigen",
|
||||
"settings.filepath.option.relative": "Zeige relative Pfade",
|
||||
"settings.global": "Globale Einstellungen",
|
||||
"settings.hourformat.label": "24-Stunden Format",
|
||||
"settings.language": "Sprache",
|
||||
"settings.library": "Bibliothekseinstellungen",
|
||||
"settings.open_library_on_start": "Bibliothek zum Start öffnen",
|
||||
@@ -229,11 +248,16 @@
|
||||
"settings.restart_required": "Bitte TagStudio neustarten, um Änderungen anzuwenden.",
|
||||
"settings.show_filenames_in_grid": "Dateinamen in Raster darstellen",
|
||||
"settings.show_recent_libraries": "Zuletzt verwendete Bibliotheken anzeigen",
|
||||
"settings.tag_click_action.add_to_search": "Tag zu Suche hinzufügen",
|
||||
"settings.tag_click_action.label": "Tag Klick Aktion",
|
||||
"settings.tag_click_action.open_edit": "Tag bearbeiten",
|
||||
"settings.tag_click_action.set_search": "Nach Tag suchen",
|
||||
"settings.theme.dark": "Dunkel",
|
||||
"settings.theme.label": "Design:",
|
||||
"settings.theme.light": "Hell",
|
||||
"settings.theme.system": "System",
|
||||
"settings.title": "Einstellungen",
|
||||
"settings.zeropadding.label": "Platzsparendes Datum",
|
||||
"sorting.direction.ascending": "Aufsteigend",
|
||||
"sorting.direction.descending": "Absteigend",
|
||||
"splash.opening_library": "Öffne Bibliothek \"{library_path}\"...",
|
||||
|
||||
@@ -217,6 +217,8 @@
|
||||
"menu.tools.fix_duplicate_files": "Fix Duplicate &Files",
|
||||
"menu.tools.fix_unlinked_entries": "Fix &Unlinked Entries",
|
||||
"menu.tools": "&Tools",
|
||||
"menu.view.decrease_thumbnail_size": "Decrease Thumbnail Size",
|
||||
"menu.view.increase_thumbnail_size": "Increase Thumbnail Size",
|
||||
"menu.view": "&View",
|
||||
"menu.window": "Window",
|
||||
"namespace.create.description_color": "Tag colors use namespaces as color palette groups. All custom colors must be under a namespace group first.",
|
||||
@@ -229,6 +231,7 @@
|
||||
"select.add_tag_to_selected": "Add Tag to Selected",
|
||||
"select.all": "Select All",
|
||||
"select.clear": "Clear Selection",
|
||||
"select.inverse": "Invert Selection",
|
||||
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
|
||||
"settings.filepath.label": "Filepath Visibility",
|
||||
"settings.filepath.option.full": "Show Full Paths",
|
||||
@@ -242,10 +245,20 @@
|
||||
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
|
||||
"settings.show_filenames_in_grid": "Show Filenames in Grid",
|
||||
"settings.show_recent_libraries": "Show Recent Libraries",
|
||||
"settings.tag_click_action.label": "Tag Click Action",
|
||||
"settings.tag_click_action.add_to_search": "Add Tag to Search",
|
||||
"settings.tag_click_action.open_edit": "Edit Tag",
|
||||
"settings.tag_click_action.set_search": "Search for Tag",
|
||||
"settings.theme.dark": "Dark",
|
||||
"settings.theme.label": "Theme:",
|
||||
"settings.theme.light": "Light",
|
||||
"settings.theme.system": "System",
|
||||
"settings.dateformat.label": "Date Format",
|
||||
"settings.dateformat.system": "System",
|
||||
"settings.dateformat.english": "English",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.hourformat.label": "24-Hour Time",
|
||||
"settings.zeropadding.label": "Date Zero-Padding",
|
||||
"settings.title": "Settings",
|
||||
"sorting.direction.ascending": "Ascending",
|
||||
"sorting.direction.descending": "Descending",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about.config_path": "Archivo de configuración",
|
||||
"about.description": "TagStudio es una aplicación para organizar fotografías y archivos que utiliza un sistema de etiquetas subyacentes centrado en dar libertad y flexibilidad al usuario. Sin programas ni formatos propios, ni un mar de archivos y sin trastornar completamente tu sistema de estructurar los archivos.",
|
||||
"about.description": "TagStudio es una aplicación para organizar fotografías y archivos que utiliza un sistema de etiquetas subyacentes centrado en dar libertad y flexibilidad al usuario. Sin programas ni formatos propios, ni un mar de archivos y sin trastornar completamente la estructura de tu sistema de archivos.",
|
||||
"about.documentation": "Documentación",
|
||||
"about.license": "Licencia",
|
||||
"about.module.found": "Encontrado",
|
||||
@@ -187,6 +187,7 @@
|
||||
"macros.running.dialog.new_entries": "Ejecución de macros configurados en {count}/{total} entradas nuevas...",
|
||||
"macros.running.dialog.title": "Ejecución de macros en entradas nuevas",
|
||||
"media_player.autoplay": "Reproducción automática",
|
||||
"media_player.loop": "Bucle",
|
||||
"menu.delete_selected_files_ambiguous": "Mover archivo(s) a la {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Mover archivos a la {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Mover archivo a la {trash_term}",
|
||||
@@ -198,6 +199,8 @@
|
||||
"menu.file": "&Archivo",
|
||||
"menu.file.clear_recent_libraries": "Borrar recientes",
|
||||
"menu.file.close_library": "&Cerrar biblioteca",
|
||||
"menu.file.missing_library.message": "La ubicación de la biblioteca \"{library}\" no se ha podido encontrar.",
|
||||
"menu.file.missing_library.title": "Biblioteca desaparecida",
|
||||
"menu.file.new_library": "Nueva biblioteca",
|
||||
"menu.file.open_create_library": "&Abrir/Crear biblioteca",
|
||||
"menu.file.open_library": "Abrir biblioteca",
|
||||
@@ -226,17 +229,35 @@
|
||||
"select.add_tag_to_selected": "Añadir etiqueta a la selección",
|
||||
"select.all": "Seleccionar todo",
|
||||
"select.clear": "Borrar selección",
|
||||
"select.inverse": "Invertir selección",
|
||||
"settings.clear_thumb_cache.title": "Borrar cache de las miniaturas",
|
||||
"settings.dateformat.english": "Inglés",
|
||||
"settings.dateformat.international": "Internacional",
|
||||
"settings.dateformat.label": "Formato fecha",
|
||||
"settings.dateformat.system": "Sistema",
|
||||
"settings.filepath.label": "Visualización ruta de archivos",
|
||||
"settings.filepath.option.full": "Mostrar rutas completas",
|
||||
"settings.filepath.option.name": "Mostrar sólo el nombre",
|
||||
"settings.filepath.option.relative": "Mostrar rutas relativas",
|
||||
"settings.global": "Ajustes globales",
|
||||
"settings.hourformat.label": "Formato 24-horas",
|
||||
"settings.language": "Idioma",
|
||||
"settings.library": "Ajustes de la biblioteca",
|
||||
"settings.open_library_on_start": "Abrir biblioteca al iniciar",
|
||||
"settings.page_size": "Tamaño de la página",
|
||||
"settings.restart_required": "Por favor, reinicia TagStudio para que se los cambios surtan efecto.",
|
||||
"settings.show_filenames_in_grid": "Mostrar el nombre de archivo en la cuadrícula",
|
||||
"settings.show_recent_libraries": "Mostrar bibliotecas recientes",
|
||||
"settings.tag_click_action.add_to_search": "Añadir Etiqueta a la Búsqueda",
|
||||
"settings.tag_click_action.label": "Acción al hacer Clic a la Etiqueta",
|
||||
"settings.tag_click_action.open_edit": "Editar Etiqueta",
|
||||
"settings.tag_click_action.set_search": "Buscar Etiqueta",
|
||||
"settings.theme.dark": "Oscuro",
|
||||
"settings.theme.label": "Tema:",
|
||||
"settings.theme.light": "Claro",
|
||||
"settings.theme.system": "Sistema",
|
||||
"settings.title": "Ajustes",
|
||||
"settings.zeropadding.label": "Rellenar ceros en fechas",
|
||||
"sorting.direction.ascending": "Ascendiente",
|
||||
"sorting.direction.descending": "Descendiente",
|
||||
"splash.opening_library": "Abriendo biblioteca \"{library_path}\"...",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"about.description": "Ang TagStudio ay isang application ng pagsasaayos ng file at larawan na may pinagbabatayan na tag-based na sistema na nakatutok sa pagbibigay ng kalayaan at kakayahang umangkop sa user. Walang mga proprietary na format o program, walang dagat ng mga sidecar file, at walang kaguluhan ng iyong estruktura ng filesystem.",
|
||||
"about.documentation": "Dokumentasyon",
|
||||
"about.license": "Lisensya",
|
||||
"about.module.found": "Nahanap",
|
||||
"about.title": "Tungkol sa TagStudio",
|
||||
"about.website": "Website",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"app.title": "{base_title} - Library '{library_dir}'",
|
||||
@@ -21,6 +23,7 @@
|
||||
"color.secondary": "Pangalawang Kulay",
|
||||
"color.title.no_color": "Walang Kulay",
|
||||
"color_manager.title": "Ipamahala ang Mga Kulay ng Tag",
|
||||
"dependency.missing.title": "Hindi nahanap ang {dependency}",
|
||||
"drop_import.description": "Ang mga sumusunod na file ay tumutugma sa mga file path na umiiral na sa library",
|
||||
"drop_import.duplicates_choice.plural": "Ang sumusunod na {count} mga file ay tumutugma sa mga file path na umiiral sa library.",
|
||||
"drop_import.duplicates_choice.singular": "Ang sumusunod na file ay tumutugma sa file path na umiiral na sa library.",
|
||||
@@ -30,12 +33,14 @@
|
||||
"drop_import.progress.window_title": "I-import ang Mga File",
|
||||
"drop_import.title": "(Mga) Sumasalungat na File",
|
||||
"edit.color_manager": "Ipamahala ang Mga Kulay ng Tag",
|
||||
"edit.copy_fields": "Kopyahin ang Mga Field",
|
||||
"edit.paste_fields": "I-paste ang Mga Field",
|
||||
"edit.tag_manager": "Ipamahala ang Mga Tag",
|
||||
"entries.duplicate.merge": "Isama ang Mga Duplicate na Entry",
|
||||
"entries.duplicate.merge.label": "Sinasama ang mga Duplicate na Entry…",
|
||||
"entries.duplicate.refresh": "I-refresh ang Mga Duplicate na Entry",
|
||||
"entries.duplicates.description": "Ang mga duplicate na entry ay tinukoy bilang maramihang mga entry na tumuturo sa parehong file sa disk. Ang pagsasama-sama ng mga ito ay pagsasama-samahin ang mga tag at metadata mula sa lahat ng mga duplicate sa isang solong pinagsama-samang entry. Ang mga ito ay hindi dapat ipagkamali sa \"mga duplicate na file\", na mga duplicate ng iyong mga file mismo sa labas ng TagStudio.",
|
||||
"entries.mirror": "&Mirror",
|
||||
"entries.mirror": "I-&Mirror",
|
||||
"entries.mirror.confirmation": "Sigurado ka ba gusto mong i-mirror ang sumusunod na {count} Mga Entry?",
|
||||
"entries.mirror.label": "Mini-mirror ang {idx}/{total} Mga Entry…",
|
||||
"entries.mirror.title": "Mini-mirror ang Mga Entry",
|
||||
@@ -58,6 +63,8 @@
|
||||
"entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…",
|
||||
"entries.unlinked.search_and_relink": "&Maghanap at Mag-link muli",
|
||||
"entries.unlinked.title": "Ayusin ang Mga Naka-unlink na Entry",
|
||||
"ffmpeg.missing.description": "Hindi nahanap ang FFmpeg at/o FFprobe. Kinakailangan ang FFmpeg para sa playback ng multimedia at mga thumbnail.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Kopyahin ang Field",
|
||||
"field.edit": "I-edit ang Field",
|
||||
"field.paste": "I-paste ang Field",
|
||||
@@ -74,15 +81,16 @@
|
||||
"file.duplicates.fix": "Ayusin ang Mga Duplicate na File",
|
||||
"file.duplicates.matches": "Mga Tumutugmang Duplicate File: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Mga Tumutugmang Duplicate File: N/A",
|
||||
"file.duplicates.mirror.description": "Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.",
|
||||
"file.duplicates.mirror.description": "I-mirror ang data ng Entry sa bawat duplicate na hanay ng tugma, pinagsasama ang lahat ng data habang hindi inaalis o kino-duplicate ang mga field. Ang operasyong ito ay hindi magtatanggal ng anumang mga file o data.",
|
||||
"file.duplicates.mirror_entries": "&I-mirror ang mga Entry",
|
||||
"file.duration": "Haba",
|
||||
"file.not_found": "Hindi nahanap ang file:",
|
||||
"file.not_found": "Hindi nahanap ang file",
|
||||
"file.open_file": "Buksan ang file",
|
||||
"file.open_file_with": "Buksan ang file gamit ang",
|
||||
"file.open_location.generic": "Ipakita ang file sa file explorer",
|
||||
"file.open_location.mac": "Ipakita sa Finder",
|
||||
"file.open_location.windows": "Ipakita sa File Explorer",
|
||||
"file.path": "Path ng File",
|
||||
"folders_to_tags.close_all": "Isara Lahat",
|
||||
"folders_to_tags.converting": "Kino-convert ang mga folder sa Tag",
|
||||
"folders_to_tags.description": "Gumawa ng mga tag base sa iyong estruktura ng folder at ia-apply sa iyong mga entry.\nAng istraktura sa ibaba ay pinapakita ang lahat ng mga tag na gagawin at anong mga entry ang ia-apply sa.",
|
||||
@@ -104,6 +112,7 @@
|
||||
"generic.edit": "I-edit",
|
||||
"generic.edit_alt": "&I-edit",
|
||||
"generic.filename": "Pangalan ng file",
|
||||
"generic.missing": "Nawawala",
|
||||
"generic.navigation.back": "Bumalik",
|
||||
"generic.navigation.next": "Susunod",
|
||||
"generic.none": "Wala",
|
||||
@@ -152,30 +161,165 @@
|
||||
"json_migration.heading.shorthands": "Mga shorthand:",
|
||||
"json_migration.heading.tags": "Mga tag:",
|
||||
"json_migration.info.description": "Ang mga library save file na ginawa gamit ng mga bersyon ng TagStudio na <b>9.4 at mas-mababa</b> ay kailangang i-migrate sa bagong <b>9.5+</b> format.<br><h2>Ano ang dapat mong alamin:</h2><ul><li>Ang iyong umiiral na library save a <b><i>HINDI</i></b> buburahin</li><li>Ang iyong mga personal na file ay <b><i>HINDI</i></b> buburahin, ililipat, o babaguhin</li><li>Ang bagong 9.5+ save format ay hindi mabubuksan ng mga lumang bersyon ng TagStudio</li></ul><h3>Ano ang nagbago:</h3><ul><li>Ang \"Mga Field ng Tag\" ay pinalitan sa \"Mga Kategorya ng Tag\". Sa halip ng pagdagdag ng mga tag sa mga field muna, ang mga tag ay direkta nang dinadagdag sa mga file entry. Awtomatiko din silang isasaayos sa mga kategorya base sa mga parent tag na nakamarka bilang \"Isang Kategorya\" na property sa menu ng pag-edit ng tag. Maaring markahan bilang kategorya ang anumang tag, at ang mga child tag ay isasaayos ang sarili nila sa ilalim ng mga parent tag na nakamarka bilang kategorya. Ang \"Paborito\" at \"Naka-archive\" na tag ay magmamana na sa bagong \"Mga Meta Tag\" na tag na nakamarka bilang kategorya bilang default.</li><li>Ang mga kulay ng tag ay na-tweak at pinalawak sa. Ang ilang mga kulay ay na-rename o pinagsama-sama, ngunit ang lahat ng mga kulay ng tag ay mako-convert pa rin sa eksakto o malapit na pagkatugma sa v9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Nililipat ang {entries:,d} mga File Entry…",
|
||||
"json_migration.migration_complete": "Tapos na ang Pag-migrate!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Tapos na ang Pag-migrate, May Nakitang Pagkakaiba",
|
||||
"json_migration.start_and_preview": "Simulan at I-preview",
|
||||
"json_migration.title": "Pag-migrate ng Format ng Save: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ na Library</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 na Library</h2>",
|
||||
"landing.open_create_library": "Buksan/Gumawa ng Library {shortcut}",
|
||||
"library.field.add": "Magdagdag ng Field",
|
||||
"library.field.confirm_remove": "Sigurado ka ba gusto mo tanggalin ang field na \"{name}\"?",
|
||||
"library.field.mixed_data": "Halo-halong Data",
|
||||
"library.field.remove": "Tanggalin ang Field",
|
||||
"library.missing": "Nawawala ang Lokasyon ng Library",
|
||||
"library.name": "Library",
|
||||
"library.refresh.scanning.plural": "Sina-scan ang Direktoryo para sa Mga Bagong File…\n{searched_count} Nahanap na File, {found_count} Nahanap na Bagong FIle",
|
||||
"library.refresh.scanning.singular": "Sina-scan ang Direktoryo para sa Mga Bagong File…\n{searched_count} Mga Nahanap na File, {found_count} Nahanap na Bagong FIle",
|
||||
"library.refresh.scanning_preparing": "Sina-scan ang Mga Direktoryo para sa Mga Bagong File...\nNaghahanda...",
|
||||
"library.refresh.title": "Nire-refresh ang Mga Direktoryo",
|
||||
"macros.running.dialog.new_entries": "Tinatakbo ang Mga Naka-configure na Macro sa {count}/{total} Mga Bagong Entry",
|
||||
"library.scan_library.title": "Sina-scan ang Library",
|
||||
"library_object.name": "Pangalan",
|
||||
"library_object.name_required": "Pangalan (Kinakailangan)",
|
||||
"library_object.slug": "Slug ng ID",
|
||||
"library_object.slug_required": "Slug ng ID (Kinakailangan)",
|
||||
"macros.running.dialog.new_entries": "Tinatakbo ang Mga Naka-configure na Macro sa {count}/{total} Mga Bagong Entry…",
|
||||
"macros.running.dialog.title": "Tumatakbo ng Mga Macro sa Mga Bagong Entry",
|
||||
"media_player.autoplay": "I-Autoplay",
|
||||
"media_player.loop": "I-loop",
|
||||
"menu.delete_selected_files_ambiguous": "Ilipat ang (mga) FIle sa {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Ilipat ang Mga File sa {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Ilipat ang File sa {trash_term}",
|
||||
"menu.edit": "I-edit",
|
||||
"menu.edit.ignore_list": "Huwag Pansinin ang Mga File at Folder",
|
||||
"menu.edit.manage_file_extensions": "Ipamahala ang Mga File Extension",
|
||||
"menu.edit.manage_tags": "Ipamahala ang Mga Tag",
|
||||
"menu.edit.new_tag": "Bagong &Tag",
|
||||
"menu.file": "File",
|
||||
"menu.file.clear_recent_libraries": "I-clear ang Kamakailan",
|
||||
"menu.file.close_library": "&Isara ang Library",
|
||||
"menu.file.missing_library.message": "Hindi mahanap ang lokasyon ng library na \"{library}\".",
|
||||
"menu.file.missing_library.title": "Nawawalang Library",
|
||||
"menu.file.new_library": "Bagong Library",
|
||||
"menu.file.open_create_library": "Magbukas/Gumawa ng Library (&O)",
|
||||
"menu.file.open_library": "Magbukas ng Library",
|
||||
"menu.file.open_recent_library": "Magbukas ng Kamakailan",
|
||||
"menu.file.refresh_directories": "I-refresh ang mga Direktoryo (&R)",
|
||||
"menu.file.save_backup": "I-save ang Backup ng Library (&S)",
|
||||
"menu.file.save_library": "I-save ang Library",
|
||||
"menu.help": "Tulong",
|
||||
"menu.help.about": "Tungkol sa",
|
||||
"menu.macros": "Mga macro",
|
||||
"menu.macros.folders_to_tags": "Mga Folder sa Tag",
|
||||
"menu.select": "Piliin",
|
||||
"menu.settings": "Mga Setting…",
|
||||
"menu.tools": "Mga tool",
|
||||
"menu.tools.fix_duplicate_files": "Ayusin ang mga Duplicate na &File",
|
||||
"menu.tools.fix_unlinked_entries": "I-fix ang mga Naka-unlink na Entry (&U)",
|
||||
"menu.view": "Pagtingin (&V)",
|
||||
"menu.window": "Window",
|
||||
"namespace.create.description": "Ginagamit ng TagStudio ang mga namespace para ihiwalay ang mga grupo ng item tulad ng mga tag at kulay sa paraan na ginagawa silang madali na i-export at ibahagi. Ang mga namespace na nagsisimula sa \"tagstudio\" ay nakareserba para sa TagStudio para sa panloob na paggamit.",
|
||||
"namespace.create.description_color": "Gumagamit ng mga namespace bilang grupo ng palette ng kulay ang mga kulay ng tag. Dapat nasa ilalim ng isang grupo ng namespace muna ang lahat ng mga custom na kulay.",
|
||||
"namespace.create.title": "Gumawa ng Namespace",
|
||||
"namespace.new.button": "Bagong Namespace",
|
||||
"namespace.new.prompt": "Gumawa ng bagong namespace para magsimulang magdagdag ng mga custom na kulay!",
|
||||
"preview.multiple_selection": "<b>{count}</b> mga Item ang Pinili",
|
||||
"preview.no_selection": "Walang Mga Item na Pinili",
|
||||
"select.add_tag_to_selected": "Idagdag ang Tag sa Pinili",
|
||||
"select.all": "Piliin Lahat",
|
||||
"select.clear": "I-clear ang Pinili",
|
||||
"select.inverse": "Baliktaran ang Pinili",
|
||||
"settings.clear_thumb_cache.title": "I-clear ang Cache ng Thumbnail",
|
||||
"settings.dateformat.english": "Ingles",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "Format ng Petsa",
|
||||
"settings.dateformat.system": "Sistema",
|
||||
"settings.filepath.label": "Visibility ng Filepath",
|
||||
"settings.filepath.option.full": "Ipakita ang Punong Path",
|
||||
"settings.filepath.option.name": "Ipakita Lamang ang Pangalan ng File",
|
||||
"settings.filepath.option.relative": "Ipakita ang Mga Relative na Path",
|
||||
"settings.global": "Mga Global na Setting",
|
||||
"settings.hourformat.label": "24-Oras",
|
||||
"settings.language": "Wika",
|
||||
"settings.library": "Mga Setting ng Library",
|
||||
"settings.open_library_on_start": "Buksan ang Library sa Pagkasimula",
|
||||
"settings.page_size": "Laki ng Pahina",
|
||||
"settings.restart_required": "Paki-restart ang TagStudio para magkabisa ang mga pagbabago.",
|
||||
"settings.show_filenames_in_grid": "Ipakita ang pangalan ng file sa Grid",
|
||||
"settings.show_recent_libraries": "Ipakita ang Mga Kamakailang Library",
|
||||
"settings.tag_click_action.add_to_search": "Idagdag ang Tag sa Paghahanap",
|
||||
"settings.tag_click_action.label": "Aksyon sa Pag-click ng Tag",
|
||||
"settings.tag_click_action.open_edit": "I-edit ang Tag",
|
||||
"settings.tag_click_action.set_search": "Hanapin ang Tag",
|
||||
"settings.theme.dark": "Madilim",
|
||||
"settings.theme.label": "Tema:",
|
||||
"settings.theme.light": "Maliwanag",
|
||||
"settings.theme.system": "Sistema",
|
||||
"settings.title": "Mga Setting",
|
||||
"settings.zeropadding.label": "Walang Padding na Petsa",
|
||||
"sorting.direction.ascending": "Pataas",
|
||||
"sorting.direction.descending": "Pababa",
|
||||
"splash.opening_library": "Binubuksan ang Library na \"{library_path}\"…",
|
||||
"status.deleted_file_plural": "Binura ang {count} mga file!",
|
||||
"status.deleted_file_singular": "Binura ang 1 file!",
|
||||
"status.deleted_none": "Walang mga naburang file.",
|
||||
"status.deleted_partial_warning": "Binura lang ang {count} (mga) file! Suriin kung ang ilan sa mga file ay nawawala o ginagamit.",
|
||||
"status.deleting_file": "Binubura ang file [{i}/{count}]: \"{path}\"…",
|
||||
"status.library_backup_in_progress": "Sine-save ang Backup ng Library…",
|
||||
"status.library_backup_success": "Na-save ang Library Backup sa: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "Sinara ang Library ({time_span})",
|
||||
"status.library_closing": "Sinasara ang Library…",
|
||||
"status.library_save_success": "Sinave at Sinara ang Library!",
|
||||
"status.library_search_query": "Hinahanap ang library para sa",
|
||||
"status.library_search_query": "Hinahanap ang library…",
|
||||
"status.library_version_expected": "Inaasahan:",
|
||||
"status.library_version_found": "Nahanap:",
|
||||
"status.library_version_mismatch": "Hindi Tumutugma ang Bersyon ng Library!",
|
||||
"status.results": "Mga Resulta",
|
||||
"status.results.invalid_syntax": "Hindi Wastong Search Syntax:",
|
||||
"status.results_found": "{count} Mga Resulta na Nahanap ({time_span})",
|
||||
"tag.add": "Magdagdag ng Tag",
|
||||
"tag.add.plural": "Magdagdag ng Mga Tag",
|
||||
"tag.add_to_search": "Idagdag sa Paghahanap",
|
||||
"tag.aliases": "Mga Alyas",
|
||||
"tag.all_tags": "Lahat ng Mga Tag",
|
||||
"tag.choose_color": "Pumili ng Kulay ng Tag",
|
||||
"tag.color": "Kulay",
|
||||
"tag.confirm_delete": "Sigurado ka ba gusto mong burahin ang tag na \"{tag_name}\"?",
|
||||
"tag.create": "Gumawa ng Tag",
|
||||
"tag.create_add": "Gumawa at Idagdag ang \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Gamitin ang tag na ito para sa disambiguation",
|
||||
"tag.edit": "I-edit ang Tag",
|
||||
"tag.is_category": "Ay Kategorya",
|
||||
"tag.name": "Pangalan",
|
||||
"tag.new": "Bagong Tag",
|
||||
"tag.parent_tags": "Mga Parent Tag",
|
||||
"tag.parent_tags.add": "Magdagdag ng Mga Parent Tag",
|
||||
"tag.parent_tags.description": "Maaaring ituring ang tag na ito bilang kapalit ng alinman sa mga Parent Tag na ito sa mga paghahanap.",
|
||||
"tag.remove": "Tanggalin ang Tag",
|
||||
"tag.search_for_tag": "Maghanap para sa Tag",
|
||||
"tag.shorthand": "Shorthand",
|
||||
"tag_manager.title": "Mga Tag ng Library"
|
||||
"tag.tag_name_required": "Pangalan ng Tag (Kinakailangan)",
|
||||
"tag.view_limit": "Limitasyon ng Pagtingin:",
|
||||
"tag_manager.title": "Mga Tag ng Library",
|
||||
"trash.context.ambiguous": "Ilipat ang (mga) file sa {trash_term}",
|
||||
"trash.context.plural": "Ilipat ang mga file sa {trash_term}",
|
||||
"trash.context.singular": "Ilipat ang file sa {trash_term}",
|
||||
"trash.dialog.disambiguation_warning.plural": "Tatanggalin ang mga ito mula sa TagStudio <i>AT</i> sa iyong file system!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Tatanggalin ito mula sa TagStudio <i>AT</i> sa iyong file system!",
|
||||
"trash.dialog.move.confirmation.plural": "Sigurado ka ba gusto mong ilipat ang {count} mga file sa {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "Sigurado ka ba gusto mong ilipat ang file na ito sa {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>BABALA!</b> Kung hindi malipat ang file na ito sa {trash_term}, <b>permanente itong buburahin!</b>",
|
||||
"trash.dialog.title.plural": "Burahin ang Mga File",
|
||||
"trash.dialog.title.singular": "Burahin ang File",
|
||||
"trash.name.generic": "Basurahan",
|
||||
"trash.name.windows": "Recycle Bin",
|
||||
"view.size.0": "Napakaliit",
|
||||
"view.size.1": "Maliit",
|
||||
"view.size.2": "Daluyan",
|
||||
"view.size.3": "Malaki",
|
||||
"view.size.4": "Napakalaki",
|
||||
"window.message.error_opening_library": "Hindi mabukan ang library.",
|
||||
"window.title.error": "Error",
|
||||
"window.title.open_create_library": "Buksan/Gumawa ng Library"
|
||||
}
|
||||
|
||||
@@ -229,12 +229,18 @@
|
||||
"select.add_tag_to_selected": "Ajouter un Tag à la sélection",
|
||||
"select.all": "Tout Sélectionner",
|
||||
"select.clear": "Effacer la Sélection",
|
||||
"select.inverse": "Inverser la Sélection",
|
||||
"settings.clear_thumb_cache.title": "Effacer le cache des vignettes",
|
||||
"settings.dateformat.english": "Anglais",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "Format de Date",
|
||||
"settings.dateformat.system": "Système",
|
||||
"settings.filepath.label": "Visibilités du Chemin de Fichier",
|
||||
"settings.filepath.option.full": "Afficher le Chemin Complet",
|
||||
"settings.filepath.option.name": "Afficher Seulement le Nom du Fichier",
|
||||
"settings.filepath.option.relative": "Afficher le Chemin Relatif",
|
||||
"settings.global": "Paramètres Globaux",
|
||||
"settings.hourformat.label": "Temps sur 24-Heure",
|
||||
"settings.language": "Langage",
|
||||
"settings.library": "Paramètres de la Bibliothèque",
|
||||
"settings.open_library_on_start": "Ouvrir la Bibliothèque au Démarrage",
|
||||
@@ -242,11 +248,16 @@
|
||||
"settings.restart_required": "Veuillez redémarré TagStudio pour que les changements prenne effet.",
|
||||
"settings.show_filenames_in_grid": "Afficher les Noms de Fichiers en Grille",
|
||||
"settings.show_recent_libraries": "Afficher les Bibliothèques Récentes",
|
||||
"settings.tag_click_action.add_to_search": "Ajouter le Tag à la recherche",
|
||||
"settings.tag_click_action.label": "Action du Clic sur les Tags",
|
||||
"settings.tag_click_action.open_edit": "Modifier le Tag",
|
||||
"settings.tag_click_action.set_search": "Faire un recherche avec le Tag",
|
||||
"settings.theme.dark": "Sombre",
|
||||
"settings.theme.label": "Thème :",
|
||||
"settings.theme.light": "Clair",
|
||||
"settings.theme.system": "Système",
|
||||
"settings.title": "Paramètres",
|
||||
"settings.zeropadding.label": "Remplissage par zéros",
|
||||
"sorting.direction.ascending": "Croissant",
|
||||
"sorting.direction.descending": "Décroissant",
|
||||
"splash.opening_library": "Ouverture de la Bibliothèque \"{library_path}\"...",
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"library_object.slug_required": "Azonosító-helyőrző (kötelező)",
|
||||
"macros.running.dialog.new_entries": "Korábban beállított makrók futtatása {total}/{count} új elemen…",
|
||||
"macros.running.dialog.title": "Makrók futtatása az új elemeken",
|
||||
"media_player.autoplay": "Automatikus lejátszás",
|
||||
"media_player.autoplay": "&Automatikus lejátszás",
|
||||
"media_player.loop": "Ismétlés",
|
||||
"menu.delete_selected_files_ambiguous": "Fájl(ok) {trash_term} &helyezése",
|
||||
"menu.delete_selected_files_plural": "Fájlok {trash_term} &helyezése",
|
||||
@@ -229,24 +229,35 @@
|
||||
"select.add_tag_to_selected": "Címk&e hozzáadása a kijelölt elemekhez",
|
||||
"select.all": "&Az összes kijelölése",
|
||||
"select.clear": "&Kijelölés megszüntetése",
|
||||
"select.inverse": "Kijelölés &megfordítása",
|
||||
"settings.clear_thumb_cache.title": "&Miniatűr-gyorsítótár ürítése",
|
||||
"settings.filepath.label": "Elérési utak láthatósága",
|
||||
"settings.dateformat.english": "Angol",
|
||||
"settings.dateformat.international": "Nemzetközi",
|
||||
"settings.dateformat.label": "Dátumformátum",
|
||||
"settings.dateformat.system": "Rendszer",
|
||||
"settings.filepath.label": "&Elérési utak láthatósága",
|
||||
"settings.filepath.option.full": "Teljes elérési út mutatása",
|
||||
"settings.filepath.option.name": "Csak a fájlnév mutatása",
|
||||
"settings.filepath.option.relative": "Relatív elérési út mutatása",
|
||||
"settings.global": "Globális beállítások",
|
||||
"settings.language": "Nyelv",
|
||||
"settings.hourformat.label": "24-órás idő",
|
||||
"settings.language": "&Nyelv",
|
||||
"settings.library": "Könyvtárbeállítások",
|
||||
"settings.open_library_on_start": "&Könyvtár megnyitása a program indulásakor",
|
||||
"settings.page_size": "Oldalméret",
|
||||
"settings.page_size": "&Oldalméret",
|
||||
"settings.restart_required": "A módosítások érvénybeléptetéséhez<br>újra kell indítani a TagStudiót.",
|
||||
"settings.show_filenames_in_grid": "&Fájlnevek megjelenítése rácsnézetben",
|
||||
"settings.show_recent_libraries": "&Legutóbbi könyvtárak megjelenítése",
|
||||
"settings.tag_click_action.add_to_search": "Címke hozzáfűzése a kereséshez",
|
||||
"settings.tag_click_action.label": "Címkére kattintási művelet",
|
||||
"settings.tag_click_action.open_edit": "Címke szerkesztése",
|
||||
"settings.tag_click_action.set_search": "Címke keresése",
|
||||
"settings.theme.dark": "Sötét",
|
||||
"settings.theme.label": "Téma:",
|
||||
"settings.theme.label": "&Téma:",
|
||||
"settings.theme.light": "Világos",
|
||||
"settings.theme.system": "Automatikus",
|
||||
"settings.title": "Beállítások",
|
||||
"settings.zeropadding.label": "Egyszámjegyű napok kezdése nullával",
|
||||
"sorting.direction.ascending": "Növekvő sorrend",
|
||||
"sorting.direction.descending": "Csökkenő sorrend",
|
||||
"splash.opening_library": "Könyvtár megnyitása folyamatban: „{library_path}”…",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about.config_path": "設定ファイルのパス",
|
||||
"about.description": "TagStudioは、自由と柔軟性を提供することを目指したタグベースのシステムを基盤とした画像・ファイル整理アプリケーションです。専売のプログラムやフォーマット、サイドカーファイルの海、ファイルシステム構造の大混乱はもうありません。",
|
||||
"about.description": "TagStudio は、タグベースのシステムに基づく、写真とファイルの整理アプリです。独自のプログラムやフォーマットは使用せず、サイドカーファイルが大量に生成されることもありません。ファイルシステム全体に大きな変更を加えることなく、ユーザーに自由で柔軟な運用を提供します。",
|
||||
"about.documentation": "ドキュメント",
|
||||
"about.license": "ライセンス",
|
||||
"about.module.found": "インストール済み",
|
||||
@@ -10,11 +10,11 @@
|
||||
"app.pre_release": "公開前",
|
||||
"app.title": "{base_title} - ライブラリ '{library_dir}'",
|
||||
"color.color_border": "境界線にアクセントカラーを使う",
|
||||
"color.confirm_delete": "\"{color_name}\" を本当に削除しますか?",
|
||||
"color.confirm_delete": "色「{color_name}」を削除してもよろしいですか?",
|
||||
"color.delete": "タグを削除",
|
||||
"color.import_pack": "色のパックをインポート",
|
||||
"color.name": "名前",
|
||||
"color.namespace.delete.prompt": "この色の名前空間を本当に削除しますか? 削除した場合、名前空間内のすべての色も一緒に削除されます!",
|
||||
"color.namespace.delete.prompt": "この色の名前空間を削除してもよろしいですか? この操作を実行すると、名前空間内のすべての色も削除されます!",
|
||||
"color.namespace.delete.title": "色の名前空間を削除",
|
||||
"color.new": "新しい色",
|
||||
"color.placeholder": "色",
|
||||
@@ -22,139 +22,304 @@
|
||||
"color.primary_required": "メインカラー (必須)",
|
||||
"color.secondary": "アクセントカラー",
|
||||
"color.title.no_color": "色なし",
|
||||
"color_manager.title": "タグの色を管理",
|
||||
"drop_import.description": "以下のファイルはすでにライブラリに存在するファイルパスにマッチします",
|
||||
"drop_import.duplicates_choice.plural": "以下の {count} 件のファイルはすでにライブラリに存在するファイルパスにマッチします。",
|
||||
"drop_import.duplicates_choice.singular": "次のファイルはすでにライブラリに存在するファイルパスにマッチします。",
|
||||
"drop_import.progress.label.initial": "新しいファイルのインポート中…",
|
||||
"drop_import.progress.label.plural": "新しいファイルのインポート中…\n{count} 件インポート済み。{suffix}",
|
||||
"drop_import.progress.label.singular": "新しいファイルをインポート中…\n1 件インポート済み。{suffix}",
|
||||
"color_manager.title": "タグの色の管理",
|
||||
"dependency.missing.title": "{dependency} が見つかりません",
|
||||
"drop_import.description": "次のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています",
|
||||
"drop_import.duplicates_choice.plural": "以下の {count} 件のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています。",
|
||||
"drop_import.duplicates_choice.singular": "次のファイルは、ライブラリ内にすでに存在するファイル パスと一致しています。",
|
||||
"drop_import.progress.label.initial": "新しいファイルをインポートしています...",
|
||||
"drop_import.progress.label.plural": "新しいファイルをインポートしています...\n{count} 件のファイルをインポートしました。{suffix}",
|
||||
"drop_import.progress.label.singular": "新しいファイルをインポートしています...\n1 件のファイルをインポートしました。{suffix}",
|
||||
"drop_import.progress.window_title": "ファイルをインポート",
|
||||
"drop_import.title": "競合ファイル",
|
||||
"edit.color_manager": "タグの色を管理",
|
||||
"edit.color_manager": "タグの色の管理",
|
||||
"edit.copy_fields": "フィールドをコピー",
|
||||
"edit.paste_fields": "フィールドを貼り付け",
|
||||
"edit.tag_manager": "タグを管理",
|
||||
"entries.duplicate.merge": "重複エントリをマージ",
|
||||
"entries.duplicate.merge.label": "重複エントリをマージ中…",
|
||||
"entries.duplicate.merge.label": "重複エントリを統合しています...",
|
||||
"entries.duplicate.refresh": "重複エントリを最新の状態にする",
|
||||
"entries.duplicates.description": "重複エントリとは、ディスク上の同じファイルを指す複数のエントリを指します。これらをマージすると、すべての重複エントリのタグとメタデータが1つのまとまったエントリに統合されます。TagStudio の外部にあるファイル自体の複製である「重複ファイル」と混同しないようにご注意ください。",
|
||||
"entries.mirror": "ミラー(&m)",
|
||||
"entries.mirror.confirmation": "以下 {count} 件のエントリを本当にミラーしますか?",
|
||||
"entries.mirror.label": "{total} 件中 {idx} 件のエントリをミラー中…",
|
||||
"entries.mirror": "ミラー(&M)",
|
||||
"entries.mirror.confirmation": "以下の {count} 件のエントリをミラーリングしてもよろしいですか?",
|
||||
"entries.mirror.label": "{total} 件中 {idx} 件のエントリをミラーリングしています...",
|
||||
"entries.mirror.title": "エントリをミラー",
|
||||
"entries.mirror.window_title": "エントリをミラー",
|
||||
"entries.running.dialog.new_entries": "新しいファイルエントリを {total} 件追加中…",
|
||||
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています...",
|
||||
"entries.running.dialog.title": "新しいファイルエントリを追加",
|
||||
"entries.tags": "タグ",
|
||||
"entries.unlinked.delete": "リンクされていないエントリを削除",
|
||||
"entries.unlinked.delete.confirm": "以下の {count} 件のエントリを本当に削除しますか?",
|
||||
"entries.unlinked.delete": "未リンクのエントリを削除",
|
||||
"entries.unlinked.delete.confirm": "以下の {count} 件のエントリを削除してもよろしいですか?",
|
||||
"entries.unlinked.delete.deleting": "エントリの削除",
|
||||
"entries.unlinked.delete.deleting_count": "{count} 件中 {idx} 件のリンクされていないエントリを削除中",
|
||||
"entries.unlinked.delete_alt": "リンクされていないエントリを削除(&l)",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio外で移動または削除された場合、リンクされていないとみなされます。<br><br>リンクされていないエントリは、ディレクトリを検索して自動的に再リンクするか、必要に応じて削除することができます。",
|
||||
"entries.unlinked.missing_count.none": "リンクされていないエントリ数: N/A",
|
||||
"entries.unlinked.missing_count.some": "リンクされていないエントリ数: {count}",
|
||||
"entries.unlinked.refresh_all": "すべて再読み込み(&r)",
|
||||
"entries.unlinked.relink.attempting": "{missing_count} 件中 {idx} 件の再リンクを試行し {fixed_count} 件の再リンクに成功",
|
||||
"entries.unlinked.relink.manual": "手動で再リンク(&m)",
|
||||
"entries.unlinked.delete.deleting_count": "{count} 件中 {idx} 件の未リンクのエントリを削除しています",
|
||||
"entries.unlinked.delete_alt": "未リンクのエントリを削除(&L)",
|
||||
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルがTagStudio以外で移動または削除された場合、そのエントリは未リンクとして扱われます。<br><br>未リンクのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
|
||||
"entries.unlinked.missing_count.none": "未リンクのエントリを削除",
|
||||
"entries.unlinked.missing_count.some": "未リンクのエントリ数: {count}",
|
||||
"entries.unlinked.refresh_all": "すべて再読み込み(&R)",
|
||||
"entries.unlinked.relink.attempting": "{missing_count} 件中 {idx} 件の項目を再リンク中、{fixed_count} 件を正常に再リンクしました",
|
||||
"entries.unlinked.relink.manual": "手動で再リンク(&M)",
|
||||
"entries.unlinked.relink.title": "エントリの再リンク",
|
||||
"entries.unlinked.scanning": "リンクされていないエントリのためにライブラリをスキャン中…",
|
||||
"entries.unlinked.search_and_relink": "検索して再リンク(&s)",
|
||||
"entries.unlinked.title": "リンクされていないエントリを修正",
|
||||
"entries.unlinked.scanning": "未リンクのエントリをライブラリ内でスキャンしています...",
|
||||
"entries.unlinked.search_and_relink": "検索して再リンク(&S)",
|
||||
"entries.unlinked.title": "未リンクのエントリを修正",
|
||||
"ffmpeg.missing.description": "FFmpeg または FFprobe が見つかりません。マルチメディアの再生とサムネイルの表示には FFmpeg のインストールが必要です。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "フィールドをコピー",
|
||||
"field.edit": "フィールドを編集",
|
||||
"field.paste": "フィールドを貼り付け",
|
||||
"file.date_added": "追加日",
|
||||
"file.date_created": "作成日",
|
||||
"file.date_modified": "更新日",
|
||||
"file.date_added": "追加日時",
|
||||
"file.date_created": "作成日時",
|
||||
"file.date_modified": "更新日時",
|
||||
"file.dimensions": "ディメンション",
|
||||
"file.duplicates.description": "TagStudio は重複ファイルの管理のために DupeGuru の結果のインポートをサポートしています。",
|
||||
"file.duplicates.dupeguru.advice": "ミラ ーした後、DupeGuru を使って不要なファイルを削除することができます。その後、TagStudio のツールメニューにある「リンクされていないエントリを修正」機能を使って、リンクされていないエントリを削除します。",
|
||||
"file.duplicates.description": "TagStudio では、重複ファイルを管理するために DupeGuru の結果をインポートできます。",
|
||||
"file.duplicates.dupeguru.advice": "ミラーリングの後は、DupeGuru を使って不要なファイルを削除できます。次に、ツール メニューの「未リンクのエントリを修正」機能を使って、リンクされていないエントリを削除します。",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru ファイル (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "DupeGuru ファイルの読み込み(&l)",
|
||||
"file.duplicates.dupeguru.load_file": "DupeGuru ファイルの読み込み(&L)",
|
||||
"file.duplicates.dupeguru.no_file": "DupeGuru ファイルが選択されていません",
|
||||
"file.duplicates.dupeguru.open_file": "DupeGuru 結果ファイルを開く",
|
||||
"file.duplicates.fix": "重複ファイルを修正",
|
||||
"file.duplicates.matches": "重複ファイルのマッチ数: {count}",
|
||||
"file.duplicates.matches_uninitialized": "重複ファイルのマッチ数: N/A",
|
||||
"file.duplicates.mirror.description": "重複する各マッチセットでエントリデータをミラーしてすべてのデータを結合しますが、フィールドの削除や複製は行いません。この操作によってファイルやデータが削除されることはありません。",
|
||||
"file.duplicates.mirror_entries": "エントリのミラー(&m)",
|
||||
"file.duplicates.matches": "重複ファイルの一致数: {count}",
|
||||
"file.duplicates.matches_uninitialized": "重複ファイルの一致数: N/A",
|
||||
"file.duplicates.mirror.description": "一致したそれぞれの重複セットにエントリ データをミラーリングし、フィールドを削除または重複させることなくすべてのデータを統合します。この操作でファイルやデータが削除されることはありません。",
|
||||
"file.duplicates.mirror_entries": "エントリのミラー(&M)",
|
||||
"file.duration": "長さ",
|
||||
"file.not_found": "ファイルが見つかりません",
|
||||
"file.open_file": "ファイルを開く",
|
||||
"file.open_file_with": "ファイルを開くプログラムを指定",
|
||||
"file.open_location.generic": "ファイルの場所をエクスプローラーで開く",
|
||||
"file.open_location.generic": "ファイルの場所をエクスプローラーで表示する",
|
||||
"file.open_location.mac": "Finder で見る",
|
||||
"file.open_location.windows": "エクスプローラーで見る",
|
||||
"file.path": "ファイルパス",
|
||||
"file.open_location.windows": "エクスプローラーで表示する",
|
||||
"file.path": "ファイル パス",
|
||||
"folders_to_tags.close_all": "すべて閉じる",
|
||||
"folders_to_tags.converting": "フォルダーをタグに変換",
|
||||
"folders_to_tags.description": "フォルダ構造に基づいてタグを作成し、エントリーに適用します。\n 以下の構造は、作成されるすべてのタグと、それらが適用されるエントリを示しています。",
|
||||
"folders_to_tags.description": "フォルダー構造に基づいてタグを作成し、エントリに適用します。\n以下の構造は、作成されるすべてのタグと、それらが適用されるエントリを示しています。",
|
||||
"folders_to_tags.open_all": "すべて開く",
|
||||
"folders_to_tags.title": "フォルダーからタグを作成",
|
||||
"folders_to_tags.title": "フォルダー構造からタグを生成",
|
||||
"generic.add": "追加",
|
||||
"generic.apply": "適用",
|
||||
"generic.apply_alt": "適用(&a)",
|
||||
"generic.apply_alt": "適用(&A)",
|
||||
"generic.cancel": "キャンセル",
|
||||
"generic.cancel_alt": "キャンセル(&c)",
|
||||
"generic.cancel_alt": "キャンセル(&C)",
|
||||
"generic.close": "閉じる",
|
||||
"generic.continue": "継続する",
|
||||
"generic.copy": "コピー",
|
||||
"generic.cut": "切り取り",
|
||||
"generic.delete": "削除",
|
||||
"generic.delete_alt": "削除(&d)",
|
||||
"generic.delete_alt": "削除(&D)",
|
||||
"generic.done": "完了",
|
||||
"generic.done_alt": "完了(&d)",
|
||||
"generic.done_alt": "完了(&D)",
|
||||
"generic.edit": "編集",
|
||||
"generic.edit_alt": "編集(&e)",
|
||||
"generic.edit_alt": "編集(&E)",
|
||||
"generic.filename": "ファイル名",
|
||||
"generic.missing": "紛失",
|
||||
"generic.navigation.back": "戻る",
|
||||
"generic.navigation.next": "次へ",
|
||||
"generic.none": "なし",
|
||||
"generic.overwrite": "上書き",
|
||||
"generic.overwrite_alt": "上書き(&o)",
|
||||
"generic.overwrite_alt": "上書き(&O)",
|
||||
"generic.paste": "貼り付け",
|
||||
"generic.recent_libraries": "最近使用したライブラリ",
|
||||
"generic.rename": "名前の変更",
|
||||
"generic.rename_alt": "名前の変更(&r)",
|
||||
"generic.rename_alt": "名前の変更(&R)",
|
||||
"generic.reset": "リセット",
|
||||
"generic.save": "保存",
|
||||
"generic.skip": "スキップ",
|
||||
"generic.skip_alt": "スキップ(&s)",
|
||||
"generic.skip_alt": "スキップ(&S)",
|
||||
"home.search": "検索",
|
||||
"home.search_entries": "エントリを検索",
|
||||
"home.search_library": "ライブラリを検索",
|
||||
"home.search_tags": "タグを検索",
|
||||
"home.thumbnail_size": "サムネイルのサイズ",
|
||||
"home.thumbnail_size.extra_large": "巨大なサムネイル",
|
||||
"home.thumbnail_size.large": "大きいサムネイル",
|
||||
"home.thumbnail_size.medium": "中くらいのサムネイル",
|
||||
"home.thumbnail_size.mini": "極小のサムネイル",
|
||||
"home.thumbnail_size.small": "小さいサムネイル",
|
||||
"home.thumbnail_size.extra_large": "特大サムネイル",
|
||||
"home.thumbnail_size.large": "大サムネイル",
|
||||
"home.thumbnail_size.medium": "中サムネイル",
|
||||
"home.thumbnail_size.mini": "極小サムネイル",
|
||||
"home.thumbnail_size.small": "小サムネイル",
|
||||
"ignore_list.add_extension": "拡張子を追加(&a)",
|
||||
"ignore_list.mode.exclude": "含めない",
|
||||
"ignore_list.mode.include": "含める",
|
||||
"ignore_list.mode.label": "リストのモード:",
|
||||
"ignore_list.mode.label": "リストのモード:",
|
||||
"ignore_list.title": "ファイルの拡張子",
|
||||
"json_migration.checking_for_parity": "パリティチェック中…",
|
||||
"json_migration.creating_database_tables": "SQLデータベース テーブルを作成中…",
|
||||
"json_migration.description": "<br>ライブラリの移行処理を開始し、結果をプレビューします。 「移行完了」をクリックしない限り、変換されたライブラリは<i>使用されません。</i><br><br>ライブラリデータは、値が一致するか、「マッチ」ラベルが表示される必要があります。 一致しない値は赤色で表示され、その横に「<b>(!)</b>」マークが表示されます。<br><center><i>大規模なライブラリの場合、この処理に数分かかることがあります。</i></center>",
|
||||
"json_migration.discrepancies_found": "ライブラリの矛盾が見つかりました",
|
||||
"json_migration.discrepancies_found.description": "元のライブラリ形式と変換後のライブラリ形式の間に矛盾が見つかりました。確認して、移行を続行するかキャンセルするかを選択してください。",
|
||||
"json_migration.checking_for_parity": "パリティチェック中...",
|
||||
"json_migration.creating_database_tables": "SQLデータベース テーブルを作成しています...",
|
||||
"json_migration.description": "<br>ライブラリの移行処理を開始し、結果をプレビューします。変換されたライブラリは、 「移行完了」をクリックしない限り<i>使用されません</i>。<br><br>ライブラリ データは、値が一致しているか、「一致」ラベルが表示されている必要があります。 値が一致しない場合は赤色で表示され、その横に「<b>(!)</b>」マークが表示されます。<br><center><i>大規模なライブラリの場合、この処理に数分かかることがあります。</i></center>",
|
||||
"json_migration.discrepancies_found": "ライブラリの差異が見つかりました",
|
||||
"json_migration.discrepancies_found.description": "元のライブラリ形式と変換後の形式との間に差異が見つかりました。内容を確認のうえ、確認して、移行を続行するかキャンセルするかを選択してください。",
|
||||
"json_migration.finish_migration": "移行完了",
|
||||
"json_migration.heading.aliases": "エイリアス:",
|
||||
"json_migration.heading.colors": "色:",
|
||||
"json_migration.heading.differ": "矛盾",
|
||||
"json_migration.heading.entires": "エントリ:",
|
||||
"json_migration.heading.extension_list_type": "拡張子リストの種類:",
|
||||
"json_migration.heading.fields": "フィールド:",
|
||||
"json_migration.heading.file_extension_list": "ファイルの拡張子リスト:",
|
||||
"json_migration.heading.match": "マッチ",
|
||||
"json_migration.heading.names": "名前:",
|
||||
"json_migration.heading.parent_tags": "親タグ:",
|
||||
"json_migration.heading.paths": "パス:",
|
||||
"json_migration.heading.shorthands": "短縮名:",
|
||||
"json_migration.heading.tags": "タグ:"
|
||||
"json_migration.heading.aliases": "エイリアス:",
|
||||
"json_migration.heading.colors": "色:",
|
||||
"json_migration.heading.differ": "差異",
|
||||
"json_migration.heading.entires": "エントリ:",
|
||||
"json_migration.heading.extension_list_type": "拡張子リストの種類:",
|
||||
"json_migration.heading.fields": "フィールド:",
|
||||
"json_migration.heading.file_extension_list": "ファイルの拡張子リスト:",
|
||||
"json_migration.heading.match": "一致",
|
||||
"json_migration.heading.names": "名前:",
|
||||
"json_migration.heading.parent_tags": "親タグ:",
|
||||
"json_migration.heading.paths": "パス:",
|
||||
"json_migration.heading.shorthands": "略称:",
|
||||
"json_migration.heading.tags": "タグ:",
|
||||
"json_migration.info.description": "TagStudio バージョン<b>9.4 以前</b>で作成されたライブラリ保存ファイルは、新しいバージョン<b>9.5 以降</b>の形式に移行する必要があります。<br><h2>ご確認ください:</h2><ul><li>既存のライブラリ保存ファイルが<b><i>削除されることはありません</i></b></li><li>個人ファイルが<b><i>削除・移動・変更されることはありません</i></b></li><li>新しい v9.5 以降の保存形式は、旧バージョンの TagStudio では開くことができません</li></ul><h3>変更点:</h3><ul><li>「タグ フィールド」は「タグ カテゴリ」に置き換えられました。従来のようにタグを先にフィールドに追加するのではなく、タグを直接ファイル エントリに追加します。その後、タグ編集メニューで「カテゴリとして扱う」プロパティが有効になっている親タグに基づいて、タグが自動的にカテゴリとして整理されます。どのタグでもカテゴリとして指定でき、カテゴリに指定された親タグの下に子タグが自動で整理されます。「お気に入り」タグおよび「アーカイブ」タグは、新しく追加された「メタタグ」というデフォルトのカテゴリ タグの下に分類されます。</li><li>タグの色が調整・拡張されました。一部の色は名前が変更されたり統合されたりしましたが、すべてのタグの色は v9.5 において同一または類似の色に変換されます。</li></ul>",
|
||||
"json_migration.migrating_files_entries": "{entries} 件のファイル エントリを移行しています...",
|
||||
"json_migration.migration_complete": "移行が完了しました!",
|
||||
"json_migration.migration_complete_with_discrepancies": "移行が完了し、差異が見つかりました",
|
||||
"json_migration.start_and_preview": "開始とプレビュー",
|
||||
"json_migration.title": "保存形式の移行: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ ライブラリ</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 ライブラリ</h2>",
|
||||
"landing.open_create_library": "ライブラリを開く/作成する {shortcut}",
|
||||
"library.field.add": "フィールドの追加",
|
||||
"library.field.confirm_remove": "「{name}」フィールドを削除してもよろしいですか?",
|
||||
"library.field.mixed_data": "混在データ",
|
||||
"library.field.remove": "フィールドの削除",
|
||||
"library.missing": "ライブラリの場所が見つかりません",
|
||||
"library.name": "ライブラリ",
|
||||
"library.refresh.scanning.plural": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
|
||||
"library.refresh.scanning.singular": "新しいファイルを検索中...\n{searched_count} 件を検索、{found_count} 件の新規ファイルを検出",
|
||||
"library.refresh.scanning_preparing": "新しいファイルを検索中...\n準備中...",
|
||||
"library.refresh.title": "ディレクトリを更新しています",
|
||||
"library.scan_library.title": "ライブラリをスキャンしています",
|
||||
"library_object.name": "名前",
|
||||
"library_object.name_required": "名前 (必須)",
|
||||
"library_object.slug": "ID スラッグ",
|
||||
"library_object.slug_required": "ID スラッグ (必須)",
|
||||
"macros.running.dialog.new_entries": "新しいファイル エントリ {total} 件中 {count} 件に設定済みマクロを実行しています...",
|
||||
"macros.running.dialog.title": "新しいエントリにマクロを実行しています",
|
||||
"media_player.autoplay": "自動再生",
|
||||
"media_player.loop": "繰り返し",
|
||||
"menu.delete_selected_files_ambiguous": "ファイルを {trash_term} に移動",
|
||||
"menu.delete_selected_files_plural": "ファイルを {trash_term} に移動",
|
||||
"menu.delete_selected_files_singular": "ファイルを {trash_term} に移動",
|
||||
"menu.edit": "編集",
|
||||
"menu.edit.ignore_list": "ファイルとフォルダを無視",
|
||||
"menu.edit.manage_file_extensions": "ファイル拡張子の管理",
|
||||
"menu.edit.manage_tags": "タグの管理",
|
||||
"menu.edit.new_tag": "新しいタグ(&T)",
|
||||
"menu.file": "ファイル(&F)",
|
||||
"menu.file.clear_recent_libraries": "最近使用したライブラリの履歴をクリア",
|
||||
"menu.file.close_library": "ライブラリを閉じる(&C)",
|
||||
"menu.file.missing_library.message": "ライブラリ \"{library}\" の場所が見つかりません。",
|
||||
"menu.file.missing_library.title": "ライブラリが見つかりません",
|
||||
"menu.file.new_library": "新しいライブラリ",
|
||||
"menu.file.open_create_library": "ライブラリを開く/作成する(&O)",
|
||||
"menu.file.open_library": "ライブラリを開く",
|
||||
"menu.file.open_recent_library": "最近使用したライブラリを開く",
|
||||
"menu.file.refresh_directories": "ディレクトリの更新(&R)",
|
||||
"menu.file.save_backup": "ライブラリ バックアップを保存(&S)",
|
||||
"menu.file.save_library": "ライブラリを保存",
|
||||
"menu.help": "ヘルプ(&H)",
|
||||
"menu.help.about": "バージョン情報",
|
||||
"menu.macros": "マクロ(&M)",
|
||||
"menu.macros.folders_to_tags": "フォルダー構造からタグを生成",
|
||||
"menu.select": "選択",
|
||||
"menu.settings": "設定...",
|
||||
"menu.tools": "ツール(&T)",
|
||||
"menu.tools.fix_duplicate_files": "重複ファイルの修正(&F)",
|
||||
"menu.tools.fix_unlinked_entries": "未リンク項目の修正(&U)",
|
||||
"menu.view": "表示(&V)",
|
||||
"menu.window": "ウィンドウ",
|
||||
"namespace.create.description": "タグや色などの項目グループを分離し、エクスポートや共有をしやすくするために、TagStudio では名前空間を使用します。「tagstudio」で始まる名前空間は、TagStudio の内部用途として予約されています。",
|
||||
"namespace.create.description_color": "タグの色は、名前空間をカラーパレットのグループとして使用します。すべてのカスタムカラーは、最初に名前空間グループに属している必要があります。",
|
||||
"namespace.create.title": "名前空間の作成",
|
||||
"namespace.new.button": "新しい名前空間",
|
||||
"namespace.new.prompt": "カスタムカラーを追加するには、新しい名前空間を作成してください!",
|
||||
"preview.multiple_selection": "<b>{count}</b> 件選択済み",
|
||||
"preview.no_selection": "選択されていません",
|
||||
"select.add_tag_to_selected": "選択項目にタグを追加",
|
||||
"select.all": "すべて選択",
|
||||
"select.clear": "選択を解除",
|
||||
"select.inverse": "選択を反転",
|
||||
"settings.clear_thumb_cache.title": "サムネイルキャッシュをクリア",
|
||||
"settings.dateformat.english": "English",
|
||||
"settings.dateformat.international": "International",
|
||||
"settings.dateformat.label": "日付形式",
|
||||
"settings.dateformat.system": "システム",
|
||||
"settings.filepath.label": "ファイルパスの表示形式",
|
||||
"settings.filepath.option.full": "フルパスを表示",
|
||||
"settings.filepath.option.name": "ファイル名のみ表示",
|
||||
"settings.filepath.option.relative": "相対パスを表示",
|
||||
"settings.global": "グローバル設定",
|
||||
"settings.hourformat.label": "24時間表示",
|
||||
"settings.language": "言語",
|
||||
"settings.library": "ライブラリ設定",
|
||||
"settings.open_library_on_start": "起動時にライブラリを開く",
|
||||
"settings.page_size": "ページサイズ",
|
||||
"settings.restart_required": "変更を反映するには、TagStudio を再起動してください。",
|
||||
"settings.show_filenames_in_grid": "グリッドにファイル名を表示",
|
||||
"settings.show_recent_libraries": "最近使用したライブラリを表示",
|
||||
"settings.tag_click_action.add_to_search": "検索にタグを追加",
|
||||
"settings.tag_click_action.label": "タグクリック操作",
|
||||
"settings.tag_click_action.open_edit": "タグを編集",
|
||||
"settings.tag_click_action.set_search": "タグを検索",
|
||||
"settings.theme.dark": "ダーク",
|
||||
"settings.theme.label": "テーマ:",
|
||||
"settings.theme.light": "ライト",
|
||||
"settings.theme.system": "システム",
|
||||
"settings.title": "設定",
|
||||
"settings.zeropadding.label": "日付ゼロ埋め",
|
||||
"sorting.direction.ascending": "昇順",
|
||||
"sorting.direction.descending": "降順",
|
||||
"splash.opening_library": "ライブラリ \"{library_path}\" を開いています...",
|
||||
"status.deleted_file_plural": "{count} 件のファイルを削除しました!",
|
||||
"status.deleted_file_singular": "1 件のファイルを削除しました!",
|
||||
"status.deleted_none": "ファイルは削除されませんでした。",
|
||||
"status.deleted_partial_warning": "{count} 件のファイルしか削除できませんでした。ファイルが存在しないか、使用中でないかを確認してください。",
|
||||
"status.deleting_file": "[{i}/{count}] 件目のファイルを削除ファイルを削除しています : \"{path}\"...",
|
||||
"status.library_backup_in_progress": "ライブラリを保存しています...",
|
||||
"status.library_backup_success": "ライブラリのバックアップを保存しました: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "ライブラリを閉じました ({time_span})",
|
||||
"status.library_closing": "ライブラリを閉じています...",
|
||||
"status.library_save_success": "ライブラリを保存して閉じました!",
|
||||
"status.library_search_query": "ライブラリを検索しています...",
|
||||
"status.library_version_expected": "想定されるバージョン:",
|
||||
"status.library_version_found": "検出されたバージョン:",
|
||||
"status.library_version_mismatch": "ライブラリのバージョンが一致しません!",
|
||||
"status.results": "結果",
|
||||
"status.results.invalid_syntax": "検索の構文が正しくありません:",
|
||||
"status.results_found": "{count} 件の結果が見つかりました ({time_span})",
|
||||
"tag.add": "タグの追加",
|
||||
"tag.add.plural": "タグの追加",
|
||||
"tag.add_to_search": "検索に追加",
|
||||
"tag.aliases": "エイリアス",
|
||||
"tag.all_tags": "全てのタグ",
|
||||
"tag.choose_color": "タグの色の選択",
|
||||
"tag.color": "色",
|
||||
"tag.confirm_delete": "タグ「{tag_name}」を削除してもよろしいですか?",
|
||||
"tag.create": "タグを作成",
|
||||
"tag.create_add": "\"{query}\" を作成して追加",
|
||||
"tag.disambiguation.tooltip": "このタグは曖昧さを解消するために使用されます",
|
||||
"tag.edit": "タグの編集",
|
||||
"tag.is_category": "カテゴリとして扱う",
|
||||
"tag.name": "名前",
|
||||
"tag.new": "新しいタグ",
|
||||
"tag.parent_tags": "親タグ",
|
||||
"tag.parent_tags.add": "親タグを追加",
|
||||
"tag.parent_tags.description": "このタグは、検索時にこれらの親タグの代わりとして扱うことができます。",
|
||||
"tag.remove": "タグの削除",
|
||||
"tag.search_for_tag": "このタグで検索",
|
||||
"tag.shorthand": "略称",
|
||||
"tag.tag_name_required": "タグの名前 (必須)",
|
||||
"tag.view_limit": "表示件数:",
|
||||
"tag_manager.title": "ライブラリ タグ",
|
||||
"trash.context.ambiguous": "ファイルを {trash_term} に移動",
|
||||
"trash.context.plural": "ファイルを {trash_term} に移動",
|
||||
"trash.context.singular": "ファイルを {trash_term} に移動",
|
||||
"trash.dialog.disambiguation_warning.plural": "これにより、TagStudio だけでなく<i>ファイル システムからも</i>削除されます!",
|
||||
"trash.dialog.disambiguation_warning.singular": "これにより、TagStudio だけでなく<i>ファイル システムからも</i>削除されます!",
|
||||
"trash.dialog.move.confirmation.plural": "{count} 件のファイルを{trash_term}に移動してもよろしいですか?",
|
||||
"trash.dialog.move.confirmation.singular": "このファイルを{trash_term}に移動してもよろしいですか?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>警告:</b> このファイルを{trash_term}に移動できない場合、<b>完全に削除されます!</b>",
|
||||
"trash.dialog.title.plural": "ファイルの削除",
|
||||
"trash.dialog.title.singular": "ファイルの削除",
|
||||
"trash.name.generic": "ごみ箱",
|
||||
"trash.name.windows": "ごみ箱",
|
||||
"view.size.0": "極小",
|
||||
"view.size.1": "小",
|
||||
"view.size.2": "中",
|
||||
"view.size.3": "大",
|
||||
"view.size.4": "特大",
|
||||
"window.message.error_opening_library": "ライブラリを開く際にエラーが発生しました。",
|
||||
"window.title.error": "エラー",
|
||||
"window.title.open_create_library": "ライブラリを開く/作成する"
|
||||
}
|
||||
|
||||
@@ -1,67 +1,325 @@
|
||||
{
|
||||
"entries.duplicate.merge.label": "Fletter duplikatoppføringer …",
|
||||
"about.config_path": "Konfigurasjonsfilbane",
|
||||
"about.description": "TagStudio er en bilde- og filorganisajonsapplikasjon med et underliggende etikettbasert system som har fokus på å gi frihet og fleksibilitet til brukeren. Ingen proprietær programvare eller format, ingen sjø av sidevognsfiler, og ingen fullstendig omvelting av din filsystemstruktur.",
|
||||
"about.documentation": "Dokumentasjon",
|
||||
"about.license": "Lisens",
|
||||
"about.module.found": "Funnet",
|
||||
"about.title": "Om TagStudio",
|
||||
"about.website": "Webside",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Forhåndsutgivelse",
|
||||
"app.title": "{base_title} - Bibliotek '{library_dir}'",
|
||||
"color.color_border": "Bruk Sekundær Farge for Kant",
|
||||
"color.confirm_delete": "Er du sikker på at du vil slette fargen \"{color_name}\"?",
|
||||
"color.delete": "Slett Etikett",
|
||||
"color.import_pack": "Importer Fargepakke",
|
||||
"color.name": "Navn",
|
||||
"color.namespace.delete.prompt": "Er du sikker på at du vil slette dette fargenavnområdet? ALLE fargene i navnområdet vil bli slettet sammen med det!",
|
||||
"color.namespace.delete.title": "Slett Fargenavnområde",
|
||||
"color.new": "Ny Farge",
|
||||
"color.placeholder": "Farge",
|
||||
"color.primary": "Primærfarge",
|
||||
"color.primary_required": "Primærfarge (Påkrevd)",
|
||||
"color.secondary": "Sekundærfarge",
|
||||
"color.title.no_color": "Ingen Farge",
|
||||
"color_manager.title": "Administrer Etikettfarger",
|
||||
"dependency.missing.title": "{dependency} Ikke Funnet",
|
||||
"drop_import.description": "Følgende filer har filbaner som allerede finnes i biblioteket",
|
||||
"drop_import.duplicates_choice.plural": "De følgende {count} filene har filbaner som allerede finnes i biblioteket.",
|
||||
"drop_import.duplicates_choice.singular": "Den følgende filen har en filbane som allerede finnes i biblioteket.",
|
||||
"drop_import.progress.label.initial": "Importerer Nye Filer...",
|
||||
"drop_import.progress.label.plural": "Importerer Nye Filer...\n{count} Filer importert.{suffix}",
|
||||
"drop_import.progress.label.singular": "Importerer Nye Filer...\n1 Fil importert.{suffix}",
|
||||
"drop_import.progress.window_title": "Importer Filer",
|
||||
"drop_import.title": "Motstridende Fil(er)",
|
||||
"edit.color_manager": "Administrer Etikettfarger",
|
||||
"edit.copy_fields": "Kopier Felter",
|
||||
"edit.paste_fields": "Lim Inn Felter",
|
||||
"edit.tag_manager": "Administrer Etiketter",
|
||||
"entries.duplicate.merge": "Flett Duplikate Oppføringer",
|
||||
"entries.duplicate.merge.label": "Fletter duplikatoppføringer…",
|
||||
"entries.duplicate.refresh": "Oppdater Duplikate Oppføringer",
|
||||
"entries.duplicates.description": "Duplikate oppføringer er definert som flere oppføringer som peker til samme fil på disken. Å slå disse sammen vil kombinere etikettene og metadataen fra alle duplikater til én enkel oppføring. Disse må ikke forveksles med \"duplikate filer\", som er duplikater av selve filene utenfor TagStudio.",
|
||||
"entries.mirror": "Speil",
|
||||
"entries.mirror.confirmation": "Er du sikker på at du vil speile følgende {count} Oppføringer?",
|
||||
"entries.mirror.label": "Speiler {idx}/{total} Oppføringer...",
|
||||
"entries.mirror.title": "Speiler Oppføringer",
|
||||
"entries.mirror.window_title": "Speil Oppføringer",
|
||||
"entries.running.dialog.new_entries": "Legger til {total} Nye Filoppføringer...",
|
||||
"entries.running.dialog.title": "Legger til Nye Filoppføringer",
|
||||
"entries.tags": "Etiketter",
|
||||
"entries.unlinked.delete.confirm": "Slett følgende {count} oppføringer?",
|
||||
"entries.unlinked.delete.deleting": "Sletting av oppføringer",
|
||||
"entries.unlinked.delete": "Slett Frakoblede Oppføringer",
|
||||
"entries.unlinked.delete.confirm": "Er du sikker på at di voø slette følgende {count} oppføringer?",
|
||||
"entries.unlinked.delete.deleting": "Sletter Oppføringer",
|
||||
"entries.unlinked.delete.deleting_count": "Sletter {idx}/{count} ulenkede oppføringer",
|
||||
"entries.unlinked.delete_alt": "&Slett Frakoblede Oppføringer",
|
||||
"entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.<br><br>Frakoblede oppføringer kan bli automatisk gjenkoblet ved å søke i mappene dine eller slettet om det er ønsket.",
|
||||
"entries.unlinked.missing_count.none": "Frakoblede Oppføringer: N/A",
|
||||
"entries.unlinked.missing_count.some": "Frakoblede Oppføringer: {count}",
|
||||
"entries.unlinked.refresh_all": "Gjenoppfrisk alle",
|
||||
"entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {idx}/{missing_count} Oppføringer, {fixed_count} Klart Gjenkoblet",
|
||||
"entries.unlinked.relink.manual": "&Manuell Gjenkobling",
|
||||
"entries.unlinked.relink.title": "Gjenkobler Oppføringer",
|
||||
"entries.unlinked.scanning": "Skanner bibliotek for ulenkede oppføringer …",
|
||||
"entries.unlinked.search_and_relink": "&Søk && Gjenkobl",
|
||||
"entries.unlinked.title": "Fiks ulenkede oppføringer",
|
||||
"ffmpeg.missing.description": "FFmpeg og/eller FFprobe ble ikke funnet. FFmpeg er påkrevd for flermediell gjenspilling og miniatyrbilde.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Kopier Felt",
|
||||
"field.edit": "Rediger Felt",
|
||||
"field.paste": "Lim Inn Felt",
|
||||
"file.date_added": "Dato Lagt til",
|
||||
"file.date_created": "Dato opprettet",
|
||||
"file.date_modified": "Endringsdato",
|
||||
"file.dimensions": "Dimensjoner",
|
||||
"file.duplicates.description": "TagStudio støtter importering av DupeGuru resultater for å administrere duplikate filer.",
|
||||
"file.duplicates.dupeguru.advice": "Du kan fritt bruke DupeGuru for å slette uønskede filer etter speiling. Deretter, bruk TagStudio sin \"Fiks Frakoblede Oppføringer\"-løsning i Verktøymenyen for å slette de frakoblede oppføringene.",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru-filer (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "Last inn DupeGuru-fil",
|
||||
"file.duplicates.dupeguru.no_file": "Ingen DupeGuru-fil valgt",
|
||||
"file.duplicates.dupeguru.open_file": "Åpne DupeGuru-resultatfil",
|
||||
"file.duplicates.fix": "Fiks duplikatfiler",
|
||||
"file.duplicates.matches": "Duplikatfiltreff: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Duplikatfiltreff: N/A",
|
||||
"file.duplicates.mirror.description": "Speil oppføringsdataen til hvert duplikate sett, hvor all data kombineres uten å fjerne eller slette duplikate felter. Denne handlingen vil ikke slette noen filer eller data.",
|
||||
"file.duplicates.mirror_entries": "Speil oppføringer",
|
||||
"file.not_found": "Fant ikke filen:",
|
||||
"file.duration": "Varighet",
|
||||
"file.not_found": "Fant ikke filen",
|
||||
"file.open_file": "Åpne fil",
|
||||
"file.open_file_with": "Åpne fil med",
|
||||
"file.open_location.generic": "Åpne fil i utforsker",
|
||||
"file.open_location.mac": "Åpne i Finder",
|
||||
"file.open_location.windows": "Vis i Filutforsker",
|
||||
"file.path": "Filbane",
|
||||
"folders_to_tags.close_all": "Lukk Alle",
|
||||
"folders_to_tags.converting": "Konverterer mapper til etiketter",
|
||||
"folders_to_tags.description": "Lager etiketter basert på din filsturktur og legger dem til dine oppføringer.\n Strukturen viser alle de etikettene som vil bli lagd og hvilke oppføringer de vil bli lagt til på.",
|
||||
"folders_to_tags.open_all": "Åpne alle",
|
||||
"folders_to_tags.title": "Opprett etiketter fra mapper",
|
||||
"generic.add": "Legg til",
|
||||
"generic.apply": "Bruk",
|
||||
"generic.apply_alt": "&Bruk",
|
||||
"generic.cancel": "Avbryt",
|
||||
"generic.cancel_alt": "&Avbryt",
|
||||
"generic.close": "Lukk",
|
||||
"generic.continue": "Fortsett",
|
||||
"generic.copy": "Kopier",
|
||||
"generic.cut": "Klipp",
|
||||
"generic.delete": "Slett",
|
||||
"generic.delete_alt": "&Slett",
|
||||
"generic.done": "Ferdig",
|
||||
"generic.done_alt": "&Ferdig",
|
||||
"generic.edit": "Rediger",
|
||||
"generic.edit_alt": "&Rediger",
|
||||
"generic.filename": "Filnavn",
|
||||
"generic.missing": "Manglende",
|
||||
"generic.navigation.back": "Tilbake",
|
||||
"generic.navigation.next": "Neste",
|
||||
"generic.none": "Ingen",
|
||||
"generic.overwrite": "Overskriv",
|
||||
"generic.overwrite_alt": "&Overskriv",
|
||||
"generic.paste": "Lim inn",
|
||||
"generic.recent_libraries": "Nylige bibliotek",
|
||||
"generic.rename": "Gi nytt navn",
|
||||
"generic.rename_alt": "&Gi nytt navn",
|
||||
"generic.reset": "Tilbakestill",
|
||||
"generic.save": "Lagre",
|
||||
"generic.skip": "Hopp over",
|
||||
"generic.skip_alt": "&Hopp over",
|
||||
"home.search": "Søk",
|
||||
"home.search_entries": "Søk etter oppføringer",
|
||||
"home.search_library": "Søk i Biblioteket",
|
||||
"home.search_tags": "Søk etter etiketter",
|
||||
"home.thumbnail_size": "Miniatyrbildestørrelse",
|
||||
"home.thumbnail_size.extra_large": "Ekstra Store Miniatyrbilder",
|
||||
"home.thumbnail_size.large": "Store Miniatyrbilder",
|
||||
"home.thumbnail_size.medium": "Medium Store Miniatyrbilder",
|
||||
"home.thumbnail_size.mini": "Bittesmå Miniatyrbilder",
|
||||
"home.thumbnail_size.small": "Små Miniatyrbilder",
|
||||
"ignore_list.add_extension": "Legg til utvidelse",
|
||||
"ignore_list.mode.exclude": "Utelat",
|
||||
"ignore_list.mode.include": "Inkluder",
|
||||
"ignore_list.mode.label": "Listemodus:",
|
||||
"ignore_list.title": "Filutvidelse",
|
||||
"json_migration.checking_for_parity": "Sjekker om det stemmer Overens...",
|
||||
"json_migration.creating_database_tables": "Lager SQL-databasetabeller...",
|
||||
"json_migration.description": "<br>Start og forhåndsvis resultatene av biblioteksmigrasjonsprosessen. Det konverterte biblioteket vil <i>ikke</i> bli brukt med mindre du trykker på \"Fullfør Migrering\". <br><br>Biblioteksdata burde enten ha matchende verdier eller ha en \"Matchet\" etikett. Verdier som ikke matcher vil bli vist i rød og ha et \"<b>(!)</b>\" symbol ved siden av dem.<br><center><i>Denne prosessen kan ta opp til flere minutter for større biblioteker</i></center>",
|
||||
"json_migration.discrepancies_found": "Biblioteksavvik Funnet",
|
||||
"json_migration.discrepancies_found.description": "Avvik ble funnet mellom det originale og det konverterte biblioteksformatet. Vennligts se gjennom og velg mellom å fortsette migreringen eller å avbryte.",
|
||||
"json_migration.finish_migration": "Fullfør Migrering",
|
||||
"json_migration.heading.aliases": "Alternative navn:",
|
||||
"json_migration.heading.colors": "Farger:",
|
||||
"json_migration.heading.differ": "Avvik",
|
||||
"json_migration.heading.entires": "Oppføringer:",
|
||||
"json_migration.heading.extension_list_type": "Type av Utvidelsesliste:",
|
||||
"json_migration.heading.fields": "Felter:",
|
||||
"json_migration.heading.file_extension_list": "Filutvidelse Liste:",
|
||||
"json_migration.heading.match": "Matchet",
|
||||
"json_migration.heading.names": "Navn:",
|
||||
"json_migration.heading.parent_tags": "Overordnede Etiketter:",
|
||||
"json_migration.heading.paths": "Baner:",
|
||||
"json_migration.heading.shorthands": "Forkortelser:",
|
||||
"json_migration.heading.tags": "Etiketter:",
|
||||
"json_migration.info.description": "Lagringsfiler til biblioteket lagetr med TagStudio av versjoner <b>9.4 eller under</b> må bli migrert ti ldet nye <b>v9.5+</b> formatet.<br><h2>Hva du trenger å vite:</h2><ul><li>Din eksisterende lagringsfil vil <b><i>ikke</i></b> bli slettet</li><li>Dine personlige filer vil <b><i>IKKE</i></b> bli slettet, flyttet, eller modifisert</li><li>Det nye v9.5+ lagringsformatet kan ikke bli åpnet i tidligere versjoner av TagStudio</li></ul><h3>Hva som har endret seg:</h3><ul><li>\"Etikettfelter\" har blitt erstattet av \"Etikettkategorier\". I stedet for å legge til etiketter til felter, så legges etiketter direkte til filoppføringen. De vil da bli automatisk organisert i kategorier basert på overordnede etiketter markert med den nye \"Er Kategori\" egenskapen i etikettredigeringsmenyen. Enhver etikett kan bli markert som en kategori, og underordnede etiketter vil sortere seg selv under overordnede etiketter som er markert som kategorier. \"Favoritt\" og \"Arkivert\" etikettene er nå underordnet en ny \"Meta-Etiketter\" etikett som er markert som en kategori automatisk.</li><li>Etikettfarger har blitt finjustert og utvidet. Enkelte farger har fått nytt navn eller blitt slått sammen, men alle etikettfarger vil fortsatt bli konvertert til eksakte eller lignende farger i v9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Migrerer {entries:,d} Filoppføringer...",
|
||||
"json_migration.migration_complete": "Migrering Ferdig!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Migrering Ferdig, Avvik Funnet",
|
||||
"json_migration.start_and_preview": "Start og Forhåndsvis",
|
||||
"json_migration.title": "Lagre Formatmigrasjon: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Bibliotek</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Bibliotek</h2>",
|
||||
"landing.open_create_library": "Åpne/Lag nytt Bibliotek {shortcut}",
|
||||
"library.field.add": "Legg til felt",
|
||||
"library.field.confirm_remove": "Fjern dette «\"{name}\"»-feltet?",
|
||||
"library.field.mixed_data": "Blandet data",
|
||||
"library.field.remove": "Fjern felt",
|
||||
"library.missing": "Posisjon mangler",
|
||||
"library.name": "Bibliotek",
|
||||
"library.refresh.scanning.plural": "Skanner Mapper for Nye Filer...\n{searched_count} Filer Sjekket, {found_count} Nye Filer Funnet",
|
||||
"library.refresh.scanning.singular": "Skanner Mapper for Nye Filer...\n{searched_count} Fil Sjekket, {found_count} Nye Filer Funnet",
|
||||
"library.refresh.scanning_preparing": "Skanner Mapper for Nye Filer...\nForbereder...",
|
||||
"library.refresh.title": "Oppdaterer Mapper",
|
||||
"library.scan_library.title": "Skanning av bibliotek",
|
||||
"library_object.name": "Navn",
|
||||
"library_object.name_required": "Navn (Påkrevd)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library_object.slug_required": "ID Slug (Påkrevd)",
|
||||
"macros.running.dialog.new_entries": "Kjører Konfigurerte Macroer på {count}/{total} Nye Filoppføringer...",
|
||||
"macros.running.dialog.title": "Kjører Macroer på Nye Oppføringer",
|
||||
"media_player.autoplay": "Spill av Automatisk",
|
||||
"media_player.loop": "Gjenta",
|
||||
"menu.delete_selected_files_ambiguous": "Flytt Fil(er) til {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Flytt Filer til {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Flytt Fil til {trash_term}",
|
||||
"menu.edit": "Rediger",
|
||||
"menu.edit.ignore_list": "Ignorer Filer og Mapper",
|
||||
"menu.edit.manage_file_extensions": "Administrer Filutvidelser",
|
||||
"menu.edit.manage_tags": "Administrer Etiketter",
|
||||
"menu.edit.new_tag": "Ny &Etikett",
|
||||
"menu.file": "Fil",
|
||||
"menu.file.clear_recent_libraries": "Fjern Nylige",
|
||||
"menu.file.close_library": "&Lukk Bibliotek",
|
||||
"menu.file.missing_library.message": "Plasseringen til biblioteket \"{library}\" kan ikke finnes.",
|
||||
"menu.file.missing_library.title": "Manglende Bibliotek",
|
||||
"menu.file.new_library": "Nytt Bibliotek",
|
||||
"menu.file.open_create_library": "&Åpne/Lag nytt Bibliotek",
|
||||
"menu.file.open_library": "Åpne Bibliotek",
|
||||
"menu.file.open_recent_library": "Åpne Nylig",
|
||||
"menu.file.refresh_directories": "&Oppdater Mapper",
|
||||
"menu.file.save_backup": "&Lagre Sikkerhetskopi av Bibliotek",
|
||||
"menu.file.save_library": "Lagre Bibliotek",
|
||||
"menu.help": "Hjelp",
|
||||
"menu.help.about": "Om",
|
||||
"menu.macros": "Makroer",
|
||||
"menu.macros.folders_to_tags": "Mapper til Etiketter",
|
||||
"menu.select": "Velg",
|
||||
"menu.settings": "Innstillinger...",
|
||||
"menu.tools": "Verktøy",
|
||||
"menu.tools.fix_duplicate_files": "Fiks Duplikate &Filer",
|
||||
"menu.tools.fix_unlinked_entries": "Fiks &Frakoblede Oppføringer",
|
||||
"menu.view": "&Se",
|
||||
"menu.window": "Vindu",
|
||||
"namespace.create.description": "Navnområder er brukt av TagStudio for å skille grupper som etikketer og farger på en måte som gjør dem enkle å eksportere og dele. Navnområder som starter med \"tagstudio\" er reservert av TagStudio for intern bruk.",
|
||||
"namespace.create.description_color": "Etikettfarger bruker navnområder som fargepalettgrupper. Alle egendefinerte farger må være under et navnområde først.",
|
||||
"namespace.create.title": "Lag Navnområde",
|
||||
"namespace.new.button": "Nytt Navnområde",
|
||||
"namespace.new.prompt": "Lag et Nytt Navnområde for å legge til Egendefinerte Farger!",
|
||||
"preview.multiple_selection": "<b>{count}</b> Enheter Valgt",
|
||||
"preview.no_selection": "Ingen elementer valgt",
|
||||
"select.add_tag_to_selected": "Legg til Etikett til Valg",
|
||||
"select.all": "Velg Alle",
|
||||
"select.clear": "Fjern valg",
|
||||
"select.inverse": "Inverter valg",
|
||||
"settings.clear_thumb_cache.title": "Tøm miniatyrbildecachen",
|
||||
"settings.dateformat.english": "Engelsk",
|
||||
"settings.dateformat.international": "Internasjonal",
|
||||
"settings.dateformat.label": "Datoformat",
|
||||
"settings.dateformat.system": "System",
|
||||
"settings.filepath.label": "Synlighet til Filbaner",
|
||||
"settings.filepath.option.full": "Vis Hele Filbaner",
|
||||
"settings.filepath.option.name": "Vis kun Filnavn",
|
||||
"settings.filepath.option.relative": "Vis Relative Filbaner",
|
||||
"settings.global": "Globale Innstillinger",
|
||||
"settings.hourformat.label": "24-timersklokke",
|
||||
"settings.language": "Språk",
|
||||
"settings.library": "Biblioteksinnstilllinger",
|
||||
"settings.open_library_on_start": "Åpne Bibliotek ved Start",
|
||||
"settings.page_size": "Sidestørrelse",
|
||||
"settings.restart_required": "Vennligst start TagStudio på nytt for at endringene skal trå i kraft.",
|
||||
"settings.show_filenames_in_grid": "Vis Filnavn i Rutenett",
|
||||
"settings.show_recent_libraries": "Vis Nylige Bibliotek",
|
||||
"settings.tag_click_action.add_to_search": "Legg til Etikett til Søk",
|
||||
"settings.tag_click_action.label": "Handling ved trykk på Etikett",
|
||||
"settings.tag_click_action.open_edit": "Rediger Etikett",
|
||||
"settings.tag_click_action.set_search": "Søk etter Etikett",
|
||||
"settings.theme.dark": "Mørkt",
|
||||
"settings.theme.label": "Tema:",
|
||||
"settings.theme.light": "Lyst",
|
||||
"settings.theme.system": "System",
|
||||
"settings.title": "Innstillinger",
|
||||
"settings.zeropadding.label": "Legg til nuller foran Datotall",
|
||||
"sorting.direction.ascending": "Økende",
|
||||
"sorting.direction.descending": "Synkende",
|
||||
"splash.opening_library": "Åpner Biblioteket \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "Slettet {count} files!",
|
||||
"status.deleted_file_singular": "Slettet 1 fil!",
|
||||
"status.deleted_none": "Ingen filer slettet.",
|
||||
"status.deleted_partial_warning": "Slettet bare {count} fil(er)! Sjekk om noen av filene mangler eller er i bruk.",
|
||||
"status.deleting_file": "Sletter fil [{i}/{count}]: \"{path}\"...",
|
||||
"status.library_backup_in_progress": "Lagrer Sikkerhetskopi av Bibliotek...",
|
||||
"status.library_backup_success": "Kopi av bibliotek lagret i: \"{path}\" ({time_span})",
|
||||
"status.library_save_success": "Bibliotek lagret og lukket.",
|
||||
"status.library_search_query": "Søker i biblioteket etter",
|
||||
"status.results": "Resultat",
|
||||
"tag.add": "Legg til etikett",
|
||||
"status.library_closed": "Bibliotek Lukket ({time_span})",
|
||||
"status.library_closing": "Lukker Bibliotek...",
|
||||
"status.library_save_success": "Bibliotek lagret og lukket!",
|
||||
"status.library_search_query": "Søker Biblioteket...",
|
||||
"status.library_version_expected": "Forventet:",
|
||||
"status.library_version_found": "Funnet:",
|
||||
"status.library_version_mismatch": "Biblioteksversjon stemmer ikke overens!",
|
||||
"status.results": "Resultater",
|
||||
"status.results.invalid_syntax": "Ugyldig Syntaks på Søk:",
|
||||
"status.results_found": "{count} Resultater Funnet ({time_span})",
|
||||
"tag.add": "Legg til Etikett",
|
||||
"tag.add.plural": "Legg til Etiketter",
|
||||
"tag.add_to_search": "Legg til søk",
|
||||
"tag.aliases": "Alternative Navn",
|
||||
"tag.all_tags": "Alle Etiketter",
|
||||
"tag.choose_color": "Velg Etikettfarge",
|
||||
"tag.color": "Farge",
|
||||
"tag.confirm_delete": "Er du sikker på at du vil slette etiketten \"{tag_name}\"?",
|
||||
"tag.create": "Lag Etikett",
|
||||
"tag.create_add": "Lag && Legg til \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Bruk denne etiketten for éntydighet",
|
||||
"tag.edit": "Rediger Etikett",
|
||||
"tag.is_category": "Er Kategori",
|
||||
"tag.name": "Navn",
|
||||
"tag.new": "Ny etikett",
|
||||
"tag.parent_tags": "Overordnede Etiketter",
|
||||
"tag.parent_tags.add": "Legg til Overordnede Etikett(er)",
|
||||
"tag.parent_tags.description": "Denne etiketten kan opptre som hvilken som helst av disse overordnede etikettene i søk.",
|
||||
"tag.remove": "Fjern Etikett",
|
||||
"tag.search_for_tag": "Søk etter etikett",
|
||||
"tag_manager.title": "Biblioteksetiketter"
|
||||
"tag.shorthand": "Forkortelse",
|
||||
"tag.tag_name_required": "Etikettnavn (Påkrevd)",
|
||||
"tag.view_limit": "Se Grense:",
|
||||
"tag_manager.title": "Biblioteksetiketter",
|
||||
"trash.context.ambiguous": "Flytt fil(er) til {trash_term}",
|
||||
"trash.context.plural": "Flytt filer til {trash_term}",
|
||||
"trash.context.singular": "Flytt fil til {trash_term}",
|
||||
"trash.dialog.disambiguation_warning.plural": "Dette vil fjerne de fra TagStudio <i>OG</i> ditt filsystem!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Dette vil fjerne den fra TagStudio <i>OG</i> ditt filsystem!",
|
||||
"trash.dialog.move.confirmation.plural": "Er du sikker på at du vil flytte disse {count} filene til {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "Er du sikker på at du vil flytte denne filen til {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>ADVARSEL!</b> Hvis denne filen ikke kan bli flyttet til {trash_term}, så vil den bli <b>slettet permanent!</b>",
|
||||
"trash.dialog.title.plural": "Slett Filer",
|
||||
"trash.dialog.title.singular": "Slett Fil",
|
||||
"trash.name.generic": "Søppelkassen",
|
||||
"trash.name.windows": "Papirkurven",
|
||||
"view.size.0": "Bitteliten",
|
||||
"view.size.1": "Liten",
|
||||
"view.size.2": "Medium Stor",
|
||||
"view.size.3": "Stor",
|
||||
"view.size.4": "Ekstra Stor",
|
||||
"window.message.error_opening_library": "Feil ved åpning av biblioteket.",
|
||||
"window.title.error": "Feil",
|
||||
"window.title.open_create_library": "Åpne/Lag nytt Bibliotek"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
{
|
||||
"about.config_path": "Configuratie Pad",
|
||||
"about.documentation": "Documentatie",
|
||||
"about.license": "Licentie",
|
||||
"about.module.found": "Gevonden",
|
||||
"about.title": "Over TagSudio",
|
||||
"about.website": "Website",
|
||||
"app.git": "Git Commit",
|
||||
"app.pre_release": "Pre-Release",
|
||||
"color.confirm_delete": "Weet u zeker dat u de kleur \"{color_name}\" wilt verwijderen?",
|
||||
"color.delete": "Verwijder Label",
|
||||
"color.name": "Naam",
|
||||
"color.new": "Nieuwe Kleur",
|
||||
"color.placeholder": "Kleur",
|
||||
"color.primary": "Primaire Kleur",
|
||||
"color.primary_required": "Primaire Kleur (Verplicht)",
|
||||
"color.secondary": "Secondaire Kleur",
|
||||
"color.title.no_color": "Geen Kleur",
|
||||
"color_manager.title": "Beheer Label Kleuren",
|
||||
"dependency.missing.title": "{dependency} Niet Gevonden",
|
||||
"drop_import.progress.label.initial": "Nieuwe bestanden importeren…",
|
||||
"drop_import.progress.label.plural": "Nieuwe bestanden importeren…\n{count} bestanden geïmporteerd.{suffix}",
|
||||
"drop_import.progress.label.singular": "Nieuwe bestanden importeren…\n1 bestand geïmporteerd.{suffix}",
|
||||
"drop_import.progress.window_title": "Importeer bestanden",
|
||||
"edit.color_manager": "Beheer Label Kleuren",
|
||||
"edit.copy_fields": "Velden Kopiëren",
|
||||
"edit.paste_fields": "Velden Plakken",
|
||||
"edit.tag_manager": "Beheer Labels",
|
||||
@@ -69,7 +88,7 @@
|
||||
"json_migration.title": "Migratie Formaat Opslaan: \"{path}\"",
|
||||
"library.field.add": "Veld Toevoegen",
|
||||
"library.field.mixed_data": "Gemixte Data",
|
||||
"library.field.remove": "Veld Verwijderen",
|
||||
"library.field.remove": "Veld Weghalen",
|
||||
"menu.delete_selected_files_ambiguous": "Bestand(en) verplaatsen naar {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Bestanden verplaatsen naar {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Bestand verplaatsen naar {trash_term}",
|
||||
@@ -100,7 +119,7 @@
|
||||
"tag.edit": "Label Aanpassen",
|
||||
"tag.name": "Naam",
|
||||
"tag.new": "Nieuw Label",
|
||||
"tag.remove": "Verwijder Label",
|
||||
"tag.remove": "Label Weghalen",
|
||||
"trash.dialog.title.plural": "Bestanden Verwijderen",
|
||||
"trash.dialog.title.singular": "Bestand Verwijden"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"about.config_path": "Ścieżka do pliku konfiguracji",
|
||||
"about.documentation": "Dokumentacja",
|
||||
"about.license": "Licencja",
|
||||
"about.module.found": "Znaleziono",
|
||||
"about.title": "O programie",
|
||||
"about.website": "Strona Główna",
|
||||
"app.git": "Migawka Git",
|
||||
"app.pre_release": "Przedpremiera",
|
||||
"app.title": "{base_title} - Biblioteka '{library_dir}'",
|
||||
@@ -17,6 +22,7 @@
|
||||
"color.secondary": "Kolor pochodny",
|
||||
"color.title.no_color": "Brak koloru",
|
||||
"color_manager.title": "Zarządzaj kolorami tagów",
|
||||
"dependency.missing.title": "Nie znaleziono {dependency}",
|
||||
"drop_import.description": "Następujące pliki pasują ścieżkami do już istniejących w bibliotece",
|
||||
"drop_import.duplicates_choice.plural": "Następujące {count} pliki pasują ścieżkami do już istniejących w bibliotece.",
|
||||
"drop_import.duplicates_choice.singular": "Następujący plik pasuje ścieżką do już istniejącego w bibliotece.",
|
||||
@@ -56,6 +62,7 @@
|
||||
"entries.unlinked.scanning": "Skanowanie biblioteki dla odłączonych wpisów...",
|
||||
"entries.unlinked.search_and_relink": "&Wyszukaj && Zalinkuj ponownie",
|
||||
"entries.unlinked.title": "Napraw odłączone wpisy",
|
||||
"ffmpeg.missing.description": "Nie odnaleziono FFmpeg lub FFprobe. FFmpeg jest wymagany do odtwarzania multimediów i do wyświetlania miniaturek.",
|
||||
"field.copy": "Skopiuj pole",
|
||||
"field.edit": "Edytuj pole",
|
||||
"field.paste": "Wklej pole",
|
||||
@@ -81,6 +88,7 @@
|
||||
"file.open_location.generic": "Pokaż plik w przeglądarce plików",
|
||||
"file.open_location.mac": "Pokaż plik w Finderze",
|
||||
"file.open_location.windows": "Pokaż plik w Eksploratorze plików",
|
||||
"file.path": "Ścieżka Pliku",
|
||||
"folders_to_tags.close_all": "Zamknij wszystko",
|
||||
"folders_to_tags.converting": "Konwertowanie folderów na tagi",
|
||||
"folders_to_tags.description": "Tworzy tagi na podstawie struktury folderów i stosuje je do wpisów.\n Poniższa struktura przedstawia wszystkie utworzone tagi i wpisy, do których zostaną one zastosowane.",
|
||||
@@ -102,6 +110,7 @@
|
||||
"generic.edit": "Edytuj",
|
||||
"generic.edit_alt": "&Edytuj",
|
||||
"generic.filename": "Nazwa pliku",
|
||||
"generic.missing": "Nie znaleziono",
|
||||
"generic.navigation.back": "Wstecz",
|
||||
"generic.navigation.next": "Dalej",
|
||||
"generic.none": "Żaden",
|
||||
@@ -182,6 +191,7 @@
|
||||
"menu.file": "&Plik",
|
||||
"menu.file.clear_recent_libraries": "Wyczyść ostatnie",
|
||||
"menu.file.close_library": "&Zamknij bibliotekę",
|
||||
"menu.file.missing_library.message": "Lokalizacja biblioteki \"{library}\" nie została odnaleziona.",
|
||||
"menu.file.new_library": "Nowa biblioteka",
|
||||
"menu.file.open_create_library": "&Otwórz/Stwórz bibliotekę",
|
||||
"menu.file.open_library": "Otwórz bibliotekę",
|
||||
@@ -209,7 +219,9 @@
|
||||
"select.add_tag_to_selected": "Dodaj tag do zaznaczonych",
|
||||
"select.all": "Zaznacz wszystko",
|
||||
"select.clear": "Odznacz zaznaczenie",
|
||||
"select.inverse": "Odwróć zaznaczenie",
|
||||
"settings.clear_thumb_cache.title": "Wyczyść pamięć podręczną miniaturek",
|
||||
"settings.filepath.option.full": "Pokaż Pełną Ścieżkę",
|
||||
"settings.language": "Język",
|
||||
"settings.open_library_on_start": "Otwieraj bibliotekę podczas startu",
|
||||
"settings.restart_required": "Zrestartuj TagStudio żeby zmiany zaczęły obowiązywać.",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"about.config_path": "Mlafuplas fu sentakuzma",
|
||||
"about.description": "TagStudio es riso & parjat fu mlafu zeting ke bruk festaretol, hadafvui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena zeting os mlafufal, jamnai mare fu flanka mlafu au perpa tumam fu kompyu.",
|
||||
"about.description": "TagStudio zeting ke parjat mlafu au riso mit festaretol, hadafvui per anta sentakuzma au mange dekizma brukdjin made. Nil kinijena zeting os mlafufal, jamnai mare fu flanka mlafu au perpa tumam fu kompyu.",
|
||||
"about.documentation": "Mahaklarazma",
|
||||
"about.license": "Bruk-ruuru",
|
||||
"about.license": "Brukruuru",
|
||||
"about.module.found": "Finnajena",
|
||||
"about.title": "Tsui TagStudio",
|
||||
"about.website": "Zelehti",
|
||||
@@ -18,14 +18,15 @@
|
||||
"color.namespace.delete.title": "Keste vargetumam",
|
||||
"color.new": "Neo varge",
|
||||
"color.placeholder": "Varge",
|
||||
"color.primary": "Lestevikti varge",
|
||||
"color.primary_required": "Lestevikti varge (Trengena)",
|
||||
"color.secondary": "Minusvikti varge",
|
||||
"color.primary": "Atama varge",
|
||||
"color.primary_required": "Atama varge (Trengena)",
|
||||
"color.secondary": "Andr varge",
|
||||
"color.title.no_color": "Nil varge",
|
||||
"color_manager.title": "Jewalt varge fu festaretol",
|
||||
"drop_import.description": "Afto mlafu sama andr mlafuplas ke umdaus ine mlafuhuomi",
|
||||
"drop_import.duplicates_choice.plural": "Afto mlafu {count} sama andr mlafuplas ke umdaus ine mlafuhuomi.",
|
||||
"drop_import.duplicates_choice.singular": "Afto mlafu sama andr mlafuplas ke umdaus ine mlafuhuomi.",
|
||||
"dependency.missing.title": "{dependency} Nai finnajena",
|
||||
"drop_import.description": "Afto mlafu sama andr mlafuplas ke vona ine mlafuhuomi",
|
||||
"drop_import.duplicates_choice.plural": "Afto mlafu {count} sama andr mlafuplas ke vona ine mlafuhuomi.",
|
||||
"drop_import.duplicates_choice.singular": "Afto mlafu sama andr mlafuplas ke vona ine mlafuhuomi.",
|
||||
"drop_import.progress.label.initial": "Gotova neo mlafu ima...",
|
||||
"drop_import.progress.label.plural": "Gotova neo mlafu ima...\n{count} Mlafu nasiijena.{suffix}",
|
||||
"drop_import.progress.label.singular": "Gotova neo mlafu ima...\n1 Mlafu nasiijena.{suffix}",
|
||||
@@ -62,7 +63,241 @@
|
||||
"entries.unlinked.scanning": "Suha mlafuhuomi ima per tsunaganaijena shiruzmakaban...",
|
||||
"entries.unlinked.search_and_relink": "&Suha &&Tsunaga gen",
|
||||
"entries.unlinked.title": "Fiks tsunaganaijena shiruzmakaban",
|
||||
"ffmpeg.missing.description": "FFmpeg au/os FFprobe nai finnajena. TagStudio treng FFmpeg per mahase riso.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Mverm shiruzmafal",
|
||||
"field.edit": "Kawari shiruzmafal",
|
||||
"field.paste": "Nasii shiruzmafal"
|
||||
"field.paste": "Nasii shiruzmafal",
|
||||
"file.date_added": "Dag nasiijenadan",
|
||||
"file.date_created": "Dag mahajenadan",
|
||||
"file.date_modified": "Dag kawarijenadan",
|
||||
"file.dimensions": "Stuuratailasku",
|
||||
"file.duplicates.description": "TagStudio kjoka nasii shiruzma DupeGuru kara per jewalt mverm.",
|
||||
"file.duplicates.dupeguru.advice": "Za melon mahajena, du deki bruk DupeGuru per keste nai vilena mlafu. Sit, bruk tel fu TagStudio, haisa \"Fiks tsunaganaijena shiruzmakaban\" na tropos sentakutumam per keste tsunaganaijena shiruzmakaban.",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru mlafu (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Gotova mlafu fu DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "Jamnai sentakujena mlafu fu DupeGuru",
|
||||
"file.duplicates.dupeguru.open_file": "Auki shiruzma mlafu fu Dupeguru",
|
||||
"file.duplicates.fix": "Fiks mverm",
|
||||
"file.duplicates.matches": "Mverm-atai: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Mverm-atai: N/A",
|
||||
"file.duplicates.mirror.description": "Maha melon fu shiruzma shiruzmakaban kara na al mverm fu samakaban, afto zol visk al shiruzma, men zolnai keste os mverm shiruzmafal. Afto zolnai keste joku mlafu os shiruzma.",
|
||||
"file.duplicates.mirror_entries": "&Maha melon fu shiruzmakaban",
|
||||
"file.duration": "Pitkatai",
|
||||
"file.not_found": "Mlafu nai finnajena",
|
||||
"file.open_file": "Auki mlafu",
|
||||
"file.open_file_with": "Auki mlafu mit",
|
||||
"file.open_location.generic": "Mahase mlafu na File Explorer",
|
||||
"file.open_location.mac": "Mahase na Finder",
|
||||
"file.open_location.windows": "Mahase na File Explorer",
|
||||
"file.path": "Mlafuplas",
|
||||
"folders_to_tags.close_all": "Kini al",
|
||||
"folders_to_tags.converting": "Kjannos mlafukaban na festaretol",
|
||||
"folders_to_tags.description": "Afto maha festaretol parjatazma fu mlafukaban kara au nasii na shiruzmakaban.\n Parjatazma una mahase al festaretol ke zol mahajena au ka shiruzmakaban hej zol nasiijena na.",
|
||||
"folders_to_tags.open_all": "Auki al",
|
||||
"folders_to_tags.title": "Maha festaretol mlafukaban kara",
|
||||
"generic.add": "Nasii",
|
||||
"generic.apply": "Maha gvir",
|
||||
"generic.apply_alt": "&Maha gvir",
|
||||
"generic.cancel": "Yamete",
|
||||
"generic.cancel_alt": "&Yamete",
|
||||
"generic.close": "Kini",
|
||||
"generic.continue": "Beng",
|
||||
"generic.copy": "Mverm",
|
||||
"generic.cut": "Cher",
|
||||
"generic.delete": "Keste",
|
||||
"generic.delete_alt": "&Keste",
|
||||
"generic.done": "Owari",
|
||||
"generic.done_alt": "&Owari",
|
||||
"generic.edit": "Kawari",
|
||||
"generic.edit_alt": "&Kawari",
|
||||
"generic.filename": "Mlafunamae",
|
||||
"generic.missing": "Harnai",
|
||||
"generic.navigation.back": "Suruk",
|
||||
"generic.navigation.next": "Mirai",
|
||||
"generic.none": "Nil",
|
||||
"generic.overwrite": "Vasu",
|
||||
"generic.overwrite_alt": "&Vasu",
|
||||
"generic.paste": "Nasii",
|
||||
"generic.recent_libraries": "Moloda mlafuhuomi",
|
||||
"generic.rename": "Haisagen",
|
||||
"generic.rename_alt": "&Haisagen",
|
||||
"generic.reset": "Gensintua",
|
||||
"generic.save": "Ufne",
|
||||
"generic.skip": "Pal",
|
||||
"generic.skip_alt": "&Pal",
|
||||
"home.search": "Suha",
|
||||
"home.search_entries": "Suha shiruzmakaban",
|
||||
"home.search_library": "Suha mlafuhuomi",
|
||||
"home.search_tags": "Suha festaretol",
|
||||
"home.thumbnail_size": "Stuuratai fu risonen",
|
||||
"home.thumbnail_size.extra_large": "Plusstuur risonen",
|
||||
"home.thumbnail_size.large": "Stuur risonen",
|
||||
"home.thumbnail_size.medium": "Mellan risonen",
|
||||
"home.thumbnail_size.mini": "Chisai risonen",
|
||||
"home.thumbnail_size.small": "Chisaidai risonen",
|
||||
"ignore_list.add_extension": "&Nasii taksanting",
|
||||
"ignore_list.mode.exclude": "Ansenai",
|
||||
"ignore_list.mode.include": "Anse",
|
||||
"ignore_list.mode.label": "Tumam fal:",
|
||||
"ignore_list.title": "Mlafufal",
|
||||
"json_migration.checking_for_parity": "Taskama per chigauzma ima...",
|
||||
"json_migration.creating_database_tables": "Maha tumam na SQL Database ima...",
|
||||
"json_migration.description": "<br>Hadji au de se shiruzma fu",
|
||||
"json_migration.heading.aliases": "Andrnamae:",
|
||||
"json_migration.heading.colors": "Varge:",
|
||||
"json_migration.heading.differ": "Tchigauzma",
|
||||
"json_migration.heading.entires": "Shiruzmakaban:",
|
||||
"json_migration.heading.extension_list_type": "Fal fu taksanting tumam:",
|
||||
"json_migration.heading.fields": "Shiruzmafal:",
|
||||
"json_migration.heading.file_extension_list": "Tumam fu mlafufal:",
|
||||
"json_migration.heading.match": "Finnajena sama",
|
||||
"json_migration.heading.names": "Namae:",
|
||||
"json_migration.heading.parent_tags": "Atama festaretol:",
|
||||
"json_migration.heading.paths": "Mlafuplas:",
|
||||
"json_migration.heading.shorthands": "Namaenen:",
|
||||
"json_migration.heading.tags": "Festaretol:",
|
||||
"json_migration.migrating_files_entries": "Ugoki {entries:,d} shiruzmakaban ima...",
|
||||
"json_migration.migration_complete": "Ugokizma owari!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Ugokizma owari, chigauzma finnajena",
|
||||
"json_migration.start_and_preview": "Hadji au senen",
|
||||
"json_migration.title": "Ufne atamafal ugokizma: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ Mlafuhuomi</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 Mlafuhuomi</h2>",
|
||||
"landing.open_create_library": "Auki/Maha mlafuhuomi {shortcut}",
|
||||
"library.field.add": "Nasii shiruzmafal",
|
||||
"library.field.confirm_remove": "Du kestetsa afto \"{name}\" shiruzmafal we?",
|
||||
"library.field.mixed_data": "Viskena shiruzma",
|
||||
"library.field.remove": "Keste shiruzmafal",
|
||||
"library.missing": "Mlafuplas fu mlafuhuomi nai finnajenadan",
|
||||
"library.name": "Mlafuhuomi",
|
||||
"library.refresh.scanning.plural": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning.singular": "Taskama mlafukaban fu neo mlafu ima...\n{searched_count} mlafu suhajenadan, {found_count} neo mlafu finnajenadan",
|
||||
"library.refresh.scanning_preparing": "Taskama mlafukaban fu neo mlafu ima...\nGotova ima...",
|
||||
"library.refresh.title": "Gengotova al mlafukaban",
|
||||
"library.scan_library.title": "Taskama mlafuhuomi ima",
|
||||
"library_object.name": "Namae",
|
||||
"library_object.name_required": "Namae (Trengjena)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library_object.slug_required": "ID Slug (Trengjena)",
|
||||
"media_player.autoplay": "Hadji-bistra",
|
||||
"media_player.loop": "Genslucha",
|
||||
"menu.delete_selected_files_ambiguous": "Ugoki mlafu {trash_term} made",
|
||||
"menu.delete_selected_files_plural": "Ugoki mlafu {trash_term} made",
|
||||
"menu.delete_selected_files_singular": "Ugoki mlafu {trash_term} made",
|
||||
"menu.edit": "Kawari",
|
||||
"menu.edit.ignore_list": "Nai shuchu na mlafu au mlafukaban",
|
||||
"menu.edit.manage_file_extensions": "Jewalt mlafufal",
|
||||
"menu.edit.manage_tags": "Jewalt festaretol",
|
||||
"menu.edit.new_tag": "Neo &festaretol",
|
||||
"menu.file": "&Mlafu",
|
||||
"menu.file.clear_recent_libraries": "Keste moloda",
|
||||
"menu.file.close_library": "&Kini mlafuhuomi",
|
||||
"menu.file.missing_library.title": "Finnajenanai mlafuhuomi",
|
||||
"menu.file.new_library": "Neo mlafuhuomi",
|
||||
"menu.file.open_create_library": "&Auki/maha mlafuhuomi",
|
||||
"menu.file.open_library": "Auki mlafuhuomi",
|
||||
"menu.file.open_recent_library": "Auki moloda",
|
||||
"menu.file.refresh_directories": "&Gengotova al mlafukaban",
|
||||
"menu.file.save_backup": "&Ufne mverm fu mlafuhuomi",
|
||||
"menu.file.save_library": "Ufne mlafuhuomi",
|
||||
"menu.help": "&Aputsa",
|
||||
"menu.help.about": "Shiruplus",
|
||||
"menu.macros": "&Zetingnen",
|
||||
"menu.macros.folders_to_tags": "Mlafukaban festaretol made",
|
||||
"menu.select": "Sentaku",
|
||||
"menu.settings": "Sentakuzma...",
|
||||
"menu.tools": "&Tropos",
|
||||
"menu.tools.fix_duplicate_files": "Fiks sama &mlafu",
|
||||
"menu.tools.fix_unlinked_entries": "Fiks &tsunagajenanai shiruzmakaban",
|
||||
"menu.view": "&Anse",
|
||||
"menu.window": "Ekrannen",
|
||||
"namespace.create.title": "Maha vargetumam",
|
||||
"namespace.new.button": "Neo vargetumam",
|
||||
"namespace.new.prompt": "Maha neo vargetumam per nasii varge fu sebja!",
|
||||
"preview.multiple_selection": "<b>{count}</b> ting sentakujena",
|
||||
"preview.no_selection": "Nil ting sentakujena",
|
||||
"select.add_tag_to_selected": "Nasii festaretol na sentakuka",
|
||||
"select.all": "Sentaku al",
|
||||
"select.clear": "Sentaku nil",
|
||||
"select.inverse": "Kundr sentaku",
|
||||
"settings.clear_thumb_cache.title": "Keste husketing fu risonen",
|
||||
"settings.filepath.label": "Mlafuplas seatai",
|
||||
"settings.filepath.option.full": "Mahase hel mlafuplas",
|
||||
"settings.filepath.option.name": "Mahase mono mlafuplas",
|
||||
"settings.filepath.option.relative": "Mahase mlafuplas sebja kara",
|
||||
"settings.global": "Sentakuzma per al",
|
||||
"settings.language": "Glossa",
|
||||
"settings.library": "Sentakuzma fu mlafuhuomi",
|
||||
"settings.open_library_on_start": "Auki mlafuhuomi kesa TagStudio aukijena",
|
||||
"settings.page_size": "Sturatai fu lehti",
|
||||
"settings.restart_required": "Bitte genauki TagStudio per se kawarizma.",
|
||||
"settings.show_filenames_in_grid": "Mahase namae fu mlafu ine karta",
|
||||
"settings.show_recent_libraries": "Mahase moloda mlafuhuomi",
|
||||
"settings.theme.dark": "Kirai",
|
||||
"settings.theme.label": "Klea fu zeting:",
|
||||
"settings.theme.light": "Kirkas",
|
||||
"settings.theme.system": "Kompyu",
|
||||
"settings.title": "Sentakuzma",
|
||||
"sorting.direction.ascending": "Leste owaris",
|
||||
"sorting.direction.descending": "Leste eins",
|
||||
"splash.opening_library": "Auki mlafuhuomi \"{library_path}\" ima...",
|
||||
"status.deleted_file_plural": "Kestejenadan {count} mlafu!",
|
||||
"status.deleted_file_singular": "Kestejenadan 1 mlafu!",
|
||||
"status.deleted_none": "Nil mlafu kestejenadan.",
|
||||
"status.deleted_partial_warning": "Mono kestejenadan {count} mlafu! Bitte taskamatsa li du dekinai finna mlafu os ke ima brukjena na andr zeting.",
|
||||
"status.deleting_file": "Keste mlafu [{i}/{count}]: \"{path}\" ima...",
|
||||
"status.library_closing": "Kini mlafuhuomi ima...",
|
||||
"status.library_save_success": "Mlafuhuomi ufnejenadan au kinijenadan!",
|
||||
"status.library_search_query": "Suha ine mlafuhuomi ima...",
|
||||
"status.library_version_expected": "Weinanjenadan:",
|
||||
"status.library_version_found": "Finnajenadan:",
|
||||
"status.library_version_mismatch": "Verso fu mlafuhuomi samanai!",
|
||||
"status.results": "Shiruzma",
|
||||
"status.results.invalid_syntax": "Uso suhazma:",
|
||||
"status.results_found": "{count} shiruzma finnajenadan ({time_span})",
|
||||
"tag.add": "Nasii festaretol",
|
||||
"tag.add.plural": "Nasii festaretol",
|
||||
"tag.add_to_search": "Nasii suhazma made",
|
||||
"tag.aliases": "Andrnamae",
|
||||
"tag.all_tags": "Al festaretol",
|
||||
"tag.choose_color": "Sentaku varge fu festaretol",
|
||||
"tag.color": "Varge",
|
||||
"tag.confirm_delete": "Du kestetsa festaretol \"{tag_name}\"?",
|
||||
"tag.create": "Maha festaretol",
|
||||
"tag.create_add": "Maha && Nasii \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Bruk afto festaretol grun plusklar",
|
||||
"tag.edit": "Kawari festaretol",
|
||||
"tag.is_category": "Parjatfal",
|
||||
"tag.name": "Namae",
|
||||
"tag.new": "Neo festaretol",
|
||||
"tag.parent_tags": "Atama festaretol",
|
||||
"tag.parent_tags.add": "Nasii atama festaretol",
|
||||
"tag.parent_tags.description": "Afto festaretol deki brukjena likk taksanting per joku afto atama festaretol ine suhazma.",
|
||||
"tag.remove": "Keste festaretol",
|
||||
"tag.search_for_tag": "Suha fu festaretol",
|
||||
"tag.shorthand": "Namaenen",
|
||||
"tag.tag_name_required": "Namae fu festaretol (Trengjena)",
|
||||
"tag.view_limit": "Lesteatai per anse:",
|
||||
"tag_manager.title": "Festaretol fu mlafuhuomi",
|
||||
"trash.context.ambiguous": "Ugoki mlafu {trash_term} made",
|
||||
"trash.context.plural": "Ugoki mlafu {trash_term} made",
|
||||
"trash.context.singular": "Ugoki mlafu {trash_term} made",
|
||||
"trash.dialog.disambiguation_warning.plural": "Afto zol keste tuo TagStudio kara <i>AU</i> kompyu fu du kara!",
|
||||
"trash.dialog.disambiguation_warning.singular": "Afto zol keste sore TagStudio kara <i>AU</i> kompyu fu du kara!",
|
||||
"trash.dialog.move.confirmation.plural": "Du ugokitsa afto {count} mlafu {trash_term} made?",
|
||||
"trash.dialog.move.confirmation.singular": "Du ugokitsa afto mlafu {trash_term} made?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>VIKTI!</b> Li afto mlafu nai dekijena ugoki {trash_term} made, sore zol <b>kestejena!!</b>",
|
||||
"trash.dialog.title.plural": "Keste mlafu",
|
||||
"trash.dialog.title.singular": "Keste mlafu",
|
||||
"trash.name.generic": "Mulbaksu",
|
||||
"trash.name.windows": "Mulbaksu",
|
||||
"view.size.0": "Chisaidai",
|
||||
"view.size.1": "Chisai",
|
||||
"view.size.2": "Mellan",
|
||||
"view.size.3": "Stur",
|
||||
"view.size.4": "Sturdai",
|
||||
"window.message.error_opening_library": "Jam deza kesa auki mlafuhuomi.",
|
||||
"window.title.error": "Deza",
|
||||
"window.title.open_create_library": "Auki/Maha mlafuhuomi"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about.config_path": "Путь к конфигурации",
|
||||
"about.description": "TagStudio — это приложение для организации фотографий и прочих файлов основанное на системе \"тегов\", которое фокусируется на пользовательских свободе и гибкости. Никаких проприетарных форматов и программ, никакой кучи сопроводительных файлов, и никакого переворота вашей файловой системы.",
|
||||
"about.description": "TagStudio — это приложение для организации фотографий и прочих файлов, основанное на системе \"тегов\", и ориентированное на предоставление пользователю свободы и гибкости. Никаких проприетарных форматов и программ, никакой кучи сопроводительных файлов, и никакого переворота вашей файловой системы.",
|
||||
"about.documentation": "Документация",
|
||||
"about.license": "Лицензия",
|
||||
"about.module.found": "Найдено",
|
||||
@@ -11,7 +11,7 @@
|
||||
"app.title": "{base_title} - Библиотека '{library_dir}'",
|
||||
"color.color_border": "Окрасить границы во вторичный цвет",
|
||||
"color.confirm_delete": "Вы уверены, что хотите удалить цвет \"{color_name}\"?",
|
||||
"color.delete": "Удалить тэг",
|
||||
"color.delete": "Удалить тег",
|
||||
"color.import_pack": "Импортировать набор цветов",
|
||||
"color.name": "Название",
|
||||
"color.namespace.delete.prompt": "Вы уверены, что хотите удалить эту цветовую палитру? Это также удалит ВСЕ цвета в этой палитре!",
|
||||
@@ -19,12 +19,13 @@
|
||||
"color.new": "Новый цвет",
|
||||
"color.placeholder": "Цвет",
|
||||
"color.primary": "Основной цвет",
|
||||
"color.primary_required": "Основной цвет (Обязательный)",
|
||||
"color.primary_required": "Основной цвет (Обязательно)",
|
||||
"color.secondary": "Вторичный цвет",
|
||||
"color.title.no_color": "Без цвета",
|
||||
"color_manager.title": "Редактировать цвета тегов",
|
||||
"dependency.missing.title": "{dependency} не найден",
|
||||
"drop_import.description": "Данные файловые пути уже существуют в библиотеке",
|
||||
"drop_import.duplicates_choice.plural": "{count} данных файловых путей уже существуют в библиотеке.",
|
||||
"drop_import.duplicates_choice.plural": "{count} файловых путей уже существуют в библиотеке.",
|
||||
"drop_import.duplicates_choice.singular": "Данный файловый путь уже существует в библиотеке.",
|
||||
"drop_import.progress.label.initial": "Импортирование новых файлов...",
|
||||
"drop_import.progress.label.plural": "Импортирование новых файлов...\n{count} файлов импортировано.{suffix}",
|
||||
@@ -62,6 +63,8 @@
|
||||
"entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...",
|
||||
"entries.unlinked.search_and_relink": "&Поиск и привязка",
|
||||
"entries.unlinked.title": "Исправить откреплённые записи",
|
||||
"ffmpeg.missing.description": "FFmpeg и/или FFprobe не были найдены. FFmpeg необходим для воспроизведения мультимедиа и превью.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "Копировать поле",
|
||||
"field.edit": "Редактировать поле",
|
||||
"field.paste": "Вставить поле",
|
||||
@@ -85,7 +88,7 @@
|
||||
"file.open_file": "Открыть файл",
|
||||
"file.open_file_with": "Открыть файл с помощью",
|
||||
"file.open_location.generic": "Открыть файл в проводнике",
|
||||
"file.open_location.mac": "Показать в поисковике",
|
||||
"file.open_location.mac": "Показать в Finder",
|
||||
"file.open_location.windows": "Открыть в проводнике",
|
||||
"file.path": "Путь до файла",
|
||||
"folders_to_tags.close_all": "Закрыть всё",
|
||||
@@ -104,11 +107,12 @@
|
||||
"generic.cut": "Вырезать",
|
||||
"generic.delete": "Удалить",
|
||||
"generic.delete_alt": "&Удалить",
|
||||
"generic.done": "Завершено",
|
||||
"generic.done": "Готово",
|
||||
"generic.done_alt": "&Завершено",
|
||||
"generic.edit": "Редактировать",
|
||||
"generic.edit_alt": "&Редактировать",
|
||||
"generic.filename": "Имя файла",
|
||||
"generic.missing": "Отсутствует",
|
||||
"generic.navigation.back": "Назад",
|
||||
"generic.navigation.next": "Далее",
|
||||
"generic.none": "Ничего",
|
||||
@@ -135,11 +139,35 @@
|
||||
"ignore_list.add_extension": "&Добавить расширение",
|
||||
"ignore_list.mode.exclude": "Исключить",
|
||||
"ignore_list.mode.include": "Включить",
|
||||
"ignore_list.mode.label": "Список режимов:",
|
||||
"ignore_list.mode.label": "Тип списка:",
|
||||
"ignore_list.title": "Расширения файлов",
|
||||
"json_migration.checking_for_parity": "Проверка целостности...",
|
||||
"json_migration.creating_database_tables": "Создание таблиц базы данных SQL...",
|
||||
"json_migration.description": "<br>Начните и просмотрите результаты процесса миграции библиотеки. Конвертированная библиотека <i>не</i> будет использоваться, если вы не нажмете \"Завершить миграцию\". Данные библиотеки должны либо иметь совпадающие значения, либо содержать метку \"Совпало\". Значения, которые не совпадают, будут отображаться красным цветом и сопровождаться символом \"<b>(!)</b>\" рядом с ними. <br><center><i>Этот процесс может занять несколько минут для больших библиотек.</i></center>",
|
||||
"json_migration.discrepancies_found": "Найдены несоответствия библиотеки",
|
||||
"json_migration.discrepancies_found.description": "Были обнаружены расхождения между оригинальными и преобразованными форматами библиотеки. Пожалуйста, просмотрите и выберите, продолжайте ли продолжить миграцию или отменить.",
|
||||
"json_migration.finish_migration": "Завершить миграцию",
|
||||
"json_migration.heading.aliases": "Псевдонимы:",
|
||||
"json_migration.heading.colors": "Цвета:",
|
||||
"json_migration.heading.differ": "Несоответствие",
|
||||
"json_migration.heading.entires": "Записи:",
|
||||
"json_migration.heading.extension_list_type": "Тип списка расширений:",
|
||||
"json_migration.heading.fields": "Поля:",
|
||||
"json_migration.heading.file_extension_list": "Список расширений файлов:",
|
||||
"json_migration.heading.match": "Совпало",
|
||||
"json_migration.heading.names": "Имена:",
|
||||
"json_migration.heading.parent_tags": "Родительские теги:",
|
||||
"json_migration.heading.paths": "Пути:",
|
||||
"json_migration.heading.shorthands": "Сокращения:",
|
||||
"json_migration.heading.tags": "Теги:",
|
||||
"json_migration.info.description": "Файлы библиотеки, созданные в TagStudio версий <b>9.4 и ниже</b>, необходимо перенести в новый формат <b>v9.5+</b>.<h2>Что важно знать:</h2><ul><li>Ваш текущий файл библиотеки <b><i>НЕ</i></b> будет удалён</li><li>Ваши личные файлы <b><i>НЕ</i></b> будут удалены, перемещены или изменены</li><li>Новый формат сохранения v9.5+ несовместим с предыдущими версиями TagStudio</li></ul><h3>Что изменилось:</h3><ul><li>\"Поля тегов\" заменены на \"Категории тегов\". Теперь теги добавляются напрямую к файлам, а не через поля, и автоматически организуются в категории на основе родительских тегов, отмеченных новым свойством \"Является категорией\" в меню редактирования тегов. Любой тег можно сделать категорией, а дочерние теги будут автоматически группироваться под родительскими тегами, помеченными как категории. Теги \"Избранное\" и \"Архив\" теперь наследуются от нового тега \"Мета-теги\", который по умолчанию помечен как категория.</li><li>Цвета тегов были доработаны и расширены. Некоторые цвета переименованы или объединены, но все существующие цвета будут преобразованы в точные или близкие аналоги в v9.5.</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "Мигрируется {entries:,d} записей...",
|
||||
"json_migration.migration_complete": "Миграция завершена!",
|
||||
"json_migration.migration_complete_with_discrepancies": "Миграция завершена, найдены несоответствия",
|
||||
"json_migration.start_and_preview": "Начало и предпросмотр",
|
||||
"json_migration.title": "Миграция формата сохранения: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>Библиотека версии 9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>Библиотека версии 9.4</h2>",
|
||||
"landing.open_create_library": "Открыть/создать библиотеку {shortcut}",
|
||||
"library.field.add": "Добавить поле",
|
||||
"library.field.confirm_remove": "Вы уверены, что хотите удалить поле \"{name}\"?",
|
||||
@@ -154,22 +182,25 @@
|
||||
"library.scan_library.title": "Сканирование библиотеки",
|
||||
"library_object.name": "Название",
|
||||
"library_object.name_required": "Название (Обязательно)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library_object.slug_required": "ID Slug (Обязательно)",
|
||||
"library_object.slug": "Уникальный ID",
|
||||
"library_object.slug_required": "Уникальный ID (обязательно)",
|
||||
"macros.running.dialog.new_entries": "Выполнение макросов на {count}/{total} новых файлах...",
|
||||
"macros.running.dialog.title": "Выполнение макросов на новых файлах",
|
||||
"media_player.autoplay": "Автовоспроизведение",
|
||||
"media_player.loop": "Зациклить",
|
||||
"menu.delete_selected_files_ambiguous": "Перемеcтить файл(ы) в {trash_term}",
|
||||
"menu.delete_selected_files_plural": "Перемеcтить файлы в {trash_term}",
|
||||
"menu.delete_selected_files_singular": "Перемеcтить файл в {trash_term}",
|
||||
"menu.edit": "Редактировать",
|
||||
"menu.edit.ignore_list": "Игнорировать файлы и папки",
|
||||
"menu.edit.manage_file_extensions": "Управлять разширениями файлов",
|
||||
"menu.edit.manage_file_extensions": "Управлять расширениями файлов",
|
||||
"menu.edit.manage_tags": "Управлять тегами",
|
||||
"menu.edit.new_tag": "Новый &тег",
|
||||
"menu.file": "&Файл",
|
||||
"menu.file.clear_recent_libraries": "Очистить последние",
|
||||
"menu.file.close_library": "&Закрыть библиотеку",
|
||||
"menu.file.missing_library.message": "Не найдено расположение библиотеки \"{library}\".",
|
||||
"menu.file.missing_library.title": "Отсутствующая библиотека",
|
||||
"menu.file.new_library": "Новая библиотека",
|
||||
"menu.file.open_create_library": "&Открыть/создать библиотеку",
|
||||
"menu.file.open_library": "Открыть библиотеку",
|
||||
@@ -188,26 +219,48 @@
|
||||
"menu.tools.fix_unlinked_entries": "Исправить &откреплённые записи",
|
||||
"menu.view": "&Вид",
|
||||
"menu.window": "Окно",
|
||||
"namespace.create.description": "TagStudio использует пространства имён, чтобы категорезировать цвета и теги для удобства их экспорта и передачи. Пространства имён начинающиеся с \"tagstudio\" зарезервированы TagStudio для внутреннего использования.",
|
||||
"namespace.create.description": "TagStudio использует пространства имён, чтобы категоризировать цвета и теги для удобства их экспорта и передачи. Пространства имён, начинающиеся с \"tagstudio\", зарезервированы TagStudio для внутреннего использования.",
|
||||
"namespace.create.description_color": "Именные пространства используются в качестве палитр для цветов тегов. Все пользовательские цвета хранятся в именных пространствах.",
|
||||
"namespace.create.title": "Создать пространство имён",
|
||||
"namespace.new.button": "Новое пространство имён",
|
||||
"namespace.new.prompt": "Создайте пространство имён, чтобы добавить пользовательские цвета!",
|
||||
"preview.multiple_selection": "Выбрано <b>{count}</b>",
|
||||
"preview.multiple_selection": "Выбрано <b>{count}</b> элементов",
|
||||
"preview.no_selection": "Ничего не выбрано",
|
||||
"select.add_tag_to_selected": "Добавить тег к выбранному",
|
||||
"select.all": "Выбрать всё",
|
||||
"select.clear": "Отменить выбор",
|
||||
"select.inverse": "Инвертировать выделение",
|
||||
"settings.clear_thumb_cache.title": "Очистить кэш иконок",
|
||||
"settings.dateformat.english": "Английский",
|
||||
"settings.dateformat.international": "Международный",
|
||||
"settings.dateformat.label": "Формат даты",
|
||||
"settings.dateformat.system": "Системный",
|
||||
"settings.filepath.label": "Видимость путей файлов",
|
||||
"settings.filepath.option.full": "Показывать путь полностью",
|
||||
"settings.filepath.option.name": "Показывать только имена файлов",
|
||||
"settings.filepath.option.relative": "Показывать относительные пути",
|
||||
"settings.global": "Глобальные настройки",
|
||||
"settings.hourformat.label": "24-часовое время",
|
||||
"settings.language": "Язык",
|
||||
"settings.library": "Настройки библиотеки",
|
||||
"settings.open_library_on_start": "Открывать библиотеку при запуске",
|
||||
"settings.page_size": "Размер страницы",
|
||||
"settings.restart_required": "Пожалуйста, перезапустите TagStudio для применения изменений.",
|
||||
"settings.show_filenames_in_grid": "Показывать имена файлов",
|
||||
"settings.show_recent_libraries": "Показывать недавние библиотеки",
|
||||
"settings.tag_click_action.add_to_search": "Добавить тег в поиск",
|
||||
"settings.tag_click_action.label": "Действие по нажатию на тег",
|
||||
"settings.tag_click_action.open_edit": "Редактировать тег",
|
||||
"settings.tag_click_action.set_search": "Найти тег",
|
||||
"settings.theme.dark": "Тёмная",
|
||||
"settings.theme.label": "Тема:",
|
||||
"settings.theme.light": "Светлая",
|
||||
"settings.theme.system": "Системная",
|
||||
"settings.title": "Настройки",
|
||||
"settings.zeropadding.label": "Ведущие нули",
|
||||
"sorting.direction.ascending": "По возрастанию",
|
||||
"sorting.direction.descending": "По убыванию",
|
||||
"splash.opening_library": "Открывание библиотеки \"{library_path}\"...",
|
||||
"splash.opening_library": "Открытие библиотеки \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "{count} файлов удалено!",
|
||||
"status.deleted_file_singular": "Файл удалён!",
|
||||
"status.deleted_none": "Ничего не удалено.",
|
||||
@@ -237,12 +290,12 @@
|
||||
"tag.create_add": "Создать и добавить \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "Использовать этот тег для пояснения",
|
||||
"tag.edit": "Редактировать тег",
|
||||
"tag.is_category": "Это категория",
|
||||
"tag.is_category": "Является категорией",
|
||||
"tag.name": "Название",
|
||||
"tag.new": "Новый тег",
|
||||
"tag.parent_tags": "Родительский тег",
|
||||
"tag.parent_tags.add": "Добавить родительский тег",
|
||||
"tag.parent_tags.description": "Этот тег может считаться заменой любого из этих родительских тэгов в поиске.",
|
||||
"tag.parent_tags.description": "Этот тег может считаться заменой любого из этих родительских тегов в поиске.",
|
||||
"tag.remove": "Убрать тег",
|
||||
"tag.search_for_tag": "Поиск тега",
|
||||
"tag.shorthand": "Сокращённое название",
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"about.config_path": "கட்டமைப்பு பாதை",
|
||||
"about.description": "டேக்ச்டுடியோ என்பது ஒரு புகைப்படம் மற்றும் கோப்பு அமைப்பு பயன்பாடாகும், இது பயனருக்கு விடுதலை மற்றும் நெகிழ்வுத்தன்மையை வழங்குவதில் கவனம் செலுத்துகிறது. தனியுரிம திட்டங்கள் அல்லது வடிவங்கள் இல்லை, பக்கவாட்டு கோப்புகளின் கடல் இல்லை, உங்கள் கோப்பு முறைமை கட்டமைப்பின் முழுமையான எழுச்சி இல்லை.",
|
||||
"about.documentation": "ஆவணங்கள்",
|
||||
"about.license": "உரிமம்",
|
||||
"about.module.found": "காணப்பட்டது",
|
||||
"about.title": "டேக்ச்டுடியோ பற்றி",
|
||||
"about.website": "வலைத்தளம்",
|
||||
"app.git": "அறிவிலி கமிட்",
|
||||
"app.pre_release": "முன் வெளியீடு",
|
||||
"app.title": "{base_title} - நூலகம் '{library_dir}'",
|
||||
@@ -20,6 +23,7 @@
|
||||
"color.secondary": "இரண்டாம் நிலை நிறம்",
|
||||
"color.title.no_color": "நிறம் இல்லை",
|
||||
"color_manager.title": "குறிச்சொல் வண்ணங்களை நிர்வகிக்கவும்",
|
||||
"dependency.missing.title": "{dependency} காணப்படவில்லை",
|
||||
"drop_import.description": "பின்வரும் கோப்புகள் ஏற்கனவே நூலகத்தில் இருக்கும் கோப்பு பாதைகளுடன் பொருந்துகின்றன",
|
||||
"drop_import.duplicates_choice.plural": "பின்வரும் {count} கோப்புகள் ஏற்கனவே நூலகத்தில் இருக்கும் கோப்பு பாதைகளுடன் பொருந்துகின்றன.",
|
||||
"drop_import.duplicates_choice.singular": "பின்வரும் கோப்பு ஏற்கனவே நூலகத்தில் இருக்கும் கோப்பு பாதையுடன் பொருந்துகிறது.",
|
||||
@@ -50,7 +54,7 @@
|
||||
"entries.unlinked.delete.deleting_count": "{idx}/{count} இணைக்கப்படாத உள்ளீடுகள் நீக்கப்படுகிறது",
|
||||
"entries.unlinked.delete_alt": "டி & லெட் இணைக்கப்படாத உள்ளீடுகள்",
|
||||
"entries.unlinked.description": "ஒவ்வொரு நூலக நுழைவும் உங்கள் கோப்பகங்களில் ஒன்றில் ஒரு கோப்போடு இணைக்கப்பட்டுள்ளது. ஒரு நுழைவுடன் இணைக்கப்பட்ட ஒரு கோப்பு டாக்ச்டுடியோவுக்கு வெளியே நகர்த்தப்பட்டால் அல்லது நீக்கப்பட்டால், அது பின்னர் இணைக்கப்படாததாகக் கருதப்படுகிறது.",
|
||||
"entries.unlinked.missing_count.none": "இணைக்கப்படாத உள்ளீடுகள்: இதற்கில்லை.",
|
||||
"entries.unlinked.missing_count.none": "இணைக்கப்படாத உள்ளீடுகள்: இதற்கில்லை",
|
||||
"entries.unlinked.missing_count.some": "இணைக்கப்படாத உள்ளீடுகள்: {count}",
|
||||
"entries.unlinked.refresh_all": "& அனைத்தையும் புதுப்பிக்கவும்",
|
||||
"entries.unlinked.relink.attempting": "{idx}/{missing_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது",
|
||||
@@ -59,6 +63,8 @@
|
||||
"entries.unlinked.scanning": "இணைக்கப்படாத நுழைவுகளை புத்தககல்லரியில் சோதனை செய்யப்படுகிறது...",
|
||||
"entries.unlinked.search_and_relink": "& தேடல் && relink",
|
||||
"entries.unlinked.title": "இணைக்கப்படாத உள்ளீடுகளைச் சரிசெய்யவும்",
|
||||
"ffmpeg.missing.description": "FFMPEG மற்றும்/அல்லது FFPROBE கண்டுபிடிக்கப்படவில்லை. மல்டிமீடியா பிளேபேக் மற்றும் சிறுபடங்களுக்கு FFMPEG தேவைப்படுகிறது.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status} <br> {ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "நகல் புலம்",
|
||||
"field.edit": "புலம் திருத்து",
|
||||
"field.paste": "புலம் ஒட்டவும்",
|
||||
@@ -78,12 +84,13 @@
|
||||
"file.duplicates.mirror.description": "ஒவ்வொரு மறுநுழைவு பொருத்தத் தொகுப்பிலும் நுழைவு தரவுகளைப் பிரதிபலிக்கவும், அனைத்து தரவுகளையும் இணைக்கவும், எந்தத் தகவல்களையும் நீக்காமலும் மறு செய்யாமலும். இந்தச் செயலில் எந்தக் கோப்புகள் அல்லது தரவுகளும் நீக்கப்பட மாட்டாது.",
|
||||
"file.duplicates.mirror_entries": "& கண்ணாடி உள்ளீடுகள்",
|
||||
"file.duration": "நீளம்",
|
||||
"file.not_found": "கோப்பு கிடைக்கவில்லை:",
|
||||
"file.not_found": "கோப்பு கிடைக்கவில்லை",
|
||||
"file.open_file": "கோப்பைத் திறக்கவும்",
|
||||
"file.open_file_with": "உடன் கோப்பைத் திறக்கவும்",
|
||||
"file.open_location.generic": "கோப்பு எக்ச்ப்ளோரரில் கோப்பைக் காட்டு",
|
||||
"file.open_location.mac": "கண்டுபிடிப்பாளரில் வெளிப்படுத்துங்கள்",
|
||||
"file.open_location.windows": "கோப்பு எக்ச்ப்ளோரரில் காண்பி",
|
||||
"file.path": "கோப்பு பாதை",
|
||||
"folders_to_tags.close_all": "அனைத்தையும் மூடு",
|
||||
"folders_to_tags.converting": "கோப்புறைகளை குறிச்சொற்களாக மாற்றப்படுகிறது",
|
||||
"folders_to_tags.description": "உங்கள் அடைவு கட்டமைப்பின் அடிப்படையில் குறிச்சொற்களை உருவாக்கி, அவற்றை உங்கள் நுழைவுகளில் பயன்படுத்துகிறது.\nகீழே காணப்படும் கட்டமைப்பானது உருவாக்கப்படும் அனைத்து குறிச்சொற்களையும், அவை எந்த நுழைவுகளில் பயன்படுத்தப்படும் என்பதையும் காட்டுகிறது.",
|
||||
@@ -105,6 +112,7 @@
|
||||
"generic.edit": "திருத்து",
|
||||
"generic.edit_alt": "திருத்து (&e)",
|
||||
"generic.filename": "கோப்புப்பெயர்",
|
||||
"generic.missing": "இல்லை",
|
||||
"generic.navigation.back": "பின்",
|
||||
"generic.navigation.next": "அடுத்தது",
|
||||
"generic.none": "எதுவுமில்லை",
|
||||
@@ -135,7 +143,7 @@
|
||||
"ignore_list.title": "கோப்பு நீட்டிப்புகள்",
|
||||
"json_migration.checking_for_parity": "சமத்துவத்தை சரிபார்க்கிறது ...",
|
||||
"json_migration.creating_database_tables": "கவிமொ தரவுத்தள அட்டவணைகளை உருவாக்குதல் ...",
|
||||
"json_migration.description": "<br> நூலக இடம்பெயர்வு செயல்முறையின் முடிவுகளைத் தொடங்கவும் முன்னோட்டமிடவும். மாற்றப்பட்ட நூலகம் <i> இல்லை </i> நீங்கள் \"இடம்பெயர்வு முடிக்கவும்\" என்பதைக் சொடுக்கு செய்யாவிட்டால் பயன்படுத்தப்படும். <br> <br> நூலகத் தரவுகள் பொருந்தக்கூடிய மதிப்புகளைக் கொண்டிருக்க வேண்டும் அல்லது \"பொருந்திய\" லேபிளைக் கொண்டிருக்க வேண்டும். பொருந்தாத மதிப்புகள் சிவப்பு நிறத்தில் காண்பிக்கப்படும் மற்றும் அவர்களுக்கு அடுத்த \"<b> (!) </B>\" சின்னத்தைக் கொண்டிருக்கும். <br> <center> <i> இந்த செயல்முறை பெரிய நூலகங்களுக்கு பல நிமிடங்கள் வரை ஆகலாம்.",
|
||||
"json_migration.description": "<br> நூலக இடம்பெயர்வு செயல்முறையின் முடிவுகளைத் தொடங்கவும் முன்னோட்டமிடவும். மாற்றப்பட்ட நூலகம் <i> இல்லை </i> நீங்கள் \"இடம்பெயர்வு முடிக்கவும்\" என்பதைக் சொடுக்கு செய்யாவிட்டால் பயன்படுத்தப்படும். <br> <br> நூலகத் தரவுகள் பொருந்தக்கூடிய மதிப்புகளைக் கொண்டிருக்க வேண்டும் அல்லது \"பொருந்திய\" லேபிளைக் கொண்டிருக்க வேண்டும். பொருந்தாத மதிப்புகள் சிவப்பு நிறத்தில் காண்பிக்கப்படும் மற்றும் அவர்களுக்கு அடுத்த \"<b> (!) </b>\" சின்னத்தைக் கொண்டிருக்கும். <br> <center> <i> இந்தச் செயல்முறை பெரிய நூலகங்களுக்குப் பல நிமிடங்கள்வரை ஆகலாம்.</i></center>",
|
||||
"json_migration.discrepancies_found": "நூலக முரண்பாடுகள் காணப்படுகின்றன",
|
||||
"json_migration.discrepancies_found.description": "அசல் மற்றும் மாற்றப்பட்ட நூலக வடிவங்களுக்கு இடையில் முரண்பாடுகள் காணப்பட்டன. தயவுசெய்து மதிப்பாய்வு செய்து இடம்பெயர்வு தொடர வேண்டுமா அல்லது ரத்து செய்ய என்பதைத் தேர்வுசெய்க.",
|
||||
"json_migration.finish_migration": "இடம்பெயர்வு முடிக்கவும்",
|
||||
@@ -158,8 +166,8 @@
|
||||
"json_migration.migration_complete_with_discrepancies": "இடம்பெயர்வு முடிந்தது, முரண்பாடுகள் காணப்படுகின்றன",
|
||||
"json_migration.start_and_preview": "தொடக்க மற்றும் முன்னோட்டம்",
|
||||
"json_migration.title": "வடிவமைப்பு இடம்பெயர்வுகளைச் சேமிக்கவும்: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<H2> V9.5+ நூலகம் </H2>",
|
||||
"json_migration.title.old_lib": "<H2> V9.4 நூலகம் </H2>",
|
||||
"json_migration.title.new_lib": "<h2> V9.5+ நூலகம் </h2>",
|
||||
"json_migration.title.old_lib": "<h2> V9.4 நூலகம் </h2>",
|
||||
"landing.open_create_library": "நூலகத்தைத் திறக்கவும்/உருவாக்கவும் {shortcut}",
|
||||
"library.field.add": "புலத்தைச் சேர்க்க",
|
||||
"library.field.confirm_remove": "இந்த \"{name}\" புலத்தை நிச்சயமாக அகற்ற விரும்புகிறீர்களா?",
|
||||
@@ -176,9 +184,10 @@
|
||||
"library_object.name_required": "பெயர் (தேவை)",
|
||||
"library_object.slug": "ஐடி ச்லக்",
|
||||
"library_object.slug_required": "ஐடி ச்லக் (தேவை)",
|
||||
"macros.running.dialog.new_entries": "{count} இல் கட்டமைக்கப்பட்ட செயல்முறைகளை இயக்கப்படுகிறது / {total} புதிய பதிவுகள்",
|
||||
"macros.running.dialog.new_entries": "{count} இல் கட்டமைக்கப்பட்ட செயல்முறைகளை இயக்கப்படுகிறது / {total} புதிய பதிவுகள்...",
|
||||
"macros.running.dialog.title": "புதிய நுழைவுகளில் செயல்முறைகளை இயக்கப்படுகின்றது",
|
||||
"media_player.autoplay": "ஆட்டோபிளே",
|
||||
"media_player.loop": "லூப்",
|
||||
"menu.delete_selected_files_ambiguous": "கோப்புகளை நகர்த்தவும்) {trash_term}",
|
||||
"menu.delete_selected_files_plural": "கோப்புகளை {trash_term} பெறுநர் க்கு நகர்த்தவும்",
|
||||
"menu.delete_selected_files_singular": "கோப்பை {trash_term} பெறுநர் க்கு நகர்த்தவும்",
|
||||
@@ -190,6 +199,8 @@
|
||||
"menu.file": "கோப்பு (&f)",
|
||||
"menu.file.clear_recent_libraries": "சமீபத்தியதை அழிக்கவும்",
|
||||
"menu.file.close_library": "& நூலகம் மூடு",
|
||||
"menu.file.missing_library.message": "\"{library}\" நூலகத்தின் இருப்பிடத்தைக் கண்டுபிடிக்க முடியாது.",
|
||||
"menu.file.missing_library.title": "நூலகம் இல்லை",
|
||||
"menu.file.new_library": "புதிய நூலகம்",
|
||||
"menu.file.open_create_library": "& நூலகத்தைத் திறக்க/உருவாக்கவும்",
|
||||
"menu.file.open_library": "திறந்த நூலகம்",
|
||||
@@ -218,13 +229,35 @@
|
||||
"select.add_tag_to_selected": "தேர்ந்தெடுக்கப்பட்டவருக்கு குறிச்சொல்லைச் சேர்க்கவும்",
|
||||
"select.all": "அனைத்தையும் தெரிவுசெய்",
|
||||
"select.clear": "தெளிவான தேர்வு",
|
||||
"select.inverse": "தலைகீழ் தேர்வு",
|
||||
"settings.clear_thumb_cache.title": "சிறுபடம் அழி",
|
||||
"settings.dateformat.english": "ஆங்கிலம்",
|
||||
"settings.dateformat.international": "பன்னாட்டு",
|
||||
"settings.dateformat.label": "தேதி வடிவம்",
|
||||
"settings.dateformat.system": "மண்டலம்",
|
||||
"settings.filepath.label": "FilePath தெரிவுநிலை",
|
||||
"settings.filepath.option.full": "முழு பாதைகளையும் காட்டு",
|
||||
"settings.filepath.option.name": "கோப்பு பெயர்களைக் காட்டு",
|
||||
"settings.filepath.option.relative": "உறவினர் பாதைகளைக் காட்டு",
|
||||
"settings.global": "உலகளாவிய அமைப்புகள்",
|
||||
"settings.hourformat.label": "24 மணி நேர நேரம்",
|
||||
"settings.language": "மொழி",
|
||||
"settings.library": "நூலக அமைப்புகள்",
|
||||
"settings.open_library_on_start": "தொடக்கத்தில் நூலகத்தைத் திறக்கவும்",
|
||||
"settings.page_size": "பக்க அளவு",
|
||||
"settings.restart_required": "மாற்றங்கள் நடைமுறைக்கு வருவதற்கு டேக்ச்டுடியோவை மறுதொடக்கம் செய்யுங்கள்.",
|
||||
"settings.show_filenames_in_grid": "கட்டத்தில் கோப்பு பெயர்களைக் காட்டு",
|
||||
"settings.show_recent_libraries": "அண்மைக் கால நூலகங்களைக் காட்டு",
|
||||
"settings.tag_click_action.add_to_search": "தேடுவதற்கு குறிச்சொல்லைச் சேர்க்கவும்",
|
||||
"settings.tag_click_action.label": "குறிச்சொல் செயலை சொடுக்கு செய்க",
|
||||
"settings.tag_click_action.open_edit": "குறிச்சொல்லைத் திருத்து",
|
||||
"settings.tag_click_action.set_search": "குறிச்சொல்லைத் தேடுங்கள்",
|
||||
"settings.theme.dark": "இருண்ட",
|
||||
"settings.theme.label": "தீம்:",
|
||||
"settings.theme.light": "ஒளி",
|
||||
"settings.theme.system": "மண்டலம்",
|
||||
"settings.title": "அமைப்புகள்",
|
||||
"settings.zeropadding.label": "தேதி பூச்சிய-பேடிங்",
|
||||
"sorting.direction.ascending": "ஏறுதல்",
|
||||
"sorting.direction.descending": "இறங்கு",
|
||||
"splash.opening_library": "\"{library_path}\" ஐ திறக்கும் ...",
|
||||
@@ -274,9 +307,9 @@
|
||||
"trash.context.singular": "கோப்பை {trash_term} பெறுநர் க்கு நகர்த்தவும்",
|
||||
"trash.dialog.disambiguation_warning.plural": "இது அவற்றை டேக்ச்டுடியோ <i> மற்றும் </i> உங்கள் கோப்பு முறைமையிலிருந்து அகற்றும்!",
|
||||
"trash.dialog.disambiguation_warning.singular": "இது டேக்ச்டுடியோ <i> மற்றும் </i> உங்கள் கோப்பு முறைமையிலிருந்து அகற்றப்படும்!",
|
||||
"trash.dialog.move.confirmation.plural": "இந்த {count} கோப்புகளை {trash_term} க்கு க்கு நகர்த்த விரும்புகிறீர்களா?",
|
||||
"trash.dialog.move.confirmation.plural": "இந்த {count} கோப்புகளை {trash_term} க்கு நகர்த்த விரும்புகிறீர்களா?",
|
||||
"trash.dialog.move.confirmation.singular": "இந்த கோப்பை {trash_term} with க்கு நகர்த்த விரும்புகிறீர்களா?",
|
||||
"trash.dialog.permanent_delete_warning": "<b> எச்சரிக்கை! </b> இந்த கோப்பை {trash_term} க்கு க்கு மாற்ற முடியாவிட்டால், <b> இது <b> நிரந்தரமாக நீக்கப்படும்! </b>",
|
||||
"trash.dialog.permanent_delete_warning": "<b> எச்சரிக்கை! </b> இந்தக் கோப்பை {trash_term} க்கு மாற்ற முடியாவிட்டால், இது <b> நிரந்தரமாக நீக்கப்படும்! </b>",
|
||||
"trash.dialog.title.plural": "கோப்புகளை நீக்கு",
|
||||
"trash.dialog.title.singular": "கோப்பை அழி",
|
||||
"trash.name.generic": "குப்பை",
|
||||
|
||||
1
src/tagstudio/resources/translations/th.json
Normal file
1
src/tagstudio/resources/translations/th.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"about.description": "ilo Tagstudio li ilo pi lawa lipu li ilo pi lawa sitelen li kepeken nasin pi poki pona. ilo Tagstudio li wile pana e lawa mute tawa e jan kepeken. nasin pi open ala li lon ala, en ma pi lipu poka li lon ala, en sina li ante ala e nasin lipu ale sina.",
|
||||
"about.config_path": "nasin pi ante lawa",
|
||||
"about.description": "ilo Tagstudio li ilo pi lawa lipu li ilo pi lawa sitelen li kepeken nasin pi poki pona. ilo Tagstudio li wile pana e lawa mute tawa jan kepeken. nasin pi open ala li lon ala, en ma pi lipu poka li lon ala, en sina li ante ala e nasin lipu ale sina.",
|
||||
"about.documentation": "lipu sona",
|
||||
"about.license": "lipu lawa",
|
||||
"about.module.found": "lukin",
|
||||
@@ -11,6 +12,7 @@
|
||||
"color.color_border": "o kepeken kule nanpa tu lon selo",
|
||||
"color.confirm_delete": "sina wile ala wile weka e kule \"{color_name}\"?",
|
||||
"color.delete": "o weka e poki",
|
||||
"color.import_pack": "o kama jo e kulupu kule",
|
||||
"color.name": "nimi",
|
||||
"color.namespace.delete.prompt": "sina wile ala wile weka e ma nimi kule ni? weka la kule ale lon ma ni li weka kin!",
|
||||
"color.namespace.delete.title": "o weka e ma nimi kule",
|
||||
@@ -25,13 +27,20 @@
|
||||
"drop_import.description": "lipu ni li sama nasin lipu lon tomo",
|
||||
"drop_import.duplicates_choice.plural": "lipu {count} ni li jo e nasin lipu sama lon tomo.",
|
||||
"drop_import.duplicates_choice.singular": "lipu ni li jo e nasin lipu sama lon tomo.",
|
||||
"drop_import.progress.label.initial": "mi kama jo e lipu sin...",
|
||||
"drop_import.progress.label.plural": "mi kama jo e lipu sin...\nmi kama jo e lipu {count}.{suffix}",
|
||||
"drop_import.progress.label.singular": "mi kama jo e lipu sin...\nmi kama jo e lipu wan.{suffix}",
|
||||
"drop_import.progress.window_title": "o kama jo e lipu",
|
||||
"drop_import.title": "lipu ike",
|
||||
"edit.color_manager": "o lawa e kule poki",
|
||||
"edit.copy_fields": "o kama jo e ma sama",
|
||||
"edit.paste_fields": "o pana e ma sama",
|
||||
"edit.tag_manager": "o lawa e poki",
|
||||
"entries.duplicate.merge": "o wan e ijo sama",
|
||||
"entries.duplicate.merge.label": "mi wan e ijo sama...",
|
||||
"entries.duplicate.refresh": "o kama jo e sona tan ijo sama",
|
||||
"entries.duplicates.description": "ken la, ijo mute li jo e ijo lon sama. ni li \"ijo sama\". sina wan e ona la, ijo sama li kama wan li jo e sona ale tan ijo sama ale.",
|
||||
"entries.mirror": "jasima",
|
||||
"entries.mirror": "jasi&ma",
|
||||
"entries.mirror.confirmation": "mi jasima e ijo {count}. ni li pona anu seme?",
|
||||
"entries.mirror.label": "mi jasima e ijo {idx}/{total}...",
|
||||
"entries.mirror.title": "mi jasima e ijo",
|
||||
@@ -43,33 +52,37 @@
|
||||
"entries.unlinked.delete.confirm": "mi weka e ijo {count}. ni li pona anu seme?",
|
||||
"entries.unlinked.delete.deleting": "mi weka e ijo",
|
||||
"entries.unlinked.delete.deleting_count": "mi weka e ijo {idx}/{count} pi ijo lon ala",
|
||||
"entries.unlinked.delete_alt": "o weka e ijo pi linja ala",
|
||||
"entries.unlinked.description": "ijo ale li jo e ijo lon. ona li tawa anu weka, ona li jo ala e ijo lon. ijo pi ijo lon li ken alasa e tomo li ken kama jo e ijo lon. ante la sina ken pana ijo lon tawa ijo. ante la sina ken weka e ijo.",
|
||||
"entries.unlinked.refresh_all": "o kama jo sin tan ale",
|
||||
"entries.unlinked.delete_alt": "o weka e ijo pi &linja ala",
|
||||
"entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.<br><br>ijo pi ijo lon li ken alasa lon tomo li ken kama jo e ijo lon. ante la sina ken weka e ona.",
|
||||
"entries.unlinked.missing_count.none": "ijo pi ijo lon ala: N/A",
|
||||
"entries.unlinked.missing_count.some": "ijo pi ijo lon ala: {count}",
|
||||
"entries.unlinked.refresh_all": "o lukin sin e ale (&R)",
|
||||
"entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {idx}/{missing_count}. mi pana e ijo lon tawa ijo {fixed_count}",
|
||||
"entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo",
|
||||
"entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo (&M)",
|
||||
"entries.unlinked.relink.title": "mi pana e ijo lon tawa ijo",
|
||||
"entries.unlinked.scanning": "mi o alasa e ijo pi ijo lon ala...",
|
||||
"entries.unlinked.search_and_relink": "o alasa o pana e ijo lon tawa ijo",
|
||||
"entries.unlinked.search_and_relink": "o ala&sa o pana e ijo lon tawa ijo",
|
||||
"entries.unlinked.title": "o pona e ijo pi ijo lon ala",
|
||||
"ffmpeg.missing.description": "mi lukin ala e ilo FFmpeg e/anu ilo FFprobe. sina wile e kepeken sin pi musi mute e sitelen lili pi musi mute la sina wile e ilo FFmpeg.",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "o jo e sona sama",
|
||||
"field.edit": "o ante e sona",
|
||||
"field.paste": "o pana e sona sama",
|
||||
"field.copy": "o kama jo e ma sama",
|
||||
"field.edit": "o ante e ma",
|
||||
"field.paste": "o pana e ma sama",
|
||||
"file.date_added": "tenpo pi kama namako",
|
||||
"file.date_created": "tenpo pi kama sin",
|
||||
"file.date_modified": "tenpo pi kama ante",
|
||||
"file.dimensions": "suli",
|
||||
"file.duplicates.dupeguru.advice": "jasima li pini la, sina ken kepeken ilo DupeGuru. ilo DupeGuru li ken weka e ijo ike. ni li pini la, o kepeken e nasin \"o pona e ijo pi ijo lon ala\" lon ilo TagStudio. ni li weka e ijo pi ijo lon ala.",
|
||||
"file.duplicates.description": "ilo TagStudio li ken kama jo e pana pi ilo DupeGuru tawa ni: ilo li lawa e lipu pi lon sama mute.",
|
||||
"file.duplicates.dupeguru.advice": "jasima li pini la, sina ken kepeken ilo DupeGuru. ilo DupeGuru li ken weka e ijo ike. ni li pini la, o kepeken nasin \"o pona e ijo pi ijo lon ala\" lon ilo TagStudio. ni li weka e ijo pi ijo lon ala.",
|
||||
"file.duplicates.dupeguru.file_extension": "ijo DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "o kama sona e ijo DupeGuru",
|
||||
"file.duplicates.dupeguru.load_file": "o kama &lon e ijo DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "sina o anu e ijo DupeGuru",
|
||||
"file.duplicates.dupeguru.open_file": "o open e sona pini tan ilo DupeGuru",
|
||||
"file.duplicates.fix": "pona e ijo sama",
|
||||
"file.duplicates.matches": "ijo sama: %{count}",
|
||||
"file.duplicates.matches_uninitialized": "ijo sama: ala",
|
||||
"file.duplicates.mirror.description": "o jasima e sona kama lon kulupu sama ale. sona ale li kama wan li weka ala li kama mute ala. ni li weka ala e sona e ijo.",
|
||||
"file.duplicates.mirror_entries": "o jasima e ijo",
|
||||
"file.duplicates.mirror.description": "o jasima e sona kama lon kulupu sama ale. sona ale li kama wan li weka ala li kama mute ala e ma. ni li weka ala e sona e ijo.",
|
||||
"file.duplicates.mirror_entries": "o jasi&ma e ijo",
|
||||
"file.duration": "suli tenpo",
|
||||
"file.not_found": "mi lukin ala e ijo",
|
||||
"file.open_file": "o open e ijo",
|
||||
@@ -79,25 +92,25 @@
|
||||
"file.open_location.windows": "o alasa lon ilo File Explorer",
|
||||
"file.path": "nasin lipu",
|
||||
"folders_to_tags.close_all": "o pini e lukin pi ijo ale",
|
||||
"folders_to_tags.converting": "mi o pali e poki tan poki tomo",
|
||||
"folders_to_tags.description": "ni li pali e poki tan poki tona li pana e poki sin tawa ijo lon tomo.\n ilo ni li pali e anpa.",
|
||||
"folders_to_tags.converting": "mi pali e poki tan poki tomo",
|
||||
"folders_to_tags.description": "ni li pali e poki tan poki lipu sina li pana e poki sin tawa ijo lon tomo sina.\n ilo ni li ken lukin e poki ale ni e ijo ni.",
|
||||
"folders_to_tags.open_all": "o open e ale",
|
||||
"folders_to_tags.title": "o pali e poki tan poki tomo",
|
||||
"generic.add": "o pana",
|
||||
"generic.apply": "o pana",
|
||||
"generic.apply_alt": "&o pana",
|
||||
"generic.apply_alt": "o p&ana",
|
||||
"generic.cancel": "o ala",
|
||||
"generic.cancel_alt": "&o ala",
|
||||
"generic.cancel_alt": "o ala (&C)",
|
||||
"generic.close": "o pini",
|
||||
"generic.continue": "o awen tawa",
|
||||
"generic.copy": "o jo e sona sama",
|
||||
"generic.cut": "o lanpan",
|
||||
"generic.delete": "o weka",
|
||||
"generic.delete_alt": "&o weka",
|
||||
"generic.delete_alt": "o weka (&D)",
|
||||
"generic.done": "pona",
|
||||
"generic.done_alt": "&pona",
|
||||
"generic.done_alt": "pona (&D)",
|
||||
"generic.edit": "o ante",
|
||||
"generic.edit_alt": "&o ante",
|
||||
"generic.edit_alt": "o ant&e",
|
||||
"generic.filename": "nimi lipu",
|
||||
"generic.missing": "weka",
|
||||
"generic.navigation.back": "o tawa weka",
|
||||
@@ -106,7 +119,7 @@
|
||||
"generic.paste": "o pana e sona sama",
|
||||
"generic.recent_libraries": "tomo pi tenpo poka",
|
||||
"generic.rename": "o nimi sin",
|
||||
"generic.rename_alt": "&o nimi sin",
|
||||
"generic.rename_alt": "o nimi sin (&R)",
|
||||
"generic.reset": "o open sin",
|
||||
"generic.save": "o awen",
|
||||
"home.search": "o alasa",
|
||||
@@ -119,18 +132,21 @@
|
||||
"home.thumbnail_size.medium": "sitelen lili meso",
|
||||
"home.thumbnail_size.mini": "sitelen lili pi lili mute",
|
||||
"home.thumbnail_size.small": "sitelen lili pi lili meso",
|
||||
"ignore_list.add_extension": "o pana e nimi anpa",
|
||||
"ignore_list.add_extension": "o p&ana e nimi anpa",
|
||||
"ignore_list.mode.exclude": "o kepeken ala",
|
||||
"ignore_list.mode.include": "o kepeken",
|
||||
"ignore_list.mode.label": "nasin kulupu:",
|
||||
"ignore_list.title": "nimi lon nimi ijo anpa",
|
||||
"json_migration.description": "<br>o open e tawa tomo o lukin e pini. sina pilin e \"o pini e tawa\" taso la, mi kepeken <i>ala</i> e tomo ante. <br><br>sona tomo li jo e nanpa sama anu toki \"sama\" la ale li pona. nanpa ante li loje li jo e sitelen \"<b>(!)</b>\" lon poka ona.<br><center><i>tomo li suli la pali ni li lanpan e tenpo mute.</i></center>",
|
||||
"json_migration.checking_for_parity": "mi alasa e nasin tu...",
|
||||
"json_migration.description": "<br>o open e tawa tomo o lukin e pini. sina pilin ala e \"o pini e tawa\" la, mi kepeken <i>ala</i> e tomo ante. <br><br>sona tomo o jo e nanpa sama anu toki \"sama\" la ale li pona. nanpa ante li loje li jo e sitelen \"<b>(!)</b>\" lon poka ona.<br><center><i>tomo li suli la pali ni li lanpan e tenpo mute.</i></center>",
|
||||
"json_migration.discrepancies_found": "mi lukin e ike pi tomo sina",
|
||||
"json_migration.discrepancies_found.description": "mi lukin e ike lon nasin tomo open lon nasin tomo ante. o lukin, o awen tawa anu ala.",
|
||||
"json_migration.finish_migration": "o pini e tawa",
|
||||
"json_migration.heading.aliases": "nimi ante:",
|
||||
"json_migration.heading.colors": "kule:",
|
||||
"json_migration.heading.differ": "ike",
|
||||
"json_migration.heading.entires": "ijo:",
|
||||
"json_migration.heading.fields": "ma:",
|
||||
"json_migration.heading.file_extension_list": "kulupu pi namako lipu:",
|
||||
"json_migration.heading.match": "sama",
|
||||
"json_migration.heading.names": "nimi:",
|
||||
@@ -142,23 +158,30 @@
|
||||
"json_migration.migrating_files_entries": "mi tawa e lipu {entries:,d}...",
|
||||
"json_migration.migration_complete": "tawa li pini!",
|
||||
"json_migration.migration_complete_with_discrepancies": "tawa li pini, mi lukin e ike",
|
||||
"json_migration.start_and_preview": "o open o lukin lon kama",
|
||||
"json_migration.title": "o kama awen e tawa nasin: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>tomo pi ilo nanpa 9.5+</h2>",
|
||||
"json_migration.title.old_lib": "<h2>tomo pi ilo nanpa 9.4</h2>",
|
||||
"landing.open_create_library": "o open anu pali sin e tomo {shortcut}",
|
||||
"library.field.add": "pana e sona",
|
||||
"library.field.confirm_remove": "sina weka e sona poki \"{name}\". ni li pona anu seme?",
|
||||
"library.field.mixed_data": "sona ante",
|
||||
"library.field.remove": "weka e sona",
|
||||
"library.field.add": "o pana e ma",
|
||||
"library.field.confirm_remove": "sina wile ala wile weka e ma \"{name}\" ni?",
|
||||
"library.field.mixed_data": "sona nasa",
|
||||
"library.field.remove": "o weka e ma",
|
||||
"library.missing": "tomo li lon ala",
|
||||
"library.name": "tomo",
|
||||
"library.refresh.scanning.plural": "mi alasa e lipu sin lon tomo...\nmi alasa e lipu {searched_count}, mi lukin e lipu {found_count}",
|
||||
"library.refresh.scanning.singular": "mi alasa e lipu sin lon tomo...\nmi alasa e lipu {searched_count}, mi lukin e lipu {found_count}",
|
||||
"library.refresh.scanning_preparing": "mi alasa e ijo sin lon tomo...\nmi kama pona...",
|
||||
"library.refresh.title": "mi kama jo e sin lon tomo",
|
||||
"library.scan_library.title": "mi o lukin e tomo",
|
||||
"library_object.name": "nimi",
|
||||
"library_object.name_required": "nimi (wile mute)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library_object.slug_required": "ID Slug (wile mute)",
|
||||
"macros.running.dialog.new_entries": "mi pali lon ijo sin {count}/{total}...",
|
||||
"macros.running.dialog.title": "mi pali lon ijo sin",
|
||||
"media_player.autoplay": "pali pi sina ala",
|
||||
"media_player.loop": "pali sike",
|
||||
"menu.delete_selected_files_ambiguous": "o tawa e lipu tawa {trash_term}",
|
||||
"menu.delete_selected_files_plural": "o tawa e lipu tawa {trash_term}",
|
||||
"menu.delete_selected_files_singular": "o tawa e lipu tawa {trash_term}",
|
||||
@@ -166,24 +189,49 @@
|
||||
"menu.edit.ignore_list": "o lukin ala e ijo ni",
|
||||
"menu.edit.manage_file_extensions": "o lawa e namako lipu",
|
||||
"menu.edit.manage_tags": "o lawa e poki",
|
||||
"menu.file": "ijo",
|
||||
"menu.file.close_library": "&o pini e tomo",
|
||||
"menu.edit.new_tag": "poki sin (&T)",
|
||||
"menu.file": "ijo (&F)",
|
||||
"menu.file.clear_recent_libraries": "o weka e poka",
|
||||
"menu.file.close_library": "o pini e tomo (&C)",
|
||||
"menu.file.missing_library.message": "mi ken ala lukin e lon pi tomo \"{library}\".",
|
||||
"menu.file.missing_library.title": "tomo pi lon ala",
|
||||
"menu.file.new_library": "o sin e tomo",
|
||||
"menu.file.open_create_library": "o open/pali e tomo",
|
||||
"menu.file.open_create_library": "o &open/pali e tomo",
|
||||
"menu.file.open_library": "o open e tomo",
|
||||
"menu.file.open_recent_library": "o open e poka",
|
||||
"menu.file.refresh_directories": "o lukin sin lon tomo (&R)",
|
||||
"menu.file.save_library": "o awen e sona tomo",
|
||||
"menu.help": "mi jo e toki seme",
|
||||
"menu.help": "mi jo e toki seme (&H)",
|
||||
"menu.help.about": "sona",
|
||||
"menu.macros": "ilo pali",
|
||||
"menu.macros.folders_to_tags": "kulupu lipu tawa poki",
|
||||
"menu.tools": "ilo",
|
||||
"menu.view": "o lukin",
|
||||
"menu.macros": "ilo pali (&M)",
|
||||
"menu.macros.folders_to_tags": "tan poki lipu tawa poki",
|
||||
"menu.select": "o jo",
|
||||
"menu.settings": "lawa toki...",
|
||||
"menu.tools": "ilo (&T)",
|
||||
"menu.tools.fix_duplicate_files": "o pona e lipu sama (&F)",
|
||||
"menu.tools.fix_unlinked_entries": "o pona e ijo pi ijo lon ala (&U)",
|
||||
"menu.view": "o lukin (&V)",
|
||||
"menu.window": "lipu",
|
||||
"namespace.create.description": "ilo TagStuidio li kulupu e poki e kule kepeken nasin pi pana pona la ilo li kepeken ma nimi. ma nimi pi nimi open \"tagstudio\" li ilo TagStudio taso. ilo TagStudio li kepeken ona lon insa.",
|
||||
"namespace.create.description_color": "kule poki li kepeken ma nimi sama kulupu pi kulupu kule. ale la open la kule ale pi pali sina li lon kulupu pi ma nimi.",
|
||||
"namespace.create.title": "o pali sin e ma nimi",
|
||||
"namespace.new.button": "o pali sin e ma nimi",
|
||||
"namespace.new.prompt": "o pali sin e ma nimi tawa pana e kule sina!",
|
||||
"preview.multiple_selection": "sina jo e ijo <b>{count}</b>",
|
||||
"preview.no_selection": "ijo ala li anu",
|
||||
"select.add_tag_to_selected": "o pana e poki tawa jo sina",
|
||||
"select.all": "o jo e ale",
|
||||
"select.clear": "o weka e jo sina",
|
||||
"select.inverse": "o jasima e ni",
|
||||
"settings.dateformat.english": "nasin Inli",
|
||||
"settings.dateformat.international": "nasin pi ma mute",
|
||||
"settings.dateformat.label": "nasin tenpo",
|
||||
"settings.dateformat.system": "nasin pi ilo sina",
|
||||
"settings.filepath.label": "ken lukin pi nasin lipu",
|
||||
"settings.filepath.option.full": "o ken lukin e nasin wan",
|
||||
"settings.filepath.option.name": "o ken lukin e nimi lipu taso",
|
||||
"settings.global": "lawa toki pi ma ale",
|
||||
"settings.hourformat.label": "tenpo pi kipisi 24",
|
||||
"settings.language": "toki",
|
||||
"settings.library": "lawa toki pi tomo mi",
|
||||
"settings.open_library_on_start": "ilo Tagstudio li open la o open e tomo ni",
|
||||
@@ -191,28 +239,37 @@
|
||||
"settings.restart_required": "sina open sin e ilo Tagstudio la ante li lon.",
|
||||
"settings.show_filenames_in_grid": "o sitelen e nimi ijo lon leko sitelen",
|
||||
"settings.show_recent_libraries": "o sitelen e tomo pi tenpo poka",
|
||||
"settings.tag_click_action.add_to_search": "o pana e poki tawa alasa",
|
||||
"settings.tag_click_action.open_edit": "o ante e poki",
|
||||
"settings.tag_click_action.set_search": "o alasa e poki",
|
||||
"settings.theme.dark": "pimeja",
|
||||
"settings.theme.label": "nasin kule:",
|
||||
"settings.theme.light": "walo",
|
||||
"settings.theme.system": "ilo sina",
|
||||
"settings.title": "lawa toki",
|
||||
"splash.opening_library": "mi open e tomo \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "mi weka e lipu {count}!",
|
||||
"status.deleted_file_singular": "mi weka e lipu 1!",
|
||||
"status.deleted_none": "mi weka e lipu ala.",
|
||||
"status.deleted_partial_warning": "mi weka e lipu {count} taso! o lukin tan ni: lipu li weka anu ijo li kepeken e ona anu seme.",
|
||||
"status.deleted_partial_warning": "mi weka e lipu {count} taso! o lukin tan ni: lipu li weka anu ijo li kepeken ona anu seme.",
|
||||
"status.deleting_file": "mi weka e lipu [{i}/{count}]: \"{path}\"...",
|
||||
"status.library_backup_success": "tomo sama li lon: \"{path}\" ({time_span})",
|
||||
"status.library_closed": "tomo li pini ({time_span})",
|
||||
"status.library_closing": "mi pini e tomo...",
|
||||
"status.library_save_success": "tomo li awen li weka!",
|
||||
"status.library_search_query": "mi alasa lon tomo...",
|
||||
"status.library_version_expected": "mi wile e:",
|
||||
"status.library_version_found": "mi lukin e:",
|
||||
"status.library_version_mismatch": "sama ala pi nanpa tomo!",
|
||||
"status.results": "jo",
|
||||
"status.results.invalid_syntax": "toki alasa ike:",
|
||||
"status.results_found": "mi sona e ijo {count} ({time_span})",
|
||||
"tag.add": "o pana e poki",
|
||||
"tag.add.plural": "o pana e poki",
|
||||
"tag.add_to_search": "pana tawa alasa",
|
||||
"tag.aliases": "nimi ante",
|
||||
"tag.all_tags": "poki ale",
|
||||
"tag.choose_color": "o kule e poki",
|
||||
"tag.color": "kule",
|
||||
"tag.confirm_delete": "sina wile ala wile weka e poki \"{tag_name}\"?",
|
||||
"tag.create": "o pali sin e poki",
|
||||
@@ -228,6 +285,7 @@
|
||||
"tag.search_for_tag": "o alasa e poki",
|
||||
"tag.shorthand": "nimi lili",
|
||||
"tag.tag_name_required": "nimi poki (wile mute)",
|
||||
"tag.view_limit": "sina ken lukin e:",
|
||||
"tag_manager.title": "poki tomo",
|
||||
"trash.context.ambiguous": "o tawa e lipu tawa {trash_term}",
|
||||
"trash.context.plural": "o tawa e lipu tawa {trash_term}",
|
||||
|
||||
325
src/tagstudio/resources/translations/zh_Hans.json
Normal file
325
src/tagstudio/resources/translations/zh_Hans.json
Normal file
@@ -0,0 +1,325 @@
|
||||
{
|
||||
"about.config_path": "配置路径",
|
||||
"about.description": "TagStudio是一款照片和文件组织应用程序,采用基于标签的系统,旨在为用户提供自由和灵活性。该应用程序不使用专有程序或格式,不会产生大量的辅助文件,也不会对您的文件系统结构造成彻底的颠覆。",
|
||||
"about.documentation": "文档",
|
||||
"about.license": "许可协议",
|
||||
"about.module.found": "存在",
|
||||
"about.title": "关于 TagStudio",
|
||||
"about.website": "网站",
|
||||
"app.git": "Git 提交更新",
|
||||
"app.pre_release": "预发布版本",
|
||||
"app.title": "{base_title} - 仓库 '{library_dir}'",
|
||||
"color.color_border": "边框使用次要颜色",
|
||||
"color.confirm_delete": "您确定要删除颜色\"{color_name}\"吗?",
|
||||
"color.delete": "删除标签",
|
||||
"color.import_pack": "导入颜色包",
|
||||
"color.name": "颜色名",
|
||||
"color.namespace.delete.prompt": "您确定要删除此颜色命名空间吗?此操作将同时删除该命名空间内的所有颜色!",
|
||||
"color.namespace.delete.title": "删除颜色命名空间",
|
||||
"color.new": "新建颜色",
|
||||
"color.placeholder": "颜色",
|
||||
"color.primary": "主要颜色",
|
||||
"color.primary_required": "主要颜色 (必选)",
|
||||
"color.secondary": "次要颜色",
|
||||
"color.title.no_color": "未选择颜色",
|
||||
"color_manager.title": "标签颜色管理",
|
||||
"dependency.missing.title": "{dependency} 未找到",
|
||||
"drop_import.description": "以下文件路径已在仓库中存在",
|
||||
"drop_import.duplicates_choice.plural": "以下 {count} 个文件的路径已在资源库中存在。",
|
||||
"drop_import.duplicates_choice.singular": "以下文件路径已在仓库中存在。",
|
||||
"drop_import.progress.label.initial": "导入新文件中...",
|
||||
"drop_import.progress.label.plural": "导入新文件中...\n已导入 {count} 个文件.{suffix}",
|
||||
"drop_import.progress.label.singular": "导入新文件中...\n已导入 1 个文件.{suffix}",
|
||||
"drop_import.progress.window_title": "导入文件",
|
||||
"drop_import.title": "文件冲突",
|
||||
"edit.color_manager": "标签颜色管理",
|
||||
"edit.copy_fields": "复制字段",
|
||||
"edit.paste_fields": "粘贴字段",
|
||||
"edit.tag_manager": "标签管理",
|
||||
"entries.duplicate.merge": "合并重复项目",
|
||||
"entries.duplicate.merge.label": "正在合并重复项目...",
|
||||
"entries.duplicate.refresh": "重新整理重复项目",
|
||||
"entries.duplicates.description": "重复项目被定义为多个指向磁盘上同一文件的项目。合并这些项目将把所有重复项目的标签和元数据整合为一个统一的项目。这与“重复文件”不同,后者是指在 TagStudio 之外的文件本身的重复。",
|
||||
"entries.mirror": "镜像(&m)",
|
||||
"entries.mirror.confirmation": "您确定要镜像以下 {count} 条项目吗?",
|
||||
"entries.mirror.label": "正在镜像 {idx}/{total} 个项目...",
|
||||
"entries.mirror.title": "进行项目镜像",
|
||||
"entries.mirror.window_title": "项目镜像",
|
||||
"entries.running.dialog.new_entries": "正在加入 {total} 个新文件项目...",
|
||||
"entries.running.dialog.title": "正在加入新文件项目",
|
||||
"entries.tags": "标签",
|
||||
"entries.unlinked.delete": "删除未链接的项目",
|
||||
"entries.unlinked.delete.confirm": "您确定要删除以下 {count} 个项目?",
|
||||
"entries.unlinked.delete.deleting": "正在删除项目",
|
||||
"entries.unlinked.delete.deleting_count": "正在删除 {idx}/{count} 个未链接的项目",
|
||||
"entries.unlinked.delete_alt": "删除未链接的项目(&l)",
|
||||
"entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。<br><br>未链接的条目可能会通过搜索目录自动重新链接,或者根据需要删除。",
|
||||
"entries.unlinked.missing_count.none": "未链接的项目: 无",
|
||||
"entries.unlinked.missing_count.some": "未链接的项目: {count}",
|
||||
"entries.unlinked.refresh_all": "全部刷新(&r)",
|
||||
"entries.unlinked.relink.attempting": "正在尝试重新链接 {idx}/{missing_count} 个项目, {fixed_count} 个项目成功重链",
|
||||
"entries.unlinked.relink.manual": "手动重新链接(&m)",
|
||||
"entries.unlinked.relink.title": "正在重新链接项目",
|
||||
"entries.unlinked.scanning": "正在扫描仓库以寻找未链接的项目...",
|
||||
"entries.unlinked.search_and_relink": "搜索并重新链接(&s)",
|
||||
"entries.unlinked.title": "修复未链接的项目",
|
||||
"ffmpeg.missing.description": "找不到 FFmpeg 或 FFprobe。多媒体播放和缩略图生成需要 FFmpeg 支持。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "复制字段",
|
||||
"field.edit": "编辑字段",
|
||||
"field.paste": "粘贴字段",
|
||||
"file.date_added": "加入日期",
|
||||
"file.date_created": "建立日期",
|
||||
"file.date_modified": "更改日期",
|
||||
"file.dimensions": "尺寸",
|
||||
"file.duplicates.description": "TagStudio 支持导入 DupeGuru 结果,以管理重复文件。",
|
||||
"file.duplicates.dupeguru.advice": "在镜像之后,你可以使用 DupeGuru 删除不需要的文件。删除文件后,使用 TagStudio 内的 “修复未链接的项目” 选项来移除未链接的项目。",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru 文件 (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "加载 DupeGuru 文件(&l)",
|
||||
"file.duplicates.dupeguru.no_file": "未选择 DupeGuru 文件",
|
||||
"file.duplicates.dupeguru.open_file": "打开 DupeGuru 结果文件",
|
||||
"file.duplicates.fix": "修复重复文件",
|
||||
"file.duplicates.matches": "重复文件: {count}",
|
||||
"file.duplicates.matches_uninitialized": "重复文件: 无",
|
||||
"file.duplicates.mirror.description": "在每组重复匹配的项目间镜像同步项目数据,合并所有数据但不会删除或重复字段。此操作不会删除任何文件或数据。",
|
||||
"file.duplicates.mirror_entries": "镜像项目(&m)",
|
||||
"file.duration": "长度",
|
||||
"file.not_found": "找不到文件",
|
||||
"file.open_file": "打开文件",
|
||||
"file.open_file_with": "使用指定程序打开文件",
|
||||
"file.open_location.generic": "在资源管理器中展示文件",
|
||||
"file.open_location.mac": "在 Finder 中显示",
|
||||
"file.open_location.windows": "在资源管理器中展示",
|
||||
"file.path": "文件路径",
|
||||
"folders_to_tags.close_all": "关闭全部",
|
||||
"folders_to_tags.converting": "正在将文件夹转换为标签",
|
||||
"folders_to_tags.description": "根据你的文件夹结构创建标签并应用在项目上。\n 下方的结构图将会显示所创建的所有标签及其对应的应用项目。",
|
||||
"folders_to_tags.open_all": "打开全部",
|
||||
"folders_to_tags.title": "从文件夹创建标签",
|
||||
"generic.add": "新增",
|
||||
"generic.apply": "应用",
|
||||
"generic.apply_alt": "应用(&a)",
|
||||
"generic.cancel": "取消",
|
||||
"generic.cancel_alt": "取消(&c)",
|
||||
"generic.close": "关闭",
|
||||
"generic.continue": "继续",
|
||||
"generic.copy": "复制",
|
||||
"generic.cut": "剪切",
|
||||
"generic.delete": "删除",
|
||||
"generic.delete_alt": "删除(&d)",
|
||||
"generic.done": "完成",
|
||||
"generic.done_alt": "完成(&d)",
|
||||
"generic.edit": "编辑",
|
||||
"generic.edit_alt": "编辑(&e)",
|
||||
"generic.filename": "文件名",
|
||||
"generic.missing": "缺失",
|
||||
"generic.navigation.back": "返回",
|
||||
"generic.navigation.next": "下一个",
|
||||
"generic.none": "无",
|
||||
"generic.overwrite": "覆盖",
|
||||
"generic.overwrite_alt": "覆盖(&o)",
|
||||
"generic.paste": "粘贴",
|
||||
"generic.recent_libraries": "最近使用的仓库",
|
||||
"generic.rename": "重命名",
|
||||
"generic.rename_alt": "重命名(&r)",
|
||||
"generic.reset": "重置",
|
||||
"generic.save": "保存",
|
||||
"generic.skip": "跳过",
|
||||
"generic.skip_alt": "跳过(&s)",
|
||||
"home.search": "搜索",
|
||||
"home.search_entries": "搜索项目",
|
||||
"home.search_library": "搜索仓库",
|
||||
"home.search_tags": "搜索标签",
|
||||
"home.thumbnail_size": "缩略图尺寸",
|
||||
"home.thumbnail_size.extra_large": "特大缩略图",
|
||||
"home.thumbnail_size.large": "大缩略图",
|
||||
"home.thumbnail_size.medium": "中等缩略图",
|
||||
"home.thumbnail_size.mini": "迷你缩略图",
|
||||
"home.thumbnail_size.small": "小缩略图",
|
||||
"ignore_list.add_extension": "添加插件(&a)",
|
||||
"ignore_list.mode.exclude": "排除",
|
||||
"ignore_list.mode.include": "包含",
|
||||
"ignore_list.mode.label": "列表模式:",
|
||||
"ignore_list.title": "文件格式",
|
||||
"json_migration.checking_for_parity": "正在校验奇偶位...",
|
||||
"json_migration.creating_database_tables": "正在建立SQL数据库表...",
|
||||
"json_migration.description": "<br>开始并预览资源库迁移结果。仓库的转换<i>不会</i>立即生效,除非您点击\"完成迁移\"按钮。<br><br>仓库数据应具有匹配值或带有\"已匹配\"标签。不匹配的值将显示为红色,并带有\"<b>(!)</b>\"警示符号。<br><center><i>大型仓库的迁移过程可能需要数分钟。</i></center>",
|
||||
"json_migration.discrepancies_found": "发现仓库差异",
|
||||
"json_migration.discrepancies_found.description": "在原始仓库格式和转换后的仓库格式中发现差异。请检查并确定是否继续执行迁移。",
|
||||
"json_migration.finish_migration": "完成迁移",
|
||||
"json_migration.heading.aliases": "别名:",
|
||||
"json_migration.heading.colors": "颜色:",
|
||||
"json_migration.heading.differ": "差异",
|
||||
"json_migration.heading.entires": "项目:",
|
||||
"json_migration.heading.extension_list_type": "扩展名列表类型:",
|
||||
"json_migration.heading.fields": "字段:",
|
||||
"json_migration.heading.file_extension_list": "文件扩展名列表:",
|
||||
"json_migration.heading.match": "已匹配",
|
||||
"json_migration.heading.names": "名字:",
|
||||
"json_migration.heading.parent_tags": "上级标签:",
|
||||
"json_migration.heading.paths": "路径:",
|
||||
"json_migration.heading.shorthands": "缩写:",
|
||||
"json_migration.heading.tags": "标签:",
|
||||
"json_migration.info.description": "在 TagStudio <b>9.4 和更早版本</b>建立的仓库存档文件需要迁移至新的 <b>v9.5+</b> 格式.<br><h2>注意事项:</h2><ul><li>你现有的仓库存档文件<b><i>不会</i></b>被删除</li><li>您的个人文件<b><i>不会</i></b>被删除,移动或修改</li><li>新版 v9.5+ 仓库存档格式无法被早期 TagStudio 读取</li></ul><h3>主要变更:</h3><ul><li>\"标签字段\"变更为\"标签分类\"。现在标签直接添加到文件条目,而非先添加至字段。系统会根据标签编辑菜单中标记为\"作为分类\"的父标签自动归类成普通标签和分类标签。任何标签都可设为分类标签,子标签会自动归至其父分类下。\"收藏\"和\"归档\"标签现在继承自新的默认分类标签\"元标签\"。</li><li>标签颜色已进行了调整和扩展。部分颜色已被重新命名或合并,但所有标签颜色在 v9.5 中都会转换为完全匹配或最接近的配色。</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "正在迁移 {entries:,d} 个项目...",
|
||||
"json_migration.migration_complete": "迁移完成!",
|
||||
"json_migration.migration_complete_with_discrepancies": "迁移完成,发现差异",
|
||||
"json_migration.start_and_preview": "开始并预览",
|
||||
"json_migration.title": "保存格式迁移: \"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ 仓库</h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 仓库</h2>",
|
||||
"landing.open_create_library": "打开/创建仓库 {shortcut}",
|
||||
"library.field.add": "新增字段",
|
||||
"library.field.confirm_remove": "您确定要移除此 \"{name}\" 字段?",
|
||||
"library.field.mixed_data": "混合数据",
|
||||
"library.field.remove": "移除字段",
|
||||
"library.missing": "仓库路径缺失",
|
||||
"library.name": "仓库",
|
||||
"library.refresh.scanning.plural": "正在扫描文件夹中的新文件...\n已找到 {searched_count} 个文件,找到 {found_count} 个新文件",
|
||||
"library.refresh.scanning.singular": "正在扫描文件夹中的新文件...\n已找到 {searched_count} 个文件,找到 {found_count} 个新文件",
|
||||
"library.refresh.scanning_preparing": "正在扫描文件夹中的新文件...\n准备中...",
|
||||
"library.refresh.title": "正在刷新目录",
|
||||
"library.scan_library.title": "正在扫描仓库",
|
||||
"library_object.name": "仓库名",
|
||||
"library_object.name_required": "仓库名(必填)",
|
||||
"library_object.slug": "ID 短链",
|
||||
"library_object.slug_required": "ID 短链(必填)",
|
||||
"macros.running.dialog.new_entries": "正在对 {count}/{total} 个新文件项目执行预设宏...",
|
||||
"macros.running.dialog.title": "正在对新文件项目执行预设宏",
|
||||
"media_player.autoplay": "自动播放",
|
||||
"media_player.loop": "循环",
|
||||
"menu.delete_selected_files_ambiguous": "移动文件到 {trash_term}",
|
||||
"menu.delete_selected_files_plural": "移动文件到 {trash_term}",
|
||||
"menu.delete_selected_files_singular": "移动文件到 {trash_term}",
|
||||
"menu.edit": "编辑",
|
||||
"menu.edit.ignore_list": "忽略文件和文件夹",
|
||||
"menu.edit.manage_file_extensions": "文件扩展名管理",
|
||||
"menu.edit.manage_tags": "标签管理",
|
||||
"menu.edit.new_tag": "新标签(&t)",
|
||||
"menu.file": "文件(&f)",
|
||||
"menu.file.clear_recent_libraries": "清除最近仓库记录",
|
||||
"menu.file.close_library": "关闭仓库(&c)",
|
||||
"menu.file.missing_library.message": "无法找到资源库 \"{library}\" 的存储位置。",
|
||||
"menu.file.missing_library.title": "仓库缺失",
|
||||
"menu.file.new_library": "新建仓库",
|
||||
"menu.file.open_create_library": "打开/创建仓库(&o)",
|
||||
"menu.file.open_library": "打开仓库",
|
||||
"menu.file.open_recent_library": "打开最近仓库",
|
||||
"menu.file.refresh_directories": "刷新文件夹(&r)",
|
||||
"menu.file.save_backup": "保存仓库备份(&s)",
|
||||
"menu.file.save_library": "保存仓库",
|
||||
"menu.help": "帮助(&h)",
|
||||
"menu.help.about": "关于",
|
||||
"menu.macros": "宏(&m)",
|
||||
"menu.macros.folders_to_tags": "文件夹到标签",
|
||||
"menu.select": "选择",
|
||||
"menu.settings": "设置...",
|
||||
"menu.tools": "工具(&t)",
|
||||
"menu.tools.fix_duplicate_files": "修复重复文件(&f)",
|
||||
"menu.tools.fix_unlinked_entries": "修复未链接项目(&u)",
|
||||
"menu.view": "显示(&v)",
|
||||
"menu.window": "选项(Window)",
|
||||
"namespace.create.description": "命名空间由 TagStudio 用于将标签和颜色等项目分组,以便于导出和共享。以 \"TagStudio\" 开头的命名空间为 TagStudio 保留,用于内部使用。",
|
||||
"namespace.create.description_color": "标签颜色使用命名空间作为颜色调色板组。所有自定义颜色必须首先归属于一个命名空间组。",
|
||||
"namespace.create.title": "创建命名空间",
|
||||
"namespace.new.button": "新建命名空间",
|
||||
"namespace.new.prompt": "创建一个新的命名空间来开始添加自定义颜色!",
|
||||
"preview.multiple_selection": "已选择 <b>{count}</b> 个项目",
|
||||
"preview.no_selection": "尚未选择项目",
|
||||
"select.add_tag_to_selected": "添加标签到已选择的项目",
|
||||
"select.all": "全选",
|
||||
"select.clear": "清除选择",
|
||||
"select.inverse": "反选",
|
||||
"settings.clear_thumb_cache.title": "清除缩略图缓存",
|
||||
"settings.dateformat.english": "英语格式",
|
||||
"settings.dateformat.international": "国际格式",
|
||||
"settings.dateformat.label": "日期格式",
|
||||
"settings.dateformat.system": "系统格式",
|
||||
"settings.filepath.label": "文件路径可见性",
|
||||
"settings.filepath.option.full": "显示完整路径",
|
||||
"settings.filepath.option.name": "仅显示文件名",
|
||||
"settings.filepath.option.relative": "显示相对路径",
|
||||
"settings.global": "全局设置",
|
||||
"settings.hourformat.label": "24小时制",
|
||||
"settings.language": "语言",
|
||||
"settings.library": "仓库设置",
|
||||
"settings.open_library_on_start": "在启动时打开仓库",
|
||||
"settings.page_size": "每页显示个数",
|
||||
"settings.restart_required": "请重启 TagStudio 以应用修改。",
|
||||
"settings.show_filenames_in_grid": "在网格中显示文件名",
|
||||
"settings.show_recent_libraries": "显示最近使用过的仓库",
|
||||
"settings.tag_click_action.add_to_search": "在搜索中添加标签",
|
||||
"settings.tag_click_action.label": "标签点击操作",
|
||||
"settings.tag_click_action.open_edit": "编辑标签",
|
||||
"settings.tag_click_action.set_search": "搜索标签",
|
||||
"settings.theme.dark": "深色模式",
|
||||
"settings.theme.label": "主题:",
|
||||
"settings.theme.light": "浅色模式",
|
||||
"settings.theme.system": "由系统决定",
|
||||
"settings.title": "设置",
|
||||
"settings.zeropadding.label": "日期补零",
|
||||
"sorting.direction.ascending": "升序",
|
||||
"sorting.direction.descending": "降序",
|
||||
"splash.opening_library": "打开仓库于 \"{library_path}\"...",
|
||||
"status.deleted_file_plural": "删除 {count} 个文件!",
|
||||
"status.deleted_file_singular": "删除 1 个文件!",
|
||||
"status.deleted_none": "没有文件被删除。",
|
||||
"status.deleted_partial_warning": "仅删除了 {count} 个文件!检查是否有任何文件当前缺失或正在使用中。",
|
||||
"status.deleting_file": "删除文件[{i}/{count}]:\"{path}\"...",
|
||||
"status.library_backup_in_progress": "正在保存仓库备份...",
|
||||
"status.library_backup_success": "仓库备份保存于:\"{path}\" (耗时 {time_span})",
|
||||
"status.library_closed": "已关闭仓库 (耗时 {time_span})",
|
||||
"status.library_closing": "正在关闭仓库...",
|
||||
"status.library_save_success": "仓库保存并关闭!",
|
||||
"status.library_search_query": "正在搜索仓库...",
|
||||
"status.library_version_expected": "包含:",
|
||||
"status.library_version_found": "找到:",
|
||||
"status.library_version_mismatch": "仓库版本不匹配!",
|
||||
"status.results": "结果",
|
||||
"status.results.invalid_syntax": "非法搜索语法:",
|
||||
"status.results_found": "已找到 {count} 个结果 (耗时 {time_span})",
|
||||
"tag.add": "新增标签",
|
||||
"tag.add.plural": "新增标签",
|
||||
"tag.add_to_search": "新增到搜索",
|
||||
"tag.aliases": "别名",
|
||||
"tag.all_tags": "全部标签",
|
||||
"tag.choose_color": "选择标签颜色",
|
||||
"tag.color": "颜色",
|
||||
"tag.confirm_delete": "您确定要删除此标签 \"{tag_name}\"?",
|
||||
"tag.create": "创建标签",
|
||||
"tag.create_add": "创建并添加 \"{query}\"",
|
||||
"tag.disambiguation.tooltip": "使用此标签消除歧义",
|
||||
"tag.edit": "编辑标签",
|
||||
"tag.is_category": "作为分类",
|
||||
"tag.name": "标签名",
|
||||
"tag.new": "新建标签",
|
||||
"tag.parent_tags": "上级标签",
|
||||
"tag.parent_tags.add": "新增上级标签",
|
||||
"tag.parent_tags.description": "该标签可以在搜索中作为任何这些上级标签的替代。",
|
||||
"tag.remove": "移除标签",
|
||||
"tag.search_for_tag": "搜索标签",
|
||||
"tag.shorthand": "缩写",
|
||||
"tag.tag_name_required": "标签名称(必填)",
|
||||
"tag.view_limit": "查看限制:",
|
||||
"tag_manager.title": "仓库标签",
|
||||
"trash.context.ambiguous": "移动文件到 {trash_term}",
|
||||
"trash.context.plural": "移动文件到 {trash_term}",
|
||||
"trash.context.singular": "移动文件到 {trash_term}",
|
||||
"trash.dialog.disambiguation_warning.plural": "这将同时从 TagStudio <i>和</i>您的文件系统中删除它们!",
|
||||
"trash.dialog.disambiguation_warning.singular": "这将同时从 TagStudio <i>和</i>您的文件系统中删除它!",
|
||||
"trash.dialog.move.confirmation.plural": "您确定要移动这 {count} 个文件到 {trash_term}?",
|
||||
"trash.dialog.move.confirmation.singular": "您确定要移动此文件到 {trash_term}?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>警告!</b>如果此文件无法被移动到 {trash_term},它将被 <b>永久删除!</b>",
|
||||
"trash.dialog.title.plural": "删除文件",
|
||||
"trash.dialog.title.singular": "删除文件",
|
||||
"trash.name.generic": "回收站",
|
||||
"trash.name.windows": "回收站",
|
||||
"view.size.0": "迷你",
|
||||
"view.size.1": "小",
|
||||
"view.size.2": "中",
|
||||
"view.size.3": "大",
|
||||
"view.size.4": "特大",
|
||||
"window.message.error_opening_library": "打开仓库时出错。",
|
||||
"window.title.error": "错误",
|
||||
"window.title.open_create_library": "打开/创建仓库"
|
||||
}
|
||||
@@ -1,214 +1,325 @@
|
||||
{
|
||||
"app.git": "Git提交更新",
|
||||
"app.pre_release": "預先發行",
|
||||
"app.title": "{base_title} - 文庫 '{library_dir}'",
|
||||
"drop_import.description": "以下檔案的檔名已經存在於文庫中",
|
||||
"drop_import.duplicates_choice.plural": "以下 {count} 個檔案的檔名已經存在於文庫中。",
|
||||
"drop_import.duplicates_choice.singular": "此檔案的檔名已經存在於文庫中。",
|
||||
"drop_import.progress.label.initial": "新的檔案匯入中...",
|
||||
"drop_import.progress.label.plural": "匯入新檔案...\n已匯入 {count} 個檔案.{suffix}",
|
||||
"drop_import.progress.label.singular": "匯入新檔案...\n已匯入一個檔案.{suffix}",
|
||||
"about.config_path": "設定路徑",
|
||||
"about.description": "TagStudio 是一個照片和檔案管理應用程式,採用基於標籤的系統來為使用者提供自由度和彈性。沒有專有的程序或格式,沒有大量的輔助檔案,也不會對檔案系統造成破壞性變更。",
|
||||
"about.documentation": "說明文件",
|
||||
"about.license": "授權條款",
|
||||
"about.module.found": "存在",
|
||||
"about.title": "關於 TagStudio",
|
||||
"about.website": "網站",
|
||||
"app.git": "Git 提交更新",
|
||||
"app.pre_release": "預先發布版本",
|
||||
"app.title": "{base_title} - 文件庫「{library_dir}」",
|
||||
"color_manager.title": "管理標籤顏色",
|
||||
"color.color_border": "在邊框使用第二個顏色",
|
||||
"color.confirm_delete": "您確定要刪除「{color_name}」嗎?",
|
||||
"color.delete": "刪除顏色",
|
||||
"color.import_pack": "匯入色彩包",
|
||||
"color.name": "名稱",
|
||||
"color.namespace.delete.prompt": "您確定要刪除此顏色命名空間嗎?這將刪除此命名空間中的所有顏色!",
|
||||
"color.namespace.delete.title": "刪除顏色命名空間",
|
||||
"color.new": "新增顏色",
|
||||
"color.placeholder": "顏色",
|
||||
"color.primary_required": "主要顏色 (必填)",
|
||||
"color.primary": "主要顏色",
|
||||
"color.secondary": "次要顏色",
|
||||
"color.title.no_color": "無顏色",
|
||||
"dependency.missing.title": "未找到 {dependency}",
|
||||
"drop_import.description": "以下檔案與文件庫中已存在的檔案路徑重複",
|
||||
"drop_import.duplicates_choice.plural": "以下 {count} 個檔案與文件庫中已存在的檔案路徑重複",
|
||||
"drop_import.duplicates_choice.singular": "以下檔案與文件庫中已存在的檔案路徑重複",
|
||||
"drop_import.progress.label.initial": "正在匯入新檔案...",
|
||||
"drop_import.progress.label.plural": "正在匯入新檔案...\n已匯入 {count} 個檔案。{suffix}",
|
||||
"drop_import.progress.label.singular": "正在匯入新檔案...\n已匯入 1 個檔案。{suffix}",
|
||||
"drop_import.progress.window_title": "匯入檔案",
|
||||
"drop_import.title": "檔案衝突",
|
||||
"edit.tag_manager": "標籤管理",
|
||||
"entries.duplicate.merge": "合併重複項目",
|
||||
"edit.color_manager": "管理標籤顏色",
|
||||
"edit.copy_fields": "複製欄位",
|
||||
"edit.paste_fields": "貼上欄位",
|
||||
"edit.tag_manager": "管理標籤",
|
||||
"entries.duplicate.merge.label": "正在合併重複項目...",
|
||||
"entries.duplicate.merge": "合併重複項目",
|
||||
"entries.duplicate.refresh": "重新整理重複項目",
|
||||
"entries.duplicates.description": "重複的項目指的是多個項目皆指向同一個檔案。合併這些重複項目會合併所有項目的標籤和後設資料。重複檔案和重複項目不同,重複檔案是在Tag Studio外的重複檔案。",
|
||||
"entries.mirror": "&鏡像",
|
||||
"entries.mirror.confirmation": "您確定要鏡像以下 {count} 個項目?",
|
||||
"entries.duplicates.description": "重複項目的定義為多個項目指向硬碟中的同一個檔案。合併這些重複項目會將其所有的標籤和元資料合併為一個單獨的項目。這些並不是重複的檔案,重複的檔案是 TagStudio 以外的重複檔案。",
|
||||
"entries.mirror.confirmation": "您確定要鏡像 {count} 個項目嗎?",
|
||||
"entries.mirror.label": "正在鏡像 {idx}/{total} 個項目...",
|
||||
"entries.mirror.title": "進行項目鏡像",
|
||||
"entries.mirror.window_title": "項目鏡像",
|
||||
"entries.mirror.title": "鏡像項目",
|
||||
"entries.mirror.window_title": "鏡像項目",
|
||||
"entries.mirror": "鏡像 (&M)",
|
||||
"entries.running.dialog.new_entries": "正在加入 {total} 個新檔案項目...",
|
||||
"entries.running.dialog.title": "正在加入新檔案項目",
|
||||
"entries.tags": "標籤",
|
||||
"entries.unlinked.delete": "刪除未連結的項目",
|
||||
"entries.unlinked.delete.confirm": "您確定要刪除 {count} 個項目?",
|
||||
"entries.unlinked.delete_alt": "刪除未連接項目",
|
||||
"entries.unlinked.delete.confirm": "您確定要刪除 {count} 個項目嗎?",
|
||||
"entries.unlinked.delete.deleting_count": "正在刪除 {idx}/{count} 個未連接項目",
|
||||
"entries.unlinked.delete.deleting": "正在刪除項目",
|
||||
"entries.unlinked.delete.deleting_count": "正在刪除 {idx}/{count} 個未連結的項目",
|
||||
"entries.unlinked.delete_alt": "刪除並解除項目連結",
|
||||
"entries.unlinked.missing_count.none": "未連結項目:無",
|
||||
"entries.unlinked.missing_count.some": "未連結項目:{count}",
|
||||
"entries.unlinked.refresh_all": "&重新整理",
|
||||
"entries.unlinked.relink.attempting": "正在重新連結{idx}/{missing_count}個項目, 成功重新連結{fixed_count}",
|
||||
"entries.unlinked.relink.manual": "&手動重新連結",
|
||||
"entries.unlinked.relink.title": "正在重新連結項目",
|
||||
"entries.unlinked.scanning": "正在掃描資料庫以尋找未連結的項目...",
|
||||
"entries.unlinked.search_and_relink": "&搜尋並重新連結",
|
||||
"entries.unlinked.title": "修復未連結項目",
|
||||
"entries.unlinked.delete": "刪除未連接項目",
|
||||
"entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。<br><br>您可以透過搜尋您的檔案來讓未連接的項目自動重新連接,或者自動刪除這些未連接項目。",
|
||||
"entries.unlinked.missing_count.none": "未連接項目:無",
|
||||
"entries.unlinked.missing_count.some": "未連接項目:{count}",
|
||||
"entries.unlinked.refresh_all": "重新整理全部 (&R)",
|
||||
"entries.unlinked.relink.attempting": "正在嘗試重新連接 {idx}/{missing_count} 個項目,已成功重新連接 {fixed_count} 個",
|
||||
"entries.unlinked.relink.manual": "手動重新連接 (&M)",
|
||||
"entries.unlinked.relink.title": "正在重新連接",
|
||||
"entries.unlinked.scanning": "正在掃描文件庫中的未連接項目...",
|
||||
"entries.unlinked.search_and_relink": "搜尋並重新連接 (&S)",
|
||||
"entries.unlinked.title": "修復未連接項目",
|
||||
"ffmpeg.missing.description": "未找到「FFmpeg」和/或「FFprobe」。必須安裝「FFmpeg」才能進行多媒體播放和縮圖產生。",
|
||||
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
|
||||
"field.copy": "複製欄位",
|
||||
"field.edit": "編輯欄位",
|
||||
"field.paste": "貼上欄位",
|
||||
"file.date_added": "新增日期",
|
||||
"file.date_created": "建立日期",
|
||||
"file.date_modified": "更改日期",
|
||||
"file.date_modified": "修改日期",
|
||||
"file.path": "檔案路徑",
|
||||
"file.dimensions": "尺寸",
|
||||
"file.duplicates.description": "TagStudio支援匯入DupeGuru結果,以管理重複檔案。",
|
||||
"file.duplicates.dupeguru.advice": "在鏡像後之後,你可以使用DupeGuru刪除不要的檔案。檔案刪除後,使用TagStudio內的 “修復未連結項目” 以移除未鏈結的項目。",
|
||||
"file.duplicates.description": "TagStudio 支援匯入 DupeGuru 結果來管理重複的檔案",
|
||||
"file.duplicates.dupeguru.advice": "在鏡像之後,您可以使用 DupeGuru 來刪除不需要的檔案。之後,利用 TagStudio 的「修復未連接項目」功能來刪除未連接項目。",
|
||||
"file.duplicates.dupeguru.file_extension": "DupeGuru 檔案 (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&匯入 DupeGuru 檔案",
|
||||
"file.duplicates.dupeguru.no_file": "尚未選擇 DupeGuru 檔案",
|
||||
"file.duplicates.dupeguru.load_file": "匯入 DupeGuru 檔案 (&L)",
|
||||
"file.duplicates.dupeguru.no_file": "沒有選擇 DupeGuru 檔案",
|
||||
"file.duplicates.dupeguru.open_file": "開啟 DupeGuru 結果檔案",
|
||||
"file.duplicates.fix": "修復重複檔案",
|
||||
"file.duplicates.matches": "重複檔案: {count}",
|
||||
"file.duplicates.matches_uninitialized": "重複檔案: 無",
|
||||
"file.duplicates.mirror.description": "將項目資料鏡像至各複寫組,在不刪除或是重複欄位的狀態下合併所有資料。此動作不會刪除任何欄位或資料。",
|
||||
"file.duplicates.mirror_entries": "&項目鏡像",
|
||||
"file.duplicates.fix": "修復重複的檔案",
|
||||
"file.duplicates.matches_uninitialized": "重複檔案:無",
|
||||
"file.duplicates.matches": "重複檔案:{count}",
|
||||
"file.duplicates.mirror_entries": "鏡像項目 (&M)",
|
||||
"file.duplicates.mirror.description": "鏡像每個重複配對集的項目資料,合併所有資料,同時不移除或重複欄位。此操作不會刪除任何檔案或資料。",
|
||||
"file.duration": "長度",
|
||||
"file.not_found": "找不到檔案",
|
||||
"file.not_found": "未法找到檔案",
|
||||
"file.open_file_with": "使用指定程式開啟",
|
||||
"file.open_file": "開啟檔案",
|
||||
"file.open_file_with": "開啟檔案",
|
||||
"file.open_location.generic": "檔案總管內顯示",
|
||||
"file.open_location.mac": "在Finder顯示",
|
||||
"file.open_location.windows": "在檔案總管內顯示",
|
||||
"file.open_location.generic": "在檔案管理員中顯示",
|
||||
"file.open_location.mac": "在 Finder 中顯示",
|
||||
"file.open_location.windows": "在檔案總管中顯示",
|
||||
"folders_to_tags.close_all": "關閉全部",
|
||||
"folders_to_tags.converting": "正在將資料夾轉換成標籤",
|
||||
"folders_to_tags.description": "依照你的資料夾結構建立標籤並套用到項目上。\n. 以下的結構提供所有會建立的標籤和會套用到的項目上。",
|
||||
"folders_to_tags.converting": "正在轉換資料夾為標籤",
|
||||
"folders_to_tags.description": "根據資料夾結構建立標籤並套用到您的項目上。\n以下結構顯示了所有將被建立的標籤和這些標籤會被套用到哪些項目上。",
|
||||
"folders_to_tags.open_all": "開啟全部",
|
||||
"folders_to_tags.title": "從資料夾建立標籤",
|
||||
"generic.add": "新增",
|
||||
"generic.apply_alt": "套用 (&A)",
|
||||
"generic.apply": "套用",
|
||||
"generic.apply_alt": "&套用",
|
||||
"generic.cancel_alt": "取消 (&C)",
|
||||
"generic.cancel": "取消",
|
||||
"generic.cancel_alt": "&取消",
|
||||
"generic.close": "關閉",
|
||||
"generic.continue": "繼續",
|
||||
"generic.copy": "複製",
|
||||
"generic.cut": "剪下",
|
||||
"generic.delete_alt": "刪除 (&D)",
|
||||
"generic.delete": "刪除",
|
||||
"generic.delete_alt": "&刪除",
|
||||
"generic.done_alt": "完成 (&D)",
|
||||
"generic.done": "完成",
|
||||
"generic.done_alt": "&完成",
|
||||
"generic.edit_alt": "編輯 (&E)",
|
||||
"generic.edit": "編輯",
|
||||
"generic.edit_alt": "&編輯",
|
||||
"generic.filename": "檔案名稱",
|
||||
"generic.navigation.back": "回到",
|
||||
"generic.missing": "遺失",
|
||||
"generic.navigation.back": "返回",
|
||||
"generic.navigation.next": "下一個",
|
||||
"generic.overwrite": "覆蓋",
|
||||
"generic.overwrite_alt": "&覆蓋",
|
||||
"generic.none": "無",
|
||||
"generic.overwrite_alt": "覆寫 (&O)",
|
||||
"generic.overwrite": "覆寫",
|
||||
"generic.paste": "貼上",
|
||||
"generic.recent_libraries": "最近使用資料庫",
|
||||
"generic.recent_libraries": "最近使用的文件庫",
|
||||
"generic.rename_alt": "重新命名 (&R)",
|
||||
"generic.rename": "重新命名",
|
||||
"generic.rename_alt": "&重新命名",
|
||||
"generic.reset": "重設",
|
||||
"generic.save": "儲存",
|
||||
"generic.skip": "略過",
|
||||
"generic.skip_alt": "&略過",
|
||||
"home.search": "搜尋",
|
||||
"generic.skip_alt": "跳過 (&S)",
|
||||
"generic.skip": "跳過",
|
||||
"home.search_entries": "搜尋項目",
|
||||
"home.search_library": "搜尋資料庫",
|
||||
"home.search_library": "搜尋文件庫",
|
||||
"home.search_tags": "搜尋標籤",
|
||||
"home.thumbnail_size": "縮圖大小",
|
||||
"home.search": "搜尋",
|
||||
"home.thumbnail_size.extra_large": "特大縮圖",
|
||||
"home.thumbnail_size.large": "大縮圖",
|
||||
"home.thumbnail_size.medium": "中縮圖",
|
||||
"home.thumbnail_size.mini": "迷你縮圖",
|
||||
"home.thumbnail_size.small": "小縮圖",
|
||||
"ignore_list.add_extension": "&新增擴充功能",
|
||||
"home.thumbnail_size": "縮圖大小",
|
||||
"ignore_list.add_extension": "新增外加套件 (&A)",
|
||||
"ignore_list.mode.exclude": "排除",
|
||||
"ignore_list.mode.include": "包含",
|
||||
"ignore_list.mode.label": "條列模式:",
|
||||
"ignore_list.title": "檔案格式",
|
||||
"json_migration.checking_for_parity": "檢查奇偶性中...",
|
||||
"json_migration.creating_database_tables": "建立SQL資料庫表格中...",
|
||||
"json_migration.description": "<br>開始並預覽文庫移轉作業。 在你點擊 “完成移轉” 前 <i>不會</i> 啟用被移轉的文庫。 <br><br>Library data should either have matching values or feature a \"Matched\" label. Values that do not match will be displayed in red and feature a \"<b>(!)</b>\" symbol next to them.<br><center><i>大文庫可能需要幾分鐘移轉。</i></center>",
|
||||
"json_migration.discrepancies_found": "發現文庫差異",
|
||||
"json_migration.discrepancies_found.description": "在原始檔案和轉換後的資料庫格式中發現差異。請檢查並確認是否繼續執行移轉。",
|
||||
"json_migration.finish_migration": "完成移轉",
|
||||
"ignore_list.mode.label": "清單模式:",
|
||||
"ignore_list.title": "檔案副檔名",
|
||||
"json_migration.checking_for_parity": "正在檢查一致性...",
|
||||
"json_migration.creating_database_tables": "正在建立資料庫表格...",
|
||||
"json_migration.description": "<br>開啟並預覽文件庫遷移過程。除非您按下「完成遷移」,否則被遷移的文件庫<i>不會</i>被使用。<br><br>文件庫資料應該是一致的或者要有個「已一致」標籤。不一致的資料會以紅色顯示並會有「<b>(!)</b>」標示在旁邊。<br><center><i>對於較大的文件庫,這個過程可能會花到幾分鐘以上。</i></center>",
|
||||
"json_migration.discrepancies_found.description": "原始和被遷移的文件庫格式出現差異。請檢查並決定是否要繼續遷移。",
|
||||
"json_migration.discrepancies_found": "找到文件庫差異",
|
||||
"json_migration.finish_migration": "完成遷移",
|
||||
"json_migration.heading.aliases": "別名:",
|
||||
"json_migration.heading.colors": "顏色:",
|
||||
"json_migration.heading.differ": "差異",
|
||||
"json_migration.heading.entires": "項目:",
|
||||
"json_migration.heading.extension_list_type": "擴展列表類型:",
|
||||
"json_migration.heading.extension_list_type": "副檔名清單類型:",
|
||||
"json_migration.heading.fields": "欄位:",
|
||||
"json_migration.heading.file_extension_list": "檔案Extension列表:",
|
||||
"json_migration.heading.match": "配對",
|
||||
"json_migration.heading.parent_tags": "上層標籤:",
|
||||
"json_migration.heading.file_extension_list": "檔案副檔名清單:",
|
||||
"json_migration.heading.match": "已一致",
|
||||
"json_migration.heading.names": "名稱:",
|
||||
"json_migration.heading.parent_tags": "父標籤:",
|
||||
"json_migration.heading.paths": "路徑:",
|
||||
"json_migration.heading.shorthands": "簡寫:",
|
||||
"json_migration.heading.tags": "標籤:",
|
||||
"json_migration.info.description": "透過TagStudio <b>9.4或更舊版本</b> 建立的文庫需要移轉到新的 <b>v9.5+</b> 版本。<br><h2>須知項目:</h2><ul><li>現有的文庫資料 <b><i>不會</i></b> 被刪除</li><li>你的個人資料 <b><i>不會</i></b> 被刪除,移動,或是更改</li><li>TagStudio v9.5+以上的檔案格式並不向下相容</li></ul>",
|
||||
"json_migration.migrating_files_entries": "移轉 {entries:,d} 個項目中...",
|
||||
"json_migration.migration_complete": "移轉完成!",
|
||||
"json_migration.migration_complete_with_discrepancies": "移轉完畢,發現差異",
|
||||
"json_migration.start_and_preview": "開始並預覽",
|
||||
"json_migration.title": "儲存格式移轉:\"{path}\"",
|
||||
"json_migration.title.new_lib": "<h2>v9.5+ 文庫 </h2>",
|
||||
"json_migration.title.old_lib": "<h2>v9.4 文庫</h2>",
|
||||
"landing.open_create_library": "開啟/ 建立文庫 {shortcut}",
|
||||
"json_migration.info.description": "TagStudio 版本<b>9.4 以下</b>的文件庫要被轉換至<b>9.5 以上</b>版本的格式。<br><h2>請注意!</h2><ul><li>您現在的文件庫<b><i>不會被</i></b>刪除</li><li>您個人的檔案<b><i>不會被</i></b>刪除、移動或變更</li><li>新的 9.5 以上版本儲存格式不能在 9.5 版本以前的 TagStudio 開啟</li></ul><h3>變更內容:</h3><ul><li>「變遷欄位」被「標籤類別」取代。現在,標籤會被直接加入至檔案項目。然後在標籤編輯選單會根據有「是一個類別」標示的父標籤被自動分類至不同的類別。任何標籤可以被標示為一個類別,而在其之下的子標籤會自己分類至被標示為類別的父標籤底下。</li><li>標籤顏色有經過調整和擴大,有些顏色又被重新命名或合併,但所有的顏色仍會轉換為完全一致或相近的顏色</li></ul><ul>",
|
||||
"json_migration.migrating_files_entries": "正在遷移 {entries:,d} 個項目...",
|
||||
"json_migration.migration_complete_with_discrepancies": "遷移完畢,找到差異",
|
||||
"json_migration.migration_complete": "遷移完成!",
|
||||
"json_migration.start_and_preview": "開啟並預覽",
|
||||
"json_migration.title.new_lib": "<h2>9.5 版本以上文件庫</h2>",
|
||||
"json_migration.title.old_lib": "<h2>9.4 版本文件庫</h2>",
|
||||
"json_migration.title": "保存格式遷移:「{path}」",
|
||||
"landing.open_create_library": "開啟/建立文件庫 {shortcut}",
|
||||
"library_object.name_required": "名稱 (必填)",
|
||||
"library_object.name": "名稱",
|
||||
"library_object.slug_required": "ID Slug (必填)",
|
||||
"library_object.slug": "ID Slug",
|
||||
"library.field.add": "新增欄位",
|
||||
"library.field.confirm_remove": "您確定要移除此 \"{name}\" 欄位?",
|
||||
"library.field.confirm_remove": "您確定要刪除「{name}」欄位嗎?",
|
||||
"library.field.mixed_data": "混合資料",
|
||||
"library.field.remove": "移除欄位",
|
||||
"library.missing": "找不到資料庫路徑",
|
||||
"library.name": "資料庫",
|
||||
"library.refresh.scanning.plural": "掃描並搜尋新項目...\n已搜尋{searched_count} 個檔案,找到 {found_count} 個新檔案",
|
||||
"library.refresh.scanning.singular": "掃描並搜尋新檔案...\n已搜尋{searched_count} 個檔案,找到 {found_count} 個新檔案",
|
||||
"library.refresh.scanning_preparing": "正在掃描目錄中的新檔案...\n準備中...",
|
||||
"library.refresh.title": "重新整理目錄中",
|
||||
"library.scan_library.title": "掃描資料庫中",
|
||||
"macros.running.dialog.new_entries": "對{count}/{total} 個新項目執行現有巨集",
|
||||
"macros.running.dialog.title": "針對新項目執行巨集",
|
||||
"library.field.remove": "刪除欄位",
|
||||
"library.missing": "文件庫路徑遺失",
|
||||
"library.name": "文件庫",
|
||||
"library.refresh.scanning_preparing": "正在掃描目錄尋找新檔案...\n準備中...",
|
||||
"library.refresh.scanning.plural": "正在掃描目錄尋找新檔案...\n已搜尋 {searched_count} 個檔案,找到 {found_count} 個新檔案",
|
||||
"library.refresh.scanning.singular": "正在掃描目錄尋找新檔案...\n已搜尋 {searched_count} 個檔案,找到 {found_count} 個新檔案",
|
||||
"library.refresh.title": "重新整理目錄",
|
||||
"library.scan_library.title": "掃描文件庫",
|
||||
"macros.running.dialog.new_entries": "正在對 {count}/{total} 個新檔案項目執行設定的巨集指令...",
|
||||
"macros.running.dialog.title": "正在對新項目執行巨集指令",
|
||||
"media_player.autoplay": "自動播放",
|
||||
"media_player.loop": "循環播放",
|
||||
"menu.delete_selected_files_ambiguous": "移動檔案至「{trash_term}」",
|
||||
"menu.delete_selected_files_plural": "移動多個檔案至「{trash_term}」",
|
||||
"menu.delete_selected_files_singular": "移動檔案至「{trash_term}」",
|
||||
"menu.edit.ignore_list": "忽略檔案和資料夾",
|
||||
"menu.edit.manage_file_extensions": "管理檔案副檔名",
|
||||
"menu.edit.manage_tags": "管理標籤",
|
||||
"menu.edit.new_tag": "新增標籤 (&N)",
|
||||
"menu.edit": "編輯",
|
||||
"menu.edit.ignore_list": "略過檔案和資料夾",
|
||||
"menu.edit.manage_file_extensions": "管理副檔名",
|
||||
"menu.edit.manage_tags": "標籤管理",
|
||||
"menu.edit.new_tag": "新 &標籤",
|
||||
"menu.file": "&檔案",
|
||||
"menu.file.close_library": "&關閉文庫",
|
||||
"menu.file.new_library": "新資料庫",
|
||||
"menu.file.open_create_library": "&開啟/建立文庫",
|
||||
"menu.file.open_library": "開啟資料庫",
|
||||
"menu.file.refresh_directories": "&重新整理目錄",
|
||||
"menu.file.save_backup": "&儲存文庫備份",
|
||||
"menu.file.save_library": "儲存儲存庫",
|
||||
"menu.help": "&提示",
|
||||
"menu.macros": "&巨集",
|
||||
"menu.macros.folders_to_tags": "檔案夾到標籤",
|
||||
"menu.file.clear_recent_libraries": "清除最近使用的文件庫",
|
||||
"menu.file.close_library": "關閉文件庫 (&C)",
|
||||
"menu.file.missing_library.message": "未找到文件庫(路徑:{library})",
|
||||
"menu.file.missing_library.title": "文件庫遺失",
|
||||
"menu.file.new_library": "新增文件庫",
|
||||
"menu.file.open_create_library": "開啟/建立文件庫 (&O)",
|
||||
"menu.file.open_library": "開啟文件庫",
|
||||
"menu.file.open_recent_library": "開啟最近使用的文件庫",
|
||||
"menu.file.refresh_directories": "重新整理目錄 (&R)",
|
||||
"menu.file.save_backup": "儲存文件庫備份 (&S)",
|
||||
"menu.file.save_library": "儲存文件庫",
|
||||
"menu.file": "檔案 (&F)",
|
||||
"menu.help.about": "關於",
|
||||
"menu.help": "幫助 (&H)",
|
||||
"menu.macros.folders_to_tags": "資料夾轉標籤",
|
||||
"menu.macros": "巨集指令 (&M)",
|
||||
"menu.select": "選擇",
|
||||
"menu.tools": "&工具",
|
||||
"menu.tools.fix_duplicate_files": "修復重複 &檔案",
|
||||
"menu.tools.fix_unlinked_entries": "修復並解除項目連結",
|
||||
"menu.view": "&檢視",
|
||||
"menu.window": "選單",
|
||||
"preview.no_selection": "尚未選擇物件",
|
||||
"select.all": "全選",
|
||||
"select.clear": "清除選項",
|
||||
"settings.open_library_on_start": "在啟動時開啟儲存庫",
|
||||
"settings.show_filenames_in_grid": "在Grid顯示檔案名稱",
|
||||
"settings.show_recent_libraries": "顯示最近使用過儲存庫",
|
||||
"splash.opening_library": "開啟文庫 \"{library_path}\"...",
|
||||
"status.library_backup_in_progress": "儲存文庫備份中...",
|
||||
"status.library_backup_success": "文庫備份路徑:\"{path}\" ({time_span})",
|
||||
"status.library_closed": "已關閉文庫({time_span})",
|
||||
"status.library_closing": "關閉文庫中...",
|
||||
"status.library_save_success": "資料庫保存成功!",
|
||||
"status.library_search_query": "正在搜尋文庫...",
|
||||
"menu.settings": "設定...",
|
||||
"menu.tools.fix_duplicate_files": "修復重複檔案",
|
||||
"menu.tools.fix_unlinked_entries": "修復未連接項目",
|
||||
"menu.tools": "工具 (&T)",
|
||||
"menu.view": "檢視 (&V)",
|
||||
"menu.window": "視窗 (&W)",
|
||||
"namespace.create.description_color": "標籤顏色使用命名空間作為色彩群組。所有自訂顏色必須先被放入一個命名空間群組。",
|
||||
"namespace.create.description": "TagStudio 使用命名空間來區分成群的物件,如標籤或顏色,以便這些物件能被匯出或分享。以「tagstudio」開頭的命名空間是 TagStudio 內部使用的命名空間。",
|
||||
"namespace.create.title": "建立命名空間",
|
||||
"namespace.new.button": "新增命名空間",
|
||||
"namespace.new.prompt": "新增一個命名空間以新增自訂顏色",
|
||||
"preview.multiple_selection": "已選取 <b>{count}</b> 個項目",
|
||||
"preview.no_selection": "無選取項目",
|
||||
"select.add_tag_to_selected": "加入標籤至選取項目",
|
||||
"select.all": "全部選取",
|
||||
"select.clear": "清除選取",
|
||||
"select.inverse": "反向選取",
|
||||
"settings.clear_thumb_cache.title": "清除縮圖快取",
|
||||
"settings.filepath.label": "檔案路徑可見性",
|
||||
"settings.filepath.option.full": "僅顯示絕對檔案路徑",
|
||||
"settings.filepath.option.name": "僅顯示檔案名稱",
|
||||
"settings.filepath.option.relative": "僅顯示相對檔案路徑",
|
||||
"settings.global": "全域設定",
|
||||
"settings.language": "語言",
|
||||
"settings.library": "文件庫設定",
|
||||
"settings.open_library_on_start": "啟動時開啟文件庫",
|
||||
"settings.page_size": "頁面大小",
|
||||
"settings.restart_required": "需要重新啟動 TagStudio 才能使變更生效",
|
||||
"settings.show_filenames_in_grid": "在網格中顯示檔案名稱",
|
||||
"settings.show_recent_libraries": "顯示最近使用的文件庫",
|
||||
"settings.tag_click_action.label": "標籤點選動作",
|
||||
"settings.tag_click_action.add_to_search": "加入標籤至搜尋範圍",
|
||||
"settings.tag_click_action.open_edit": "編輯標籤",
|
||||
"settings.tag_click_action.set_search": "搜尋標籤",
|
||||
"settings.theme.dark": "深色模式",
|
||||
"settings.theme.label": "主題:",
|
||||
"settings.theme.light": "淺色模式",
|
||||
"settings.theme.system": "系統主題",
|
||||
"settings.dateformat.label": "日期格式",
|
||||
"settings.dateformat.system": "系統",
|
||||
"settings.dateformat.english": "英文",
|
||||
"settings.dateformat.international": "國際",
|
||||
"settings.hourformat.label": "24 小時制",
|
||||
"settings.zeropadding.label": "日期補零",
|
||||
"settings.title": "設定",
|
||||
"sorting.direction.ascending": "升序",
|
||||
"sorting.direction.descending": "降序",
|
||||
"splash.opening_library": "正在開啟「{library_path}」...",
|
||||
"status.deleted_file_plural": "已刪除 {count} 個檔案!",
|
||||
"status.deleted_file_singular": "已刪除一個檔案!",
|
||||
"status.deleted_none": "未刪除任何檔案",
|
||||
"status.deleted_partial_warning": "只刪除了 {count} 個檔案!請檢查檔案是否遺失或正在被使用。",
|
||||
"status.deleting_file": "正在刪除 [{i}/{count}]:「{path}」...",
|
||||
"status.library_backup_in_progress": "正在儲存文件庫備份...",
|
||||
"status.library_backup_success": "文件庫備份已儲存至:「{path}」({time_span})",
|
||||
"status.library_closed": "文件庫已關閉 ({time_span})",
|
||||
"status.library_closing": "正在關閉文件庫...",
|
||||
"status.library_save_success": "文件庫已成功儲存並關閉",
|
||||
"status.library_search_query": "正在搜尋文件庫...",
|
||||
"status.library_version_expected": "預期版本:",
|
||||
"status.library_version_found": "找到版本:",
|
||||
"status.library_version_mismatch": "文件庫版本不符!",
|
||||
"status.results_found": "找到 {count} 個結果 ({time_span})",
|
||||
"status.results.invalid_syntax": "搜尋語法錯誤:",
|
||||
"status.results": "結果",
|
||||
"status.results_found": "用({time_span})找到 {count} 個結果",
|
||||
"tag.add": "新增標籤",
|
||||
"tag_manager.title": "文件庫標籤",
|
||||
"tag.add_to_search": "加入至搜尋範圍",
|
||||
"tag.add.plural": "新增標籤",
|
||||
"tag.add_to_search": "新增到搜尋",
|
||||
"tag.add": "新增標籤",
|
||||
"tag.aliases": "別名",
|
||||
"tag.color": "顏色",
|
||||
"tag.confirm_delete": "你確定要刪除此標籤 \"{tag_name}\"?",
|
||||
"tag.all_tags": "所有標籤",
|
||||
"tag.choose_color": "選擇標籤顏色",
|
||||
"tag.color": "標籤顏色",
|
||||
"tag.confirm_delete": "您確定要刪除「{tag_name}」嗎?",
|
||||
"tag.create_add": "建立並新增「{query}」",
|
||||
"tag.create": "建立標籤",
|
||||
"tag.disambiguation.tooltip": "使用此標籤消除歧義",
|
||||
"tag.edit": "編輯標籤",
|
||||
"tag.name": "名稱",
|
||||
"tag.new": "新標籤",
|
||||
"tag.parent_tags": "上層標籤",
|
||||
"tag.parent_tags.add": "新增上層標籤",
|
||||
"tag.parent_tags.description": "此標籤可以在搜尋時替代以下的上層標籤。",
|
||||
"tag.remove": "移除標籤",
|
||||
"tag.search_for_tag": "尋找標籤",
|
||||
"tag.shorthand": "速記",
|
||||
"tag.tag_name_required": "標籤名稱(必要)",
|
||||
"tag_manager.title": "儲存庫標籤",
|
||||
"tag.is_category": "是一個類別",
|
||||
"tag.name": "標籤名稱",
|
||||
"tag.new": "新增標籤",
|
||||
"tag.parent_tags.add": "新增父標籤",
|
||||
"tag.parent_tags.description": "在搜尋中,該可以被當作任何其他父標籤。",
|
||||
"tag.parent_tags": "父標籤",
|
||||
"tag.remove": "刪除標籤",
|
||||
"tag.search_for_tag": "搜尋標籤",
|
||||
"tag.shorthand": "簡寫",
|
||||
"tag.tag_name_required": "標籤名稱 (必填)",
|
||||
"tag.view_limit": "檢視限制:",
|
||||
"trash.context.ambiguous": "移動檔案至「{trash_term}」",
|
||||
"trash.context.plural": "移動多個檔案移至「{trash_term}」",
|
||||
"trash.context.singular": "移動檔案至「{trash_term}」",
|
||||
"trash.dialog.disambiguation_warning.plural": "這將從 TagStudio <i>和</i> 您的檔案系統中刪除!",
|
||||
"trash.dialog.disambiguation_warning.singular": "這將從 TagStudio <i>和</i> 您的檔案系統中刪除!",
|
||||
"trash.dialog.move.confirmation.plural": "您確定要移動這 {count} 個檔案至「{trash_term}」?",
|
||||
"trash.dialog.move.confirmation.singular": "您確定要將此檔案移至「{trash_term}」?",
|
||||
"trash.dialog.permanent_delete_warning": "<b>警告!</b>如果檔案無法移至「{trash_term}」,它將會被<b>永久刪除!</b>",
|
||||
"trash.dialog.title.plural": "刪除多個檔案",
|
||||
"trash.dialog.title.singular": "刪除檔案",
|
||||
"trash.name.generic": "垃圾桶",
|
||||
"trash.name.windows": "回收站",
|
||||
"view.size.0": "迷你",
|
||||
"view.size.1": "小",
|
||||
"view.size.2": "中",
|
||||
"view.size.3": "大",
|
||||
"view.size.4": "特大",
|
||||
"window.message.error_opening_library": "開啟文庫時錯誤了。",
|
||||
"window.message.error_opening_library": "開啟文件庫時發生錯誤",
|
||||
"window.title.error": "錯誤",
|
||||
"window.title.open_create_library": "開啟/建立文庫"
|
||||
}
|
||||
"window.title.open_create_library": "開啟/建立文件庫"
|
||||
}
|
||||
@@ -9,6 +9,7 @@ CWD = Path(__file__).parent
|
||||
# this needs to be above `src` imports
|
||||
sys.path.insert(0, str(CWD.parent))
|
||||
|
||||
from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -50,18 +51,30 @@ def file_mediatypes_library():
|
||||
return lib
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def library_dir():
|
||||
"""Creates a shared library path for tests, that cleans up after the session."""
|
||||
with TemporaryDirectory() as tmp_dir_name:
|
||||
library_path = Path(tmp_dir_name)
|
||||
|
||||
thumbs_path = library_path / TS_FOLDER_NAME / THUMB_CACHE_NAME
|
||||
thumbs_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
yield library_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library(request):
|
||||
def library(request, library_dir: Path):
|
||||
# when no param is passed, use the default
|
||||
library_path = "/dev/null/"
|
||||
library_path = library_dir
|
||||
if hasattr(request, "param"):
|
||||
if isinstance(request.param, TemporaryDirectory):
|
||||
library_path = request.param.name
|
||||
library_path = Path(request.param.name)
|
||||
else:
|
||||
library_path = request.param
|
||||
library_path = Path(request.param)
|
||||
|
||||
lib = Library()
|
||||
status = lib.open_library(Path(library_path), ":memory:")
|
||||
status = lib.open_library(library_path, ":memory:")
|
||||
assert status.success
|
||||
|
||||
tag = Tag(
|
||||
@@ -121,43 +134,42 @@ def search_library() -> Library:
|
||||
|
||||
@pytest.fixture
|
||||
def entry_min(library):
|
||||
yield next(library.get_entries())
|
||||
yield next(library.all_entries())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entry_full(library: Library):
|
||||
yield next(library.get_entries(with_joins=True))
|
||||
yield next(library.all_entries(with_joins=True))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qt_driver(qtbot, library):
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
def qt_driver(qtbot, library, library_dir: Path):
|
||||
class Args:
|
||||
settings_file = library_dir / "settings.toml"
|
||||
cache_file = library_dir / "tagstudio.ini"
|
||||
open = library_dir
|
||||
ci = True
|
||||
|
||||
class Args:
|
||||
settings_file = Path(tmp_dir) / "settings.toml"
|
||||
cache_file = Path(tmp_dir) / "tagstudio.ini"
|
||||
open = Path(tmp_dir)
|
||||
ci = True
|
||||
with patch("tagstudio.qt.ts_qt.Consumer"), patch("tagstudio.qt.ts_qt.CustomRunnable"):
|
||||
driver = QtDriver(Args())
|
||||
|
||||
with patch("tagstudio.qt.ts_qt.Consumer"), patch("tagstudio.qt.ts_qt.CustomRunnable"):
|
||||
driver = QtDriver(Args())
|
||||
driver.app = Mock()
|
||||
driver.main_window = Mock()
|
||||
driver.main_window.preview_panel = Mock()
|
||||
driver.main_window.thumb_grid = Mock()
|
||||
driver.main_window.thumb_size = 128
|
||||
driver.item_thumbs = []
|
||||
driver.main_window.menu_bar.autofill_action = Mock()
|
||||
|
||||
driver.app = Mock()
|
||||
driver.main_window = Mock()
|
||||
driver.preview_panel = Mock()
|
||||
driver.flow_container = Mock()
|
||||
driver.item_thumbs = []
|
||||
driver.autofill_action = Mock()
|
||||
driver.copy_buffer = {"fields": [], "tags": []}
|
||||
driver.main_window.menu_bar.copy_fields_action = Mock()
|
||||
driver.main_window.menu_bar.paste_fields_action = Mock()
|
||||
|
||||
driver.copy_buffer = {"fields": [], "tags": []}
|
||||
driver.copy_fields_action = Mock()
|
||||
driver.paste_fields_action = Mock()
|
||||
|
||||
driver.lib = library
|
||||
# TODO - downsize this method and use it
|
||||
# driver.start()
|
||||
driver.frame_content = list(library.get_entries())
|
||||
yield driver
|
||||
driver.lib = library
|
||||
# TODO - downsize this method and use it
|
||||
# driver.start()
|
||||
driver.frame_content = list(library.all_entries())
|
||||
yield driver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
BIN
tests/fixtures/sample.epub
vendored
BIN
tests/fixtures/sample.epub
vendored
Binary file not shown.
BIN
tests/fixtures/sample.ods
vendored
BIN
tests/fixtures/sample.ods
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user