Compare commits

..
153 Commits
Author SHA1 Message Date
yyc12345 e01718928c feat: remove checkerboard icon and change bottom toolbar 2026-07-25 12:23:00 +08:00
yyc12345 f5dbf44ec7 refactor: add possible optimization in graphics view 2026-07-25 12:15:17 +08:00
yyc12345 f55f8c731d refactor: use extensible design for fetching playlist from single file 2026-07-25 12:06:11 +08:00
yyc12345 4ed491c78a refactor: remove closeWindow because no one need it 2026-07-24 15:17:05 +08:00
yyc12345 b54cde2cbf refactor: remove "auto" in window-size behavior
- "auto" is not essential for our hard fork, so i simply remove it.
- remove all useless code after removing it.
- enlarge the min size of window because previous may trigger bad layout when showing navigation view.
2026-07-24 15:14:24 +08:00
yyc12345 b1d12cc406 refactor: remove double-click-window behavior
- remove double-click-window behavior entirely
- remove the icon of copy image action
2026-07-24 15:07:30 +08:00
yyc12345 aa13355966 refactor: use UI file for main window
- main window now use UI file for layout.
- remove all manual computed widget position code. use full Qt layout implementation instead.
- remove useless functions in main window
- change style changed slot as private
2026-07-24 14:57:41 +08:00
yyc12345 bd35a8c3e0 feat: use submenu in right click menu to reduce the height of menu panel 2026-07-15 21:59:30 +08:00
yyc12345 81413b51da feat: support explorer sort and hidden on Window
now playlist can use Windows Explorer's sort and hidden file display options to build it own for better browse experience on Windows
2026-07-14 13:22:23 +08:00
yyc12345 a01d640c27 refactor: remove frameless window
remove frameless window and merge it into mainwindow. because we do not need frameless and transparent window, so most of its functions are removed in previous commits.
2026-07-13 15:59:39 +08:00
yyc12345 16800df138 refactor: remove useless function in main window 2026-07-13 15:50:30 +08:00
yyc12345 7e8fd4ea21 refactor: use class member menu instead of runtime created menu 2026-07-13 15:47:17 +08:00
yyc12345 04fa491cd8 refactor: fully refactor playlist manager
- refactor playlist manager. remove useless qt model design.
- update action updator function.
2026-07-13 15:40:59 +08:00
yyc12345 dbf8c09368 feat: update action manager
- add and delete some actions in action manager.
- rename "fit to view" to "fit to screen".
- update main window right click menu and toolbar.
- use special state update slot in action manager instead of function located in main window.
2026-07-12 21:00:11 +08:00
yyc12345 369a17ed7d fix: fix typo about checkerboard 2026-07-12 19:54:37 +08:00
yyc12345 b19a4f8723 doc: add help for checkboard shift modifier 2026-07-09 13:06:14 +08:00
yyc12345 a8e5464e9f fix: remove useless mouse tracking setting 2026-07-08 20:18:52 +08:00
yyc12345 46f46480cb feat: change application style
- change default dark theme to light
- disable dark mode on windows 10 because Qt's windows 11 theme has bad support on windows 10 (icon display error)
- level required minimum qt version up to 6.5 for color schema.
2026-07-08 20:15:22 +08:00
yyc12345 c06f900ac5 refactor: use ui file to replace manual layout in metadata dialog 2026-07-07 12:16:36 +08:00
yyc12345 31303a20c7 refactor: set openExternalLinks in ui file instead of cpp file 2026-07-06 20:42:23 +08:00
yyc12345 319fd64fc6 refactor: use html resource to replace manual html in code 2026-07-06 20:34:41 +08:00
yyc12345 cdd0b764a7 refactor: use .ui to replace manual layout for aboutdialog 2026-07-06 09:44:38 +08:00
yyc12345 2a69482adb fix: change default settings
- change default settings to make the default behavior more close to legacy Windows Photo Viewer.
- remove redundant fallback for fetching enum settings.
2026-07-05 09:05:58 +08:00
yyc12345 fc3ef1cd7f fix: fix not thread-safe settings singleton.
fix not thread-safe way for fetching settings singleton.
remove qApp from Settings' parent, use nullptr replace it and leak it anyway.
because we may initialize it before initializing QApplication.
in that time, qApp is nullptr.
2026-07-05 08:57:49 +08:00
yyc12345 1de1517a81 refactor: use .ui file instead of manual layout in settings dialog.
- enable Qt UIC in cmake file.
- use .ui file to replace manually written layout in settings dialog.
2026-07-04 22:17:40 +08:00
yyc12345 04a4ab9bff feat: make the default window size bigger 2026-07-04 16:54:48 +08:00
yyc12345 011c1e858f feat: update the layout of main window.
update the layout of main window to make it looks more like
legacy Windows Photo Viewer.
2026-07-04 15:20:14 +08:00
yyc12345 4fe09757b3 revert: remove user custom shortcut 2026-07-04 14:59:02 +08:00
yyc12345 82f1f10cca refactor: rename m_gv to m_nav and m_graphicsView to m_gv for better abbr. 2026-07-04 14:35:28 +08:00
yyc12345 60eebea741 revert: remove the ability that drag window body to move 2026-07-04 14:32:20 +08:00
blumiaandyyc12345 9b151e2fea chore: update release info to 1.5.0 2026-07-03 19:37:54 +08:00
blumiaandyyc12345 9db9c3e9f7 chore(AppStream): add vcs-browser link 2026-07-03 19:37:03 +08:00
yyc12345 9a18a41b8f revert: remove OpacityHelper
- remove OpacityHelper
- remove all related animation
- now bottom toolbar and navigation view are always visible
2026-07-03 16:40:04 +08:00
yyc12345 c93ed94210 refactor: fully remove ToolButton because no one require it.
- fully remove ToolButton.
- make loadHidpiIcon as a static function in ActionManager because no outside function may call it.
2026-07-03 15:37:20 +08:00
yyc12345 f4d272e014 refactor: merge retranslateUi into setupAction in ActionManager 2026-07-03 15:12:15 +08:00
yyc12345 d536e8c611 refactor: move prev/next picture button to bottom toolbar. 2026-07-03 14:56:54 +08:00
yyc12345 c8dfccb460 revert: remove protected mode
protected mode is designed for borderless and transparent window.
it is not suit for the goal of this fork.
2026-07-03 14:32:36 +08:00
yyc12345 6fde9474ae revert: remove stay-on-top feature 2026-07-03 14:26:27 +08:00
yyc12345 c19fcaa560 revert: remove close window animation 2026-07-03 14:26:22 +08:00
yyc12345 602e2cccf8 revert: remove redundant close button 2026-07-03 14:26:16 +08:00
yyc12345 3c1a3c54a7 revert: restore system default window instead of custom one.
custom window is not the goal of this project.
we remove it and use system default window instead.
2026-07-03 14:26:09 +08:00
yyc12345 032752faf9 revert: remove the ability that disable built-in close window animation
no need to preserve this option because frameless window and
close window animation will be fully removed in future.
2026-07-02 20:32:30 +08:00
yyc12345 457647c59f revert: remove auto long image mode
- remove auto long image mode.
- there is no "auto" requirement for this.
- user can utilize fit by height/width to achieve the same goal by manually click them.
- some optimization code about graphic view are preserved.
2026-07-02 20:14:44 +08:00
yyc12345 821e0f9fd6 revert: remove passoc support on Windows.
- remove passoc executable support on Windows.
- but preserve the icons created by upstream for future expanding.
2026-07-02 15:48:56 +08:00
yyc12345 26f51e418d ci: remove deepin 25 support
This partially reverts commit afcd126c00.

This fork do not need to preserve the support to deepin.
So I simply remove its CI.
2026-07-02 15:48:55 +08:00
yyc12345 cd2eac44f1 revert: revert feature about saving image as new file with different format.
This reverts commit 5b2255e903.

The feature about saving image as new file with different format is not the goal of an image viewer.
There is no need to keep feature diversity in this fork.
So I simply remove it.
2026-07-02 15:48:50 +08:00
yyc12345 f0bb945c7d doc: update README
- update README to notice the development of SarasaCW Picture.
- temporarily remove the most of old README by HTML comment syntax. these contents will be restored and fixed once I finish refactoring.
2026-07-02 14:44:01 +08:00
yyc12345 870e472f53 ci: remove appveyor config file
- all build work are passing by github. there is no need to keep appvoyer config file.
2026-07-02 14:43:42 +08:00
yyc12345 76677381de build: remove QMake project file
- remove QMake project file. it is already deprecated in original repository. all build should be run as CMake project.
2026-07-02 14:43:20 +08:00
blumiaandyyc12345 d2bd521ad0 fix: show empty url list doesn't reset window title 2026-07-02 14:41:56 +08:00
blumiaandyyc12345 265e894b3f fix(CI): Windows build FTBFS 2026-07-02 14:41:42 +08:00
blumiaandyyc12345 6339a93c6c feat(Windows): open window will be able to use system animation 2026-07-02 14:39:33 +08:00
6da53ced81 i18n: Translations update from Hosted Weblate
Co-authored-by: VenusGirl <VenusGirl@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ko/
Translation: pineapple-pictures/Application
2026-06-29 19:18:11 +08:00
blumia 8f0d4da7ad chore: update release info to 1.4.1 2026-04-05 17:09:13 +08:00
blumia 9c8ffb6c1b chore(CI): bump Qt and action version 2026-04-04 16:52:18 +08:00
blumia 4c5c5e6fc6 fix(CI): zlib dll renamed 2026-03-27 17:55:23 +08:00
blumia b8cb10bb33 fix(CI): fixed kf6 version for appveyor build 2026-03-27 15:52:00 +08:00
blumia 6e2f40633e fix: correct version in CMakeLists, update CI for zlib 2026-03-27 15:06:11 +08:00
a0ba7c1d17 i18n: Translations update from Hosted Weblate
Co-authored-by: Heimen Stoffels <vistausss@fastmail.com>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/nl/
Translation: pineapple-pictures/Application
2026-03-27 15:02:18 +08:00
b6d759d7c6 i18n: Translations update from Hosted Weblate
Co-authored-by: Loc Huynh <huynhloc.contact@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/vi/
Translation: pineapple-pictures/Application
2026-03-11 20:32:50 +08:00
blumia 437c2f5618 chore: update release info to 1.4.0 2026-02-09 21:00:34 +08:00
7fd4924e64 i18n: Translations update from Hosted Weblate
Co-authored-by: Andrej Poženel - SDT <anderlli0053@live.com>
Co-authored-by: Andrey <andrey@mailbox.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ru/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/sl/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/sl/
Translation: pineapple-pictures/AppStream Metadata
Translation: pineapple-pictures/Application
2026-01-31 00:11:45 +08:00
blumia c77808f3d8 chore(CI): update Qt version to 6.10.2 2026-01-31 00:00:49 +08:00
blumia 86075a1ad0 chore: update copyright year in rc file 2026-01-20 23:22:37 +08:00
blumia a8e40a725c i18n: update ts files 2026-01-18 14:02:31 +08:00
8edb6b015e i18n: Translations update from Hosted Weblate
Co-authored-by: Gary Wang <wzc782970009@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Loc Huynh <huynhloc.contact@gmail.com>
Co-authored-by: Sabri Ünal <yakushabb@gmail.com>
Co-authored-by: VenusGirl <VenusGirl@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ko/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/tr/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/vi/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/ko/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/vi/
Translation: pineapple-pictures/AppStream Metadata
Translation: pineapple-pictures/Application
2026-01-18 13:51:16 +08:00
blumia 5b2255e903 feat: action for save image as new file with different format 2026-01-18 13:08:39 +08:00
blumia 4948ea7445 chore: hello 2026 2026-01-06 00:17:12 +08:00
blumia e09348e064 chore: bump release info to 1.3.0 2025-12-18 00:44:57 +08:00
Hosted WeblateandAndrey fce6a835b7 i18n: Translations update from Hosted Weblate
Co-authored-by: Andrey <andrey@mailbox.org>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ru/
Translation: pineapple-pictures/Application
2025-12-16 17:32:28 +01:00
blumia afcd126c00 feat(CI): add deepin 25 build 2025-12-17 00:31:42 +08:00
blumia aa025b0f3b Revert "chore: avoid use screenAt(QCursor::pos())"
I actually forgot why I did this, I might thought this API
is deprecated?

Reverting this resolved the issue that window is not appeared
at the screen that currently the cursor is at.

This reverts commit 4375fe1c2d.
2025-12-14 11:26:21 +08:00
blumia e6e738412d chore(CI): add passoc binary for msvc build and minor tweaks
Resolve https://github.com/BLumia/pineapple-pictures/issues/13
2025-12-14 00:23:55 +08:00
blumia 2e748e19e1 fix: correct license for files under dist/passoc 2025-12-11 00:47:19 +08:00
blumia 4b5c9e9ebd chore: add icons for Windows file assoc 2025-12-11 00:07:31 +08:00
blumia cdfa8b78a4 feat(Windows): install passoc default config file 2025-12-03 00:10:42 +08:00
blumia 1ba5ae60dc feat: option to limit svg support to svg tiny 1.2 only 2025-12-02 00:04:12 +08:00
08d1c09033 i18n: Translations update from Hosted Weblate
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: VenusGirl <VenusGirl@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ko/
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/ko/
Translation: pineapple-pictures/AppStream Metadata
Translation: pineapple-pictures/Application
2025-12-01 23:54:11 +08:00
blumia 612b542e2a fix(CI): fix broken avif support
The following commit from libavif is related:
https://github.com/AOMediaCodec/libavif/commit/f7ce882698307cad524603b9d528ce70c0d0d299

Resolve: https://github.com/BLumia/pineapple-pictures/issues/164
2025-11-24 19:10:07 +08:00
blumia d74db66ab5 chore(CI): tweak msvc CI and make use of cmake install command
We also no longer include msvcrt in msvc cmake CI build
2025-11-22 17:40:43 +08:00
blumia 9dd34b3fce chore: bump release info to 1.2.1 2025-11-21 20:42:44 +08:00
blumia adfe6c51da chore(CI): bump Qt version to 6.10.1 2025-11-20 23:29:43 +08:00
96565b2ccf i18n: Translated using Weblate (Italian) (#162)
Currently translated at 100.0% (168 of 168 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/it/

Co-authored-by: albanobattistella <albanobattistella@gmail.com>
2025-11-08 23:19:06 +08:00
blumia 107440145a chore(CI): bump dependencies version for msvc build 2025-11-06 00:28:32 +08:00
blumia 1c55e3194e fix: missing QColorSpace include and Qt version check 2025-11-05 20:34:06 +08:00
blumia 9021ff2579 fix: inaccurate color when display CMYK images
Resolve https://github.com/BLumia/pineapple-pictures/issues/100
2025-11-05 20:23:40 +08:00
blumia 10e1e91e7b chore: add donation link to AppStream metainfo 2025-10-13 00:04:07 +08:00
blumia ef887d7a06 chore(CI): switch back to Qt 6.9.3 and add cache for install-qt-action 2025-10-08 13:48:12 +08:00
blumia 5446d94966 chore(CI): use Qt 6.10.0 2025-10-08 13:25:42 +08:00
blumia a2f42c2e3d chore: update Contribution section in README.md 2025-09-27 15:50:25 +08:00
blumia 56b18353db chore: update release info to 1.2.0 2025-09-20 00:03:34 +08:00
blumia 9170765344 fix(CI): appveyor FTBFS due to mingw location change 2025-09-17 00:19:30 +08:00
ee7b3beda4 i18n: Translated using Weblate (Turkish) (#161)
Currently translated at 100.0% (168 of 168 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/tr/

Co-authored-by: Sabri Ünal <yakushabb@gmail.com>
2025-09-13 20:47:59 +08:00
blumia 919a672c17 chore(CI): bump Qt version to 6.9.2 2025-08-28 00:28:05 +08:00
blumia e52a6d8eed feat: new action for fit long image 2025-08-25 12:48:35 +08:00
blumia 8b0c8ec194 i18n: update ts files 2025-08-25 12:47:42 +08:00
1da7fff518 i18n: Translations update from Hosted Weblate (#159)
* Translated using Weblate (Italian)

Currently translated at 100.0% (167 of 167 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/it/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (167 of 167 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/nl/

---------

Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
Co-authored-by: Heimen Stoffels <vistausss@fastmail.com>
2025-08-25 12:44:28 +08:00
blumia e216592205 fix(CI): reuse lint 2025-08-24 14:08:00 +08:00
blumia 8fc9d06937 chore: add product version to windows build rc file 2025-08-24 13:53:14 +08:00
blumia 73b15da182 chore: remove gitee release page link from README.zh_CN.md 2025-08-23 15:24:00 +08:00
blumia f4f7d93e38 chore: simplified long image mode code 2025-08-05 00:06:05 +08:00
blumia 591b01a83a i18n: update ts files 2025-08-04 21:56:58 +08:00
blumia f8d3dcc899 feat: auto long image mode 2025-08-04 20:41:49 +08:00
blumia f0ed9d0ca1 chore: update release info to 1.1.1 2025-08-02 13:30:14 +08:00
13227cfac9 i18n: Translations update from Hosted Weblate (#157)
* Translated using Weblate (Korean)

Currently translated at 100.0% (166 of 166 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ko/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (166 of 166 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/nl/

---------

Co-authored-by: VenusGirl <VenusGirl@outlook.com>
Co-authored-by: Heimen Stoffels <vistausss@fastmail.com>
2025-08-01 23:44:43 +08:00
blumia b73df5ea1a refactor: make fitByOrientation() more sane 2025-07-27 15:53:23 +08:00
blumia 4375fe1c2d chore: avoid use screenAt(QCursor::pos()) 2025-07-26 16:03:13 +08:00
blumia 4654cb21a0 fix: REUSE compliance 2025-07-23 21:29:18 +08:00
blumia ba23208a7a chore: add config file for CRLF to LF change 2025-07-23 21:22:41 +08:00
blumia ed5a602332 chore: let's use LF all the time 2025-07-23 21:20:34 +08:00
blumia 347681e604 feat(macOS): click dock icon to show window when it's hidden 2025-07-22 00:08:06 +08:00
blumia 505ab9e2a6 chore: update README 2025-07-11 19:41:09 +08:00
blumia c787e14a69 chore(CI): bump exiv2 version for msvc build 2025-07-11 00:31:10 +08:00
blumia 29c5783a66 chore: update release info to 1.1.0 2025-07-06 00:05:52 +08:00
ddc75f7cd0 i18n: Translated using Weblate (Tamil) (#155)
Currently translated at 100.0% (166 of 166 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ta/

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-07-05 23:19:32 +08:00
blumia f976fea418 fix: shouldn't loop when navigate gallery via shortcuts
Related: https://github.com/BLumia/pineapple-pictures/issues/153
2025-06-29 16:24:26 +08:00
blumia 2846e4907b feat: support load m3u8 playlist
This change is sponsored by @superuser7777

Related: https://github.com/BLumia/pineapple-pictures/issues/153
2025-06-28 09:27:30 +08:00
blumia c828f86b74 fix: FTBFS, and tidy code 2025-06-25 21:55:08 +08:00
blumia 040e2a7b5f i18n: update ts files 2025-06-25 21:47:31 +08:00
blumia b566096b1f feat: option to disable gallery looping
This change is sponsored by @superuser7777

Related: https://github.com/BLumia/pineapple-pictures/issues/153
2025-06-25 20:55:42 +08:00
blumia 2bee79c064 i18n: update ts files 2025-06-22 15:14:08 +08:00
91696963ae i18n: Translations update from Hosted Weblate (#152)
* Translated using Weblate (Dutch)

Currently translated at 100.0% (164 of 164 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/nl/

* Translated using Weblate (Italian)

Currently translated at 100.0% (164 of 164 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/it/

---------

Co-authored-by: Heimen Stoffels <vistausss@fastmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
2025-06-22 15:11:56 +08:00
blumia 1623ca315a feat: able to disable built-in close window animation 2025-06-22 15:04:15 +08:00
blumia 3cfb25db9a chore(CI): use Qt 6.9.1 2025-06-04 20:37:08 +08:00
blumia 36854d19f4 i18n: update ts files 2025-06-01 13:26:49 +08:00
blumiaandGitHub 3f7ebd2f6c chore: make use of Qt::Literals::StringLiterals u""_s
Migrate string literal handling to Qt’s built‐in `u""_s` literals for consistency and readability.
2025-05-31 13:10:21 +08:00
blumia 291a98ea97 fix(CI): macOS and Ubuntu CI FTBFS
macOS's CI uses conan to build exiv2, which seems still rely on
the exiv2lib target...
2025-05-24 23:19:26 +08:00
blumia 24a0b581f3 chore: drop Qt 5 support
The required exiv2 version is now 0.28.x as well since it was
keeped for Qt 5 build.

Due to exiv2ConfigVersion.cmake doesn't allow us write something
like find_package(exiv2 "0.28") to require version >= 0.28.0,
we simply don't put the required version number here for now.
2025-05-24 20:34:36 +08:00
blumia 369aa13be3 chore: correct feature list in release note 2025-05-11 13:56:38 +08:00
blumia b964fdc77f chore: update release info to 1.0.0 2025-05-01 22:07:42 +08:00
blumia d6d2703c93 chore: ensure could get correct locale on macOS 2025-04-28 00:51:17 +08:00
blumia c6068ba23d fix: qmake build FTBFS 2025-04-26 17:23:56 +08:00
blumia fbdd858fbd chore: merge Qt translations into app translations as well 2025-04-26 17:02:24 +08:00
blumia 8333f17199 chore(CI): add portable mode to qmake build
Resolve https://github.com/BLumia/pineapple-pictures/issues/148

This change is sponsored by @EdgarHartel.
2025-04-11 12:54:29 +08:00
blumia 30eb06cba7 chore: make use of the showMessageAndExit api in Qt 6.9 2025-04-09 19:24:27 +08:00
blumia 0a45cd7c22 chore: add the ability to know if it's the first loaded user media
This could make it easier to allow set “Keep Transform” mode at
start-up.

This change is sponsored by @EdgarHartel.

Related: https://github.com/BLumia/pineapple-pictures/issues/146#issuecomment-2778192785
2025-04-08 23:19:02 +08:00
blumia fc417b30e5 chore(CI): bump msvc build AOM and libavif version 2025-04-04 17:35:59 +08:00
8568f6d4ef i18n: Translations update from Hosted Weblate (#145)
* Translated using Weblate (Dutch)

Currently translated at 100.0% (163 of 163 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/nl/

* Translated using Weblate (Italian)

Currently translated at 100.0% (163 of 163 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/it/

---------

Co-authored-by: Heimen Stoffels <vistausss@fastmail.com>
Co-authored-by: albanobattistella <albano_battistella@hotmail.com>
2025-04-04 17:16:16 +08:00
blumia 8832c2daa9 chore(CI): bump Qt version to Qt 6.9.0 2025-04-02 19:39:20 +08:00
blumia da9d7d6989 fix: add a small delay when file watcher triggers a reload
The file might still being write by other program, thus we add a
500ms delay to reload the image.
2025-03-31 01:28:03 +08:00
blumia 317d296507 chore(CI): AppVeyor FTBFS, bump GH Action Qt version 2025-03-30 00:16:40 +08:00
blumia f24743e381 feat: reload image when current image gets updated
This feature is sponsored by @EdgarHartel.

Issue: https://github.com/BLumia/pineapple-pictures/issues/143
2025-03-29 13:41:55 +08:00
blumia fd4af282af i18n: update ts files 2025-03-26 22:53:57 +08:00
blumia 4de03dd02e feat: support enforce windowed mode on start-up
This feature is sponsored by @EdgarHartel.

Issue: https://github.com/BLumia/pineapple-pictures/issues/143
2025-03-26 22:51:27 +08:00
bfaf702606 i18n: Translated using Weblate (Japanese) (#144)
Currently translated at 100.0% (162 of 162 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ja/

Co-authored-by: mmahhi <mmahhi@users.noreply.hosted.weblate.org>
2025-03-26 22:50:36 +08:00
blumia 3203d7f598 chore(macOS): use native text for ShortcutEdit's label
This means that it will be shown translated and on Apple platforms
it will resemble a key sequence from the menu bar.
2025-03-08 00:31:49 +08:00
blumia cbd006bae7 chore: update release info to 0.9.2 2025-02-26 20:58:16 +08:00
Pino Toscano fc13a497d3 fix: refer to the right CMake module for Exiv2
The exiv2 library provides its own CMake config module, which is named
"exiv2". On case-sensitive systems (all the Unix systems by default)
trying to look for it as "Exiv2" does not work.

Hence properly look for "exiv2", and adapt the associated CMake
variables accoding to that.
2025-02-20 08:13:16 +01:00
blumia 183a61b2f6 i18n: update AppStream metainfo translation and translators.html 2025-02-16 13:34:41 +08:00
ae07c5fe44 i18n: Translations update from Hosted Weblate (#140)
* Added translation using Weblate (Tamil)

* Added translation using Weblate (Tamil)

* Translated using Weblate (Tamil)

Currently translated at 100.0% (162 of 162 strings)

Translation: pineapple-pictures/Application
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/application/ta/

* Translated using Weblate (Tamil)

Currently translated at 100.0% (7 of 7 strings)

Translation: pineapple-pictures/AppStream Metadata
Translate-URL: https://hosted.weblate.org/projects/pineapple-pictures/appstream-metadata/ta/

---------

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-02-16 13:08:25 +08:00
blumia a33381557b chore: sync playlistmanager changes from other project 2025-02-16 00:50:14 +08:00
blumia 6d8ec6a54a chore(CI): bump msvc windows CI Qt version to 6.8.2 2025-02-06 12:48:21 +08:00
blumia dd3209e3ff chore(REUSE): convert from DEP5 to REUSE.toml 2025-02-02 12:50:44 +08:00
109 changed files with 12544 additions and 7404 deletions
+3
View File
@@ -0,0 +1,3 @@
# .git-blame-ignore-revs
# CR LF to LF
ed5a6023326fd2ab420ded76976501be33e0b389
+5
View File
@@ -0,0 +1,5 @@
*.txt text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.ui text eol=lf
*.qml text eol=lf
+4 -3
View File
@@ -8,12 +8,13 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: '6.8.1'
version: '6.10.3'
modules: 'qtimageformats'
cache: true
- name: Install Conan and Dependencies
id: conan
working-directory: ./
@@ -32,7 +33,7 @@ jobs:
run: |
macdeployqt ./ppic.app -dmg
ls
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: "macos-bundle"
path: "*.dmg"
+5 -3
View File
@@ -7,9 +7,11 @@ name: REUSE Compliance Check
on: [push, pull_request]
jobs:
test:
reuse-compliance-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v2
uses: fsfe/reuse-action@v5
+4 -28
View File
@@ -1,36 +1,12 @@
name: Ubuntu CI
on: [push, pull_request]
on: [push, pull_request, workflow_dispatch]
jobs:
ubuntu-22-04-build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Get build dept.
run: |
sudo apt update
sudo apt install cmake qtbase5-dev libqt5svg5-dev qttools5-dev libexiv2-dev
- name: Build it
run: |
mkdir build
cd build
cmake ../ -DPREFER_QT_5=ON
make
cpack -G DEB
- name: Try install it
run: |
cd build
sudo apt install ./*.deb
- uses: actions/upload-artifact@v4
with:
name: ubuntu-22.04-deb-package
path: build/*.deb
ubuntu-24-04-build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get build dept.
run: |
sudo apt update
@@ -39,14 +15,14 @@ jobs:
run: |
mkdir build
cd build
cmake ../ -DPREFER_QT_5=OFF
cmake ../
make
cpack -G DEB
- name: Try install it
run: |
cd build
sudo apt install ./*.deb
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: ubuntu-24.04-deb-package
path: build/*.deb
+42 -30
View File
@@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
include:
- qt_ver: '6.8.1'
- qt_ver: '6.10.3'
vs: '2022'
aqt_arch: 'win64_msvc2022_64'
msvc_arch: 'x64'
@@ -16,13 +16,14 @@ jobs:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
arch: ${{ matrix.aqt_arch }}
version: ${{ matrix.qt_ver }}
modules: 'qtimageformats'
cache: true
- name: Build
shell: cmd
run: |
@@ -34,7 +35,7 @@ jobs:
nmake
nmake clean
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation release\ppic.exe
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: "windows-msvc${{ matrix.vs }}-qt${{ matrix.qt_ver }}-qmake-package"
path: release/*
@@ -44,7 +45,7 @@ jobs:
strategy:
matrix:
include:
- qt_ver: '6.8.1'
- qt_ver: '6.10.3'
vs: '2022'
aqt_arch: 'win64_msvc2022_64'
msvc_arch: 'x64'
@@ -52,13 +53,14 @@ jobs:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
arch: ${{ matrix.aqt_arch }}
version: ${{ matrix.qt_ver }}
modules: 'qtimageformats'
cache: true
- name: Build
shell: cmd
run: |
@@ -71,55 +73,65 @@ jobs:
:: ------ dep ------
set CMAKE_PREFIX_PATH=%PWD%/dependencies_bin
mkdir dependencies_src
:: ===== exiv2 =====
curl -fsSL -o exiv2_bin.zip https://github.com/Exiv2/exiv2/releases/download/v0.28.3/exiv2-0.28.3-2019msvc64.zip
echo ::group::===== exiv2 =====
curl -fsSL -o exiv2_bin.zip https://github.com/Exiv2/exiv2/releases/download/v0.28.7/exiv2-0.28.7-2022msvc-AMD64.zip
7z x exiv2_bin.zip -y
ren .\exiv2-0.28.3-2019msvc64 dependencies_bin
:: ===== zlib =====
curl -fsSL -o zlib_src.zip https://zlib.net/zlib131.zip
ren .\exiv2-0.28.7-2022msvc-AMD64 dependencies_bin
echo ::endgroup::
echo ::group::===== zlib =====
curl -fsSL -o zlib_src.zip https://github.com/madler/zlib/releases/download/v1.3.2/zlib132.zip
7z x zlib_src.zip -y -o"dependencies_src"
ren .\dependencies_src\zlib-1.3.1 zlib || goto :error
ren .\dependencies_src\zlib-1.3.2 zlib || goto :error
cmake ./dependencies_src/zlib -Bbuild_dependencies/zlib -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake --build build_dependencies/zlib --config Release --target=install || goto :error
curl -fsSL -o expat_src.zip https://github.com/libexpat/libexpat/archive/R_2_6_2.zip
:: ===== AOM for libavif AVI decoding support =====
git clone -q -b v3.10.0 --depth 1 https://aomedia.googlesource.com/aom dependencies_src/aom
echo ::endgroup::
echo ::group::===== AOM for libavif AVI decoding support =====
git clone -q -b v3.13.1 --depth 1 https://aomedia.googlesource.com/aom dependencies_src/aom
cmake ./dependencies_src/aom -Bbuild_dependencies/aom -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DENABLE_DOCS=OFF -DBUILD_SHARED_LIBS=ON -DAOM_TARGET_CPU=generic -DENABLE_TESTS=OFF -DENABLE_TESTDATA=OFF -DENABLE_TOOLS=OFF -DENABLE_EXAMPLES=0 || goto :error
cmake --build build_dependencies/aom --config Release --target=install || goto :error
:: ===== libavif =====
curl -fsSL -o libavif-v1_1_1.zip https://github.com/AOMediaCodec/libavif/archive/v1.1.1.zip
7z x libavif-v1_1_1.zip -y -o"dependencies_src"
ren .\dependencies_src\libavif-1.1.1 libavif || goto :error
cmake ./dependencies_src/libavif -Bbuild_dependencies/libavif -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_LIBYUV=ON
echo ::endgroup::
echo ::group::===== libavif =====
curl -fsSL -o libavif-v1_3_0.zip https://github.com/AOMediaCodec/libavif/archive/v1.3.0.zip
7z x libavif-v1_3_0.zip -y -o"dependencies_src"
ren .\dependencies_src\libavif-1.3.0 libavif || goto :error
cmake ./dependencies_src/libavif -Bbuild_dependencies/libavif -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DAVIF_CODEC_AOM=SYSTEM -DAVIF_CODEC_AOM_DECODE=ON -DAVIF_CODEC_AOM_ENCODE=ON -DAVIF_LIBYUV=LOCAL
cmake --build build_dependencies/libavif --config Release --target=install || goto :error
:: ===== expat =====
echo ::endgroup::
echo ::group::===== expat =====
curl -fsSL -o expat_src.zip https://github.com/libexpat/libexpat/archive/R_2_7_3.zip
7z x expat_src.zip -y -o"dependencies_src"
ren .\dependencies_src\libexpat-R_2_6_2 expat || goto :error
ren .\dependencies_src\libexpat-R_2_7_3 expat || goto :error
cmake ./dependencies_src/expat/expat -Bbuild_dependencies/expat -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake --build build_dependencies/expat --config Release --target=install || goto :error
:: ===== ECM =====
echo ::endgroup::
echo ::group::===== ECM =====
git clone -q https://invent.kde.org/frameworks/extra-cmake-modules.git dependencies_src/extra-cmake-modules
cmake .\dependencies_src\extra-cmake-modules -Bbuild_dependencies/extra-cmake-modules -DCMAKE_INSTALL_PREFIX="dependencies_bin" -DBUILD_TESTING=OFF || goto :error
cmake --build build_dependencies/extra-cmake-modules --config Release --target=install || goto :error
:: ===== KArchive =====
echo ::endgroup::
echo ::group::===== KArchive =====
git clone -q https://invent.kde.org/frameworks/karchive.git dependencies_src/karchive
cmake .\dependencies_src\karchive -Bbuild_dependencies/karchive -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake .\dependencies_src\karchive -Bbuild_dependencies/karchive -DBUILD_TESTING=OFF -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
cmake --build build_dependencies/karchive --config Release --target=install || goto :error
:: ===== KImageFormats =====
echo ::endgroup::
echo ::group::===== KImageFormats =====
git clone -q https://invent.kde.org/frameworks/kimageformats.git dependencies_src/kimageformats
cmake .\dependencies_src\kimageformats -Bbuild_dependencies/kimageformats -DKDE_INSTALL_QTPLUGINDIR=%QT_ROOT_DIR%\plugins || goto :error
cmake .\dependencies_src\kimageformats -Bbuild_dependencies/kimageformats -DBUILD_TESTING=OFF -DKDE_INSTALL_QTPLUGINDIR=%QT_ROOT_DIR%\plugins || goto :error
cmake --build build_dependencies/kimageformats --config Release --target=install || goto :error
echo ::endgroup::
:: ------ app ------
cmake -Bbuild . -DCMAKE_INSTALL_PREFIX="%PWD%\build\"
echo ::group::===== App (Build'n'Install) =====
cmake -Bbuild .
cmake --build build --config Release
cmake --build build --config Release --target=install
cmake --install build --config Release --prefix "%PWD%\build"
echo ::endgroup::
:: ------ pkg ------
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation build\bin\ppic.exe
windeployqt --verbose=2 --no-compiler-runtime --no-quick-import --no-translations --no-network --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types tls,networkinformation build\bin\ppic.exe
robocopy ./dependencies_bin/bin build/bin *.dll
if ErrorLevel 8 (exit /B 1)
copy LICENSE build\bin
exit /B 0
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: "windows-msvc${{ matrix.vs }}-qt${{ matrix.qt_ver }}-cmake-package"
path: build/bin/*
+6
View File
@@ -11,6 +11,12 @@
# Generic Build Dir
[Bb]uild/
cmake-build-*/
# IDE/Editor config folders
.vscode/
.idea/
.qtcreator/
# Clangd cache
.cache/
-28
View File
@@ -1,28 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Pineapple Pictures
Source: https://github.com/BLumia/pineapple-pictures
# Config files
Files: .gitignore appveyor.yml .github/*
Copyright: None
License: CC0-1.0
# README, resource files and Metadata files
Files: README*.md NEWS assets/*.rc assets/*.qrc dist/*
Copyright: None
License: CC0-1.0
# Translation files
# See assets/plain/translators.html for a list of translators
Files: app/translations/*.ts assets/plain/translators.html
Copyright: Translators from hosted.weblate.org
License: MIT
# Assets
Files: assets/icons/*.svg
Copyright: 2022 Gary Wang
License: MIT
Files: assets/icons/app-icon.*
Copyright: 2020 Lovelyblack
License: MIT
+56 -44
View File
@@ -1,65 +1,50 @@
# SPDX-FileCopyrightText: 2022 - 2024 Gary Wang <git@blumia.net>
# SPDX-FileCopyrightText: 2022 - 2025 Gary Wang <git@blumia.net>
#
# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.16)
project(pineapple-pictures VERSION 0.9.1) # don't forget to update NEWS file and AppStream metadata.
project(pineapple-pictures VERSION 1.5.0) # don't forget to update NEWS file and AppStream metadata.
include(GNUInstallDirs)
include(FeatureSummary)
option (EXIV2_METADATA_SUPPORT "Better image metadata support via libexiv2" ON)
option (PREFER_QT_5 "Prefer to use Qt 5" OFF)
option (TRANSLATION_RESOURCE_EMBEDDING "Embedding .qm translation files inside resource" OFF)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_AUTOMOC ON)
set (CMAKE_AUTORCC ON)
set (CMAKE_AUTOUIC ON)
if (PREFER_QT_5)
find_package(QT NAMES Qt5 REQUIRED COMPONENTS Core)
else ()
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core)
endif ()
if (${QT_VERSION_MAJOR} EQUAL "5")
set (QT_MINIMUM_VERSION "5.15.2")
else ()
set (QT_MINIMUM_VERSION "6.4")
endif ()
set (QT_MINIMUM_VERSION "6.5")
find_package(Qt${QT_VERSION_MAJOR} ${QT_MINIMUM_VERSION} REQUIRED
COMPONENTS Widgets Svg LinguistTools
COMPONENTS Widgets Svg SvgWidgets LinguistTools
OPTIONAL_COMPONENTS DBus
)
if (${QT_VERSION_MAJOR} EQUAL "6")
find_package(Qt${QT_DEFAULT_MAJOR_VERSION} ${QT_MINIMUM_VERSION} CONFIG REQUIRED SvgWidgets)
endif ()
if (EXIV2_METADATA_SUPPORT)
find_package(Exiv2)
set_package_properties(Exiv2 PROPERTIES
find_package(exiv2)
set_package_properties(exiv2 PROPERTIES
URL "https://www.exiv2.org"
DESCRIPTION "image metadata support"
TYPE OPTIONAL
TYPE RECOMMENDED
PURPOSE "Bring better image metadata support"
)
endif ()
set (PPIC_CPP_FILES
app/main.cpp
app/framelesswindow.cpp
app/mainwindow.cpp
app/actionmanager.cpp
app/graphicsview.cpp
app/graphicsscene.cpp
app/bottombuttongroup.cpp
app/navigatorview.cpp
app/opacityhelper.cpp
app/toolbutton.cpp
app/settings.cpp
app/settingsdialog.cpp
app/aboutdialog.cpp
@@ -67,20 +52,19 @@ set (PPIC_CPP_FILES
app/metadatadialog.cpp
app/exiv2wrapper.cpp
app/playlistmanager.cpp
app/shortcutedit.cpp
app/fileopeneventhandler.cpp
app/playlistscout.cpp
app/playlistscouts/qtscout.cpp
app/playlistscouts/explorerscout.cpp
)
set (PPIC_HEADER_FILES
app/framelesswindow.h
app/mainwindow.h
app/actionmanager.h
app/graphicsview.h
app/graphicsscene.h
app/bottombuttongroup.h
app/navigatorview.h
app/opacityhelper.h
app/toolbutton.h
app/settings.h
app/settingsdialog.h
app/aboutdialog.h
@@ -88,8 +72,17 @@ set (PPIC_HEADER_FILES
app/metadatadialog.h
app/exiv2wrapper.h
app/playlistmanager.h
app/shortcutedit.h
app/fileopeneventhandler.h
app/playlistscout.h
app/playlistscouts/qtscout.h
app/playlistscouts/explorerscout.h
)
set (PPIC_UI_FILES
app/settingsdialog.ui
app/aboutdialog.ui
app/metadatadialog.ui
app/mainwindow.ui
)
set (PPIC_QRC_FILES
@@ -107,47 +100,53 @@ file (GLOB PPIC_TS_FILES app/translations/*.ts)
set (PPIC_CPP_FILES_FOR_I18N ${PPIC_CPP_FILES})
if (WIN32)
list(APPEND PPIC_RC_FILES assets/pineapple-pictures.rc)
configure_file(assets/pineapple-pictures.rc.in pineapple-pictures.rc @ONLY)
list(APPEND PPIC_RC_FILES ${CMAKE_CURRENT_BINARY_DIR}/pineapple-pictures.rc)
endif ()
add_executable (${EXE_NAME}
${PPIC_HEADER_FILES}
${PPIC_CPP_FILES}
${PPIC_UI_FILES}
${PPIC_QRC_FILES}
${PPIC_RC_FILES}
)
if (${QT_VERSION_MAJOR} EQUAL "6")
set(ADD_TRANSLATIONS_ADDITIONAL_ARGS)
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
set(ADD_TRANSLATIONS_ADDITIONAL_ARGS MERGE_QT_TRANSLATIONS)
endif()
if (TRANSLATION_RESOURCE_EMBEDDING)
qt_add_translations(${EXE_NAME} TS_FILES ${PPIC_TS_FILES})
qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES})
else()
qt_add_translations(${EXE_NAME} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES)
endif()
else()
qt_create_translation(PPIC_QM_FILES ${PPIC_CPP_FILES_FOR_I18N} ${PPIC_TS_FILES})
qt_add_translations(${EXE_NAME} ${ADD_TRANSLATIONS_ADDITIONAL_ARGS} TS_FILES ${PPIC_TS_FILES} QM_FILES_OUTPUT_VARIABLE PPIC_QM_FILES)
endif()
# YYC MARK:
# This is essential, otherwise Qt UIC generated UI header file
# can not find our header files if there are custom widgets inside UI file.
target_include_directories(${EXE_NAME} PRIVATE app/)
target_sources(${EXE_NAME} PRIVATE ${PPIC_QM_FILES})
target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg)
if (${QT_VERSION_MAJOR} EQUAL "6")
target_link_libraries (${EXE_NAME} Qt::SvgWidgets)
endif ()
target_link_libraries (${EXE_NAME} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::SvgWidgets)
if (Exiv2_FOUND)
if (exiv2_FOUND)
if(NOT TARGET Exiv2::exiv2lib AND TARGET exiv2lib)
# for exiv2 0.27.x
# for exiv2 0.27.x and (macOS?) conan build
add_library(Exiv2::exiv2lib ALIAS exiv2lib)
endif()
target_link_libraries (${EXE_NAME}
Exiv2::exiv2lib
)
target_compile_definitions(${EXE_NAME} PRIVATE
HAVE_EXIV2_VERSION="${Exiv2_VERSION}"
HAVE_EXIV2_VERSION="${exiv2_VERSION}"
)
endif ()
if (TARGET Qt5::DBus OR TARGET Qt6::DBus)
if (TARGET Qt6::DBus)
target_link_libraries (${EXE_NAME}
Qt${QT_VERSION_MAJOR}::DBus
)
@@ -283,6 +282,19 @@ if (DEFINED QM_FILE_INSTALL_DIR)
)
endif()
if (WIN32)
install(
FILES
dist/passoc/generic.ico
dist/passoc/jpeg.ico
dist/passoc/gif.ico
dist/passoc/svg.ico
dist/passoc/png.ico
dist/passoc/psd.ico
DESTINATION "${CMAKE_INSTALL_BINDIR}/icons"
)
endif()
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
# CPACK: General Settings
@@ -298,7 +310,7 @@ elseif (APPLE)
elseif (UNIX)
set (CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set (CPACK_DEBIAN_PACKAGE_RECOMMENDS "kimageformat-plugins")
set (CPACK_DEBIAN_PACKAGE_RECOMMENDS "kimageformat6-plugins")
endif()
include(CPack)
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 BLumia
Copyright (c) 2026 BLumia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+162 -2
View File
@@ -1,3 +1,164 @@
Version 1.5.0
~~~~~~~~~~~~~
Released: 2026-07-04
Features:
* Introduced an optional titlebar that display file name and allows drag and double-click to move and toggle maximize state
* Windows: Now the application will use the system's built-in animation for open window animation
Bugfixes:
* Show empty url list should also reset window title to blank
Miscellaneous:
* Update translations
* Add vcs-browser link to AppStream metadata
Contributors:
Heimen Stoffels (vistaus), VenusGirl (VenusGirl)
Version 1.4.1
~~~~~~~~~~~~~
Released: 2026-04-05
Miscellaneous:
* Update translations
* Update Qt version to 6.10.3 for Windows and macOS pre-built binaries
Contributors:
Heimen Stoffels (vistaus), Loc Huynh (hthienloc)
Version 1.4.0
~~~~~~~~~~~~~
Released: 2026-02-11
Features:
* Action to save image as new file with different format
* Add Vietnamese and Slovenian translation
Miscellaneous:
* Update copyright year to 2026
* Update translations
* Update Qt version to 6.10.2 for Windows and macOS pre-built binaries
Contributors:
Loc Huynh, Sabri Ünal, VenusGirl, Andrej Poženel, Andrey
Version 1.3.0
~~~~~~~~~~~~~
Released: 2025-12-20
Features:
* New option to limit SVG support to SVG Tiny 1.2 standard
* Windows: ship tools and configurations that help user associate image files to this program
Bugfixes:
* Fix window not appear at the screen that the mouse cursor is at
* Fix Windows prebuilt binary having AVIF plugin included but not actually support it
Miscellaneous:
* CPack DEB: will now recommend "kimageformat6-plugins" instead of the KF5 version
* Update translations
* Update exiv2 version for Windows binary build
Contributors:
VenusGirl, Andrey
Version 1.2.1
~~~~~~~~~~~~~
Released: 2025-11-22
Bugfixes:
* Fix incorrect color when display some CMYK images.
Miscellaneous:
* Now use Qt 6.10.1 for pre-built binary
* Update README description and AppStream metainfo
* Update translations
* Update libavif, AOM and expat version for Windows binary build
Contributors:
albanobattistella
Version 1.2.0
~~~~~~~~~~~~~
Released: 2025-09-21
Features:
* Long image mode that detect extra wide image and auto fit by its thin side
* Windows: Add product version to binary metadata / resource file
Miscellaneous:
* Update translations
* Update libavif version for Windows binary build
Contributors:
Heimen Stoffels, albanobattistella, Sabri Ünal
Version 1.1.1
~~~~~~~~~~~~~
Released: 2025-08-02
Features:
* Click dock icon should show window when it's hidden on macOS
Bugfixes:
* Ensure "Fit by Width" position the view to the beginning of the image
Miscellaneous:
* Update translations
* Update Exiv2 version for Windows binary build
Contributors:
Heimen Stoffels, VenusGirl, தமிழ்நேரம்
Version 1.1.0
~~~~~~~~~~~~~
Released: 2025-07-06
Features:
* New option to disable built-in close window animation
* New option to disable gallery looping
* Support load m3u8 as image gallery playlist
Miscellaneous:
* Drop Qt 5 support
Contributors:
Heimen Stoffels, albanobattistella, தமிழ்நேரம்
Version 1.0.0
~~~~~~~~~~~~~
Released: 2025-05-03
Features:
* Support enforces windowed mode on start-up
* Reload image automatically when current image gets updated
Bugfixes:
* Display correct text language on macOS
Miscellaneous:
* Use native text for shortcut editor's label
* Display native commandline message when possible
* Merge Qt translations into app applications as well
Contributors:
Heimen Stoffels, albanobattistella, mmahhi
Version 0.9.2
~~~~~~~~~~~~~
Released: 2025-03-05
Bugfixes:
* Refer to the right exiv2 CMake module so it can be found on Linux
Miscellaneous:
* Convert DEP5 to REUSE.toml for better REUSE compliance
* Update translations
Contributors:
Pino Toscano, TamilNeram
Version 0.9.1
~~~~~~~~~~~~~
Released: 2025-01-25
@@ -109,7 +270,7 @@ Version 0.7.1
Released: 2023-07-08
Features:
* TIF and TIFF format files in the same folder will now be automatedly added to the gallery
* TIF and TIFF format files in the same folder will now be automatically added to the gallery
* Built-in window resizing now also supports Linux desktop. (macOS might also works as well)
Bugfixes:
@@ -118,4 +279,3 @@ Bugfixes:
Contributors:
yyc12345
+28 -8
View File
@@ -1,4 +1,17 @@
Yet another image viewer.
# Sarasas Chip Workshop Picture
Yet another legacy Windows Photo Viewer with cross-platform support.
> [!IMPORTANT]
> SarasaCW Picture are still in refactoring. Please do not use it until this message removes.
## Summary
Sarasas Chip Workshop Picture (abbr. SarasaCW Picture) is a lightweight image viewer that has cross-platform support and similar user interface about legacy Windows Photo Viewer. It allows you view JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF and some other frequently used image formats files quickly and easily without changing your long-term Windows usage experience.
SarasaCW Picture is the **hard fork** of [Pineapple Pictures](https://github.com/BLumia/pineapple-pictures) since Pineapple Pictures 1.4.1 version.
<!--
|CI|Build Status|
|---|---|
@@ -8,10 +21,6 @@ Yet another image viewer.
![Pineapple Pictures - Main Window](https://repository-images.githubusercontent.com/211888654/e8697600-e370-11eb-9b2a-b71e05262954)
## Summary
Pineapple Pictures is a lightweight image viewer that allows you view JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF and some other frequently used image formats files quickly and easily, and also provide a Stay-on-Top window setting that allows you pin the window so you can use it to pin a reference image at the top and then you can work with other software.
## Get it!
### Maintained by the original author
@@ -23,13 +32,22 @@ Pineapple Pictures is a lightweight image viewer that allows you view JPEG, PNG,
### Maintained by contributors / certain distro's package maintainers
- Debian (since bullseye) or Ubuntu (since 21.04): `sudo apt install pineapple-pictures`
- Nix / NixOS: [pineapple-pictures](https://search.nixos.org/packages?channel=unstable&show=pineapple-pictures&from=0&size=50&sort=relevance&type=packages&query=pineapple-pictures) (maintained by @wineee)
[![Packaging status](https://repology.org/badge/vertical-allrepos/pineapple-pictures.svg?columns=4)](https://repology.org/project/pineapple-pictures/versions)
## Help Translation!
## Contribution
Beside feedback and code contribution, other contributions are also welcome!
### Help Translation!
[Translate this project on Weblate!](https://hosted.weblate.org/projects/pineapple-pictures/)
### Funding
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/blumia)
[![Afdian](https://static.afdiancdn.com/static/img/logo/logo.png)Afdian](https://afdian.com/a/BLumia)
## Build it manually:
Current state, we need:
@@ -62,3 +80,5 @@ It's possible to build it under Windows, Linux, macOS, and maybe other desktop p
## License
Pineapple Pictures as a whole is licensed under MIT license. Individual files may have a different, but compatible license.
-->
+30 -8
View File
@@ -1,3 +1,18 @@
# Sarasas Chip Workshop 照片查看器
简单轻量的、具有跨平台支持的、经典样式的 Windows 照片查看器。
> [!IMPORTANT]
> SarasaCW 照片查看器还在重构中。在这条信息被移除之前请不要使用它。
## 简介
Sarasas Chip Workshop 照片查看器(简称 SarasaCW 照片查看器)是一个轻量级的图片查看器。支持跨平台。界面风格类似于经典的 Windows 照片查看器。它可以让你快速轻松地浏览 JPEG、PNG、GIF、SVG、PSD、KRA、XCF、TGA、HDR、AVIF 以及其他一些常用的图片格式,且无需改变你长期的 Windows 使用习惯。
SarasaCW 照片查看器是[菠萝看图](https://github.com/BLumia/pineapple-pictures)的**硬分叉**,该分叉自菠萝看图 1.4.1 版本开始。
<!--
简单轻量的跨平台看图工具。
|CI|构建状态|
@@ -8,15 +23,11 @@
![Pineapple Pictures - Main Window](https://repository-images.githubusercontent.com/211888654/e8697600-e370-11eb-9b2a-b71e05262954)
## 简介
菠萝看图是一个轻量图像查看器,允许你简单快捷的查看 JPEG, PNG, GIF, SVG, PSD, KRA, XCF, TGA, HDR, AVIF 等常用格式的图像文件,并提供了置顶窗口的选项以便你在使用其它软件时也可以将参考图片固定在顶端。
## 立即获取!
### 由原作者维护
- [GitHub Release 页面](https://github.com/BLumia/pineapple-pictures/releases) | [gitee 发布页面](https://gitee.com/blumia/pineapple-pictures/releases)
- [GitHub Release 页面](https://github.com/BLumia/pineapple-pictures/releases)
- [SourceForge](https://sourceforge.net/projects/pineapple-pictures/)
- Archlinux AUR: [pineapple-pictures](https://aur.archlinux.org/packages/pineapple-pictures/) | [pineapple-pictures-git](https://aur.archlinux.org/packages/pineapple-pictures-git/)
- [Itch.io 商店](https://blumia.itch.io/pineapple-pictures)
@@ -24,13 +35,22 @@
### 由贡献者/对应发行版的打包人员维护
- Debian (自 bullseye 起) 或 Ubuntu (自 21.04 起): `sudo apt install pineapple-pictures`
- Nix / NixOS: [pineapple-pictures](https://search.nixos.org/packages?channel=unstable&show=pineapple-pictures&from=0&size=50&sort=relevance&type=packages&query=pineapple-pictures) (由 [@wineee](https://github.com/wineee) 维护)
[![打包状态](https://repology.org/badge/vertical-allrepos/pineapple-pictures.svg?columns=4)](https://repology.org/project/pineapple-pictures/versions)
## 帮助翻译!
## 参与贡献
除了参与反馈和代码贡献外,也欢迎各种类型的贡献!
### 帮助翻译!
[在 Weblate 上帮助此项目翻译到更多语言!](https://hosted.weblate.org/projects/pineapple-pictures/)
### 打赏
[![Afdian](https://static.afdiancdn.com/static/img/logo/logo.png)Afdian | 爱发电](https://afdian.com/a/BLumia)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/blumia)
## 手动构建步骤:
当前状态,我们需要先确保如下依赖可用:
@@ -63,3 +83,5 @@ $ cmake --build . # 如果你使用 Makefile 作为 CMake 生成器,也可以
## 许可协议
菠萝看图整体使用 MIT 协议进行发布。项目所随的部分源文件可能具备不同但与之兼容的许可协议。
-->
+39
View File
@@ -0,0 +1,39 @@
version = 1
SPDX-PackageName = "Pineapple Pictures"
SPDX-PackageDownloadLocation = "https://github.com/BLumia/pineapple-pictures"
[[annotations]]
path = [".gitattributes", ".git-blame-ignore-revs", ".gitignore", "appveyor.yml", ".github/**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ["README**.md", "NEWS", "assets/**.rc.in", "assets/**.qrc", "dist/appstream/**", "dist/**.in", "dist/**.desktop"]
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"
[[annotations]]
path = ["dist/passoc/**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Gary Wang"
SPDX-License-Identifier = "MIT"
[[annotations]]
path = ["app/translations/**.ts", "assets/plain/translators.html"]
precedence = "aggregate"
SPDX-FileCopyrightText = "Translators from hosted.weblate.org"
SPDX-License-Identifier = "MIT"
[[annotations]]
path = "assets/icons/**.svg"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Gary Wang"
SPDX-License-Identifier = "MIT"
[[annotations]]
path = "assets/icons/app-icon.**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2020 Lovelyblack"
SPDX-License-Identifier = "MIT"
+62 -142
View File
@@ -1,176 +1,96 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "aboutdialog.h"
#include "ui_aboutdialog.h"
#include <QAbstractButton>
#include <QDialogButtonBox>
#include <QTextBrowser>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QFile>
using namespace Qt::Literals::StringLiterals;
static QString loadEmbeddedHtml(QString&& path) {
QFile htmlFile(path);
bool canOpenFile = htmlFile.open(QIODevice::ReadOnly);
QByteArray htmlRawContent = canOpenFile ? htmlFile.readAll() : QByteArrayLiteral("");
return QString::fromUtf8(htmlRawContent);
}
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
, m_tabWidget(new QTabWidget)
, m_buttonBox(new QDialogButtonBox)
, m_helpTextEdit(new QTextBrowser)
, m_aboutTextEdit(new QTextBrowser)
, m_specialThanksTextEdit(new QTextBrowser)
, m_licenseTextEdit(new QTextBrowser)
, m_3rdPartyLibsTextEdit(new QTextBrowser)
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::AboutDialog))
{
this->setWindowTitle(tr("About"));
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
const QStringList helpStr {
QStringLiteral("<p>%1</p>").arg(tr("Launch application with image file path as argument to load the file.")),
QStringLiteral("<p>%1</p>").arg(tr("Drag and drop image file onto the window is also supported.")),
QStringLiteral("<p>%1</p>").arg(tr("None of the operations in this application will alter the pictures on disk.")),
QStringLiteral("<p>%1</p>").arg(tr("Context menu option explanation:")),
QStringLiteral("<ul>"),
// blumia: Chain two arg() here since it seems lupdate will remove one of them if we use
// the old `arg(QCoreApp::translate(), tr())` way, but it's worth to mention
// `arg(QCoreApp::translate(), this->tr())` works, but lupdate will complain about the usage.
QStringLiteral("<li><b>%1</b>:<br/>%2</li>")
.arg(QCoreApplication::translate("MainWindow", "Stay on top"))
.arg(tr("Make window stay on top of all other windows.")),
QStringLiteral("<li><b>%1</b>:<br/>%2</li>")
.arg(QCoreApplication::translate("MainWindow", "Protected mode"))
.arg(tr("Avoid close window accidentally. (eg. by double clicking the window)")),
QStringLiteral("<li><b>%1</b>:<br/>%2</li>")
.arg(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"))
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.")),
QStringLiteral("</ul>")
};
const QString HELP_HTML = loadEmbeddedHtml(u":/plain/help.html"_s)
.arg(tr("General Help"))
.arg(tr("Launch application with image file path as argument to load the file."))
.arg(tr("Passing in a single image file will load all the images in that directory and start viewing from that image."))
.arg(tr("Passing in multiple image files lets you view them one by one."))
.arg(tr("Drag and drop image file onto the window is also supported."))
.arg(tr("None of the operations in this application will alter the pictures on disk."))
.arg(tr("Context Menu Help"))
.arg(tr("Keep transformation", "The same meaning of MainWindow's Keep transformation."))
.arg(tr("Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images."))
.arg(tr("Checkerboard", "The same meaning of MainWindow's Checkerboard."))
.arg(tr("Checkerboard light and dark theme can be adjusted in the configuration window, or easily switch it by holding Shift and clicking the button."));
const QStringList aboutStr {
QStringLiteral("<center><img width='128' height='128' src=':/icons/app-icon.svg'/><br/>"),
qApp->applicationDisplayName(),
(QStringLiteral("<br/>") + tr("Version: %1").arg(
const QString APP_NAME_ARG = qApp->applicationDisplayName();
const QString APP_VERSION_ARG =
#ifdef GIT_DESCRIBE_VERSION_STRING
GIT_DESCRIBE_VERSION_STRING
GIT_DESCRIBE_VERSION_STRING;
#else
qApp->applicationVersion()
qApp->applicationVersion();
#endif // GIT_DESCRIBE_VERSION_STRING
)),
QStringLiteral("<hr/>"),
tr("Copyright (c) %1 %2", "%1 is year, %2 is the name of copyright holder(s)")
.arg(QStringLiteral("2025"), QStringLiteral("<a href='https://github.com/BLumia'>@BLumia</a>")),
QStringLiteral("<br/>"),
tr("Logo designed by %1").arg(QStringLiteral("<a href='https://github.com/Lovelyblack'>@Lovelyblack</a>")),
QStringLiteral("<hr/>"),
tr("Built with Qt %1 (%2)").arg(QT_VERSION_STR, QSysInfo::buildCpuArchitecture()),
QStringLiteral("<br/><a href='%1'>%2</a>").arg("https://github.com/BLumia/pineapple-pictures", tr("Source code")),
QStringLiteral("</center>")
};
const QString QT_VERSION_ARG = QT_VERSION_STR;
const QString QT_ARCH_ARG = QSysInfo::buildCpuArchitecture();
const QString ABOUT_HTML = loadEmbeddedHtml(u":/plain/about.html"_s)
.arg(APP_NAME_ARG)
.arg(tr("Version: %1").arg(APP_VERSION_ARG))
.arg(tr("Upstream author"))
.arg(tr("Hard fork author"))
.arg(tr("Logo designer"))
.arg(tr("Built with Qt %1 (%2)").arg(QT_VERSION_ARG).arg(QT_ARCH_ARG))
.arg(tr("Source code"));
QFile translaterHtml(":/plain/translators.html");
bool canOpenFile = translaterHtml.open(QIODevice::ReadOnly);
const QByteArray & translatorList = canOpenFile ? translaterHtml.readAll() : QByteArrayLiteral("");
const QString SPECIAL_THANKS_HTML = loadEmbeddedHtml(u":/plain/special-thanks.html"_s)
.arg(tr("Contributors"))
.arg(tr("List of contributors on GitHub"))
.arg(tr("Thanks to all people who contributed to this project."))
.arg(tr("Translators"))
.arg(tr("I would like to thank the following people who volunteered to translate this application."));
const QStringList specialThanksStr {
QStringLiteral("<h1 align='center'>%1</h1><a href='%2'>%3</a><p>%4</p>").arg(
tr("Contributors"),
QStringLiteral("https://github.com/BLumia/pineapple-pictures/graphs/contributors"),
tr("List of contributors on GitHub"),
tr("Thanks to all people who contributed to this project.")
),
const QString LICENSE_HTML = loadEmbeddedHtml(u":/plain/license.html"_s)
.arg(tr("License"))
.arg(tr("This application is released under the MIT License."))
.arg(tr("License Verbatim Copy"));
QStringLiteral("<h1 align='center'>%1</h1><p>%2</p>%3").arg(
tr("Translators"),
tr("I would like to thank the following people who volunteered to translate this application."),
translatorList
)
};
const QString THIRD_PARTY_LIBS_HTML = loadEmbeddedHtml(u":/plain/third-party-libs.html"_s)
.arg(tr("Third-party Libraries"))
.arg(tr("This application is built on the following free software libraries.", "Free as in freedom"))
.arg(tr("Some of them are optional and might not be included with this distribution depending on the build environment."));
const QStringList licenseStr {
QStringLiteral("<h1 align='center'><b>%1</b></h1>").arg(tr("Your Rights")),
QStringLiteral("<p>%1</p><p>%2</p><ul><li>%3</li><li>%4</li><li>%5</li><li>%6</li></ul>").arg(
tr("%1 is released under the MIT License."), // %1
tr("This license grants people a number of freedoms:"), // %2
tr("You are free to use %1, for any purpose"), // %3
tr("You are free to distribute %1"), // %4
tr("You can study how %1 works and change it"), // %5
tr("You can distribute changed versions of %1") // %6
).arg(QStringLiteral("<i>%1</i>")),
QStringLiteral("<p>%1</p>").arg(tr("The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.")),
QStringLiteral("<hr/><pre>%2</pre>")
};
ui->m_helpTextEdit->setHtml(HELP_HTML);
ui->m_aboutTextEdit->setHtml(ABOUT_HTML);
ui->m_specialThanksTextEdit->setHtml(SPECIAL_THANKS_HTML);
ui->m_licenseTextEdit->setHtml(LICENSE_HTML);
ui->m_3rdPartyLibsTextEdit->setHtml(THIRD_PARTY_LIBS_HTML);
const QString mitLicense(QStringLiteral(R"(Expat/MIT License
Copyright (c) 2025 BLumia
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 NONINFRINGEMENT. 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.
)"));
const QStringList thirdPartyLibsStr {
QStringLiteral("<h1 align='center'><b>%1</b></h1>").arg(tr("Third-party Libraries used by %1")),
tr("%1 is built on the following free software libraries:", "Free as in freedom"),
QStringLiteral("<ul>"),
#ifdef HAVE_EXIV2_VERSION
QStringLiteral("<li><a href='%1'>%2</a>: %3</li>").arg("https://www.exiv2.org/", "Exiv2", "GPLv2"),
#endif // EXIV2_VERSION
QStringLiteral("<li><a href='%1'>%2</a>: %3</li>").arg("https://www.qt.io/", "Qt", "GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3"),
QStringLiteral("</ul>")
};
m_helpTextEdit->setText(helpStr.join('\n'));
m_aboutTextEdit->setText(aboutStr.join('\n'));
m_aboutTextEdit->setOpenExternalLinks(true);
m_specialThanksTextEdit->setText(specialThanksStr.join('\n'));
m_specialThanksTextEdit->setOpenExternalLinks(true);
m_licenseTextEdit->setText(licenseStr.join('\n').arg(qApp->applicationDisplayName(), mitLicense));
m_3rdPartyLibsTextEdit->setText(thirdPartyLibsStr.join('\n').arg(QStringLiteral("<i>%1</i>").arg(qApp->applicationDisplayName())));
m_3rdPartyLibsTextEdit->setOpenExternalLinks(true);
m_tabWidget->addTab(m_helpTextEdit, tr("&Help"));
m_tabWidget->addTab(m_aboutTextEdit, tr("&About"));
m_tabWidget->addTab(m_specialThanksTextEdit, tr("&Special Thanks"));
m_tabWidget->addTab(m_licenseTextEdit, tr("&License"));
m_tabWidget->addTab(m_3rdPartyLibsTextEdit, tr("&Third-party Libraries"));
m_buttonBox->setStandardButtons(QDialogButtonBox::Close);
connect(m_buttonBox, QOverload<QAbstractButton *>::of(&QDialogButtonBox::clicked), this, [this](){
connect(ui->m_buttonBox, QOverload<QAbstractButton *>::of(&QDialogButtonBox::clicked), this, [this](QAbstractButton * btn){
Q_UNUSED(btn)
this->close();
});
setLayout(new QVBoxLayout);
layout()->addWidget(m_tabWidget);
layout()->addWidget(m_buttonBox);
setMinimumSize(361, 161); // not sure why it complain "Unable to set geometry"
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
}
AboutDialog::~AboutDialog()
{
delete ui;
}
QSize AboutDialog::sizeHint() const
+4 -11
View File
@@ -8,9 +8,9 @@
#include <QDialog>
QT_BEGIN_NAMESPACE
class QTextBrowser;
class QTabWidget;
class QDialogButtonBox;
namespace Ui {
class AboutDialog;
}
QT_END_NAMESPACE
class AboutDialog : public QDialog
@@ -23,14 +23,7 @@ public:
QSize sizeHint() const override;
private:
QTabWidget * m_tabWidget = nullptr;
QDialogButtonBox * m_buttonBox = nullptr;
QTextBrowser * m_helpTextEdit = nullptr;
QTextBrowser * m_aboutTextEdit = nullptr;
QTextBrowser * m_specialThanksTextEdit = nullptr;
QTextBrowser * m_licenseTextEdit = nullptr;
QTextBrowser * m_3rdPartyLibsTextEdit = nullptr;
QT_PREPEND_NAMESPACE(Ui::AboutDialog) *ui;
};
#endif // ABOUTDIALOG_H
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutDialog</class>
<widget class="QDialog" name="AboutDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="windowTitle">
<string>About</string>
</property>
<property name="minimumSize">
<size>
<width>361</width>
<height>161</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="m_tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="helpTab">
<attribute name="title">
<string>&amp;Help</string>
</attribute>
<layout class="QVBoxLayout" name="helpTabLayout">
<item>
<widget class="QTextBrowser" name="m_helpTextEdit">
<property name="openExternalLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="aboutTab">
<attribute name="title">
<string>&amp;About</string>
</attribute>
<layout class="QVBoxLayout" name="aboutTabLayout">
<item>
<widget class="QTextBrowser" name="m_aboutTextEdit">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="specialThanksTab">
<attribute name="title">
<string>&amp;Special Thanks</string>
</attribute>
<layout class="QVBoxLayout" name="specialThanksTabLayout">
<item>
<widget class="QTextBrowser" name="m_specialThanksTextEdit">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="licenseTab">
<attribute name="title">
<string>&amp;License</string>
</attribute>
<layout class="QVBoxLayout" name="licenseTabLayout">
<item>
<widget class="QTextBrowser" name="m_licenseTextEdit">
<property name="openExternalLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="thirdPartyLibsTab">
<attribute name="title">
<string>&amp;Third-party Libraries</string>
</attribute>
<layout class="QVBoxLayout" name="thirdPartyLibsTabLayout">
<item>
<widget class="QTextBrowser" name="m_3rdPartyLibsTextEdit">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="m_buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+137 -86
View File
@@ -4,29 +4,17 @@
#include "actionmanager.h"
#include "settings.h"
#include "mainwindow.h"
#include "playlistmanager.h"
#include "graphicsview.h"
#include "graphicsscene.h"
#include <QGuiApplication>
#include <QSvgRenderer>
#include <QPainter>
#define ICON_NAME(name)\
QStringLiteral(":/icons/" #name ".svg")
#define ACTION_NAME(s) QStringLiteral(STRIFY(s))
#define STRIFY(s) #s
ActionManager::ActionManager()
{
}
ActionManager::~ActionManager()
{
}
QIcon ActionManager::loadHidpiIcon(const QString &resp, QSize sz)
static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32))
{
QSvgRenderer r(resp);
QPixmap pm = QPixmap(sz * qApp->devicePixelRatio());
@@ -39,87 +27,110 @@ QIcon ActionManager::loadHidpiIcon(const QString &resp, QSize sz)
void ActionManager::setupAction(MainWindow *mainWindow)
{
// region: Action and Its Icon Creation
// Declare some convenient macros and lambdas.
#define ICON_NAME(name)\
QStringLiteral(":/icons/" #name ".svg")
#define ACTION_NAME(s) QStringLiteral(STRIFY(s))
#define STRIFY(s) #s
auto create_action = [] (QWidget *w, QAction **a, QString i, QString an, bool iconFromTheme = false) {
*a = new QAction(w);
if (!i.isNull())
(*a)->setIcon(iconFromTheme ? QIcon::fromTheme(i) : ActionManager::loadHidpiIcon(i));
(*a)->setIcon(iconFromTheme ? QIcon::fromTheme(i) : loadHidpiIcon(i));
(*a)->setObjectName(an);
w->addAction(*a);
};
#define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a))
/// This macro for register normal action.
#define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a))
/// This macro for register Qt theme action.
#define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true)
/// This macro for register action with our custom icon.
#define CREATE_NEW_ICON_ACTION(w, a, i) create_action(w, &a, ICON_NAME(i), ACTION_NAME(a), false)
// TODO: We may replace all Qt theme icons to our custom icon for visual consistency.
// TODO: Some icons are invalid.
CREATE_NEW_ACTION(mainWindow, actionCopyPixmap);
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
CREATE_NEW_ICON_ACTION(mainWindow, actionPrevPicture, go-previous);
CREATE_NEW_ICON_ACTION(mainWindow, actionNextPicture, go-next);
CREATE_NEW_ICON_ACTION(mainWindow, actionActualSize, zoom-original);
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleMaximize, view-fullscreen);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitToScreen, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByWidth, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFitByHeight, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomIn, zoom-in);
CREATE_NEW_ICON_ACTION(mainWindow, actionZoomOut, zoom-out);
CREATE_NEW_ICON_ACTION(mainWindow, actionToggleCheckerboard, view-background-checkerboard);
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipHorizontal, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionFlipVertical, app-icon);
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateClockwise, object-rotate-right);
#undef CREATE_NEW_ICON_ACTION
#define CREATE_NEW_ACTION(w, a) create_action(w, &a, QString(), ACTION_NAME(a))
#define CREATE_NEW_THEMEICON_ACTION(w, a, i) create_action(w, &a, QLatin1String(STRIFY(i)), ACTION_NAME(a), true)
CREATE_NEW_ACTION(mainWindow, actionRotateCounterClockwise);
CREATE_NEW_ACTION(mainWindow, actionPrevPicture);
CREATE_NEW_ACTION(mainWindow, actionNextPicture);
CREATE_NEW_ICON_ACTION(mainWindow, actionRotateCounterClockwise, app-icon);
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
CREATE_NEW_ACTION(mainWindow, actionToggleCheckerboard);
CREATE_NEW_ACTION(mainWindow, actionTogglePauseAnimation);
CREATE_NEW_ACTION(mainWindow, actionAnimationNextFrame);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionOpen, document-open);
CREATE_NEW_ACTION(mainWindow, actionHorizontalFlip);
CREATE_NEW_ACTION(mainWindow, actionFitInView);
CREATE_NEW_ACTION(mainWindow, actionFitByWidth);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionCopyPixmap, edit-copy);
CREATE_NEW_ACTION(mainWindow, actionCopyFilePath);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionPaste, edit-paste);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionTrash, edit-delete);
CREATE_NEW_ACTION(mainWindow, actionToggleStayOnTop);
CREATE_NEW_ACTION(mainWindow, actionToggleProtectMode);
CREATE_NEW_ACTION(mainWindow, actionToggleAvoidResetTransform);
CREATE_NEW_ACTION(mainWindow, actionSettings);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionLocateInFileManager, system-file-manager);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionProperties, document-properties);
CREATE_NEW_ACTION(mainWindow, actionQuitApp);
#undef CREATE_NEW_ACTION
CREATE_NEW_ACTION(mainWindow, actionSettings);
CREATE_NEW_THEMEICON_ACTION(mainWindow, actionHelp, system-help);
// Remove convenient macros
#undef CREATE_NEW_ICON_ACTION
#undef CREATE_NEW_THEMEICON_ACTION
#undef CREATE_NEW_ACTION
retranslateUi(mainWindow);
#undef STRIFY
#undef ACTION_NAME
#undef ICON_NAME
QMetaObject::connectSlotsByName(mainWindow);
}
// endregion
void ActionManager::retranslateUi(MainWindow *mainWindow)
{
Q_UNUSED(mainWindow);
// region: Setup Checkable Action
actionOpen->setText(QCoreApplication::translate("MainWindow", "&Open...", nullptr));
actionToggleAvoidResetTransform->setCheckable(true);
actionToggleCheckerboard->setCheckable(true);
actionTogglePauseAnimation->setCheckable(true);
actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr));
actionToggleMaximize->setText(QCoreApplication::translate("MainWindow", "Toggle maximize", nullptr));
actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr));
actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr));
actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr));
actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr));
actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr));
// endregion
// region: Action Translatable Text Setup
actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr));
actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr));
actionPrevPicture->setText(QCoreApplication::translate("MainWindow", "Previous image", nullptr));
actionNextPicture->setText(QCoreApplication::translate("MainWindow", "Next image", nullptr));
actionActualSize->setText(QCoreApplication::translate("MainWindow", "Actual size", nullptr));
actionFitToScreen->setText(QCoreApplication::translate("MainWindow", "Fit to screen", nullptr));
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit by width", nullptr));
actionFitByHeight->setText(QCoreApplication::translate("MainWindow", "Fit by height", nullptr));
actionZoomIn->setText(QCoreApplication::translate("MainWindow", "Zoom in", nullptr));
actionZoomOut->setText(QCoreApplication::translate("MainWindow", "Zoom out", nullptr));
actionFlipHorizontal->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
actionFlipVertical->setText(QCoreApplication::translate("MainWindow", "Flip &Vertically", nullptr));
actionRotateClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate right", nullptr));
actionRotateCounterClockwise->setText(QCoreApplication::translate("MainWindow", "Rotate left", nullptr));
actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"));
actionToggleCheckerboard->setText(QCoreApplication::translate("MainWindow", "Toggle Checkerboard", nullptr));
actionTogglePauseAnimation->setText(QCoreApplication::translate("MainWindow", "Pause/Resume Animation", nullptr));
actionAnimationNextFrame->setText(QCoreApplication::translate("MainWindow", "Animation Go to Next Frame", nullptr));
actionHorizontalFlip->setText(QCoreApplication::translate("MainWindow", "Flip &Horizontally", nullptr));
actionFitInView->setText(QCoreApplication::translate("MainWindow", "Fit to view", nullptr));
actionFitByWidth->setText(QCoreApplication::translate("MainWindow", "Fit to width", nullptr));
actionCopyPixmap->setText(QCoreApplication::translate("MainWindow", "Copy P&ixmap", nullptr));
actionCopyFilePath->setText(QCoreApplication::translate("MainWindow", "Copy &File Path", nullptr));
actionPaste->setText(QCoreApplication::translate("MainWindow", "&Paste", nullptr));
actionTrash->setText(QCoreApplication::translate("MainWindow", "Move to Trash", nullptr));
actionToggleStayOnTop->setText(QCoreApplication::translate("MainWindow", "Stay on top", nullptr));
actionToggleProtectMode->setText(QCoreApplication::translate("MainWindow", "Protected mode", nullptr));
actionToggleAvoidResetTransform->setText(QCoreApplication::translate("MainWindow", "Keep transformation", "The 'transformation' means the flip/rotation status that currently applied to the image view"));
actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr));
#ifdef Q_OS_WIN
actionLocateInFileManager->setText(
QCoreApplication::translate(
@@ -131,33 +142,73 @@ void ActionManager::retranslateUi(MainWindow *mainWindow)
actionLocateInFileManager->setText(QCoreApplication::translate("MainWindow", "Show in directory", nullptr));
#endif // Q_OS_WIN
actionProperties->setText(QCoreApplication::translate("MainWindow", "Properties", nullptr));
actionQuitApp->setText(QCoreApplication::translate("MainWindow", "Quit", nullptr));
actionSettings->setText(QCoreApplication::translate("MainWindow", "Configure...", nullptr));
actionHelp->setText(QCoreApplication::translate("MainWindow", "Help", nullptr));
// endregion
QMetaObject::connectSlotsByName(mainWindow);
}
void ActionManager::setupShortcuts()
{
actionOpen->setShortcut(QKeySequence::Open);
actionCopyPixmap->setShortcut(QKeySequence::Copy);
actionPrevPicture->setShortcuts({
QKeySequence(Qt::Key_Left),
QKeySequence(Qt::Key_PageUp),
});
actionNextPicture->setShortcuts({
QKeySequence(Qt::Key_Right),
QKeySequence(Qt::Key_PageDown),
});
actionActualSize->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
actionZoomIn->setShortcut(QKeySequence::ZoomIn);
actionZoomOut->setShortcut(QKeySequence::ZoomOut);
actionPrevPicture->setShortcuts({
QKeySequence(Qt::Key_PageUp),
QKeySequence(Qt::Key_Left),
});
actionNextPicture->setShortcuts({
QKeySequence(Qt::Key_PageDown),
QKeySequence(Qt::Key_Right),
});
actionHorizontalFlip->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
actionCopyPixmap->setShortcut(QKeySequence::Copy);
actionPaste->setShortcut(QKeySequence::Paste);
actionTrash->setShortcut(QKeySequence::Delete);
actionHelp->setShortcut(QKeySequence::HelpContents);
actionSettings->setShortcut(QKeySequence::Preferences);
actionProperties->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_I));
actionQuitApp->setShortcuts({
QKeySequence(Qt::Key_Space),
QKeySequence(Qt::Key_Escape)
});
actionSettings->setShortcut(QKeySequence::Preferences);
actionHelp->setShortcut(QKeySequence::HelpContents);
}
void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv)
{
const auto* gs = gv->scene();
const auto* settings = Settings::instance();
// Update copy actions
bool canCopy = pm->currentIndex().has_value();
actionCopyPixmap->setEnabled(canCopy);
actionCopyFilePath->setEnabled(canCopy);
// Update Prev/Next Picture State
const int galleryFileCount = pm->totalCount();
const bool loopGallery = settings->loopGallery();
actionPrevPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isFirstIndex()));
actionNextPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isLastIndex()));
// Update Avoid Reset Transform
actionToggleAvoidResetTransform->setChecked(gv->isAvoidResetTransform());
// Update Checkerboard
actionToggleCheckerboard->setChecked(gv->isCheckerboard());
// Update Animation Graphics Action
bool canPauseAnimation = gs->canPauseAnimation();
actionTogglePauseAnimation->setEnabled(canPauseAnimation);
if (canPauseAnimation) {
actionTogglePauseAnimation->setChecked(gs->isPauseAnimation());
} else {
actionTogglePauseAnimation->setChecked(false);
}
bool canSkipAnimation = gs->canSkipAnimationFrame();
actionAnimationNextFrame->setEnabled(canSkipAnimation);
// Update trash, located in explorer and properties.
bool hasLoadedFile = pm->currentIndex().has_value();
actionTrash->setEnabled(hasLoadedFile);
actionLocateInFileManager->setEnabled(hasLoadedFile);
actionProperties->setEnabled(hasLoadedFile);
}
+28 -26
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
@@ -8,51 +8,53 @@
#include <QAction>
class MainWindow;
class PlaylistManager;
class GraphicsView;
/// Action Manager is separated from Main Window
/// to manage all actions specifically.
class ActionManager
{
public:
ActionManager();
~ActionManager();
explicit ActionManager() = default;
~ActionManager() = default;
void setupAction(MainWindow * mainWindow);
void retranslateUi(MainWindow *MainWindow);
void setupShortcuts();
static QIcon loadHidpiIcon(const QString &resp, QSize sz = QSize(32, 32));
public slots:
void updateActionState(PlaylistManager* pm, GraphicsView* gv);
public:
QAction *actionOpen;
QAction *actionActualSize;
QAction *actionToggleMaximize;
QAction *actionZoomIn;
QAction *actionZoomOut;
QAction *actionToggleCheckerboard;
QAction *actionRotateClockwise;
QAction *actionRotateCounterClockwise;
QAction *actionCopyPixmap;
QAction *actionCopyFilePath;
QAction *actionPrevPicture;
QAction *actionNextPicture;
QAction *actionActualSize;
QAction *actionFitToScreen;
QAction *actionFitByWidth;
QAction *actionFitByHeight;
QAction *actionZoomIn;
QAction *actionZoomOut;
QAction *actionFlipHorizontal;
QAction *actionFlipVertical;
QAction *actionRotateClockwise;
QAction *actionRotateCounterClockwise;
QAction *actionToggleAvoidResetTransform;
QAction *actionToggleCheckerboard;
QAction *actionTogglePauseAnimation;
QAction *actionAnimationNextFrame;
QAction *actionHorizontalFlip;
QAction *actionFitInView;
QAction *actionFitByWidth;
QAction *actionCopyPixmap;
QAction *actionCopyFilePath;
QAction *actionPaste;
QAction *actionTrash;
QAction *actionToggleStayOnTop;
QAction *actionToggleProtectMode;
QAction *actionToggleAvoidResetTransform;
QAction *actionSettings;
QAction *actionHelp;
QAction *actionLocateInFileManager;
QAction *actionProperties;
QAction *actionQuitApp;
QAction *actionSettings;
QAction *actionHelp;
};
#endif // ACTIONMANAGER_H
+21 -35
View File
@@ -4,56 +4,42 @@
#include "bottombuttongroup.h"
#include "opacityhelper.h"
#include <functional>
#include <QToolButton>
#include <QVBoxLayout>
#include <QDebug>
BottomButtonGroup::BottomButtonGroup(const std::vector<QAction *> &actionList, QWidget *parent)
BottomButtonGroup::BottomButtonGroup(QWidget *parent)
: QGroupBox (parent)
, m_opacityHelper(new OpacityHelper(this))
{
QHBoxLayout * mainLayout = new QHBoxLayout(this);
mainLayout->setSizeConstraint(QLayout::SetFixedSize);
this->setLayout(mainLayout);
this->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
this->setStyleSheet("BottomButtonGroup {"
"border: 1px solid gray;"
"border-top-left-radius: 10px;"
"border-top-right-radius: 10px;"
"border-style: none;"
"background-color:rgba(0,0,0,120)"
"}"
"QToolButton {"
"background:transparent;"
"}"
"QToolButton:!focus {"
"border-style: none;"
"}");
this->setStyleSheet(R"#(
BottomButtonGroup {
border-style: none;
background: transparent;
}
QToolButton {
background: transparent;
border-style: none;
}
)#");
}
auto newActionBtn = [this](QAction * action) -> QToolButton * {
void BottomButtonGroup::addActions(const std::vector<QAction *> & actionList) {
for (QAction * action : actionList) {
// Create button
QToolButton * btn = new QToolButton(this);
btn->setDefaultAction(action);
btn->setIconSize(QSize(32, 32));
btn->setFixedSize(40, 40);
return btn;
};
for (QAction * action : actionList) {
addButton(newActionBtn(action));
// Add button
layout()->addWidget(btn);
}
}
void BottomButtonGroup::setOpacity(qreal opacity, bool animated)
{
m_opacityHelper->setOpacity(opacity, animated);
}
void BottomButtonGroup::addButton(QAbstractButton *button)
{
layout()->addWidget(button);
updateGeometry();
}
void BottomButtonGroup::addAction(QAction* action) {
addActions({action});
}
+3 -7
View File
@@ -10,18 +10,14 @@
#include <QAbstractButton>
#include <QGroupBox>
class OpacityHelper;
class BottomButtonGroup : public QGroupBox
{
Q_OBJECT
public:
explicit BottomButtonGroup(const std::vector<QAction *> & actionList, QWidget *parent = nullptr);
explicit BottomButtonGroup(QWidget *parent = nullptr);
void setOpacity(qreal opacity, bool animated = true);
void addButton(QAbstractButton *button);
private:
OpacityHelper * m_opacityHelper;
void addActions(const std::vector<QAction *> & actionList);
void addAction(QAction* action);
};
#endif // BOTTOMBUTTONGROUP_H
-147
View File
@@ -1,147 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2023 Tad Young <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "framelesswindow.h"
#include <QMouseEvent>
#include <QHoverEvent>
#include <QApplication>
#include <QVBoxLayout>
#include <QWindow>
FramelessWindow::FramelessWindow(QWidget *parent)
: QWidget(parent)
, m_centralLayout(new QVBoxLayout(this))
, m_oldCursorShape(Qt::ArrowCursor)
, m_oldEdges()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
#else
// There is a bug in Qt 5 that will make pressing Meta+Up cause the app
// fullscreen under Windows, see QTBUG-91226 to learn more.
// The bug seems no longer exists in Qt 6 (I only tested it under Qt 6.3.0).
this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint);
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
this->setMouseTracking(true);
this->setAttribute(Qt::WA_Hover, true);
this->installEventFilter(this);
m_centralLayout->setContentsMargins(QMargins());
}
void FramelessWindow::setCentralWidget(QWidget *widget)
{
if (m_centralWidget) {
m_centralLayout->removeWidget(m_centralWidget);
m_centralWidget->deleteLater();
}
m_centralLayout->addWidget(widget);
m_centralWidget = widget;
}
void FramelessWindow::installResizeCapture(QObject* widget)
{
widget->installEventFilter(this);
}
bool FramelessWindow::eventFilter(QObject* o, QEvent* e)
{
switch (e->type()) {
case QEvent::HoverMove:
{
QWidget* wg = qobject_cast<QWidget*>(o);
if (wg != nullptr)
return mouseHover(static_cast<QHoverEvent*>(e), wg);
break;
}
case QEvent::MouseButtonPress:
return mousePress(static_cast<QMouseEvent*>(e));
}
return QWidget::eventFilter(o, e);
}
bool FramelessWindow::mouseHover(QHoverEvent* event, QWidget* wg)
{
if (!isMaximized() && !isFullScreen()) {
QWindow* win = window()->windowHandle();
Qt::Edges edges = this->getEdgesByPos(wg->mapToGlobal(event->oldPos()), win->frameGeometry());
// backup & restore cursor shape
if (edges && !m_oldEdges)
// entering the edge. backup cursor shape
m_oldCursorShape = win->cursor().shape();
if (!edges && m_oldEdges)
// leaving the edge. restore cursor shape
win->setCursor(m_oldCursorShape);
// save the latest edges status
m_oldEdges = edges;
// show resize cursor shape if cursor is within border
if (edges) {
win->setCursor(this->getCursorByEdge(edges, Qt::ArrowCursor));
return true;
}
}
return false;
}
bool FramelessWindow::mousePress(QMouseEvent* event)
{
if (event->buttons() & Qt::LeftButton && !isMaximized() && !isFullScreen()) {
QWindow* win = window()->windowHandle();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
Qt::Edges edges = this->getEdgesByPos(event->globalPosition().toPoint(), win->frameGeometry());
#else
Qt::Edges edges = this->getEdgesByPos(event->globalPos(), win->frameGeometry());
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (edges) {
win->startSystemResize(edges);
return true;
}
}
return false;
}
Qt::CursorShape FramelessWindow::getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor)
{
if ((edges == (Qt::TopEdge | Qt::LeftEdge)) || (edges == (Qt::RightEdge | Qt::BottomEdge)))
return Qt::SizeFDiagCursor;
else if ((edges == (Qt::TopEdge | Qt::RightEdge)) || (edges == (Qt::LeftEdge | Qt::BottomEdge)))
return Qt::SizeBDiagCursor;
else if (edges & (Qt::TopEdge | Qt::BottomEdge))
return Qt::SizeVerCursor;
else if (edges & (Qt::LeftEdge | Qt::RightEdge))
return Qt::SizeHorCursor;
else
return default_cursor;
}
Qt::Edges FramelessWindow::getEdgesByPos(const QPoint gpos, const QRect& winrect)
{
const int borderWidth = 8;
Qt::Edges edges;
int x = gpos.x() - winrect.x();
int y = gpos.y() - winrect.y();
if (x < borderWidth)
edges |= Qt::LeftEdge;
if (x > (winrect.width() - borderWidth))
edges |= Qt::RightEdge;
if (y < borderWidth)
edges |= Qt::TopEdge;
if (y > (winrect.height() - borderWidth))
edges |= Qt::BottomEdge;
return edges;
}
-39
View File
@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#ifndef FRAMELESSWINDOW_H
#define FRAMELESSWINDOW_H
#include <QWidget>
QT_BEGIN_NAMESPACE
class QVBoxLayout;
QT_END_NAMESPACE
class FramelessWindow : public QWidget
{
Q_OBJECT
public:
explicit FramelessWindow(QWidget *parent = nullptr);
void setCentralWidget(QWidget * widget);
void installResizeCapture(QObject* widget);
protected:
bool eventFilter(QObject *o, QEvent *e) override;
bool mouseHover(QHoverEvent* event, QWidget* wg);
bool mousePress(QMouseEvent* event);
private:
Qt::Edges m_oldEdges;
Qt::CursorShape m_oldCursorShape;
Qt::CursorShape getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor);
Qt::Edges getEdgesByPos(const QPoint pos, const QRect& winrect);
QVBoxLayout * m_centralLayout = nullptr;
QWidget * m_centralWidget = nullptr; // just a pointer, doesn't take the ownership.
};
#endif // FRAMELESSWINDOW_H
+86 -13
View File
@@ -4,6 +4,11 @@
#include "graphicsscene.h"
#include "settings.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QGuiApplication>
#include <QGraphicsSceneMouseEvent>
#include <QMimeData>
#include <QDebug>
@@ -14,6 +19,33 @@
#include <QMovie>
#include <QPainter>
static const QColor& ColorSchemaToTextForeground(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
static QColor WHITE = QColor("White");
return WHITE;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
static QColor BLACK = QColor("Black");
return BLACK;
}
}
class PGraphicsPixmapItem : public QGraphicsPixmapItem
{
public:
@@ -97,7 +129,8 @@ private:
GraphicsScene::GraphicsScene(QObject *parent)
: QGraphicsScene(parent)
{
showText(tr("Drag image here"));
showText(tr("Drag image here", "The same meaning of MainWindow's hint."));
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsScene::updateStyle);
}
GraphicsScene::~GraphicsScene()
@@ -119,7 +152,7 @@ void GraphicsScene::showText(const QString &text)
{
this->clear();
QGraphicsTextItem * textItem = this->addText(text);
textItem->setDefaultTextColor(QColor("White"));
textItem->setDefaultTextColor(ColorSchemaToTextForeground(qGuiApp->styleHints()->colorScheme()));
m_theThing = textItem;
this->setSceneRect(m_theThing->boundingRect());
}
@@ -130,10 +163,9 @@ void GraphicsScene::showSvg(const QString &filepath)
QGraphicsSvgItem * svgItem = new QGraphicsSvgItem();
QSvgRenderer * render = new QSvgRenderer(svgItem);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
// Qt 6.7.0's SVG support is terrible caused by huge memory usage, see QTBUG-124287
// Qt 6.7.1's is somewhat better, memory issue seems fixed, but still laggy when zoom in,
// see QTBUG-126771. Anyway let's disable it for now.
if (Settings::instance()->svgTiny12Only()) {
render->setOptions(QtSvg::Tiny12FeaturesOnly);
}
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
render->load(filepath);
svgItem->setSharedRenderer(render);
@@ -168,28 +200,58 @@ bool GraphicsScene::trySetTransformationMode(Qt::TransformationMode mode, float
return false;
}
/// Fetch graphics item as our custom animated item.
/// Return nullptr means this item is not animated item.
static PGraphicsMovieItem* fetchAnimatedItem(QGraphicsItem *item) {
return qgraphicsitem_cast<PGraphicsMovieItem *>(item);
}
bool GraphicsScene::canPauseAnimation() const {
return fetchAnimatedItem(m_theThing) != nullptr;
}
bool GraphicsScene::isPauseAnimation() const {
auto* animatedItem = fetchAnimatedItem(m_theThing);
if (animatedItem != nullptr) {
return animatedItem->movie()->state() == QMovie::Paused;
} else {
return false;
}
}
bool GraphicsScene::togglePauseAnimation()
{
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
if (animatedItem) {
auto* animatedItem = fetchAnimatedItem(m_theThing);
if (animatedItem != nullptr) {
animatedItem->movie()->setPaused(animatedItem->movie()->state() != QMovie::Paused);
return true;
}
} else {
return false;
}
}
bool GraphicsScene::canSkipAnimationFrame() const {
return isPauseAnimation();
}
bool GraphicsScene::skipAnimationFrame(int delta)
{
PGraphicsMovieItem * animatedItem = qgraphicsitem_cast<PGraphicsMovieItem *>(m_theThing);
if (animatedItem) {
auto* animatedItem = fetchAnimatedItem(m_theThing);
// Only paused animated item can skip frame
if (animatedItem == nullptr) {
return false;
}
if (animatedItem->movie()->state() != QMovie::Paused) {
return false;
}
// Perform skip
const int frameCount = animatedItem->movie()->frameCount();
const int currentFrame = animatedItem->movie()->currentFrameNumber();
const int targetFrame = (currentFrame + delta) % frameCount;
animatedItem->movie()->setPaused(true);
return animatedItem->movie()->jumpToFrame(targetFrame);
}
return false;
}
QPixmap GraphicsScene::renderToPixmap()
{
@@ -205,3 +267,14 @@ QPixmap GraphicsScene::renderToPixmap()
return pixmap;
}
void GraphicsScene::updateStyle(Qt::ColorScheme colorScheme) {
Q_UNUSED(colorScheme)
QGraphicsItem * item = this->m_theThing;
if (item == nullptr) return;
QGraphicsTextItem * textItem = qgraphicsitem_cast<QGraphicsTextItem*>(item);
if (textItem == nullptr) return;
textItem->setDefaultTextColor(ColorSchemaToTextForeground(colorScheme));
}
+15
View File
@@ -21,11 +21,26 @@ public:
bool trySetTransformationMode(Qt::TransformationMode mode, float scaleHint);
/// Check whether current thing is animation.
/// Only animation thing can be paused.
bool canPauseAnimation() const;
/// Check whether we are pasuing an animation graphics.
bool isPauseAnimation() const;
/// Toggle animation graphics pause state.
/// Return true for successfully toggle, otherwise false.
bool togglePauseAnimation();
/// Check whether we can skip animation frame.
/// Only paused animation graphics can skip frame.
bool canSkipAnimationFrame() const;
/// Skip paused animation graphics with given frame.
/// Return true for successfully skip, otherwise false.
bool skipAnimationFrame(int delta = 1);
QPixmap renderToPixmap();
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
QGraphicsItem * m_theThing = nullptr;
};
+172 -22
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
@@ -7,7 +7,10 @@
#include "graphicsscene.h"
#include "settings.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QDebug>
#include <QColorSpace>
#include <QMouseEvent>
#include <QScrollBar>
#include <QMimeData>
@@ -22,11 +25,12 @@ GraphicsView::GraphicsView(QWidget *parent)
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setResizeAnchor(QGraphicsView::AnchorUnderMouse);
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
setStyleSheet("background-color: rgba(0, 0, 0, 220);"
"border-radius: 3px;");
setAcceptDrops(false);
setCheckerboardEnabled(false);
updateStyle(qGuiApp->styleHints()->colorScheme());
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsView::updateStyle);
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &GraphicsView::viewportRectChanged);
}
@@ -41,9 +45,7 @@ void GraphicsView::showFileFromPath(const QString &filePath)
QImageReader imageReader(filePath);
imageReader.setAutoTransform(true);
imageReader.setDecideFormatFromContent(true);
#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
imageReader.setAllocationLimit(0);
#endif //QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
// Since if the image format / plugin does not support this feature, imageFormat() will returns an invalid format.
// So we cannot use imageFormat() and check if it returns QImage::Format_Invalid to detect if we support the file.
@@ -55,6 +57,18 @@ void GraphicsView::showFileFromPath(const QString &filePath)
} else if (!imageReader.canRead()) {
showText(tr("Image data is invalid or currently unsupported"));
} else {
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
QImage && img = imageReader.read();
if (img.isNull()) {
showText(tr("Image data is invalid or currently unsupported"));
} else {
if (img.format() == QImage::Format_CMYK8888) {
img.convertToColorSpace(QColorSpace::SRgb);
}
img.setDevicePixelRatio(devicePixelRatioF());
showImage(img);
}
#else
QPixmap && pixmap = QPixmap::fromImageReader(&imageReader);
if (pixmap.isNull()) {
showText(tr("Image data is invalid or currently unsupported"));
@@ -62,6 +76,7 @@ void GraphicsView::showFileFromPath(const QString &filePath)
pixmap.setDevicePixelRatio(devicePixelRatioF());
showImage(pixmap);
}
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
}
}
}
@@ -118,14 +133,14 @@ qreal GraphicsView::scaleFactor() const
void GraphicsView::resetTransform()
{
if (!m_avoidResetTransform) {
if (!shouldAvoidTransform()) {
QGraphicsView::resetTransform();
}
}
void GraphicsView::zoomView(qreal scaleFactor)
{
m_enableFitInView = false;
m_enableFitToScreen = false;
scale(scaleFactor, scaleFactor);
applyTransformationModeByScaleFactor();
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
@@ -142,6 +157,10 @@ void GraphicsView::rotateView(bool clockwise)
0, 0, 1);
tf = transform() * tf;
setTransform(tf);
// Apply transformation mode but don't emit navigator signal here
// Let displayScene() handle the navigator visibility correctly
applyTransformationModeByScaleFactor();
}
void GraphicsView::flipView(bool horizontal)
@@ -173,22 +192,104 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
{
resetScale();
QRectF viewRect = this->viewport()->rect().adjusted(2, 2, -2, -2);
QRectF viewRect = this->viewport()->rect();
QRectF imageRect = transform().mapRect(sceneRect());
QSize viewSize = viewRect.size().toSize();
qreal ratio;
if (ori == Qt::Horizontal) {
// Horizontal fit means fit by width
if (scaleDownOnly && imageRect.width() <= viewSize.width()) {
// Image width already fits, no scaling needed
ratio = 1;
} else {
ratio = viewRect.width() / imageRect.width();
}
} else {
// Vertical fit means fit by height
if (scaleDownOnly && imageRect.height() <= viewSize.height()) {
// Image height already fits, no scaling needed
ratio = 1;
} else {
ratio = viewRect.height() / imageRect.height();
}
}
if (scaleDownOnly && ratio > 1) ratio = 1;
if (ratio != 1) {
scale(ratio, ratio);
centerOn(imageRect.top(), 0);
m_enableFitInView = false;
}
// Position the image correctly based on orientation with rotation consideration
QRectF originalScene = sceneRect();
QTransform currentTransform = transform();
// TODO:
// I guess following code may be optimized by matrix arithmetic operation.
// There is an AI given solution. But I didn't use it.
// Because I want to keep the same behavior of upstream.
// // Horizontal fit: Let the position of top center of viewport, (cx, 0), be inversely mapped to scene.
// // And compensate the behavior of `centerOn` which put given point to the center of viewport.
// QTransform inv = currentTransform.inverted();
// QPointF viewCx = viewport()->rect().center().x();
// QPointF viewCy = viewport()->rect().center().y();
// // The target is viewport top center. Its y=0, so `centerOn` required scene point is (viewCx, -viewCy).
// centerOn(inv.map(QPointF(viewCx, -viewCy)));
if (ori == Qt::Horizontal) {
// For horizontal fit (fit by width), position at top (for tall images)
// Find the scene point that corresponds to the top-center of the transformed image
QPointF sceneTopCenter;
if (qFuzzyIsNull(currentTransform.m12()) && qFuzzyIsNull(currentTransform.m21())) {
// 0° or 180° rotation
if (currentTransform.m11() > 0 && currentTransform.m22() > 0) {
// 0° rotation: use original top-center
sceneTopCenter = QPointF(originalScene.center().x(), originalScene.top());
} else {
// 180° rotation: the visual "top" is now at the scene bottom
sceneTopCenter = QPointF(originalScene.center().x(), originalScene.bottom());
}
} else {
// 90/270 degree rotation: the "top" in view corresponds to left/right in scene
if (currentTransform.m12() > 0) {
// 90 degree: top in view = left in scene
sceneTopCenter = QPointF(originalScene.left(), originalScene.center().y());
} else {
// 270 degree: top in view = right in scene
sceneTopCenter = QPointF(originalScene.right(), originalScene.center().y());
}
}
centerOn(sceneTopCenter);
} else {
// For vertical fit (fit by height), position at left (for wide images)
// Find the scene point that corresponds to the left-center of the transformed image
QPointF sceneLeftCenter;
if (qFuzzyIsNull(currentTransform.m12()) && qFuzzyIsNull(currentTransform.m21())) {
// 0° or 180° rotation
if (currentTransform.m11() > 0 && currentTransform.m22() > 0) {
// 0° rotation: use original left-center
sceneLeftCenter = QPointF(originalScene.left(), originalScene.center().y());
} else {
// 180° rotation: the visual "left" is now at the scene right
sceneLeftCenter = QPointF(originalScene.right(), originalScene.center().y());
}
} else {
// 90/270 degree rotation: the "left" in view corresponds to top/bottom in scene
if (currentTransform.m21() > 0) {
// 90 degree: left in view = top in scene
sceneLeftCenter = QPointF(originalScene.center().x(), originalScene.top());
} else {
// 270 degree: left in view = bottom in scene
sceneLeftCenter = QPointF(originalScene.center().x(), originalScene.bottom());
}
}
centerOn(sceneLeftCenter);
}
m_enableFitToScreen = false;
applyTransformationModeByScaleFactor();
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
@@ -196,16 +297,23 @@ void GraphicsView::fitByOrientation(Qt::Orientation ori, bool scaleDownOnly)
void GraphicsView::displayScene()
{
if (m_avoidResetTransform) {
if (shouldAvoidTransform()) {
emit navigatorViewRequired(!isThingSmallerThanWindowWith(transform()), transform());
return;
}
if (isSceneBiggerThanView()) {
// Do fit-in-view
fitInView(sceneRect(), Qt::KeepAspectRatio);
// After fitInView, the image should fit the window, so hide navigator
emit navigatorViewRequired(false, transform());
} else {
// Image is already smaller than window, no navigator needed
emit navigatorViewRequired(false, transform());
}
m_enableFitInView = true;
m_enableFitToScreen = true;
m_firstUserMediaLoaded = true;
}
bool GraphicsView::isSceneBiggerThanView() const
@@ -218,12 +326,12 @@ bool GraphicsView::isSceneBiggerThanView() const
}
// Automately do fit in view when viewport(window) smaller than image original size.
void GraphicsView::setEnableAutoFitInView(bool enable)
void GraphicsView::setEnableAutoFitToScreen(bool enable)
{
m_enableFitInView = enable;
m_enableFitToScreen = enable;
}
bool GraphicsView::avoidResetTransform() const
bool GraphicsView::isAvoidResetTransform() const
{
return m_avoidResetTransform;
}
@@ -233,6 +341,15 @@ void GraphicsView::setAvoidResetTransform(bool avoidReset)
m_avoidResetTransform = avoidReset;
}
bool GraphicsView::isCheckerboard() const {
return m_checkerboardEnabled;
}
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
{
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
}
inline double zeroOrOne(double number)
{
return qFuzzyIsNull(number) ? 0 : (number > 0 ? 1 : -1);
@@ -247,9 +364,37 @@ QTransform GraphicsView::resetScale(const QTransform & orig)
orig.dx(), orig.dy());
}
void GraphicsView::toggleCheckerboard(bool invertCheckerboardColor)
{
setCheckerboardEnabled(!m_checkerboardEnabled, invertCheckerboardColor);
void GraphicsView::updateStyle(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
setStyleSheet(R"#(
border-style: none;
background-color: black;
)#");
break;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
setStyleSheet(R"#(
border-style: none;
background-color: white;
)#");
break;
}
}
void GraphicsView::mousePressEvent(QMouseEvent *event)
@@ -295,11 +440,11 @@ void GraphicsView::wheelEvent(QWheelEvent *event)
void GraphicsView::resizeEvent(QResizeEvent *event)
{
if (m_enableFitInView) {
if (m_enableFitToScreen) {
bool originalSizeSmallerThanWindow = isThingSmallerThanWindowWith(resetScale(transform()));
if (originalSizeSmallerThanWindow && scaleFactor() >= 1) {
// no longer need to do fitInView()
// but we leave the m_enableFitInView value unchanged in case
// but we leave the m_enableToScreen value unchanged in case
// user resize down the window again.
} else if (originalSizeSmallerThanWindow && scaleFactor() < 1) {
resetScale();
@@ -365,3 +510,8 @@ void GraphicsView::applyTransformationModeByScaleFactor()
scene()->trySetTransformationMode(Qt::FastTransformation, this->scaleFactor());
}
}
bool GraphicsView::shouldAvoidTransform() const
{
return m_firstUserMediaLoaded && m_avoidResetTransform;
}
+11 -5
View File
@@ -38,19 +38,22 @@ public:
void displayScene();
bool isSceneBiggerThanView() const;
void setEnableAutoFitInView(bool enable = true);
void setEnableAutoFitToScreen(bool enable = true);
bool avoidResetTransform() const;
bool isAvoidResetTransform() const;
void setAvoidResetTransform(bool avoidReset);
bool isCheckerboard() const;
void toggleCheckerboard(bool invertCheckerboardColor = false);
static QTransform resetScale(const QTransform & orig);
signals:
void navigatorViewRequired(bool required, QTransform transform);
void viewportRectChanged();
public slots:
void toggleCheckerboard(bool invertCheckerboardColor = false);
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
void mousePressEvent(QMouseEvent * event) override;
@@ -64,13 +67,16 @@ private:
void setCheckerboardEnabled(bool enabled, bool invertColor = false);
void applyTransformationModeByScaleFactor();
inline bool shouldAvoidTransform() const;
// Consider switch to 3 state for "no fit", "always fit" and "fit when view is smaller"?
// ... or even more? e.g. "fit/snap width" things...
// Currently it's "no fit" when it's false and "fit when view is smaller" when it's true.
bool m_enableFitInView = false;
bool m_enableFitToScreen = false;
bool m_avoidResetTransform = false;
bool m_checkerboardEnabled = false;
bool m_useLightCheckerboard = false;
bool m_firstUserMediaLoaded = false;
};
#endif // GRAPHICSVIEW_H
+25 -16
View File
@@ -4,7 +4,6 @@
#include "mainwindow.h"
#include "playlistmanager.h"
#include "settings.h"
#ifdef Q_OS_MACOS
@@ -17,33 +16,32 @@
#include <QTranslator>
#include <QUrl>
using namespace Qt::Literals::StringLiterals;
int main(int argc, char *argv[])
{
QCoreApplication::setApplicationName("Pineapple Pictures");
QCoreApplication::setApplicationName(u"Pineapple Pictures"_s);
QCoreApplication::setApplicationVersion(PPIC_VERSION_STRING);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Settings::instance()->hiDpiScaleFactorBehavior());
QApplication a(argc, argv);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
a.setAttribute(Qt::ApplicationAttribute::AA_UseHighDpiPixmaps);
#endif
QTranslator translator;
#if defined(TRANSLATION_RESOURCE_EMBEDDING)
const QString qmDir = QLatin1String(":/i18n/");
const QString qmDir = u":/i18n/"_s;
#elif defined(QM_FILE_INSTALL_ABSOLUTE_DIR)
const QString qmDir = QT_STRINGIFY(QM_FILE_INSTALL_ABSOLUTE_DIR);
#else
const QString qmDir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath("translations");
#endif
if (translator.load(QLocale(), QLatin1String("PineapplePictures"), QLatin1String("_"), qmDir)) {
if (translator.load(QLocale(), u"PineapplePictures"_s, u"_"_s, qmDir)) {
QCoreApplication::installTranslator(&translator);
}
QGuiApplication::setApplicationDisplayName(QCoreApplication::translate("main", "Pineapple Pictures"));
// commandline options
QCommandLineOption supportedImageFormats(QStringLiteral("supported-image-formats"), QCoreApplication::translate("main", "List supported image format suffixes, and quit program."));
QCommandLineOption supportedImageFormats(u"supported-image-formats"_s, QCoreApplication::translate("main", "List supported image format suffixes, and quit program."));
// parse commandline arguments
QCommandLineParser parser;
parser.addOption(supportedImageFormats);
@@ -52,8 +50,13 @@ int main(int argc, char *argv[])
parser.process(a);
if (parser.isSet(supportedImageFormats)) {
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
fputs(qPrintable(MainWindow::supportedImageFormats().join(QChar('\n'))), stdout);
::exit(EXIT_SUCCESS);
#else
QCommandLineParser::showMessageAndExit(QCommandLineParser::MessageType::Information,
MainWindow::supportedImageFormats().join(QChar('\n')));
#endif
}
MainWindow w;
@@ -64,22 +67,28 @@ int main(int argc, char *argv[])
a.installEventFilter(fileOpenEventHandler);
a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){
if (w.isHidden()) {
w.setWindowOpacity(1);
w.showNormal();
} else {
w.activateWindow();
}
w.showUrls({url});
w.showFiles({url});
w.initWindowSize();
});
// Handle dock icon clicks to show hidden window
a.connect(&a, &QApplication::applicationStateChanged, [&w](Qt::ApplicationState state) {
if (state == Qt::ApplicationActive && w.isHidden()) {
w.showFiles({});
w.galleryCurrent(false);
w.showNormal();
w.raise();
w.activateWindow();
}
});
#endif // Q_OS_MACOS
QStringList urlStrList = parser.positionalArguments();
QList<QUrl> && urlList = PlaylistManager::convertToUrlList(urlStrList);
if (!urlList.isEmpty()) {
w.showUrls(urlList);
}
QStringList fileList = parser.positionalArguments();
w.showFiles(fileList);
w.initWindowSize();
+273 -597
View File
File diff suppressed because it is too large Load Diff
+37 -72
View File
@@ -1,34 +1,23 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "framelesswindow.h"
#include <QParallelAnimationGroup>
#include <QPropertyAnimation>
#include <QPushButton>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
typedef QEnterEvent QT_ENTER_EVENT;
#else
typedef QEvent QT_ENTER_EVENT;
#endif // QT_VERSION_CHECK(6, 0, 0)
#include <QWidget>
QT_BEGIN_NAMESPACE
class QGraphicsOpacityEffect;
class QGraphicsView;
class QFileSystemWatcher;
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class ActionManager;
class PlaylistManager;
class ToolButton;
class GraphicsView;
class NavigatorView;
class BottomButtonGroup;
class MainWindow : public FramelessWindow
class MainWindow : public QWidget
{
Q_OBJECT
@@ -36,42 +25,26 @@ public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
void showUrls(const QList<QUrl> &urls);
void showFiles(const QStringList& files);
void showFiles(const QList<QUrl>& urls);
void initWindowSize();
void adjustWindowSizeBySceneRect();
QUrl currentImageFileUrl() const;
void clearGallery();
void galleryPrev();
void galleryNext();
void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage);
void galleryCurrent(bool fromFileWatcher);
static QStringList supportedImageFormats();
protected slots:
void showEvent(QShowEvent *event) override;
void enterEvent(QT_ENTER_EVENT *event) override;
void leaveEvent(QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void centerWindow();
void closeWindow();
void updateWidgetsPosition();
void toggleProtectedMode();
void toggleStayOnTop();
void toggleAvoidResetTransform();
bool stayOnTop() const;
bool canPaste() const;
void quitAppAction(bool force = false);
void toggleFullscreen();
void toggleMaximize();
@@ -79,54 +52,46 @@ protected:
QSize sizeHint() const override;
private slots:
void on_actionOpen_triggered();
void on_actionActualSize_triggered();
void on_actionToggleMaximize_triggered();
void on_actionZoomIn_triggered();
void on_actionZoomOut_triggered();
void on_actionToggleCheckerboard_triggered();
void on_actionRotateClockwise_triggered();
void on_actionRotateCounterClockwise_triggered();
void on_actionCopyPixmap_triggered();
void on_actionCopyFilePath_triggered();
void on_actionPrevPicture_triggered();
void on_actionNextPicture_triggered();
void on_actionActualSize_triggered();
void on_actionFitToScreen_triggered();
void on_actionFitByWidth_triggered();
void on_actionFitByHeight_triggered();
void on_actionZoomIn_triggered();
void on_actionZoomOut_triggered();
void on_actionFlipHorizontal_triggered();
void on_actionFlipVertical_triggered();
void on_actionRotateClockwise_triggered();
void on_actionRotateCounterClockwise_triggered();
void on_actionToggleAvoidResetTransform_triggered();
void on_actionToggleCheckerboard_triggered();
void on_actionTogglePauseAnimation_triggered();
void on_actionAnimationNextFrame_triggered();
void on_actionHorizontalFlip_triggered();
void on_actionFitInView_triggered();
void on_actionFitByWidth_triggered();
void on_actionCopyPixmap_triggered();
void on_actionCopyFilePath_triggered();
void on_actionPaste_triggered();
void on_actionTrash_triggered();
void on_actionToggleStayOnTop_triggered();
void on_actionToggleProtectMode_triggered();
void on_actionToggleAvoidResetTransform_triggered();
void on_actionLocateInFileManager_triggered();
void on_actionProperties_triggered();
void on_actionSettings_triggered();
void on_actionHelp_triggered();
void on_actionProperties_triggered();
void on_actionLocateInFileManager_triggered();
void on_actionQuitApp_triggered();
private:
bool updateFileWatcher(const QString & basePath = QString());
void updateActionState();
private:
QT_PREPEND_NAMESPACE(Ui::MainWindow) *ui;
ActionManager *m_am;
PlaylistManager *m_pm;
QPoint m_oldMousePos;
QPropertyAnimation *m_fadeOutAnimation;
QPropertyAnimation *m_floatUpAnimation;
QParallelAnimationGroup *m_exitAnimationGroup;
ToolButton *m_closeButton;
ToolButton *m_prevButton;
ToolButton *m_nextButton;
GraphicsView *m_graphicsView;
NavigatorView *m_gv;
BottomButtonGroup *m_bottomButtonGroup;
bool m_protectedMode = false;
bool m_clickedOnWindow = false;
QFileSystemWatcher *m_fileSystemWatcher;
};
#endif // MAINWINDOW_H
+172
View File
@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QWidget" name="MainWindow">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="minimumSize">
<size>
<width>640</width>
<height>480</height>
</size>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="m_centralLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="GraphicsView" name="m_gv">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="bottomBar" native="true">
<property name="minimumSize">
<size>
<width>0</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>72</height>
</size>
</property>
<layout class="QHBoxLayout" name="bottomBarLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="m_navPH" native="true">
<property name="minimumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="leftSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="BottomButtonGroup" name="m_bottomButtonGroup"/>
</item>
<item>
<spacer name="rightSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</spacer>
</item>
<item>
<widget class="NavigatorView" name="m_nav">
<property name="minimumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>99</width>
<height>72</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
<widget class="QMenu" name="m_subMenuCopy">
<property name="title">
<string>&amp;Copy</string>
</property>
<property name="icon">
<iconset theme="edit-copy"/>
</property>
</widget>
<widget class="QMenu" name="m_subMenuTransform">
<property name="title">
<string>&amp;Transform</string>
</property>
</widget>
<widget class="QMenu" name="m_menu"/>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsView</class>
<extends>QGraphicsView</extends>
<header>graphicsview.h</header>
</customwidget>
<customwidget>
<class>NavigatorView</class>
<extends>QGraphicsView</extends>
<header>navigatorview.h</header>
</customwidget>
<customwidget>
<class>BottomButtonGroup</class>
<extends>QGroupBox</extends>
<header>bottombuttongroup.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+18 -15
View File
@@ -3,12 +3,12 @@
// SPDX-License-Identifier: MIT
#include "metadatadialog.h"
#include "ui_metadatadialog.h"
#include <QDialogButtonBox>
#include <QPainter>
#include <QStyledItemDelegate>
#include <QTreeView>
#include <QVBoxLayout>
#include <QHeaderView>
#include "metadatamodel.h"
@@ -73,30 +73,33 @@ protected:
};
MetadataDialog::MetadataDialog(QWidget *parent)
: QDialog(parent)
, m_treeView(new PropertyTreeView(this))
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::MetadataDialog))
{
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
// YYC MARK:
// Because we use custom QTreeView and we do not want to expose it to public domain.
// So I have to use this way as the compromise.
// Create our custom QTreeView and insert them
m_treeView = new PropertyTreeView(this);
auto* layout = ui->verticalLayout;
const int index = layout->indexOf(ui->m_buttonBox);
layout->insertWidget(index, m_treeView);
// Setup our custom QTreeView
m_treeView->setRootIsDecorated(false);
m_treeView->setIndentation(0);
m_treeView->setItemDelegate(new PropertyTreeItemDelegate(m_treeView));
m_treeView->header()->resizeSection(0, sizeHint().width() / 2);
setWindowTitle(tr("Image Metadata"));
QDialogButtonBox * buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
setLayout(new QVBoxLayout);
layout()->addWidget(m_treeView);
layout()->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
connect(ui->m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
}
MetadataDialog::~MetadataDialog()
{
delete ui;
}
void MetadataDialog::setMetadataModel(MetadataModel * model)
+7
View File
@@ -11,6 +11,12 @@ QT_BEGIN_NAMESPACE
class QTreeView;
QT_END_NAMESPACE
QT_BEGIN_NAMESPACE
namespace Ui {
class MetadataDialog;
}
QT_END_NAMESPACE
class MetadataModel;
class MetadataDialog : public QDialog
{
@@ -25,6 +31,7 @@ public:
private:
QTreeView * m_treeView = nullptr;
QT_PREPEND_NAMESPACE(Ui::MetadataDialog) *ui;
};
#endif // METADATADIALOG_H
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MetadataDialog</class>
<widget class="QDialog" name="MetadataDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="windowTitle">
<string>Image Metadata</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QDialogButtonBox" name="m_buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+97 -95
View File
@@ -11,6 +11,8 @@
#include <QFileInfo>
#include <QImageReader>
using namespace Qt::Literals::StringLiterals;
MetadataModel::MetadataModel(QObject *parent)
: QAbstractItemModel(parent)
{
@@ -37,131 +39,131 @@ void MetadataModel::setFile(const QString &imageFilePath)
const QString & imageDimensionsString = imageSize(imgReader.size());
const QString & imageRatioString = imageSizeRatio(imgReader.size());
appendSection(QStringLiteral("Description"), tr("Description", "Section name."));
appendSection(QStringLiteral("Origin"), tr("Origin", "Section name."));
appendSection(QStringLiteral("Image"), tr("Image", "Section name."));
appendSection(QStringLiteral("Camera"), tr("Camera", "Section name."));
appendSection(QStringLiteral("AdvancedPhoto"), tr("Advanced photo", "Section name."));
appendSection(QStringLiteral("GPS"), tr("GPS", "Section name."));
appendSection(QStringLiteral("File"), tr("File", "Section name."));
appendSection(u"Description"_s, tr("Description", "Section name."));
appendSection(u"Origin"_s, tr("Origin", "Section name."));
appendSection(u"Image"_s, tr("Image", "Section name."));
appendSection(u"Camera"_s, tr("Camera", "Section name."));
appendSection(u"AdvancedPhoto"_s, tr("Advanced photo", "Section name."));
appendSection(u"GPS"_s, tr("GPS", "Section name."));
appendSection(u"File"_s, tr("File", "Section name."));
if (imgReader.supportsOption(QImageIOHandler::Size)) {
appendProperty(QStringLiteral("Image"), QStringLiteral("Image.Dimensions"),
appendProperty(u"Image"_s, u"Image.Dimensions"_s,
tr("Dimensions"), imageDimensionsString);
appendProperty(QStringLiteral("Image"), QStringLiteral("Image.SizeRatio"),
appendProperty(u"Image"_s, u"Image.SizeRatio"_s,
tr("Aspect ratio"), imageRatioString);
}
if (imgReader.supportsAnimation() && imgReader.imageCount() > 1) {
appendProperty(QStringLiteral("Image"), QStringLiteral("Image.FrameCount"),
appendProperty(u"Image"_s, u"Image.FrameCount"_s,
tr("Frame count"), QString::number(imgReader.imageCount()));
}
appendProperty(QStringLiteral("File"), QStringLiteral("File.Name"),
appendProperty(u"File"_s, u"File.Name"_s,
tr("Name"), fileInfo.fileName());
appendProperty(QStringLiteral("File"), QStringLiteral("File.ItemType"),
appendProperty(u"File"_s, u"File.ItemType"_s,
tr("Item type"), itemTypeString);
appendProperty(QStringLiteral("File"), QStringLiteral("File.Path"),
appendProperty(u"File"_s, u"File.Path"_s,
tr("Folder path"), QDir::toNativeSeparators(fileInfo.path()));
appendProperty(QStringLiteral("File"), QStringLiteral("File.Size"),
appendProperty(u"File"_s, u"File.Size"_s,
tr("Size"), sizeString);
appendProperty(QStringLiteral("File"), QStringLiteral("File.CreatedTime"),
appendProperty(u"File"_s, u"File.CreatedTime"_s,
tr("Date created"), birthTimeString);
appendProperty(QStringLiteral("File"), QStringLiteral("File.LastModified"),
appendProperty(u"File"_s, u"File.LastModified"_s,
tr("Date modified"), lastModifiedTimeString);
Exiv2Wrapper wrapper;
if (wrapper.load(imageFilePath)) {
wrapper.cacheSections();
appendExivPropertyIfExist(wrapper, QStringLiteral("Description"),
QStringLiteral("Xmp.dc.title"), tr("Title"), true);
appendExivPropertyIfExist(wrapper, QStringLiteral("Description"),
QStringLiteral("Exif.Image.ImageDescription"), tr("Subject"), true);
appendExivPropertyIfExist(wrapper, QStringLiteral("Description"),
QStringLiteral("Exif.Image.Rating"), tr("Rating"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Description"),
QStringLiteral("Xmp.dc.subject"), tr("Tags"));
appendPropertyIfNotEmpty(QStringLiteral("Description"), QStringLiteral("Description.Comments"),
appendExivPropertyIfExist(wrapper, u"Description"_s,
u"Xmp.dc.title"_s, tr("Title"), true);
appendExivPropertyIfExist(wrapper, u"Description"_s,
u"Exif.Image.ImageDescription"_s, tr("Subject"), true);
appendExivPropertyIfExist(wrapper, u"Description"_s,
u"Exif.Image.Rating"_s, tr("Rating"));
appendExivPropertyIfExist(wrapper, u"Description"_s,
u"Xmp.dc.subject"_s, tr("Tags"));
appendPropertyIfNotEmpty(u"Description"_s, u"Description.Comments"_s,
tr("Comments"), wrapper.comment());
appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"),
QStringLiteral("Exif.Image.Artist"), tr("Authors"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"),
QStringLiteral("Exif.Photo.DateTimeOriginal"), tr("Date taken"));
appendExivPropertyIfExist(wrapper, u"Origin"_s,
u"Exif.Image.Artist"_s, tr("Authors"));
appendExivPropertyIfExist(wrapper, u"Origin"_s,
u"Exif.Photo.DateTimeOriginal"_s, tr("Date taken"));
// FIXME: We may fetch the same type of metadata from different metadata collection.
// Current implementation is not pretty and may need to do a rework...
// appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"),
// QStringLiteral("Xmp.xmp.CreatorTool"), tr("Program name"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"),
QStringLiteral("Exif.Image.Software"), tr("Program name"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Origin"),
QStringLiteral("Exif.Image.Copyright"), tr("Copyright"));
appendExivPropertyIfExist(wrapper, u"Origin"_s,
u"Exif.Image.Software"_s, tr("Program name"));
appendExivPropertyIfExist(wrapper, u"Origin"_s,
u"Exif.Image.Copyright"_s, tr("Copyright"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Image"),
QStringLiteral("Exif.Image.XResolution"), tr("Horizontal resolution"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Image"),
QStringLiteral("Exif.Image.YResolution"), tr("Vertical resolution"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Image"),
QStringLiteral("Exif.Image.ResolutionUnit"), tr("Resolution unit"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Image"),
QStringLiteral("Exif.Photo.ColorSpace"), tr("Colour representation"));
appendExivPropertyIfExist(wrapper, u"Image"_s,
u"Exif.Image.XResolution"_s, tr("Horizontal resolution"));
appendExivPropertyIfExist(wrapper, u"Image"_s,
u"Exif.Image.YResolution"_s, tr("Vertical resolution"));
appendExivPropertyIfExist(wrapper, u"Image"_s,
u"Exif.Image.ResolutionUnit"_s, tr("Resolution unit"));
appendExivPropertyIfExist(wrapper, u"Image"_s,
u"Exif.Photo.ColorSpace"_s, tr("Colour representation"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Image.Make"), tr("Camera maker"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Image.Model"), tr("Camera model"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.FNumber"), tr("F-stop"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.ExposureTime"), tr("Exposure time"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.ISOSpeedRatings"), tr("ISO speed"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.ExposureBiasValue"), tr("Exposure bias"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.FocalLength"), tr("Focal length"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.MaxApertureValue"), tr("Max aperture"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.MeteringMode"), tr("Metering mode"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.SubjectDistance"), tr("Subject distance"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.Flash"), tr("Flash mode"));
appendExivPropertyIfExist(wrapper, QStringLiteral("Camera"),
QStringLiteral("Exif.Photo.FocalLengthIn35mmFilm"), tr("35mm focal length"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Image.Make"_s, tr("Camera maker"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Image.Model"_s, tr("Camera model"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.FNumber"_s, tr("F-stop"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.ExposureTime"_s, tr("Exposure time"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.ISOSpeedRatings"_s, tr("ISO speed"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.ExposureBiasValue"_s, tr("Exposure bias"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.FocalLength"_s, tr("Focal length"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.MaxApertureValue"_s, tr("Max aperture"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.MeteringMode"_s, tr("Metering mode"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.SubjectDistance"_s, tr("Subject distance"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.Flash"_s, tr("Flash mode"));
appendExivPropertyIfExist(wrapper, u"Camera"_s,
u"Exif.Photo.FocalLengthIn35mmFilm"_s, tr("35mm focal length"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.LensModel"), tr("Lens model"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.Contrast"), tr("Contrast"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.BrightnessValue"), tr("Brightness"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.ExposureProgram"), tr("Exposure program"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.Saturation"), tr("Saturation"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.Sharpness"), tr("Sharpness"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.WhiteBalance"), tr("White balance"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.DigitalZoomRatio"), tr("Digital zoom"));
appendExivPropertyIfExist(wrapper, QStringLiteral("AdvancedPhoto"),
QStringLiteral("Exif.Photo.ExifVersion"), tr("EXIF version"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.LensModel"_s, tr("Lens model"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.Contrast"_s, tr("Contrast"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.BrightnessValue"_s, tr("Brightness"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.ExposureProgram"_s, tr("Exposure program"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.Saturation"_s, tr("Saturation"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.Sharpness"_s, tr("Sharpness"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.WhiteBalance"_s, tr("White balance"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.DigitalZoomRatio"_s, tr("Digital zoom"));
appendExivPropertyIfExist(wrapper, u"AdvancedPhoto"_s,
u"Exif.Photo.ExifVersion"_s, tr("EXIF version"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSLatitudeRef"), tr("Latitude reference"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSLatitude"), tr("Latitude"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSLongitudeRef"), tr("Longitude reference"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSLongitude"), tr("Longitude"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSAltitudeRef"), tr("Altitude reference"));
appendExivPropertyIfExist(wrapper, QStringLiteral("GPS"),
QStringLiteral("Exif.GPSInfo.GPSAltitude"), tr("Altitude"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSLatitudeRef"_s, tr("Latitude reference"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSLatitude"_s, tr("Latitude"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSLongitudeRef"_s, tr("Longitude reference"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSLongitude"_s, tr("Longitude"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSAltitudeRef"_s, tr("Altitude reference"));
appendExivPropertyIfExist(wrapper, u"GPS"_s,
u"Exif.GPSInfo.GPSAltitude"_s, tr("Altitude"));
}
}
+43 -9
View File
@@ -5,20 +5,22 @@
#include "navigatorview.h"
#include "graphicsview.h"
#include "opacityhelper.h"
#include <QStyleHints>
#include <QOperatingSystemVersion>
#include <QMouseEvent>
#include <QDebug>
#include <QTimer>
NavigatorView::NavigatorView(QWidget *parent)
: QGraphicsView (parent)
, m_viewportRegion(this->rect())
, m_opacityHelper(new OpacityHelper(this))
{
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setStyleSheet("background-color: rgba(0, 0, 0, 120);"
"border-radius: 3px;");
updateStyle(qGuiApp->styleHints()->colorScheme());
connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &NavigatorView::updateStyle);
}
// doesn't take or manage its ownership
@@ -27,17 +29,49 @@ void NavigatorView::setMainView(GraphicsView *mainView)
m_mainView = mainView;
}
void NavigatorView::setOpacity(qreal opacity, bool animated)
{
m_opacityHelper->setOpacity(opacity, animated);
}
void NavigatorView::updateMainViewportRegion()
{
// Use QTimer::singleShot with lambda to delay the update
// This ensures all geometry updates are complete before calculating viewport region
QTimer::singleShot(0, [this]() {
if (m_mainView != nullptr) {
m_viewportRegion = mapFromScene(m_mainView->mapToScene(m_mainView->rect()));
update();
}
});
}
void NavigatorView::updateStyle(Qt::ColorScheme colorScheme) {
// YYC MARK:
// If we are using Windows 10 and lower system or Qt which do not support "Windows 11"
// theme (lower than Qt 6.7), we should only support light mode.
// Because Qt's dark-mode-aware "Windows 11" theme is only well-supported on Windows 11.
#if defined(Q_OS_WIN)
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
colorScheme = Qt::ColorScheme::Light;
#else
if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11) {
colorScheme = Qt::ColorScheme::Light;
}
#endif // QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
#endif // defined(Q_OS_WIN)
switch (colorScheme) {
case Qt::ColorScheme::Dark:
setStyleSheet(R"#(
border-style: none;
background-color: rgba(255, 255, 255, 70);
)#");
break;
case Qt::ColorScheme::Light:
case Qt::ColorScheme::Unknown:
default:
setStyleSheet(R"#(
border-style: none;
background-color: rgba(0, 0, 0, 70);
)#");
break;
}
}
void NavigatorView::mousePressEvent(QMouseEvent *event)
+3 -3
View File
@@ -7,7 +7,6 @@
#include <QGraphicsView>
class OpacityHelper;
class GraphicsView;
class NavigatorView : public QGraphicsView
{
@@ -16,11 +15,13 @@ public:
NavigatorView(QWidget *parent = nullptr);
void setMainView(GraphicsView *mainView);
void setOpacity(qreal opacity, bool animated = true);
public slots:
void updateMainViewportRegion();
private slots:
void updateStyle(Qt::ColorScheme colorScheme);
private:
void mousePressEvent(QMouseEvent * event) override;
void mouseMoveEvent(QMouseEvent * event) override;
@@ -32,7 +33,6 @@ private:
bool m_mouseDown = false;
QPolygon m_viewportRegion;
QGraphicsView *m_mainView = nullptr;
OpacityHelper *m_opacityHelper = nullptr;
};
#endif // NAVIGATORVIEW_H
-31
View File
@@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#include "opacityhelper.h"
#include <QGraphicsOpacityEffect>
#include <QPropertyAnimation>
OpacityHelper::OpacityHelper(QWidget *parent)
: QObject(parent)
, m_opacityFx(new QGraphicsOpacityEffect(parent))
, m_opacityAnimation(new QPropertyAnimation(m_opacityFx, "opacity"))
{
parent->setGraphicsEffect(m_opacityFx);
m_opacityAnimation->setDuration(300);
}
void OpacityHelper::setOpacity(qreal opacity, bool animated)
{
if (!animated) {
m_opacityFx->setOpacity(opacity);
return;
}
m_opacityAnimation->stop();
m_opacityAnimation->setStartValue(m_opacityFx->opacity());
m_opacityAnimation->setEndValue(opacity);
m_opacityAnimation->start();
}
-27
View File
@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#ifndef OPACITYHELPER_H
#define OPACITYHELPER_H
#include <QWidget>
QT_BEGIN_NAMESPACE
class QGraphicsOpacityEffect;
class QPropertyAnimation;
QT_END_NAMESPACE
class OpacityHelper : QObject
{
public:
OpacityHelper(QWidget * parent);
void setOpacity(qreal opacity, bool animated = true);
protected:
QGraphicsOpacityEffect * m_opacityFx;
QPropertyAnimation * m_opacityAnimation;
};
#endif // OPACITYHELPER_H
+233 -193
View File
@@ -1,162 +1,22 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "playlistmanager.h"
#include "playlistscout.h"
#include <QCollator>
#include <QDir>
#include <QFileInfo>
#include <QUrl>
PlaylistModel::PlaylistModel(QObject *parent)
: QAbstractListModel(parent)
{
}
PlaylistModel::~PlaylistModel()
{
}
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
{
beginResetModel();
m_playlist = urls;
endResetModel();
}
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
{
if (urls.isEmpty()) return QModelIndex();
if (urls.count() == 1) {
return loadPlaylist(urls.constFirst());
} else {
setPlaylist(urls);
return index(0);
}
}
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
{
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString && currentFileName = info.fileName();
if (dir.path() == m_currentDir) {
int idx = indexOf(url);
return idx == -1 ? appendToPlaylist(url) : index(idx);
}
QStringList entryList = dir.entryList(
m_autoLoadSuffixes,
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> playlist;
int idx = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & fileName = entryList.at(i);
const QString & oneEntry = dir.absoluteFilePath(fileName);
const QUrl & url = QUrl::fromLocalFile(oneEntry);
playlist.append(url);
if (fileName == currentFileName) {
idx = i;
}
}
if (idx == -1) {
idx = playlist.count();
playlist.append(url);
}
m_currentDir = dir.path();
setPlaylist(playlist);
return index(idx);
}
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
{
const int lastIndex = rowCount();
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
m_playlist.append(url);
endInsertRows();
return index(lastIndex);
}
bool PlaylistModel::removeAt(int index)
{
if (index < 0 || index >= rowCount()) return false;
beginRemoveRows(QModelIndex(), index, index);
m_playlist.removeAt(index);
endRemoveRows();
return true;
}
int PlaylistModel::indexOf(const QUrl &url) const
{
return m_playlist.indexOf(url);
}
QUrl PlaylistModel::urlByIndex(int index) const
{
return m_playlist.value(index);
}
QStringList PlaylistModel::autoLoadFilterSuffixes() const
{
return m_autoLoadSuffixes;
}
QHash<int, QByteArray> PlaylistModel::roleNames() const
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result.insert(UrlRole, "url");
return result;
}
int PlaylistModel::rowCount(const QModelIndex &parent) const
{
return m_playlist.count();
}
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
switch (role) {
case Qt::DisplayRole:
return m_playlist.at(index.row()).fileName();
case UrlRole:
return m_playlist.at(index.row());
}
return QVariant();
}
PlaylistManager::PlaylistManager(QObject *parent)
: QObject(parent)
, m_currentIndex(std::nullopt)
, m_playlist()
, m_allowedSuffixes()
{
connect(&m_model, &PlaylistModel::rowsRemoved, this,
[this](const QModelIndex &, int, int) {
if (m_model.rowCount() <= m_currentIndex) {
setProperty("currentIndex", m_currentIndex - 1);
}
});
auto onRowCountChanged = [this](){
emit totalCountChanged(m_model.rowCount());
};
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
}
PlaylistManager::~PlaylistManager()
@@ -164,92 +24,272 @@ PlaylistManager::~PlaylistManager()
}
PlaylistModel *PlaylistManager::model()
// region: Allowed Suffixes Operations
const QStringList& PlaylistManager::allowedSuffixes() const {
return m_allowedSuffixes;
}
void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) {
m_allowedSuffixes = nameFilters;
emit allowedSuffixesChanged(m_allowedSuffixes);
}
// endregion
// region: Playlist Loader and Reset
bool PlaylistManager::loadPlaylist(const QStringList& files) {
// Convert string list to url list first
QList<QUrl> urls;
for (const QString & file : std::as_const(files)) {
urls.append(urlFromString(file));
}
return loadPlaylist(urls);
}
bool PlaylistManager::loadPlaylist(const QList<QUrl>& urls) {
// Check its count, and check M3U8 playlist for single file case.
if (urls.isEmpty()) {
clearPlaylist();
return true;
} else if (urls.count() == 1) {
const QUrl& firstUrl = urls.first();
const QString lowerCaseUrlPath(firstUrl.path().toLower());
if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) {
return loadM3U8Playlist(firstUrl);
} else {
return loadSingleFilePlaylist(firstUrl);
}
} else {
return loadMultipleFilesPlaylist(std::move(urls));
}
}
bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) {
// Check URL at first
if (!checkUrl(url))
return false;
// Scout playlist by given file
auto scoutResult = PlaylistScout::scout(url, m_allowedSuffixes);
if (!scoutResult.has_value()) {
return false;
}
// Setup playlist with index
setPlaylist(std::move(scoutResult->playlist), scoutResult->indexHint);
return true;
}
bool PlaylistManager::loadMultipleFilesPlaylist(const QList<QUrl>&& urls) {
// Check all of them are valid
for (const auto& url : std::as_const(urls)) {
if (!checkUrl(url)) {
return false;
}
}
// Set playlist and return
setPlaylist(std::move(urls));
return true;
}
bool PlaylistManager::loadM3U8Playlist(const QUrl& url) {
// Check URL at first
if (!checkUrl(url))
return false;
QList<QUrl> urls;
QFile file(url.toLocalFile());
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
while (!file.atEnd()) {
QString line = file.readLine();
if (line.startsWith('#')) {
continue;
}
QFileInfo fileInfo(file);
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
if (!checkUrl(item)) {
return false;
}
urls.append(item);
}
} else {
return false;
}
setPlaylist(std::move(urls));
return true;
}
void PlaylistManager::setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint) {
// Setup playlist
m_playlist = std::move(urls);
// Set index with hint
if (indexHint.has_value()) {
m_currentIndex = *indexHint;
}
sanitizeIndex();
// Raise signal
emit playlistChanged(totalCount());
}
void PlaylistManager::clearPlaylist() {
m_playlist.clear();
sanitizeIndex();
emit playlistChanged(this->totalCount());
}
// endregion
// region: Playlist Operations
void PlaylistManager::appendToPlaylist(const QString &file)
{
return &m_model;
return appendToPlaylist(urlFromString(file));
}
void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
void PlaylistManager::appendToPlaylist(const QUrl &url)
{
m_model.setPlaylist(urls);
m_playlist.append(url);
sanitizeIndex();
emit playlistChanged(totalCount());
}
QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file)
{
QModelIndex idx = m_model.loadPlaylist(urls);
setProperty("currentIndex", idx.row());
return idx;
return insertIntoPlaylist(index, urlFromString(file));
}
QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url)
{
QModelIndex idx = m_model.loadPlaylist(url);
setProperty("currentIndex", idx.row());
return idx;
if (index < 0 || index > totalCount()) {
return false;
} else {
m_playlist.insert(index, url);
sanitizeIndex();
emit playlistChanged(totalCount());
return true;
}
}
int PlaylistManager::totalCount() const
bool PlaylistManager::removeFromPlaylist(qsizetype index)
{
return m_model.rowCount();
if (index < 0 || index >= totalCount()) {
return false;
} else {
m_playlist.removeAt(index);
sanitizeIndex();
emit playlistChanged(totalCount());
return true;
}
}
QModelIndex PlaylistManager::previousIndex() const
// endregion
// region: Index Operations
qsizetype PlaylistManager::totalCount() const
{
int count = totalCount();
if (count == 0) return QModelIndex();
return m_model.index(m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1);
return m_playlist.count();
}
QModelIndex PlaylistManager::nextIndex() const
std::optional<qsizetype> PlaylistManager::previousIndex() const
{
int count = totalCount();
if (count == 0) return QModelIndex();
return m_model.index(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1);
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1);
}
QModelIndex PlaylistManager::curIndex() const
std::optional<qsizetype> PlaylistManager::nextIndex() const
{
return m_model.index(m_currentIndex);
if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt;
return (isLastIndex() ? 0 : *m_currentIndex + 1);
}
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
bool PlaylistManager::isFirstIndex() const
{
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
setProperty("currentIndex", index.row());
}
return m_currentIndex.has_value() && *m_currentIndex == 0;
}
QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
bool PlaylistManager::isLastIndex() const
{
return m_model.urlByIndex(index.row());
return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount();
}
QString PlaylistManager::localFileByIndex(const QModelIndex &index)
std::optional<qsizetype> PlaylistManager::currentIndex() const
{
return m_currentIndex;
}
bool PlaylistManager::setCurrentIndex(qsizetype index)
{
if (!m_currentIndex.has_value()) {
return false;
}
if (index < 0 || index >= totalCount()) {
return false;
}
if (*m_currentIndex == index) {
return true;
}
m_currentIndex = index;
emit currentIndexChanged(index);
return true;
}
QUrl PlaylistManager::urlByIndex(qsizetype index) const
{
return m_playlist.value(index);
}
QString PlaylistManager::localFileByIndex(qsizetype index) const
{
return urlByIndex(index).toLocalFile();
}
bool PlaylistManager::removeAt(const QModelIndex &index)
{
return m_model.removeAt(index.row());
}
// endregion
void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
{
m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
}
// region: Misc
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)
void PlaylistManager::sanitizeIndex()
{
QList<QUrl> urlList;
for (const QString & str : std::as_const(files)) {
QUrl url = QUrl::fromLocalFile(str);
if (url.isValid()) {
urlList.append(url);
auto cnt = totalCount();
if (cnt == 0) {
// If playlist is empty, current index should be invalid.
m_currentIndex = std::nullopt;
} else {
if (m_currentIndex.has_value()) {
// Clamp current index in playlist range.
// If it is already in range, not change was done.
auto currentIndex = *m_currentIndex;
if (currentIndex < 0) {
m_currentIndex = 0;
} else if (currentIndex >= cnt) {
m_currentIndex = cnt - 1;
} else {
; // No change.
}
} else {
// If there is no set value for current index,
// set it at the beginning of playlist.
m_currentIndex = 0;
}
}
}
return urlList;
bool PlaylistManager::checkUrl(const QUrl& url)
{
return url.isValid() && url.isLocalFile();
}
QUrl PlaylistManager::urlFromString(const QString& file)
{
return QUrl::fromLocalFile(file);
}
// endregion
+108 -63
View File
@@ -1,85 +1,130 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QUrl>
#include <QAbstractListModel>
class PlaylistModel : public QAbstractListModel
{
Q_OBJECT
public:
enum PlaylistRole {
UrlRole = Qt::UserRole
};
Q_ENUM(PlaylistRole)
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
explicit PlaylistModel(QObject *parent = nullptr);
~PlaylistModel();
void setPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QUrl & url);
QModelIndex appendToPlaylist(const QUrl & url);
bool removeAt(int index);
int indexOf(const QUrl & url) const;
QUrl urlByIndex(int index) const;
QStringList autoLoadFilterSuffixes() const;
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void autoLoadFilterSuffixesChanged(QStringList suffixes);
private:
// model data
QList<QUrl> m_playlist;
// properties
QStringList m_autoLoadSuffixes = {};
// internal
QString m_currentDir;
};
#include <QObject>
#include <optional>
/// The manager of playlist.
class PlaylistManager : public QObject
{
Q_OBJECT
public:
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes)
Q_PROPERTY(PlaylistModel * model READ model CONSTANT)
/// Constructs a PlaylistManager with an optional parent QObject.
explicit PlaylistManager(QObject* parent = nullptr);
/// Destructor.
~PlaylistManager();
PlaylistModel * model();
/// Get the allowed file suffixes of this playlist.
/// @return The current list of allowed suffixes.
const QStringList& allowedSuffixes() const;
/// Set the allowed file suffixes of this playlist.
/// @remark This only works for "load the whole directory by one file" case.
/// @param[in] nameFilters The new list of allowed suffixes.
void setAllowedSuffixes(const QStringList& nameFilters);
void setPlaylist(const QList<QUrl> & url);
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
/// Load user specified files as playlist.
/// @remark The wrapper for another overload.
/// @return True for successful load, otherwise false.
bool loadPlaylist(const QStringList& files);
/// Load user specified URLs as playlist.
/// @remark This function automatically dispatch these files into correct sub-load functions.
/// @return True for successful load, otherwise false.
bool loadPlaylist(const QList<QUrl>& urls);
/// Clear the entire playlist and reset current index.
void clearPlaylist();
int totalCount() const;
QModelIndex previousIndex() const;
QModelIndex nextIndex() const;
QModelIndex curIndex() const;
void setCurrentIndex(const QModelIndex & index);
QUrl urlByIndex(const QModelIndex & index);
QString localFileByIndex(const QModelIndex & index);
bool removeAt(const QModelIndex & index);
/// Append a local file path to the playlist.
void appendToPlaylist(const QString& file);
/// Append a URL to the playlist.
void appendToPlaylist(const QUrl& url);
/// Insert a local file path at the given index.
/// @return True if inserted successfully, false if index is out of range.
bool insertIntoPlaylist(qsizetype index, const QString& file);
/// Insert a URL at the given index.
/// @return True if inserted successfully, false if index is out of range.
bool insertIntoPlaylist(qsizetype index, const QUrl& url);
/// Remove the entry at the given index.
/// @return True if removed successfully, false if index is out of range.
bool removeFromPlaylist(qsizetype index);
void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
static QList<QUrl> convertToUrlList(const QStringList & files);
/// Returns the total number of items in the playlist.
/// @return Total count of entries in the playlist.
qsizetype totalCount() const;
/// Try fetching previous index.
/// @details This previous index is gallery-loop-aware.
/// @return std::nullopt if playlist is empty, otherwise the previous index considering gallery loop.
std::optional<qsizetype> previousIndex() const;
/// Try fetching next index.
/// @details This next index is gallery-loop-aware.
/// @return std::nullopt if playlist is empty, otherwise the next index considering gallery loop.
std::optional<qsizetype> nextIndex() const;
/// Check whether current index is the first index.
/// @return False if current index is not the first index or playlist is empty, otherwise true.
bool isFirstIndex() const;
/// Check whether current index is the last index.
/// @return False if current index is not the last index or playlist is empty, otherwise true.
bool isLastIndex() const;
/// Returns the current index.
/// @return std::nullopt if the playlist is empty, otherwise the current index.
std::optional<qsizetype> currentIndex() const;
/// Set the current index.
/// @remark Index only can be set when playlist is not empty.
/// @return True if successful set, including "not changed" set, otherwise false.
/// This return value only indicates whether we can set index and given index is valid for setting.
/// If "no changed" set occurs, this function returns true, but doesn't raise current index changed signal.
bool setCurrentIndex(qsizetype index);
/// Returns the URL at the given index.
/// @return The URL if index is valid.
/// This class guarantee that this QUrl is pointing to a local file.
QUrl urlByIndex(qsizetype index) const;
/// Returns the local file path at the given index.
/// @return The local file path if index is valid.
/// This class guarantee that local file path always is valid.
QString localFileByIndex(qsizetype index) const;
signals:
void currentIndexChanged(int index);
void totalCountChanged(int count);
void allowedSuffixesChanged(const QStringList& suffixes);
/// Raised when index was changed bu setter.
/// The argument holds the new current index.
/// Please note that this signal will NOT be raised by the whole playlist change caused index change.
void currentIndexChanged(qsizetype index);
/// The whole playlist was changed, involving loading, editing and deleting some or full entries.
/// The argument holds the total count of new play list.
void playlistChanged(qsizetype count);
private:
int m_currentIndex = -1;
PlaylistModel m_model;
/// The fundamental playlist setter.
/// @details All playlist load functions will call this function to set playlist at last.
/// @param[in] urls ALl urls in this list should be checked by checkUrl() function.
/// @param[in] indexHint Try to set current index to this index if possible.
void setPlaylist(const QList<QUrl>&& urls, std::optional<qsizetype> indexHint = std::nullopt);
/// Load playlist from a single file.
/// @details This function scans the same directory for siblings matching allowed suffixes, sorted naturally.
/// @return True if the playlist was loaded successfully, false otherwise.
bool loadSingleFilePlaylist(const QUrl& url);
/// Load playlist from multiple URLs directly.
/// @return True if all URLs are valid, false otherwise.
bool loadMultipleFilesPlaylist(const QList<QUrl>&& urls);
/// Load playlist from an M3U/M3U8 playlist file.
/// @return True if loaded successfully, false otherwise.
bool loadM3U8Playlist(const QUrl& url);
/// Make sure that current index is valid.
/// @details This function usually is used by those functions manipulating playlist.
void sanitizeIndex();
/// Check whether given QUrl is proper to be put into playlist.
/// @details Only the url pointing to local file can be put into playlist.
static bool checkUrl(const QUrl& url);
/// Convert a string to local file into a QUrl.
/// @param[in] file The string pointing to local file.
static QUrl urlFromString(const QString& file);
std::optional<qsizetype> m_currentIndex;
QList<QUrl> m_playlist;
QStringList m_allowedSuffixes;
};
+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "playlistscout.h"
#include "playlistscouts/qtscout.h"
#include "playlistscouts/explorerscout.h"
std::optional<PlaylistScout::ScoutResult> PlaylistScout::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
#ifdef Q_OS_WIN
// In Windows, Qt approach can not aware Windows Explorer custom sort order.
// So we need to try using Win32 functions for fetching it at first.
if (auto rv = PlaylistScout::Explorer::scout(url, allowedSuffixes); rv.has_value()) {
return rv;
}
#endif // Q_OS_WIN
// TODO: We may add macOS Finder, KDE Dolphin and etc supports in future.
// Qt way is the last fallback.
return PlaylistScout::Qt::scout(url, allowedSuffixes);
}
+31
View File
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QUrl>
#include <QStringList>
#include <QList>
#include <optional>
namespace PlaylistScout {
/// The result of scout
struct ScoutResult {
/// The final fetched playlist (it can be empty).
/// Each item must be a QUrl pointing to a local file.
QList<QUrl> playlist;
/// The index of given file located in playlist.
/// Nullopt if it is not presented in playlist.
std::optional<qsizetype> indexHint;
};
/// @brief Scout available image files in given file's parent dir with given allowed suffixes.
/// @param[in] url The path to file whose parent dir is our target dir for finding image files.
/// @param[in] allowedSuffixes
/// Each items in this list is a wildcard pattern for matching allowed file name, like \c *.png .
/// The fetched item will be preserved if its name matching any of them.
/// If this list is empty, there is no restrictions for finding files.
/// @return Scout result if success, otherwise nullopt.
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
+332
View File
@@ -0,0 +1,332 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "explorerscout.h"
#ifdef Q_OS_WIN
#include <QDir>
#include <QDebug>
#include <QFileInfo>
#include <QRegularExpression>
#include <optional>
#include <exdisp.h> // IShellWindows, CLSID_ShellWindows
#include <servprov.h> // IServiceProvider
#include <shlguid.h> // SID_STopLevelBrowser
#include <shlobj.h>
// YYC MARK:
// We use custom RAII wrappers instead of existing COM smart pointers because the
// project may be built with MinGW, which does not have good compatibility with
// CComPtr and Microsoft's WRL ComPtr (<wrl/client.h>).
// Furthermore, PIDLs are not COM objects (they are freed via ILFree, not Release),
// so no standard smart pointer covers them regardless of compiler.
/// RAII wrapper for COM initialization (CoInitialize / CoUninitialize).
/// Must be declared before any COM objects (e.g., ComPtr, UniquePidl) so that
/// CoUninitialize runs after all COM objects are released during scope exit.
class ComGuard {
public:
ComGuard() : m_hr(CoInitialize(nullptr)) {}
~ComGuard() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
ComGuard(const ComGuard&) = delete;
ComGuard& operator=(const ComGuard&) = delete;
bool IsInitialized() const { return SUCCEEDED(m_hr); }
private:
HRESULT m_hr;
};
/// RAII wrapper for COM interface pointers.
template <typename T>
class ComPtr {
public:
ComPtr() = default;
ComPtr(std::nullptr_t) {}
~ComPtr() { Reset(); }
ComPtr(const ComPtr&) = delete;
ComPtr& operator=(const ComPtr&) = delete;
ComPtr(ComPtr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
ComPtr& operator=(ComPtr&& other) noexcept {
if (this != &other) {
Reset();
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
}
return *this;
}
T* operator->() const { return m_ptr; }
T* Get() const { return m_ptr; }
T** GetAddressOf() { return &m_ptr; }
explicit operator bool() const { return m_ptr != nullptr; }
void Reset() { if (m_ptr) { m_ptr->Release(); m_ptr = nullptr; } }
private:
T* m_ptr = nullptr;
};
/// RAII wrapper for PIDL (Item ID List).
class UniquePidl {
public:
UniquePidl() = default;
explicit UniquePidl(LPITEMIDLIST pidl) : m_pidl(pidl) {}
~UniquePidl() { Reset(); }
UniquePidl(const UniquePidl&) = delete;
UniquePidl& operator=(const UniquePidl&) = delete;
UniquePidl(UniquePidl&& other) noexcept : m_pidl(other.m_pidl) { other.m_pidl = nullptr; }
UniquePidl& operator=(UniquePidl&& other) noexcept {
if (this != &other) {
Reset();
m_pidl = other.m_pidl;
other.m_pidl = nullptr;
}
return *this;
}
LPCITEMIDLIST Get() const { return m_pidl; }
LPITEMIDLIST* GetAddressOf() { return &m_pidl; }
explicit operator bool() const { return m_pidl != nullptr; }
void Reset() { if (m_pidl) { ILFree(m_pidl); m_pidl = nullptr; } }
private:
LPITEMIDLIST m_pidl = nullptr;
};
/// Outputs a diagnostic warning for debugging Windows Explorer integration failures.
static void ReportError(const QString& detail)
{
qWarning("Windows Explorer file list fetch error: %s",
qPrintable(detail));
}
/// Normalizes a Windows directory path for case-insensitive comparison:
/// converts to native separators, lowercases, and ensures a trailing backslash.
static QString NormalizePath(const QString& path)
{
QString result = QDir::toNativeSeparators(path).toLower();
if (!result.endsWith(QChar('\\'))) {
result += QChar('\\');
}
return result;
}
/// Checks whether the Explorer window represented by the given IDispatch is
/// displaying the target folder. Returns its IFolderView2 on match, or null.
static ComPtr<IFolderView2> TryMatchWindow(IDispatch* dispatch, const QString& normalizedTarget)
{
// Navigate: IDispatch -> IServiceProvider -> IShellBrowser.
ComPtr<IServiceProvider> serviceProvider;
if (FAILED(dispatch->QueryInterface(IID_PPV_ARGS(serviceProvider.GetAddressOf())))) {
return nullptr;
}
ComPtr<IShellBrowser> shellBrowser;
if (FAILED(serviceProvider->QueryService(SID_STopLevelBrowser,
IID_PPV_ARGS(shellBrowser.GetAddressOf())))) {
return nullptr;
}
// IShellBrowser -> active IShellView.
ComPtr<IShellView> shellView;
if (FAILED(shellBrowser->QueryActiveShellView(shellView.GetAddressOf()))) {
return nullptr;
}
// Retrieve the folder's path via IPersistIDList.
ComPtr<IPersistIDList> persistIdList;
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(persistIdList.GetAddressOf())))) {
return nullptr;
}
UniquePidl folderPidl;
if (FAILED(persistIdList->GetIDList(folderPidl.GetAddressOf()))) {
return nullptr;
}
PWSTR pathStr = nullptr;
if (FAILED(SHGetNameFromIDList(folderPidl.Get(), SIGDN_DESKTOPABSOLUTEPARSING, &pathStr))) {
return nullptr;
}
QString windowPath = NormalizePath(QString::fromWCharArray(pathStr));
CoTaskMemFree(pathStr);
if (windowPath != normalizedTarget) {
return nullptr;
}
// Path matches — query IFolderView2 from the ShellView.
ComPtr<IFolderView2> folderView2;
if (FAILED(shellView->QueryInterface(IID_PPV_ARGS(folderView2.GetAddressOf())))) {
return nullptr;
}
return folderView2;
}
/// Tries to find a live Explorer window that is currently displaying the given
/// folder path. Returns the IFolderView2 from that window, or an empty ComPtr.
///
/// Explorer's sort order is stored only in the running Explorer process's memory.
/// Creating a new shell view via IShellFolder::CreateViewObject yields a default
/// sort (always by name), so we must find the actual live IFolderView2 instead.
///
/// Reference:
/// - https://www.stephenli.ca/getting-windows-explorers-current-directory-sort-order
/// - https://github.com/d2phap/ImageGlass/blob/75a296287f338edcf409eee3917531b25ff5d927/v9/Components/ImageGlass.Base/FileSystem/FileFinder.cs
/// - NuGet Package: D2Phap.EggShell
static ComPtr<IFolderView2> FindLiveFolderView(const QString& targetFolderPath)
{
// Create the Shell windows collection (all open Explorer windows).
ComPtr<IShellWindows> shellWindows;
if (FAILED(CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL,
IID_PPV_ARGS(shellWindows.GetAddressOf())))) {
ReportError("Failed to create IShellWindows instance");
return nullptr;
}
QString normalizedTarget = NormalizePath(targetFolderPath);
// 1. Try the Desktop window first — it may contain the file the user opened.
{
VARIANT varLoc;
VariantInit(&varLoc);
varLoc.vt = VT_I4;
varLoc.lVal = 0;
VARIANT varLocRoot;
VariantInit(&varLocRoot);
long desktopHwnd = 0;
ComPtr<IDispatch> desktopDispatch;
HRESULT findHr = shellWindows->FindWindowSW(
&varLoc, &varLocRoot, SWC_DESKTOP,
&desktopHwnd, SWFO_NEEDDISPATCH,
desktopDispatch.GetAddressOf());
VariantClear(&varLoc);
VariantClear(&varLocRoot);
if (SUCCEEDED(findHr) && desktopDispatch) {
ComPtr<IFolderView2> result = TryMatchWindow(desktopDispatch.Get(), normalizedTarget);
if (result) return result;
}
}
// 2. Iterate regular Explorer windows.
LONG count = 0;
shellWindows->get_Count(&count);
for (LONG i = 0; i < count; i++) {
VARIANT varIndex;
VariantInit(&varIndex);
varIndex.vt = VT_I4;
varIndex.lVal = i;
ComPtr<IDispatch> dispatch;
HRESULT itemHr = shellWindows->Item(varIndex, dispatch.GetAddressOf());
VariantClear(&varIndex);
if (FAILED(itemHr) || !dispatch) {
continue;
}
ComPtr<IFolderView2> result = TryMatchWindow(dispatch.Get(), normalizedTarget);
if (result) return result;
}
return nullptr;
}
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Explorer::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
PlaylistScout::ScoutResult rv;
QFileInfo info(url.toLocalFile());
QString folderPath = info.path();
QString currentFileName = info.fileName();
// Pre-compile wildcard suffix filters into regular expressions for performance.
QList<QRegularExpression> nameRegexps;
for (const QString &pattern : allowedSuffixes) {
nameRegexps.append(QRegularExpression(
QRegularExpression::wildcardToRegularExpression(pattern),
QRegularExpression::CaseInsensitiveOption));
}
// Initialize COM. The ComGuard must be declared before any COM objects
// below so that CoUninitialize runs after all of them are released.
ComGuard comGuard;
if (!comGuard.IsInitialized()) {
ReportError("COM initialization failed");
return std::nullopt;
}
// Find a live Explorer window that is currently displaying the target folder.
ComPtr<IFolderView2> folderView2 = FindLiveFolderView(folderPath);
if (!folderView2) {
ReportError("No open Explorer window matches the folder: " + folderPath);
return std::nullopt;
}
// Get items already sorted in Explorer's view order, as an IShellItemArray.
// SVGIO_ALLVIEW returns all items; SVGIO_FLAG_VIEWORDER respects the view's sort.
ComPtr<IShellItemArray> itemArray;
if (FAILED(folderView2->Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER,
IID_PPV_ARGS(itemArray.GetAddressOf())))) {
ReportError("IFolderView2::Items(SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER) failed");
return std::nullopt;
}
DWORD itemCount = 0;
itemArray->GetCount(&itemCount);
// Enumerate sorted items, filter by pattern, and build the playlist.
for (DWORD i = 0; i < itemCount; i++) {
ComPtr<IShellItem> shellItem;
if (FAILED(itemArray->GetItemAt(i, shellItem.GetAddressOf()))) {
continue;
}
// Get the full parsing path of the item.
PWSTR pathStr = nullptr;
if (FAILED(shellItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &pathStr)) || !pathStr) {
continue;
}
QString filePath = QString::fromWCharArray(pathStr);
CoTaskMemFree(pathStr);
// Skip directories — the view may include folders alongside files.
QFileInfo fileInfo(filePath);
if (!fileInfo.isFile()) {
continue;
}
QString fileName = fileInfo.fileName();
// Match against compiled wildcard patterns.
bool matches = nameRegexps.isEmpty();
for (const QRegularExpression &re : std::as_const(nameRegexps)) {
if (re.match(fileName).hasMatch()) {
matches = true;
break;
}
}
if (!matches) {
continue;
}
rv.playlist.append(QUrl::fromLocalFile(filePath));
if (fileName == currentFileName) {
rv.indexHint = rv.playlist.size() - 1;
}
}
return rv;
}
#endif // Q_OS_WIN
+16
View File
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include "../playlistscout.h"
#include <QtSystemDetection>
#ifdef Q_OS_WIN
namespace PlaylistScout::Explorer {
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
#endif // Q_OS_WIN
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#include "qtscout.h"
#include <QFileInfo>
#include <QDir>
#include <QCollator>
std::optional<PlaylistScout::ScoutResult> PlaylistScout::Qt::scout(
const QUrl &url, const QStringList &allowedSuffixes) {
PlaylistScout::ScoutResult rv;
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString currentFileName = info.fileName();
QStringList entryList = dir.entryList(
allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
for (qsizetype i = 0; i < entryList.count(); i++) {
const QString &fileName = entryList.at(i);
const QString &oneEntry = dir.absoluteFilePath(fileName);
const QUrl &url = QUrl::fromLocalFile(oneEntry);
rv.playlist.append(url);
if (fileName == currentFileName) {
rv.indexHint = i;
}
}
return rv;
}
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2026 yyc12345 <yyc12321@outlook.com>
//
// SPDX-License-Identifier: MIT
#pragma once
#include "../playlistscout.h"
namespace PlaylistScout::Qt {
std::optional<ScoutResult> scout(const QUrl &url, const QStringList &allowedSuffixes);
}
+37 -78
View File
@@ -13,6 +13,14 @@
#include <QKeySequence>
#include <QMetaEnum>
Settings *Settings::instance()
{
// SAFETY:
// This is a global singleton and it is leaked by design.
static Settings* INSTANCE = new Settings(nullptr);
return INSTANCE;
}
namespace QEnumHelper
{
template <typename E>
@@ -34,70 +42,62 @@ namespace QEnumHelper
}
}
Settings *Settings::m_settings_instance = nullptr;
Settings *Settings::instance()
{
if (!m_settings_instance) {
m_settings_instance = new Settings;
}
return m_settings_instance;
}
bool Settings::stayOnTop()
{
return m_qsettings->value("stay_on_top", true).toBool();
}
bool Settings::useLightCheckerboard()
bool Settings::useLightCheckerboard() const
{
return m_qsettings->value("use_light_checkerboard", false).toBool();
}
Settings::DoubleClickBehavior Settings::doubleClickBehavior() const
bool Settings::loopGallery() const
{
QString result = m_qsettings->value("double_click_behavior", "Close").toString();
return m_qsettings->value("loop_gallery", true).toBool();
}
return QEnumHelper::fromString<DoubleClickBehavior>(result, DoubleClickBehavior::Close);
bool Settings::svgTiny12Only() const
{
// Qt 6.7.0's SVG support is terrible caused by huge memory usage, see QTBUG-124287
// Qt 6.7.1's is somewhat better, memory issue seems fixed, but still laggy when zoom in, see QTBUG-126771.
// Qt 6.9.3 and Qt 6.10.1 seems no longer have the laggy issue, so let's make the default value different
// based on Qt version.
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 3)
return m_qsettings->value("svg_tiny12_only", true).toBool();
#else
return m_qsettings->value("svg_tiny12_only", false).toBool();
#endif // QT_VERSION < QT_VERSION_CHECK(6, 9, 3)
}
Settings::MouseWheelBehavior Settings::mouseWheelBehavior() const
{
QString result = m_qsettings->value("mouse_wheel_behavior", "Zoom").toString();
QString result = m_qsettings->value("mouse_wheel_behavior", "").toString();
return QEnumHelper::fromString<MouseWheelBehavior>(result, MouseWheelBehavior::Zoom);
}
Settings::WindowSizeBehavior Settings::initWindowSizeBehavior() const
{
QString result = m_qsettings->value("init_window_size_behavior", "Auto").toString();
return QEnumHelper::fromString<WindowSizeBehavior>(result, WindowSizeBehavior::Auto);
QString result = m_qsettings->value("init_window_size_behavior", "").toString();
return QEnumHelper::fromString<WindowSizeBehavior>(result, WindowSizeBehavior::Windowed);
}
Qt::HighDpiScaleFactorRoundingPolicy Settings::hiDpiScaleFactorBehavior() const
{
QString result = m_qsettings->value("hidpi_scale_factor_behavior", "PassThrough").toString();
QString result = m_qsettings->value("hidpi_scale_factor_behavior", "").toString();
return QEnumHelper::fromString<Qt::HighDpiScaleFactorRoundingPolicy>(result, Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
}
void Settings::setStayOnTop(bool on)
{
m_qsettings->setValue("stay_on_top", on);
m_qsettings->sync();
}
void Settings::setUseLightCheckerboard(bool light)
{
m_qsettings->setValue("use_light_checkerboard", light);
m_qsettings->sync();
}
void Settings::setDoubleClickBehavior(DoubleClickBehavior dcb)
void Settings::setLoopGallery(bool on)
{
m_qsettings->setValue("double_click_behavior", QEnumHelper::toString(dcb));
m_qsettings->setValue("loop_gallery", on);
m_qsettings->sync();
}
void Settings::setSvgTiny12Only(bool on)
{
m_qsettings->setValue("svg_tiny12_only", on);
m_qsettings->sync();
}
@@ -119,47 +119,6 @@ void Settings::setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy
m_qsettings->sync();
}
void Settings::applyUserShortcuts(QWidget *widget)
{
m_qsettings->beginGroup("shortcuts");
const QStringList shortcutNames = m_qsettings->allKeys();
for (const QString & name : shortcutNames) {
QList<QKeySequence> shortcuts = m_qsettings->value(name).value<QList<QKeySequence>>();
setShortcutsForAction(widget, name, shortcuts, false);
}
m_qsettings->endGroup();
}
bool Settings::setShortcutsForAction(QWidget *widget, const QString &objectName,
QList<QKeySequence> shortcuts, bool writeConfig)
{
QAction * targetAction = nullptr;
for (QAction * action : widget->actions()) {
if (action->objectName() == objectName) {
targetAction = action;
} else {
for (const QKeySequence & shortcut : std::as_const(shortcuts)) {
if (action->shortcuts().contains(shortcut)) {
return false;
}
}
}
}
if (targetAction) {
targetAction->setShortcuts(shortcuts);
}
if (targetAction && writeConfig) {
m_qsettings->beginGroup("shortcuts");
m_qsettings->setValue(objectName, QVariant::fromValue(shortcuts));
m_qsettings->endGroup();
m_qsettings->sync();
}
return true;
}
#if defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN)
#include <windows.h>
// QCoreApplication::applicationDirPath() parses the "applicationDirPath" from arg0, which...
@@ -178,8 +137,8 @@ QString getApplicationDirPath()
}
#endif // defined(FLAG_PORTABLE_MODE_SUPPORT) && defined(Q_OS_WIN)
Settings::Settings()
: QObject(qApp)
Settings::Settings(QObject *parent)
: QObject(parent)
{
QString configPath;
+7 -21
View File
@@ -11,14 +11,6 @@ class Settings : public QObject
{
Q_OBJECT
public:
enum DoubleClickBehavior {
Ignore,
Close,
Maximize,
FullScreen,
};
Q_ENUM(DoubleClickBehavior)
enum MouseWheelBehavior {
Zoom,
Switch,
@@ -26,35 +18,29 @@ public:
Q_ENUM(MouseWheelBehavior)
enum WindowSizeBehavior {
Auto,
Maximized,
Windowed,
};
Q_ENUM(WindowSizeBehavior)
static Settings *instance();
bool stayOnTop();
bool useLightCheckerboard();
DoubleClickBehavior doubleClickBehavior() const;
bool useLightCheckerboard() const;
bool loopGallery() const;
bool svgTiny12Only() const;
MouseWheelBehavior mouseWheelBehavior() const;
WindowSizeBehavior initWindowSizeBehavior() const;
Qt::HighDpiScaleFactorRoundingPolicy hiDpiScaleFactorBehavior() const;
void setStayOnTop(bool on);
void setUseLightCheckerboard(bool light);
void setDoubleClickBehavior(DoubleClickBehavior dcb);
void setLoopGallery(bool on);
void setSvgTiny12Only(bool on);
void setMouseWheelBehavior(MouseWheelBehavior mwb);
void setInitWindowSizeBehavior(WindowSizeBehavior wsb);
void setHiDpiScaleFactorBehavior(Qt::HighDpiScaleFactorRoundingPolicy hidpi);
void applyUserShortcuts(QWidget * widget);
bool setShortcutsForAction(QWidget * widget, const QString & objectName,
QList<QKeySequence> shortcuts, bool writeConfig = true);
private:
Settings();
static Settings *m_settings_instance;
Settings(QObject *parent = nullptr);
QSettings *m_qsettings;
+109 -146
View File
@@ -3,175 +3,138 @@
// SPDX-License-Identifier: MIT
#include "settingsdialog.h"
#include "ui_settingsdialog.h"
#include "settings.h"
#include "shortcutedit.h"
#include <QAction>
#include <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QKeySequenceEdit>
#include <QScrollArea>
#include <QSplitter>
#include <QApplication>
#include <QTimer>
#include <QStringListModel>
#include <QMessageBox>
#include <type_traits>
template <typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
class EnumComboBoxTransformer {
public:
using Pair = QPair<E, QString>;
using Pairs = QList<QPair<E, QString>>;
EnumComboBoxTransformer(Pairs &&pairs) : pairs(std::move(pairs)) {}
~EnumComboBoxTransformer() = default;
Q_DISABLE_COPY_MOVE(EnumComboBoxTransformer)
/// Build Qt model used for QComboBox
QStringListModel* buildComboBoxModel(QObject *parent = nullptr) const {
QStringList dropDownEntries;
for (const auto& pair : std::as_const(this->pairs)) {
dropDownEntries.append(pair.second);
}
return new QStringListModel(dropDownEntries, parent);
}
/// Convert QComboBox index to enum value.
E to_value(int index) const {
return this->pairs.at(static_cast<qsizetype>(index)).first;
}
/// Convert enum value to QComboBox index.
int to_index(E value) const {
for (qsizetype i = 0; i < pairs.size(); ++i) {
if (pairs.at(i).first == value) {
return static_cast<int>(i);
}
}
// Value not found
throw std::invalid_argument("invalid enum value for finding index");
}
private:
Pairs pairs;
};
static EnumComboBoxTransformer<Settings::MouseWheelBehavior, QString> MW_OPTIONS {
{
{ Settings::MouseWheelBehavior::Zoom, QCoreApplication::translate("SettingsDialog", "Zoom in and out") },
{ Settings::MouseWheelBehavior::Switch, QCoreApplication::translate("SettingsDialog", "View next or previous item") }
}
};
static EnumComboBoxTransformer<Settings::WindowSizeBehavior, QString> IWS_OPTIONS {
{
{ Settings::WindowSizeBehavior::Maximized, QCoreApplication::translate("SettingsDialog", "Maximized") },
{ Settings::WindowSizeBehavior::Windowed, QCoreApplication::translate("SettingsDialog", "Windowed") }
}
};
static EnumComboBoxTransformer<Qt::HighDpiScaleFactorRoundingPolicy, QString> HIDPI_OPTIONS {
{
{ Qt::HighDpiScaleFactorRoundingPolicy::Round, QCoreApplication::translate("SettingsDialog", "Round (Integer scaling)", "This option means round up for .5 and above") },
{ Qt::HighDpiScaleFactorRoundingPolicy::Ceil, QCoreApplication::translate("SettingsDialog", "Ceil (Integer scaling)", "This option means always round up") },
{ Qt::HighDpiScaleFactorRoundingPolicy::Floor, QCoreApplication::translate("SettingsDialog", "Floor (Integer scaling)", "This option means always round down") },
{ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough, QCoreApplication::translate("SettingsDialog", "Follow system (Fractional scaling)", "This option means don't round") }
}
};
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)
, m_stayOnTop(new QCheckBox)
, m_useLightCheckerboard(new QCheckBox)
, m_doubleClickBehavior(new QComboBox)
, m_mouseWheelBehavior(new QComboBox)
, m_initWindowSizeBehavior(new QComboBox)
, m_hiDpiRoundingPolicyBehavior(new QComboBox)
: QDialog(parent), ui(new QT_PREPEND_NAMESPACE(Ui::SettingsDialog))
{
this->setWindowTitle(tr("Settings"));
ui->setupUi(this);
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
QHBoxLayout * mainLayout = new QHBoxLayout(this);
QTabWidget * settingsTabs = new QTabWidget(this);
mainLayout->addWidget(settingsTabs);
auto* settings = Settings::instance();
ui->m_useLightCheckerboard->setChecked(settings->useLightCheckerboard());
ui->m_loopGallery->setChecked(settings->loopGallery());
ui->m_svgTiny12Only->setChecked(settings->svgTiny12Only());
ui->m_mouseWheelBehavior->setModel(MW_OPTIONS.buildComboBoxModel(this));
Settings::MouseWheelBehavior mwb = settings->mouseWheelBehavior();
ui->m_mouseWheelBehavior->setCurrentIndex(MW_OPTIONS.to_index(mwb));
ui->m_initWindowSizeBehavior->setModel(IWS_OPTIONS.buildComboBoxModel(this));
Settings::WindowSizeBehavior iwsb = settings->initWindowSizeBehavior();
ui->m_initWindowSizeBehavior->setCurrentIndex(IWS_OPTIONS.to_index(iwsb));
ui->m_hiDpiRoundingPolicyBehavior->setModel(HIDPI_OPTIONS.buildComboBoxModel(this));
Qt::HighDpiScaleFactorRoundingPolicy hidpi = settings->hiDpiScaleFactorBehavior();
ui->m_hiDpiRoundingPolicyBehavior->setCurrentIndex(HIDPI_OPTIONS.to_index(hidpi));
QWidget * settingsFormHolder = new QWidget;
QFormLayout * settingsForm = new QFormLayout(settingsFormHolder);
settingsTabs->addTab(settingsFormHolder, tr("Options"));
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::checkStateChanged
# define QT_CHECKSTATE Qt::CheckState
#else
# define QCHECKBOX_CHECKSTATECHANGED QCheckBox::stateChanged
# define QT_CHECKSTATE int
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
QSplitter * shortcutEditorSplitter = new QSplitter;
shortcutEditorSplitter->setOrientation(Qt::Vertical);
shortcutEditorSplitter->setChildrenCollapsible(false);
QScrollArea * shortcutScrollArea = new QScrollArea;
shortcutEditorSplitter->addWidget(shortcutScrollArea);
shortcutScrollArea->setWidgetResizable(true);
shortcutScrollArea->setMinimumHeight(200);
QWidget * shortcutsFormHolder = new QWidget;
QFormLayout * shortcutsForm = new QFormLayout(shortcutsFormHolder);
shortcutScrollArea->setWidget(shortcutsFormHolder);
settingsTabs->addTab(shortcutEditorSplitter, tr("Shortcuts"));
for (const QAction * action : parent->actions()) {
ShortcutEdit * shortcutEdit = new ShortcutEdit;
shortcutEdit->setObjectName(QLatin1String("shortcut_") + action->objectName());
shortcutEdit->setShortcuts(action->shortcuts());
shortcutsForm->addRow(action->text(), shortcutEdit);
connect(shortcutEdit, &ShortcutEdit::editButtonClicked, this, [=](){
if (shortcutEditorSplitter->count() == 1) shortcutEditorSplitter->addWidget(new QWidget);
ShortcutEditor * shortcutEditor = new ShortcutEditor(shortcutEdit);
shortcutEditor->setDescription(tr("Editing shortcuts for action \"%1\":").arg(action->text()));
QWidget * oldEditor = shortcutEditorSplitter->replaceWidget(1, shortcutEditor);
shortcutEditorSplitter->setSizes({shortcutEditorSplitter->height(), 1});
oldEditor->deleteLater();
});
connect(shortcutEdit, &ShortcutEdit::applyShortcutsRequested, this, [=](QList<QKeySequence> newShortcuts){
bool succ = Settings::instance()->setShortcutsForAction(parent, shortcutEdit->objectName().mid(9),
newShortcuts);
if (!succ) {
QMessageBox::warning(this, tr("Failed to set shortcuts"),
tr("Please check if shortcuts are duplicated with existing shortcuts."));
}
shortcutEdit->setShortcuts(action->shortcuts());
});
}
static QList< QPair<Settings::DoubleClickBehavior, QString> > _dc_options {
{ Settings::DoubleClickBehavior::Ignore, tr("Do nothing") },
{ Settings::DoubleClickBehavior::Close, tr("Close the window") },
{ Settings::DoubleClickBehavior::Maximize, tr("Toggle maximize") },
{ Settings::DoubleClickBehavior::FullScreen, tr("Toggle fullscreen") }
};
static QList< QPair<Settings::MouseWheelBehavior, QString> > _mw_options {
{ Settings::MouseWheelBehavior::Zoom, tr("Zoom in and out") },
{ Settings::MouseWheelBehavior::Switch, tr("View next or previous item") }
};
static QList< QPair<Settings::WindowSizeBehavior, QString> > _iws_options {
{ Settings::WindowSizeBehavior::Auto, tr("Auto size") },
{ Settings::WindowSizeBehavior::Maximized, tr("Maximized") }
};
static QList< QPair<Qt::HighDpiScaleFactorRoundingPolicy, QString> > _hidpi_options {
{ Qt::HighDpiScaleFactorRoundingPolicy::Round, tr("Round (Integer scaling)", "This option means round up for .5 and above") },
{ Qt::HighDpiScaleFactorRoundingPolicy::Ceil, tr("Ceil (Integer scaling)", "This option means always round up") },
{ Qt::HighDpiScaleFactorRoundingPolicy::Floor, tr("Floor (Integer scaling)", "This option means always round down") },
{ Qt::HighDpiScaleFactorRoundingPolicy::PassThrough, tr("Follow system (Fractional scaling)", "This option means don't round") }
};
QStringList dcbDropDown;
for (const QPair<Settings::DoubleClickBehavior, QString> & dcOption : _dc_options) {
dcbDropDown.append(dcOption.second);
}
QStringList mwbDropDown;
for (const QPair<Settings::MouseWheelBehavior, QString> & mwOption : _mw_options) {
mwbDropDown.append(mwOption.second);
}
QStringList iwsbDropDown;
for (const QPair<Settings::WindowSizeBehavior, QString> & iwsOption : _iws_options) {
iwsbDropDown.append(iwsOption.second);
}
QStringList hidpiDropDown;
for (const QPair<Qt::HighDpiScaleFactorRoundingPolicy, QString> & hidpiOption : _hidpi_options) {
hidpiDropDown.append(hidpiOption.second);
}
settingsForm->addRow(tr("Stay on top when start-up"), m_stayOnTop);
settingsForm->addRow(tr("Use light-color checkerboard"), m_useLightCheckerboard);
settingsForm->addRow(tr("Double-click behavior"), m_doubleClickBehavior);
settingsForm->addRow(tr("Mouse wheel behavior"), m_mouseWheelBehavior);
settingsForm->addRow(tr("Default window size"), m_initWindowSizeBehavior);
settingsForm->addRow(tr("HiDPI scale factor rounding policy"), m_hiDpiRoundingPolicyBehavior);
m_stayOnTop->setChecked(Settings::instance()->stayOnTop());
m_useLightCheckerboard->setChecked(Settings::instance()->useLightCheckerboard());
m_doubleClickBehavior->setModel(new QStringListModel(dcbDropDown));
Settings::DoubleClickBehavior dcb = Settings::instance()->doubleClickBehavior();
m_doubleClickBehavior->setCurrentIndex(static_cast<int>(dcb));
m_mouseWheelBehavior->setModel(new QStringListModel(mwbDropDown));
Settings::MouseWheelBehavior mwb = Settings::instance()->mouseWheelBehavior();
m_mouseWheelBehavior->setCurrentIndex(static_cast<int>(mwb));
m_initWindowSizeBehavior->setModel(new QStringListModel(iwsbDropDown));
Settings::WindowSizeBehavior iwsb = Settings::instance()->initWindowSizeBehavior();
m_initWindowSizeBehavior->setCurrentIndex(static_cast<int>(iwsb));
m_hiDpiRoundingPolicyBehavior->setModel(new QStringListModel(hidpiDropDown));
Qt::HighDpiScaleFactorRoundingPolicy hidpi = Settings::instance()->hiDpiScaleFactorBehavior();
for (int i = 0; i < _hidpi_options.count(); i++) {
if (_hidpi_options.at(i).first == hidpi) {
m_hiDpiRoundingPolicyBehavior->setCurrentIndex(i);
break;
}
}
connect(m_stayOnTop, &QCheckBox::stateChanged, this, [ = ](int state){
Settings::instance()->setStayOnTop(state == Qt::Checked);
});
connect(m_useLightCheckerboard, &QCheckBox::stateChanged, this, [ = ](int state){
connect(ui->m_useLightCheckerboard, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
Settings::instance()->setUseLightCheckerboard(state == Qt::Checked);
});
connect(m_doubleClickBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setDoubleClickBehavior(_dc_options.at(index).first);
connect(ui->m_loopGallery, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
Settings::instance()->setLoopGallery(state == Qt::Checked);
});
connect(m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setMouseWheelBehavior(_mw_options.at(index).first);
connect(ui->m_svgTiny12Only, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){
Settings::instance()->setSvgTiny12Only(state == Qt::Checked);
});
connect(m_initWindowSizeBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setInitWindowSizeBehavior(_iws_options.at(index).first);
connect(ui->m_mouseWheelBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setMouseWheelBehavior(MW_OPTIONS.to_value(index));
});
connect(m_hiDpiRoundingPolicyBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setHiDpiScaleFactorBehavior(_hidpi_options.at(index).first);
connect(ui->m_initWindowSizeBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setInitWindowSizeBehavior(IWS_OPTIONS.to_value(index));
});
adjustSize();
setWindowFlag(Qt::WindowContextHelpButtonHint, false);
connect(ui->m_hiDpiRoundingPolicyBehavior, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [ = ](int index){
Settings::instance()->setHiDpiScaleFactorBehavior(HIDPI_OPTIONS.to_value(index));
});
// YYC MARK:
// Use adjustSize() to make sure all ComboBox are expanded by its items.
// Because items are added after inserting them into layout.
// It is inviable to call this function in ctor because this relys on Qt event system.
// It only can be done by QTimer::singleShot().
QTimer::singleShot(0, this, [this](){
this->adjustSize();
});
}
SettingsDialog::~SettingsDialog()
{
delete ui;
}
+7 -13
View File
@@ -5,11 +5,14 @@
#ifndef SETTINGSDIALOG_H
#define SETTINGSDIALOG_H
#include <QObject>
#include <QDialog>
class QCheckBox;
class QComboBox;
QT_BEGIN_NAMESPACE
namespace Ui {
class SettingsDialog;
}
QT_END_NAMESPACE
class SettingsDialog : public QDialog
{
Q_OBJECT
@@ -17,17 +20,8 @@ public:
explicit SettingsDialog(QWidget *parent = nullptr);
~SettingsDialog();
signals:
public slots:
private:
QCheckBox * m_stayOnTop = nullptr;
QCheckBox * m_useLightCheckerboard = nullptr;
QComboBox * m_doubleClickBehavior = nullptr;
QComboBox * m_mouseWheelBehavior = nullptr;
QComboBox * m_initWindowSizeBehavior = nullptr;
QComboBox * m_hiDpiRoundingPolicyBehavior = nullptr;
QT_PREPEND_NAMESPACE(Ui::SettingsDialog) *ui;
};
#endif // SETTINGSDIALOG_H
+81
View File
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QHBoxLayout" name="mainLayout">
<item>
<widget class="QTabWidget" name="settingsTabs">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="optionsTab">
<attribute name="title">
<string>Options</string>
</attribute>
<layout class="QFormLayout" name="settingsForm">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="m_useLightCheckerboard">
<property name="text">
<string>Use light-color checkerboard</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="m_loopGallery">
<property name="text">
<string>Loop the loaded gallery</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="m_svgTiny12Only">
<property name="text">
<string>Limit SVG support to SVG Tiny 1.2</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="m_mouseWheelBehaviorLabel">
<property name="text">
<string>Mouse wheel behavior</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="m_mouseWheelBehavior"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="m_initWindowSizeBehaviorLabel">
<property name="text">
<string>Default window size</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_initWindowSizeBehavior"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="m_hiDpiRoundingPolicyBehaviorLabel">
<property name="text">
<string>HiDPI scale factor rounding policy</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="m_hiDpiRoundingPolicyBehavior"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
-133
View File
@@ -1,133 +0,0 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <opensource@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "shortcutedit.h"
#include <QLabel>
#include <QToolButton>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QKeySequenceEdit>
ShortcutEditor::ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent)
: QWidget(parent)
, m_descriptionLabel(new QLabel)
, m_shortcutEdit(shortcutEdit)
, m_shortcutLayout(new QFormLayout)
{
Q_CHECK_PTR(m_shortcutEdit);
QDialogButtonBox * buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Discard);
QVBoxLayout * layout = new QVBoxLayout(this);
layout->addWidget(m_descriptionLabel);
layout->addLayout(m_shortcutLayout);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::clicked, this, [=](QAbstractButton *button){
if ((QPushButton *)button == buttons->button(QDialogButtonBox::Apply)) {
applyShortcuts();
} else {
reloadShortcuts();
}
});
connect(shortcutEdit, &ShortcutEdit::shortcutsChanged, this, &ShortcutEditor::reloadShortcuts);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
reloadShortcuts();
}
ShortcutEditor::~ShortcutEditor()
{
}
void ShortcutEditor::setDescription(const QString &desc)
{
m_descriptionLabel->setText(desc);
}
void ShortcutEditor::reloadShortcuts()
{
if (!m_keySequenceEdits.isEmpty()) {
for (QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) {
m_shortcutLayout->removeRow(keyseqEdit);
}
m_keySequenceEdits.clear();
}
QList<QKeySequence> shortcuts = m_shortcutEdit->shortcuts();
shortcuts.append(QKeySequence());
for (const QKeySequence & shortcut : shortcuts) {
QKeySequenceEdit * keyseqEdit = new QKeySequenceEdit(this);
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
keyseqEdit->setClearButtonEnabled(true);
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
keyseqEdit->setMaximumSequenceLength(1);
#endif // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
keyseqEdit->setKeySequence(shortcut);
m_keySequenceEdits.append(keyseqEdit);
}
for (int i = 0; i < m_keySequenceEdits.count(); i++) {
m_shortcutLayout->addRow(tr("Shortcut #%1").arg(i + 1), m_keySequenceEdits.at(i));
}
}
void ShortcutEditor::applyShortcuts()
{
QList<QKeySequence> shortcuts;
for (const QKeySequenceEdit * keyseqEdit : m_keySequenceEdits) {
if (!keyseqEdit->keySequence().isEmpty() && !shortcuts.contains(keyseqEdit->keySequence())) {
shortcuts.append(keyseqEdit->keySequence());
}
}
emit m_shortcutEdit->applyShortcutsRequested(shortcuts);
}
// ----------------------------------------
ShortcutEdit::ShortcutEdit(QWidget *parent)
: QWidget(parent)
, m_shortcutsLabel(new QLabel(this))
, m_setShortcutButton(new QToolButton(this))
{
m_setShortcutButton->setText("...");
QHBoxLayout * layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_shortcutsLabel, 1);
layout->addWidget(m_setShortcutButton);
connect(this, &ShortcutEdit::shortcutsChanged, this, [=](){
QStringList shortcutTexts;
for (const QKeySequence & shortcut : m_shortcuts) {
shortcutTexts.append(shortcut.toString());
}
m_shortcutsLabel->setText(shortcutTexts.isEmpty() ? tr("No shortcuts") : shortcutTexts.join(", "));
m_shortcutsLabel->setDisabled(shortcutTexts.isEmpty());
});
connect(m_setShortcutButton, &QToolButton::clicked, this, &ShortcutEdit::editButtonClicked);
adjustSize();
}
ShortcutEdit::~ShortcutEdit()
{
}
QList<QKeySequence> ShortcutEdit::shortcuts() const
{
return m_shortcuts;
}
void ShortcutEdit::setShortcuts(const QList<QKeySequence> &shortcuts)
{
m_shortcuts = shortcuts;
emit shortcutsChanged();
}
-55
View File
@@ -1,55 +0,0 @@
// SPDX-FileCopyrightText: 2024 Gary Wang <opensource@blumia.net>
//
// SPDX-License-Identifier: MIT
#pragma once
#include <QWidget>
#include <QList>
#include <QKeySequence>
class QLabel;
class QFormLayout;
class QToolButton;
class QKeySequenceEdit;
class ShortcutEdit;
class ShortcutEditor : public QWidget
{
Q_OBJECT
public:
explicit ShortcutEditor(ShortcutEdit * shortcutEdit, QWidget * parent = nullptr);
~ShortcutEditor();
void setDescription(const QString & desc);
void reloadShortcuts();
void applyShortcuts();
private:
QLabel * m_descriptionLabel;
ShortcutEdit * m_shortcutEdit;
QFormLayout * m_shortcutLayout;
QList<QKeySequenceEdit *> m_keySequenceEdits;
};
class ShortcutEdit : public QWidget
{
Q_OBJECT
Q_PROPERTY(QList<QKeySequence> shortcuts MEMBER m_shortcuts WRITE setShortcuts NOTIFY shortcutsChanged)
public:
explicit ShortcutEdit(QWidget * parent = nullptr);
~ShortcutEdit();
QList<QKeySequence> shortcuts() const;
void setShortcuts(const QList<QKeySequence> &shortcuts);
signals:
void shortcutsChanged();
void editButtonClicked();
void applyShortcutsRequested(QList<QKeySequence> newShortcuts);
private:
QList<QKeySequence> m_shortcuts;
QLabel * m_shortcutsLabel;
QToolButton * m_setShortcutButton;
};
-38
View File
@@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#include "toolbutton.h"
#include "actionmanager.h"
#include "opacityhelper.h"
#include <QPainter>
#include <QGraphicsOpacityEffect>
#include <QPropertyAnimation>
ToolButton::ToolButton(bool hoverColor, QWidget *parent)
: QPushButton(parent)
, m_opacityHelper(new OpacityHelper(this))
{
setFlat(true);
QString qss = "QPushButton {"
"background: transparent;"
"}";
if (hoverColor) {
qss += "QPushButton:hover {"
"background: red;"
"}";
}
setStyleSheet(qss);
}
void ToolButton::setIconResourcePath(const QString &iconp)
{
this->setIcon(ActionManager::loadHidpiIcon(iconp, this->iconSize()));
}
void ToolButton::setOpacity(qreal opacity, bool animated)
{
m_opacityHelper->setOpacity(opacity, animated);
}
-25
View File
@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com>
//
// SPDX-License-Identifier: MIT
#ifndef TOOLBUTTON_H
#define TOOLBUTTON_H
#include <QPushButton>
class OpacityHelper;
class ToolButton : public QPushButton
{
Q_OBJECT
public:
ToolButton(bool hoverColor = false, QWidget * parent = nullptr);
void setIconResourcePath(const QString &iconp);
public slots:
void setOpacity(qreal opacity, bool animated = true);
private:
OpacityHelper * m_opacityHelper;
};
#endif // TOOLBUTTON_H
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+935
View File
@@ -0,0 +1,935 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="sl">
<context>
<name>AboutDialog</name>
<message>
<location filename="../aboutdialog.cpp" line="29"/>
<source>About</source>
<translation>O</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="32"/>
<source>Launch application with image file path as argument to load the file.</source>
<translation>Zaženite aplikacijo s potjo do slikovne datoteke kot argumentom za nalaganje datoteke.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="33"/>
<source>Drag and drop image file onto the window is also supported.</source>
<translation>Podprta je tudi funkcija povleci in spusti, da povlečete slikovno datoteko v okno.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="34"/>
<source>None of the operations in this application will alter the pictures on disk.</source>
<translation>Nobena od operacij v tej aplikaciji ne bo spremenila slik na disku.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="35"/>
<source>Context menu option explanation:</source>
<translation>Razlaga možnosti kontekstnega menija:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="42"/>
<source>Make window stay on top of all other windows.</source>
<translation>Okno naj ostane nad vsemi drugimi okni.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="45"/>
<source>Avoid close window accidentally. (eg. by double clicking the window)</source>
<translation>Izogibajte se nenamernemu zapiranju okna (npr. z dvojnim klikom na okno)</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="48"/>
<source>Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.</source>
<translation>Pri preklapljanju med slikami se izogibajte ponastavitvi stanja povečave/vrtenja/preobrata, ki je bilo uporabljeno za pogled slike.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="55"/>
<source>Version: %1</source>
<translation>Različica: %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="66"/>
<source>Logo designed by %1</source>
<translation>Logotip je oblikoval %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="68"/>
<source>Built with Qt %1 (%2)</source>
<translation>Zgrajeno s Qt %1 (%2)</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="69"/>
<source>Source code</source>
<translation>Izvorna koda</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="79"/>
<source>Contributors</source>
<translation>Sodelavci</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="81"/>
<source>List of contributors on GitHub</source>
<translation>Seznam sodelavcev na GitHubu</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="82"/>
<source>Thanks to all people who contributed to this project.</source>
<translation>Hvala vsem ljudem, ki so prispevali k temu projektu.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="86"/>
<source>Translators</source>
<translation>Prevajalci</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="87"/>
<source>I would like to thank the following people who volunteered to translate this application.</source>
<translation>Rad bi se zahvalil naslednjim ljudem, ki so se prostovoljno javili za prevod te aplikacije.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="131"/>
<source>%1 is built on the following free software libraries:</source>
<comment>Free as in freedom</comment>
<translation>%1 je zgrajen na naslednjih knjižnicah brezplačne programske opreme:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="155"/>
<source>&amp;Special Thanks</source>
<translation>&amp;Posebna zahvala</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="157"/>
<source>&amp;Third-party Libraries</source>
<translation>&amp;Knjižnice tretjih oseb</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="93"/>
<source>Your Rights</source>
<translation>Vaše pravice</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="63"/>
<source>Copyright (c) %1 %2</source>
<comment>%1 is year, %2 is the name of copyright holder(s)</comment>
<translation>Avtorske pravice (c) %1 %2</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="95"/>
<source>%1 is released under the MIT License.</source>
<translation>%1 je izdan pod licenco MIT.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="96"/>
<source>This license grants people a number of freedoms:</source>
<translation>Ta licenca daje ljudem številne svoboščine:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="97"/>
<source>You are free to use %1, for any purpose</source>
<translation>%1 lahko uporabljate za kakršen koli namen</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="98"/>
<source>You are free to distribute %1</source>
<translation>Prosto lahko distribuirate %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="99"/>
<source>You can study how %1 works and change it</source>
<translation>Lahko preučite, kako deluje %1, in ga spremenite</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="100"/>
<source>You can distribute changed versions of %1</source>
<translation>Spremenjene različice %1 lahko distribuirate</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="102"/>
<source>The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.</source>
<translation>Licenca MIT vam zagotavlja to svobodo. Nihče vam je ne sme vzeti.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="130"/>
<source>Third-party Libraries used by %1</source>
<translation>Knjižnice tretjih oseb, ki jih uporablja %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="153"/>
<source>&amp;Help</source>
<translation>&amp;Pomoč</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="154"/>
<source>&amp;About</source>
<translation>&amp;O</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="156"/>
<source>&amp;License</source>
<translation>&amp;Licenca</translation>
</message>
</context>
<context>
<name>GraphicsScene</name>
<message>
<location filename="../mainwindow.cpp" line="293"/>
<location filename="../graphicsscene.cpp" line="102"/>
<source>Drag image here</source>
<translation>Povlecite sliko sem</translation>
</message>
</context>
<context>
<name>GraphicsView</name>
<message>
<location filename="../graphicsview.cpp" line="51"/>
<source>File is not a valid image</source>
<translation>Datoteka ni veljavna slika</translation>
</message>
<message>
<location filename="../graphicsview.cpp" line="55"/>
<location filename="../graphicsview.cpp" line="60"/>
<location filename="../graphicsview.cpp" line="71"/>
<source>Image data is invalid or currently unsupported</source>
<translation>Podatki o sliki so neveljavni ali trenutno niso podprti</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="191"/>
<location filename="../mainwindow.cpp" line="568"/>
<source>File url list is empty</source>
<translation>Seznam url-jev datotek je prazen</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="462"/>
<source>&amp;Copy</source>
<translation>&amp;Kopiraj</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Image data is invalid</source>
<translation>Podatki slike so neveljavni</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="583"/>
<source>Not supported mimedata: %1</source>
<translation>Ni podprtih podatkov mime: %1</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<location filename="../mainwindow.cpp" line="727"/>
<location filename="../mainwindow.cpp" line="734"/>
<location filename="../mainwindow.cpp" line="764"/>
<source>Save As</source>
<translation>Shrani kot</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<source>No image is currently open.</source>
<translation>Trenutno ni odprta nobena slika.</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="722"/>
<source>%1 Image (*.%2)</source>
<translation>Slika %1 (*.%2)</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="728"/>
<source>No supported image formats are available.</source>
<translation>Nobenih podprtih formatov slik ni na razpolago.</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="765"/>
<source>Failed to save image: %1</source>
<translation>Shranjevanje slike ni uspelo: %1</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="854"/>
<source>Image From Clipboard</source>
<translation>Slika iz odložišča</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="872"/>
<source>Are you sure you want to move &quot;%1&quot; to recycle bin?</source>
<translation>Ali ste prepričani, da želite premakniti &quot;%1&quot; v koš?</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="876"/>
<source>Failed to move file to trash</source>
<translation>Datoteke ni bilo mogoče premakniti v koš</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="877"/>
<source>Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation.</source>
<translation>Premik v koš ni uspel. Morda je vzrok težava z dovoljenji za datoteke, omejitev datotečnega sistema ali omejitev platforme.</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="108"/>
<source>Copy P&amp;ixmap</source>
<translation>Kopiraj P&amp;ixmapo</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="109"/>
<source>Copy &amp;File Path</source>
<translation>Kopiraj &amp;pot datoteke</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="127"/>
<source>Properties</source>
<translation>Lastnosti</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="112"/>
<location filename="../aboutdialog.cpp" line="41"/>
<source>Stay on top</source>
<translation>Ostani na vrhu</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="113"/>
<location filename="../aboutdialog.cpp" line="44"/>
<source>Protected mode</source>
<translation>Zaščiten način</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="114"/>
<location filename="../aboutdialog.cpp" line="47"/>
<source>Keep transformation</source>
<comment>The &apos;transformation&apos; means the flip/rotation status that currently applied to the image view</comment>
<translation>Ohrani transformacijo</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="92"/>
<source>Zoom in</source>
<translation>Povečaj</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="88"/>
<source>Save &amp;As...</source>
<translation>Shrani &amp;kot...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="93"/>
<source>Zoom out</source>
<translation>Pomanjšaj</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="101"/>
<source>Pause/Resume Animation</source>
<translation>Začasna ustavitev/nadaljevanje animacije</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="102"/>
<source>Animation Go to Next Frame</source>
<translation>Animacija Pojdi na naslednji okvir</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="104"/>
<source>Flip &amp;Horizontally</source>
<translation>Obrni &amp;vodoravno</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="105"/>
<source>Fit to view</source>
<translation>Prilagodi pogledu</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="106"/>
<source>Fit to width</source>
<translation>Prilagodi širini</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="107"/>
<source>Fit long image</source>
<translation>Prilagodi dolgi sliki</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="110"/>
<source>&amp;Paste</source>
<translation>&amp;Prilepi</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="94"/>
<source>Toggle Checkerboard</source>
<translation>Preklopi šahovnico</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="87"/>
<source>&amp;Open...</source>
<translation>&amp;Odpri...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="90"/>
<source>Actual size</source>
<translation>Dejanska velikost</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="91"/>
<source>Toggle maximize</source>
<translation>Preklopi maksimiranje</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="95"/>
<source>Rotate right</source>
<translation>Zavrti desno</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="96"/>
<source>Rotate left</source>
<translation>Zavrti levo</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="98"/>
<source>Previous image</source>
<translation>Prejšnja slika</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="99"/>
<source>Next image</source>
<translation>Naslednja slika</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="871"/>
<location filename="../actionmanager.cpp" line="111"/>
<source>Move to Trash</source>
<translation>Premakni v koš</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="115"/>
<source>Configure...</source>
<translation>Konfiguriraj...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="116"/>
<source>Help</source>
<translation>Pomoč</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="119"/>
<source>Show in File Explorer</source>
<comment>File Explorer is the name of explorer.exe under Windows</comment>
<translation>Prikaži v Raziskovalcu</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="125"/>
<source>Show in directory</source>
<translation>Prikaži v imeniku</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="128"/>
<source>Quit</source>
<translation>Izhod</translation>
</message>
</context>
<context>
<name>MetadataDialog</name>
<message>
<location filename="../metadatadialog.cpp" line="84"/>
<source>Image Metadata</source>
<translation>Metapodatki slike</translation>
</message>
</context>
<context>
<name>MetadataModel</name>
<message>
<location filename="../metadatamodel.cpp" line="43"/>
<source>Origin</source>
<comment>Section name.</comment>
<translation>Izvor</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="44"/>
<source>Image</source>
<comment>Section name.</comment>
<translation>Slika</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="48"/>
<source>File</source>
<comment>Section name.</comment>
<translation>Datoteka</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="45"/>
<source>Camera</source>
<comment>Section name.</comment>
<translation>Kamera</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="35"/>
<source>%1 File</source>
<translation>%1 Datoteka</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="42"/>
<source>Description</source>
<comment>Section name.</comment>
<translation>Opis</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="46"/>
<source>Advanced photo</source>
<comment>Section name.</comment>
<translation>Napredna fotografija</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="47"/>
<source>GPS</source>
<comment>Section name.</comment>
<translation>GPS</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="52"/>
<source>Dimensions</source>
<translation>Dimenzije</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="54"/>
<source>Aspect ratio</source>
<translation>Razmerje stranic</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="58"/>
<source>Frame count</source>
<translation>Število sličic</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="62"/>
<source>Name</source>
<translation>Ime</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="64"/>
<source>Item type</source>
<translation>Vrsta elementa</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="66"/>
<source>Folder path</source>
<translation>Pot do imenika</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="68"/>
<source>Size</source>
<translation>Velikost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="70"/>
<source>Date created</source>
<translation>Datum nastanka</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="72"/>
<source>Date modified</source>
<translation>Datum spremembe</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="79"/>
<source>Title</source>
<translation>Naslov</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="81"/>
<source>Subject</source>
<translation>Zadeva</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="83"/>
<source>Rating</source>
<translation>Ocena</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="85"/>
<source>Tags</source>
<translation>Oznake</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="87"/>
<source>Comments</source>
<translation>Komentarji</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="90"/>
<source>Authors</source>
<translation>Avtorji</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="92"/>
<source>Date taken</source>
<translation>Datum posnetka</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="98"/>
<source>Program name</source>
<translation>Ime programa</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="100"/>
<source>Copyright</source>
<translation>Avtorske pravice</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="103"/>
<source>Horizontal resolution</source>
<translation>Horizontalna ločljivost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="105"/>
<source>Vertical resolution</source>
<translation>Navpična ločljivost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="107"/>
<source>Resolution unit</source>
<translation>Enota ločljivosti</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="109"/>
<source>Colour representation</source>
<translation>Barvna predstavitev</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="112"/>
<source>Camera maker</source>
<translation>Izdelovalec kamer</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="114"/>
<source>Camera model</source>
<translation>Model kamere</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="116"/>
<source>F-stop</source>
<translation>F-stop</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="118"/>
<source>Exposure time</source>
<translation>Čas izpostavljenosti</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="120"/>
<source>ISO speed</source>
<translation>Hitrost ISO</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="122"/>
<source>Exposure bias</source>
<translation>Pristranskost izpostavljenosti</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="124"/>
<source>Focal length</source>
<translation>Goriščna razdalja</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="126"/>
<source>Max aperture</source>
<translation>Največja zaslonka</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="128"/>
<source>Metering mode</source>
<translation>Način merjenja</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="130"/>
<source>Subject distance</source>
<translation>Razdalja motiva</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="132"/>
<source>Flash mode</source>
<translation>Način bliskavice</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="134"/>
<source>35mm focal length</source>
<translation>Goriščna razdalja 35 mm</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="137"/>
<source>Lens model</source>
<translation>Model objektiva</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="139"/>
<source>Contrast</source>
<translation>Kontrast</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="141"/>
<source>Brightness</source>
<translation>Svetlost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="143"/>
<source>Exposure program</source>
<translation>Program izpostavljenosti</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="145"/>
<source>Saturation</source>
<translation>Nasičenost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="147"/>
<source>Sharpness</source>
<translation>Ostrina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="149"/>
<source>White balance</source>
<translation>Ravnovesje beline</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="151"/>
<source>Digital zoom</source>
<translation>Digitalna povečava</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="153"/>
<source>EXIF version</source>
<translation>Različica EXIF</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="156"/>
<source>Latitude reference</source>
<translation>Referenčna zemljepisna širina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="158"/>
<source>Latitude</source>
<translation>Zemljepisna širina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="160"/>
<source>Longitude reference</source>
<translation>Referenčna dolžina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="162"/>
<source>Longitude</source>
<translation>Zemljepisna dolžina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="164"/>
<source>Altitude reference</source>
<translation>Referenčna višina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="166"/>
<source>Altitude</source>
<translation>Nadmorska višina</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="176"/>
<source>%1 x %2</source>
<translation>%1 x %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="194"/>
<source>%1 : %2</source>
<translation>%1 : %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Property</source>
<translation>Lastnost</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Value</source>
<translation>Vrednost</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="../settingsdialog.cpp" line="33"/>
<source>Settings</source>
<translation>Nastavitve</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="41"/>
<source>Options</source>
<translation>Možnosti</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="53"/>
<source>Shortcuts</source>
<translation>Bližnjice</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="63"/>
<source>Editing shortcuts for action &quot;%1&quot;:</source>
<translation>Urejanje bližnjic za dejanje &quot;%1&quot;:</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="72"/>
<source>Failed to set shortcuts</source>
<translation>Nastavitev bližnjic ni uspela</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="73"/>
<source>Please check if shortcuts are duplicated with existing shortcuts.</source>
<translation>Preverite, ali so bližnjice podvojene z obstoječimi bližnjicami.</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="80"/>
<source>Do nothing</source>
<translation>Ne naredi ničesar</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="81"/>
<source>Close the window</source>
<translation>Zapri okno</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="82"/>
<source>Toggle maximize</source>
<translation>Preklopi maksimiranje</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="83"/>
<source>Toggle fullscreen</source>
<translation>Preklopi celozaslonski način</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="87"/>
<source>Zoom in and out</source>
<translation>Povečava in pomanjšava</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="88"/>
<source>View next or previous item</source>
<translation>Ogled naslednjega ali prejšnjega elementa</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="92"/>
<source>Auto size</source>
<translation>Samodejna velikost</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="93"/>
<source>Maximized</source>
<translation>Maksimizirano</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="94"/>
<source>Windowed</source>
<translation>Okenski način</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="98"/>
<source>Round (Integer scaling)</source>
<comment>This option means round up for .5 and above</comment>
<translation>Zaokroženo (celoštevilsko skaliranje)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="99"/>
<source>Ceil (Integer scaling)</source>
<comment>This option means always round up</comment>
<translation>Zaokroževanje navzgor (celoštevilsko skaliranje)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="100"/>
<source>Floor (Integer scaling)</source>
<comment>This option means always round down</comment>
<translation>Zaokroževanje navzdol (celoštevilsko skaliranje)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="101"/>
<source>Follow system (Fractional scaling)</source>
<comment>This option means don&apos;t round</comment>
<translation>Sledi sistemu (delno skaliranje)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="124"/>
<source>Stay on top when start-up</source>
<translation>Ostani na vrhu pri zagonu</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="125"/>
<source>Use built-in close window animation</source>
<translation>Uporabi vgrajeno animacijo zapiranja okna</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="126"/>
<source>Use light-color checkerboard</source>
<translation>Uporabite svetlo šahovnico</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="127"/>
<source>Loop the loaded gallery</source>
<translation>Ponavljaj naloženo galerijo</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="128"/>
<source>Auto long image mode</source>
<translation>Samodejni način dolge slike</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="129"/>
<source>Limit SVG support to SVG Tiny 1.2</source>
<translation>Omeji podporo za SVG na SVG Tiny 1.2</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="130"/>
<source>Double-click behavior</source>
<translation>Obnašanje dvojnega klika</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="131"/>
<source>Mouse wheel behavior</source>
<translation>Obnašanje miškinega kolesca</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="132"/>
<source>Default window size</source>
<translation>Privzeta velikost okna</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="133"/>
<source>HiDPI scale factor rounding policy</source>
<translation>Politika zaokroževanja faktorja merila HiDPI</translation>
</message>
</context>
<context>
<name>ShortcutEdit</name>
<message>
<location filename="../shortcutedit.cpp" line="104"/>
<source>No shortcuts</source>
<translation>Brez bližnjic</translation>
</message>
</context>
<context>
<name>ShortcutEditor</name>
<message>
<location filename="../shortcutedit.cpp" line="70"/>
<source>Shortcut #%1</source>
<translation>Bližnjica #%1</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../main.cpp" line="42"/>
<source>Pineapple Pictures</source>
<translation>Pineapple Slike</translation>
</message>
<message>
<location filename="../main.cpp" line="45"/>
<source>List supported image format suffixes, and quit program.</source>
<translation>Naštej podprte pripone slikovnih formatov in zapri program.</translation>
</message>
<message>
<location filename="../main.cpp" line="49"/>
<source>File list.</source>
<translation>Seznam datotek.</translation>
</message>
</context>
</TS>
+935
View File
@@ -0,0 +1,935 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ta">
<context>
<name>AboutDialog</name>
<message>
<location filename="../aboutdialog.cpp" line="29"/>
<source>About</source>
<translation>ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="32"/>
<source>Launch application with image file path as argument to load the file.</source>
<translation> .</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="33"/>
<source>Drag and drop image file onto the window is also supported.</source>
<translation> ி ி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="34"/>
<source>None of the operations in this application will alter the pictures on disk.</source>
<translation> ி ி .</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="35"/>
<source>Context menu option explanation:</source>
<translation> ி ி ி:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="42"/>
<source>Make window stay on top of all other windows.</source>
<translation> .</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="45"/>
<source>Avoid close window accidentally. (eg. by double clicking the window)</source>
<translation> ி. (.. )</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="48"/>
<source>Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.</source>
<translation> ி ி /ி/ிி ி ி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="55"/>
<source>Version: %1</source>
<translation>ி: %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="66"/>
<source>Logo designed by %1</source>
<translation> %1 ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="68"/>
<source>Built with Qt %1 (%2)</source>
<translation>ிி %1 ( %2) </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="69"/>
<source>Source code</source>
<translation> ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="79"/>
<source>Contributors</source>
<translation>ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="81"/>
<source>List of contributors on GitHub</source>
<translation>ிி ிி ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="82"/>
<source>Thanks to all people who contributed to this project.</source>
<translation> ிி ி ி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="86"/>
<source>Translators</source>
<translation>ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="87"/>
<source>I would like to thank the following people who volunteered to translate this application.</source>
<translation> ி ி ி ி ிி ிி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="131"/>
<source>%1 is built on the following free software libraries:</source>
<comment>Free as in freedom</comment>
<translation>%1 ி ி :</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="155"/>
<source>&amp;Special Thanks</source>
<translation>&amp; ி ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="157"/>
<source>&amp;Third-party Libraries</source>
<translation> </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="93"/>
<source>Your Rights</source>
<translation> ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="63"/>
<source>Copyright (c) %1 %2</source>
<comment>%1 is year, %2 is the name of copyright holder(s)</comment>
<translation>ிி (ி) %1 %2</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="95"/>
<source>%1 is released under the MIT License.</source>
<translation>%1 ி ிி ிிி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="96"/>
<source>This license grants people a number of freedoms:</source>
<translation> ி ி ி:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="97"/>
<source>You are free to use %1, for any purpose</source>
<translation> ி %1 </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="98"/>
<source>You are free to distribute %1</source>
<translation>%1 ிிி </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="99"/>
<source>You can study how %1 works and change it</source>
<translation>%1 ி ி </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="100"/>
<source>You can distribute changed versions of %1</source>
<translation> ி %1 ிிி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="102"/>
<source>The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.</source>
<translation>ி ி ி ி. ிி.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="130"/>
<source>Third-party Libraries used by %1</source>
<translation>%1 </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="153"/>
<source>&amp;Help</source>
<translation>ி (&amp;h)</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="154"/>
<source>&amp;About</source>
<translation>&amp;ி</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="156"/>
<source>&amp;License</source>
<translation>&amp; ி</translation>
</message>
</context>
<context>
<name>GraphicsScene</name>
<message>
<location filename="../mainwindow.cpp" line="293"/>
<location filename="../graphicsscene.cpp" line="102"/>
<source>Drag image here</source>
<translation> </translation>
</message>
</context>
<context>
<name>GraphicsView</name>
<message>
<location filename="../graphicsview.cpp" line="51"/>
<source>File is not a valid image</source>
<translation> ி </translation>
</message>
<message>
<location filename="../graphicsview.cpp" line="55"/>
<location filename="../graphicsview.cpp" line="60"/>
<location filename="../graphicsview.cpp" line="71"/>
<source>Image data is invalid or currently unsupported</source>
<translation> ி</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="191"/>
<location filename="../mainwindow.cpp" line="568"/>
<source>File url list is empty</source>
<translation> ி ி ி </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="462"/>
<source>&amp;Copy</source>
<translation> (&amp;c)</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Image data is invalid</source>
<translation> </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="583"/>
<source>Not supported mimedata: %1</source>
<translation> ிி: %1</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<location filename="../mainwindow.cpp" line="727"/>
<location filename="../mainwindow.cpp" line="734"/>
<location filename="../mainwindow.cpp" line="764"/>
<source>Save As</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<source>No image is currently open.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="722"/>
<source>%1 Image (*.%2)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="728"/>
<source>No supported image formats are available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="765"/>
<source>Failed to save image: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="854"/>
<source>Image From Clipboard</source>
<translation>ிிிி </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="872"/>
<source>Are you sure you want to move &quot;%1&quot; to recycle bin?</source>
<translation>ி ி &quot;%1&quot; ிி?</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="876"/>
<source>Failed to move file to trash</source>
<translation> ி ி</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="877"/>
<source>Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation.</source>
<translation> ி, ி, ி .</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="108"/>
<source>Copy P&amp;ixmap</source>
<translation>ி &amp; </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="109"/>
<source>Copy &amp;File Path</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="127"/>
<source>Properties</source>
<translation></translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="112"/>
<location filename="../aboutdialog.cpp" line="41"/>
<source>Stay on top</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="113"/>
<location filename="../aboutdialog.cpp" line="44"/>
<source>Protected mode</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="114"/>
<location filename="../aboutdialog.cpp" line="47"/>
<source>Keep transformation</source>
<comment>The &apos;transformation&apos; means the flip/rotation status that currently applied to the image view</comment>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="92"/>
<source>Zoom in</source>
<translation>ி</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="88"/>
<source>Save &amp;As...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="93"/>
<source>Zoom out</source>
<translation>ிி</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="101"/>
<source>Pause/Resume Animation</source>
<translation>ி/ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="102"/>
<source>Animation Go to Next Frame</source>
<translation>ி ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="104"/>
<source>Flip &amp;Horizontally</source>
<translation>ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="105"/>
<source>Fit to view</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="106"/>
<source>Fit to width</source>
<translation>ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="107"/>
<source>Fit long image</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="110"/>
<source>&amp;Paste</source>
<translation> (&amp;p)</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="94"/>
<source>Toggle Checkerboard</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="87"/>
<source>&amp;Open...</source>
<translation>&amp; ி ...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="90"/>
<source>Actual size</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="91"/>
<source>Toggle maximize</source>
<translation>ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="95"/>
<source>Rotate right</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="96"/>
<source>Rotate left</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="98"/>
<source>Previous image</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="99"/>
<source>Next image</source>
<translation> </translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="871"/>
<location filename="../actionmanager.cpp" line="111"/>
<source>Move to Trash</source>
<translation> </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="115"/>
<source>Configure...</source>
<translation> ...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="116"/>
<source>Help</source>
<translation>ி</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="119"/>
<source>Show in File Explorer</source>
<comment>File Explorer is the name of explorer.exe under Windows</comment>
<translation> ி ி</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="125"/>
<source>Show in directory</source>
<translation>ி </translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="128"/>
<source>Quit</source>
<translation>ி</translation>
</message>
</context>
<context>
<name>MetadataDialog</name>
<message>
<location filename="../metadatadialog.cpp" line="84"/>
<source>Image Metadata</source>
<translation> ி </translation>
</message>
</context>
<context>
<name>MetadataModel</name>
<message>
<location filename="../metadatamodel.cpp" line="43"/>
<source>Origin</source>
<comment>Section name.</comment>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="44"/>
<source>Image</source>
<comment>Section name.</comment>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="48"/>
<source>File</source>
<comment>Section name.</comment>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="45"/>
<source>Camera</source>
<comment>Section name.</comment>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="35"/>
<source>%1 File</source>
<translation>%1 </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="42"/>
<source>Description</source>
<comment>Section name.</comment>
<translation>ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="46"/>
<source>Advanced photo</source>
<comment>Section name.</comment>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="47"/>
<source>GPS</source>
<comment>Section name.</comment>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="52"/>
<source>Dimensions</source>
<translation>ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="54"/>
<source>Aspect ratio</source>
<translation> ிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="58"/>
<source>Frame count</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="62"/>
<source>Name</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="64"/>
<source>Item type</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="66"/>
<source>Folder path</source>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="68"/>
<source>Size</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="70"/>
<source>Date created</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="72"/>
<source>Date modified</source>
<translation>ி ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="79"/>
<source>Title</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="81"/>
<source>Subject</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="83"/>
<source>Rating</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="85"/>
<source>Tags</source>
<translation>ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="87"/>
<source>Comments</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="90"/>
<source>Authors</source>
<translation>ிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="92"/>
<source>Date taken</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="98"/>
<source>Program name</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="100"/>
<source>Copyright</source>
<translation>ிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="103"/>
<source>Horizontal resolution</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="105"/>
<source>Vertical resolution</source>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="107"/>
<source>Resolution unit</source>
<translation>ிி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="109"/>
<source>Colour representation</source>
<translation> ிிிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="112"/>
<source>Camera maker</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="114"/>
<source>Camera model</source>
<translation> ிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="116"/>
<source>F-stop</source>
<translation>-</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="118"/>
<source>Exposure time</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="120"/>
<source>ISO speed</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="122"/>
<source>Exposure bias</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="124"/>
<source>Focal length</source>
<translation>ி, ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="126"/>
<source>Max aperture</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="128"/>
<source>Metering mode</source>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="130"/>
<source>Subject distance</source>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="132"/>
<source>Flash mode</source>
<translation>ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="134"/>
<source>35mm focal length</source>
<translation>35 ி ி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="137"/>
<source>Lens model</source>
<translation> ிி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="139"/>
<source>Contrast</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="141"/>
<source>Brightness</source>
<translation>ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="143"/>
<source>Exposure program</source>
<translation>ி ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="145"/>
<source>Saturation</source>
<translation>ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="147"/>
<source>Sharpness</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="149"/>
<source>White balance</source>
<translation> </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="151"/>
<source>Digital zoom</source>
<translation>ிி </translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="153"/>
<source>EXIF version</source>
<translation>Exif ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="156"/>
<source>Latitude reference</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="158"/>
<source>Latitude</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="160"/>
<source>Longitude reference</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="162"/>
<source>Longitude</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="164"/>
<source>Altitude reference</source>
<translation> ி</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="166"/>
<source>Altitude</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="176"/>
<source>%1 x %2</source>
<translation>%1 %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="194"/>
<source>%1 : %2</source>
<translation>%1: %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Property</source>
<translation></translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Value</source>
<translation>ி</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="../settingsdialog.cpp" line="33"/>
<source>Settings</source>
<translation></translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="41"/>
<source>Options</source>
<translation>ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="53"/>
<source>Shortcuts</source>
<translation>ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="63"/>
<source>Editing shortcuts for action &quot;%1&quot;:</source>
<translation> ி ி &quot;%1&quot;:</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="72"/>
<source>Failed to set shortcuts</source>
<translation>ி ி ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="73"/>
<source>Please check if shortcuts are duplicated with existing shortcuts.</source>
<translation> ி ி ி.</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="80"/>
<source>Do nothing</source>
<translation> </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="81"/>
<source>Close the window</source>
<translation> </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="82"/>
<source>Toggle maximize</source>
<translation>ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="83"/>
<source>Toggle fullscreen</source>
<translation> ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="87"/>
<source>Zoom in and out</source>
<translation> ி ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="88"/>
<source>View next or previous item</source>
<translation> ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="92"/>
<source>Auto size</source>
<translation> </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="93"/>
<source>Maximized</source>
<translation>ி</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="94"/>
<source>Windowed</source>
<translation></translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="98"/>
<source>Round (Integer scaling)</source>
<comment>This option means round up for .5 and above</comment>
<translation> ( ி)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="99"/>
<source>Ceil (Integer scaling)</source>
<comment>This option means always round up</comment>
<translation> ( ி)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="100"/>
<source>Floor (Integer scaling)</source>
<comment>This option means always round down</comment>
<translation>ி ( ி)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="101"/>
<source>Follow system (Fractional scaling)</source>
<comment>This option means don&apos;t round</comment>
<translation>ிி ி (ி ி)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="124"/>
<source>Stay on top when start-up</source>
<translation>ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="125"/>
<source>Use built-in close window animation</source>
<translation> ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="126"/>
<source>Use light-color checkerboard</source>
<translation>ி- </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="127"/>
<source>Loop the loaded gallery</source>
<translation> ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="128"/>
<source>Auto long image mode</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="129"/>
<source>Limit SVG support to SVG Tiny 1.2</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="130"/>
<source>Double-click behavior</source>
<translation> </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="131"/>
<source>Mouse wheel behavior</source>
<translation>ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="132"/>
<source>Default window size</source>
<translation>ி </translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="133"/>
<source>HiDPI scale factor rounding policy</source>
<translation>HIDPI ி ி ி </translation>
</message>
</context>
<context>
<name>ShortcutEdit</name>
<message>
<location filename="../shortcutedit.cpp" line="104"/>
<source>No shortcuts</source>
<translation>ி </translation>
</message>
</context>
<context>
<name>ShortcutEditor</name>
<message>
<location filename="../shortcutedit.cpp" line="70"/>
<source>Shortcut #%1</source>
<translation>ி #%1</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../main.cpp" line="42"/>
<source>Pineapple Pictures</source>
<translation>ி </translation>
</message>
<message>
<location filename="../main.cpp" line="45"/>
<source>List supported image format suffixes, and quit program.</source>
<translation>ி ி ி ிி, ி ி.</translation>
</message>
<message>
<location filename="../main.cpp" line="49"/>
<source>File list.</source>
<translation> ி.</translation>
</message>
</context>
</TS>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+936
View File
@@ -0,0 +1,936 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="vi">
<context>
<name>AboutDialog</name>
<message>
<location filename="../aboutdialog.cpp" line="29"/>
<source>About</source>
<translation>Giới thiệu về</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="32"/>
<source>Launch application with image file path as argument to load the file.</source>
<translation>Khởi chạy ng dụng với đưng dẫn tệp hình nh làm đi số đ tải tệp.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="33"/>
<source>Drag and drop image file onto the window is also supported.</source>
<translation>Kéo thả tệp hình nh vào cửa sổ cũng đưc hỗ trợ.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="34"/>
<source>None of the operations in this application will alter the pictures on disk.</source>
<translation>Không thao tác nào trong ng dụng này sẽ làm thay đi hình nh trên đĩa.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="35"/>
<source>Context menu option explanation:</source>
<translation>Giải thích tùy chọn menu ngữ cảnh:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="42"/>
<source>Make window stay on top of all other windows.</source>
<translation>Đt cửa sổ trên cùng của tất cả các cửa sổ khác.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="45"/>
<source>Avoid close window accidentally. (eg. by double clicking the window)</source>
<translation>Tránh tình đóng cửa sổ. ( dụ: bằng cách nhấp đúp vào cửa sổ)</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="48"/>
<source>Avoid resetting the zoom/rotation/flip state that was applied to the image view when switching between images.</source>
<translation>Tránh đt lại trạng thái zoom/xoay/lật đã đưc áp dụng cho xem hình nh khi chuyển đi giữa các hình nh.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="55"/>
<source>Version: %1</source>
<translation>Phiên bản: %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="66"/>
<source>Logo designed by %1</source>
<translation>Logo đưc thiết kế bởi %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="68"/>
<source>Built with Qt %1 (%2)</source>
<translation>Đưc xây dựng với Qt %1 (%2)</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="69"/>
<source>Source code</source>
<translation> nguồn</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="79"/>
<source>Contributors</source>
<translation>Người đóng góp</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="81"/>
<source>List of contributors on GitHub</source>
<translation>Danh sách những người đóng góp trên GitHub</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="82"/>
<source>Thanks to all people who contributed to this project.</source>
<translation>Cảm ơn tất cả những người đã đóng góp cho dự án này.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="86"/>
<source>Translators</source>
<translation>Vietnam Linux L10n &lt;https://github.com/linux-l10n-vi&gt;
Loc Huynh &lt;https://github.com/hthienloc&gt;</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="87"/>
<source>I would like to thank the following people who volunteered to translate this application.</source>
<translation>Tôi xin cảm ơn những người sau đây đã tình nguyện dịch ng dụng này.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="131"/>
<source>%1 is built on the following free software libraries:</source>
<comment>Free as in freedom</comment>
<translation>%1 đưc xây dựng trên các thư viện phần mềm miễn phí sau:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="155"/>
<source>&amp;Special Thanks</source>
<translation>&amp;Cảm ơn đc biệt</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="157"/>
<source>&amp;Third-party Libraries</source>
<translation>&amp;Thư viện của bên thứ ba</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="93"/>
<source>Your Rights</source>
<translation>Quyền của bạn</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="63"/>
<source>Copyright (c) %1 %2</source>
<comment>%1 is year, %2 is the name of copyright holder(s)</comment>
<translation>Bản quyền (c) %1 %2</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="95"/>
<source>%1 is released under the MIT License.</source>
<translation>%1 đưc phát hành theo Giấy phép MIT.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="96"/>
<source>This license grants people a number of freedoms:</source>
<translation>Giấy phép này cấp cho mọi người một số quyền tự do:</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="97"/>
<source>You are free to use %1, for any purpose</source>
<translation>Bạn đưc tự do sử dụng %1, cho bất kỳ mục đích nào</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="98"/>
<source>You are free to distribute %1</source>
<translation>Bạn đưc tự do phân phối %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="99"/>
<source>You can study how %1 works and change it</source>
<translation>Bạn thể nghiên cứu cách %1 hoạt đng thay đi </translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="100"/>
<source>You can distribute changed versions of %1</source>
<translation>Bạn thể phân phối các phiên bản đã thay đi của %1</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="102"/>
<source>The MIT license guarantees you this freedom. Nobody is ever permitted to take it away.</source>
<translation>Giấy phép MIT đm bảo cho bạn sự tự do này. Không ai đưc phép mang đi.</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="130"/>
<source>Third-party Libraries used by %1</source>
<translation>Thư viện của bên thứ ba đưc %1 sử dụng</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="153"/>
<source>&amp;Help</source>
<translation>&amp;Trợ giúp</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="154"/>
<source>&amp;About</source>
<translation>&amp;Giới thiệu giới thiệu về</translation>
</message>
<message>
<location filename="../aboutdialog.cpp" line="156"/>
<source>&amp;License</source>
<translation>&amp;Giấy phép</translation>
</message>
</context>
<context>
<name>GraphicsScene</name>
<message>
<location filename="../mainwindow.cpp" line="293"/>
<location filename="../graphicsscene.cpp" line="102"/>
<source>Drag image here</source>
<translation>Kéo hình nh vào đây</translation>
</message>
</context>
<context>
<name>GraphicsView</name>
<message>
<location filename="../graphicsview.cpp" line="51"/>
<source>File is not a valid image</source>
<translation>Tệp không phải hình nh hợp lệ</translation>
</message>
<message>
<location filename="../graphicsview.cpp" line="55"/>
<location filename="../graphicsview.cpp" line="60"/>
<location filename="../graphicsview.cpp" line="71"/>
<source>Image data is invalid or currently unsupported</source>
<translation>Dữ liệu hình nh không hợp lệ hoặc hiện không đưc hỗ trợ</translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
<location filename="../mainwindow.cpp" line="191"/>
<location filename="../mainwindow.cpp" line="568"/>
<source>File url list is empty</source>
<translation>Danh sách url tệp trống</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="462"/>
<source>&amp;Copy</source>
<translation>&amp;Sao chép</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="576"/>
<source>Image data is invalid</source>
<translation>Dữ liệu hình nh không hợp lệ</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="583"/>
<source>Not supported mimedata: %1</source>
<translation>Dữ liệu mimedata không đưc hỗ trợ: %1</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<location filename="../mainwindow.cpp" line="727"/>
<location filename="../mainwindow.cpp" line="734"/>
<location filename="../mainwindow.cpp" line="764"/>
<source>Save As</source>
<translation>Lưu dưới dạng</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="710"/>
<source>No image is currently open.</source>
<translation>Hiện tại không hình nh nào đưc mở.</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="722"/>
<source>%1 Image (*.%2)</source>
<translation>Hình nh %1 (*.%2)</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="728"/>
<source>No supported image formats are available.</source>
<translation>Không đnh dạng hình nh đưc hỗ trợ.</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="765"/>
<source>Failed to save image: %1</source>
<translation>Không lưu đưc hình nh: %1</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="854"/>
<source>Image From Clipboard</source>
<translation>Hình nh từ bộ nhớ tạm</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="872"/>
<source>Are you sure you want to move &quot;%1&quot; to recycle bin?</source>
<translation>Bạn chắc chắn muốn chuyển &quot;%1&quot; vào thùng rác không?</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="876"/>
<source>Failed to move file to trash</source>
<translation>Không thể di chuyển tệp vào thùng rác</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="877"/>
<source>Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation.</source>
<translation>Di chuyển vào thùng rác không thành công, nguyên nhân thể do vấn đ về quyền đi với tệp, giới hạn hệ thống tệp hoặc giới hạn nền tảng.</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="108"/>
<source>Copy P&amp;ixmap</source>
<translation>Sao chép P&amp;ixmap</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="109"/>
<source>Copy &amp;File Path</source>
<translation>Sao chép Đưng &amp;dẫn tệp</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="127"/>
<source>Properties</source>
<translation>Thuộc tính</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="112"/>
<location filename="../aboutdialog.cpp" line="41"/>
<source>Stay on top</source>
<translation>Luôn trên đu</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="113"/>
<location filename="../aboutdialog.cpp" line="44"/>
<source>Protected mode</source>
<translation>Chế đ bảo vệ</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="114"/>
<location filename="../aboutdialog.cpp" line="47"/>
<source>Keep transformation</source>
<comment>The &apos;transformation&apos; means the flip/rotation status that currently applied to the image view</comment>
<translation>Giữ sự chuyển đi</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="92"/>
<source>Zoom in</source>
<translation>Phóng to</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="88"/>
<source>Save &amp;As...</source>
<translation>Lưu &amp;dưới dạng...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="93"/>
<source>Zoom out</source>
<translation>Thu nhỏ</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="101"/>
<source>Pause/Resume Animation</source>
<translation>Tạm dừng/Tiếp tục hoạt nh</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="102"/>
<source>Animation Go to Next Frame</source>
<translation>Hoạt hình Chuyển đến khung tiếp theo</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="104"/>
<source>Flip &amp;Horizontally</source>
<translation>Lật &amp;Theo chiều ngang</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="105"/>
<source>Fit to view</source>
<translation>Phù hợp đ xem</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="106"/>
<source>Fit to width</source>
<translation>Vừa với chiều rộng</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="107"/>
<source>Fit long image</source>
<translation>Vừa với hình nh dài</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="110"/>
<source>&amp;Paste</source>
<translation>&amp;Dán</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="94"/>
<source>Toggle Checkerboard</source>
<translation>Chuyển đi bàn cờ</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="87"/>
<source>&amp;Open...</source>
<translation>&amp;Mở...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="90"/>
<source>Actual size</source>
<translation>Kích thước thực tế</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="91"/>
<source>Toggle maximize</source>
<translation>Chuyển đi tối đa hóa</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="95"/>
<source>Rotate right</source>
<translation>Xoay phải</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="96"/>
<source>Rotate left</source>
<translation>Xoay trái</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="98"/>
<source>Previous image</source>
<translation>Hình nh trước đó</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="99"/>
<source>Next image</source>
<translation>Hình nh tiếp theo</translation>
</message>
<message>
<location filename="../mainwindow.cpp" line="871"/>
<location filename="../actionmanager.cpp" line="111"/>
<source>Move to Trash</source>
<translation>Chuyển vào Thùng rác</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="115"/>
<source>Configure...</source>
<translation>Cấu hình...</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="116"/>
<source>Help</source>
<translation>Trợ giúp</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="119"/>
<source>Show in File Explorer</source>
<comment>File Explorer is the name of explorer.exe under Windows</comment>
<translation>Hiển thị trong tệp Explorer</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="125"/>
<source>Show in directory</source>
<translation>Hiển thị trong thư mục</translation>
</message>
<message>
<location filename="../actionmanager.cpp" line="128"/>
<source>Quit</source>
<translation>Thoát</translation>
</message>
</context>
<context>
<name>MetadataDialog</name>
<message>
<location filename="../metadatadialog.cpp" line="84"/>
<source>Image Metadata</source>
<translation>Siêu dữ liệu hình nh</translation>
</message>
</context>
<context>
<name>MetadataModel</name>
<message>
<location filename="../metadatamodel.cpp" line="43"/>
<source>Origin</source>
<comment>Section name.</comment>
<translation>Nguồn</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="44"/>
<source>Image</source>
<comment>Section name.</comment>
<translation>Hình nh</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="48"/>
<source>File</source>
<comment>Section name.</comment>
<translation>Tệp</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="45"/>
<source>Camera</source>
<comment>Section name.</comment>
<translation>Camera</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="35"/>
<source>%1 File</source>
<translation>Tệp %1</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="42"/>
<source>Description</source>
<comment>Section name.</comment>
<translation> tả</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="46"/>
<source>Advanced photo</source>
<comment>Section name.</comment>
<translation>nh nâng cao</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="47"/>
<source>GPS</source>
<comment>Section name.</comment>
<translation>GPS</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="52"/>
<source>Dimensions</source>
<translation>Kích thước</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="54"/>
<source>Aspect ratio</source>
<translation>Tỷ lệ khung hình</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="58"/>
<source>Frame count</source>
<translation>Số khung hình</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="62"/>
<source>Name</source>
<translation>Tên</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="64"/>
<source>Item type</source>
<translation>Loại mặt hàng</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="66"/>
<source>Folder path</source>
<translation>Đưng dẫn thư mục</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="68"/>
<source>Size</source>
<translation>Kích thước</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="70"/>
<source>Date created</source>
<translation>Ngày tạo</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="72"/>
<source>Date modified</source>
<translation>Ngày sửa đi</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="79"/>
<source>Title</source>
<translation>Tiêu đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="81"/>
<source>Subject</source>
<translation>Chủ thể</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="83"/>
<source>Rating</source>
<translation>Đánh giá</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="85"/>
<source>Tags</source>
<translation>Thẻ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="87"/>
<source>Comments</source>
<translation>Bình luận</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="90"/>
<source>Authors</source>
<translation>Các tác giả</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="92"/>
<source>Date taken</source>
<translation>Ngày chụp</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="98"/>
<source>Program name</source>
<translation>Tên chương trình</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="100"/>
<source>Copyright</source>
<translation>Bản quyền</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="103"/>
<source>Horizontal resolution</source>
<translation>Đ phân giải ngang</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="105"/>
<source>Vertical resolution</source>
<translation>Đ phân giải dọc</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="107"/>
<source>Resolution unit</source>
<translation>Đơn vị đ phân giải</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="109"/>
<source>Colour representation</source>
<translation>Thể hiện màu sắc</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="112"/>
<source>Camera maker</source>
<translation>Nhà sản xuất máy nh</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="114"/>
<source>Camera model</source>
<translation>Mẫu máy nh</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="116"/>
<source>F-stop</source>
<translation>F-dừng lại</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="118"/>
<source>Exposure time</source>
<translation>Thời gian phơi nhiễm</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="120"/>
<source>ISO speed</source>
<translation>Tốc đ ISO</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="122"/>
<source>Exposure bias</source>
<translation>Xu hướng tiếp xúc</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="124"/>
<source>Focal length</source>
<translation>Tiêu cự</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="126"/>
<source>Max aperture</source>
<translation>Khẩu đ tối đa</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="128"/>
<source>Metering mode</source>
<translation>Chế đ đo sáng</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="130"/>
<source>Subject distance</source>
<translation>Khoảng cách chủ đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="132"/>
<source>Flash mode</source>
<translation>Chế đ đèn nháy</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="134"/>
<source>35mm focal length</source>
<translation>tiêu cự 35mm</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="137"/>
<source>Lens model</source>
<translation>Mẫu ng kính</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="139"/>
<source>Contrast</source>
<translation>Đ tương phản</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="141"/>
<source>Brightness</source>
<translation>Đ sáng</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="143"/>
<source>Exposure program</source>
<translation>Chương trình phơi sáng</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="145"/>
<source>Saturation</source>
<translation>Đ bão hòa</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="147"/>
<source>Sharpness</source>
<translation>Đ sắc nét</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="149"/>
<source>White balance</source>
<translation>Cân bằng trắng</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="151"/>
<source>Digital zoom</source>
<translation>Zoom kỹ thuật số</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="153"/>
<source>EXIF version</source>
<translation>Phiên bản EXIF</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="156"/>
<source>Latitude reference</source>
<translation>Tham chiếu đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="158"/>
<source>Latitude</source>
<translation> đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="160"/>
<source>Longitude reference</source>
<translation>Tham chiếu kinh đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="162"/>
<source>Longitude</source>
<translation>Kinh đ</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="164"/>
<source>Altitude reference</source>
<translation>Tham chiếu đ cao</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="166"/>
<source>Altitude</source>
<translation>Đ cao</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="176"/>
<source>%1 x %2</source>
<translation>%1 x %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="194"/>
<source>%1 : %2</source>
<translation>%1 : %2</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Property</source>
<translation>Tài sản</translation>
</message>
<message>
<location filename="../metadatamodel.cpp" line="319"/>
<source>Value</source>
<translation>Giá trị</translation>
</message>
</context>
<context>
<name>SettingsDialog</name>
<message>
<location filename="../settingsdialog.cpp" line="33"/>
<source>Settings</source>
<translation>Cài đt</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="41"/>
<source>Options</source>
<translation>Tùy chọn</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="53"/>
<source>Shortcuts</source>
<translation>Phím tắt</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="63"/>
<source>Editing shortcuts for action &quot;%1&quot;:</source>
<translation>Chỉnh sửa phím tắt cho hành đng &quot;%1&quot;:</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="72"/>
<source>Failed to set shortcuts</source>
<translation>Không đt đưc phím tắt</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="73"/>
<source>Please check if shortcuts are duplicated with existing shortcuts.</source>
<translation>Vui lòng kiểm tra xem các phím tắt bị trùng lặp với các phím tắt hiện hay không.</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="80"/>
<source>Do nothing</source>
<translation>Không làm cả</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="81"/>
<source>Close the window</source>
<translation>Đóng cửa sổ</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="82"/>
<source>Toggle maximize</source>
<translation>Chuyển đi tối đa hóa</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="83"/>
<source>Toggle fullscreen</source>
<translation>Chuyển đi toàn màn hình</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="87"/>
<source>Zoom in and out</source>
<translation>Zoom thu nhỏ</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="88"/>
<source>View next or previous item</source>
<translation>Xem mục tiếp theo hoặc trước đó</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="92"/>
<source>Auto size</source>
<translation>Kích thước tự đng</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="93"/>
<source>Maximized</source>
<translation>Tối đa hóa</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="94"/>
<source>Windowed</source>
<translation> cửa sổ</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="98"/>
<source>Round (Integer scaling)</source>
<comment>This option means round up for .5 and above</comment>
<translation>Tròn (Tỷ lệ số nguyên)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="99"/>
<source>Ceil (Integer scaling)</source>
<comment>This option means always round up</comment>
<translation>Ceil (Tỷ lệ số nguyên)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="100"/>
<source>Floor (Integer scaling)</source>
<comment>This option means always round down</comment>
<translation>Tầng (Tỷ lệ số nguyên)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="101"/>
<source>Follow system (Fractional scaling)</source>
<comment>This option means don&apos;t round</comment>
<translation>Hệ thống theo dõi (Chia tỷ lệ phân số)</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="124"/>
<source>Stay on top when start-up</source>
<translation>Luôn dẫn đu khi khởi nghiệp</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="125"/>
<source>Use built-in close window animation</source>
<translation>Sử dụng hoạt nh đóng cửa sổ tích hợp</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="126"/>
<source>Use light-color checkerboard</source>
<translation>Sử dụng bàn cờ màu sáng</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="127"/>
<source>Loop the loaded gallery</source>
<translation>Lặp lại thư viện đã tải</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="128"/>
<source>Auto long image mode</source>
<translation>Chế đ hình nh dài tự đng</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="129"/>
<source>Limit SVG support to SVG Tiny 1.2</source>
<translation>Giới hạn hỗ trợ SVG SVG Tiny 1.2</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="130"/>
<source>Double-click behavior</source>
<translation>Hành vi bấm đúp</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="131"/>
<source>Mouse wheel behavior</source>
<translation>Hành vi của bánh xe chuột</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="132"/>
<source>Default window size</source>
<translation>Kích thước cửa sổ mặc đnh</translation>
</message>
<message>
<location filename="../settingsdialog.cpp" line="133"/>
<source>HiDPI scale factor rounding policy</source>
<translation>Chính sách làm tròn hệ số tỷ lệ HiDPI</translation>
</message>
</context>
<context>
<name>ShortcutEdit</name>
<message>
<location filename="../shortcutedit.cpp" line="104"/>
<source>No shortcuts</source>
<translation>Không phím tắt</translation>
</message>
</context>
<context>
<name>ShortcutEditor</name>
<message>
<location filename="../shortcutedit.cpp" line="70"/>
<source>Shortcut #%1</source>
<translation>Phím tắt #%1</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../main.cpp" line="42"/>
<source>Pineapple Pictures</source>
<translation>Pineapple Pictures</translation>
</message>
<message>
<location filename="../main.cpp" line="45"/>
<source>List supported image format suffixes, and quit program.</source>
<translation>Liệt các hậu tố đnh dạng hình nh đưc hỗ trợ thoát khỏi chương trình.</translation>
</message>
<message>
<location filename="../main.cpp" line="49"/>
<source>File list.</source>
<translation>Danh sách tệp.</translation>
</message>
</context>
</TS>
File diff suppressed because it is too large Load Diff
-166
View File
@@ -1,166 +0,0 @@
image:
- Visual Studio 2022
environment:
CMAKE_INSTALL_PREFIX: C:\projects\cmake
LIBZ: C:\projects\zlib
LIBEXPAT: C:\projects\libexpat
LIBAVIF: C:\projects\libavif
LIBEXIV2: C:\projects\exiv2
PPKG: C:\projects\ppkg
matrix:
- job_name: mingw_64_qt6_8
QTDIR: C:\Qt\6.8\mingw_64
MINGW64: C:\Qt\Tools\mingw1310_64
KF_BRANCH: master
EXIV2_VERSION: "0.28.3"
EXIV2_CMAKE_OPTIONS: "-DEXIV2_ENABLE_BROTLI=OFF -DEXIV2_ENABLE_INIH=OFF -DEXIV2_BUILD_EXIV2_COMMAND=OFF"
PPIC_CMAKE_OPTIONS: "-DPREFER_QT_5=OFF"
WINDEPLOYQT_ARGS: "--verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --skip-plugin-types tls,networkinformation"
- job_name: mingw81_64_qt5_15_2
QTDIR: C:\Qt\5.15.2\mingw81_64
MINGW64: C:\Qt\Tools\mingw810_64
KF_BRANCH: kf5
EXIV2_VERSION: "0.27.7"
EXIV2_CMAKE_OPTIONS: "-DEXIV2_BUILD_SAMPLES=OFF -DEXIV2_ENABLE_WIN_UNICODE=ON -DEXIV2_BUILD_EXIV2_COMMAND=OFF"
PPIC_CMAKE_OPTIONS: "-DPREFER_QT_5=ON"
WINDEPLOYQT_ARGS: "--verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-angle --no-system-d3d-compiler"
install:
- mkdir %CMAKE_INSTALL_PREFIX%
- mkdir %LIBZ%
- mkdir %LIBEXPAT%
- mkdir %LIBAVIF%
- mkdir %LIBEXIV2%
- mkdir %PPKG%
- cd %APPVEYOR_BUILD_FOLDER%
- git submodule update --init --recursive
- set PATH=%PATH%;%CMAKE_INSTALL_PREFIX%;%QTDIR%\bin;%MINGW64%\bin;%PPKG%
- set CC=%MINGW64%\bin\gcc.exe
- set CXX=%MINGW64%\bin\g++.exe
build_script:
# prepare
- mkdir 3rdparty
- choco install ninja
- cd %PPKG%
- curl -fsSL -o ppkg.exe https://github.com/BLumia/pineapple-package-manager/releases/latest/download/ppkg.exe
- cd %APPVEYOR_BUILD_FOLDER%
# download and install zlib for KArchive
- cd %LIBZ%
- curl -fsSL -o zlib131.zip https://zlib.net/zlib131.zip
- 7z x zlib131.zip -y
- cd zlib-1.3.1
- mkdir build
- cd build
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX%
- cmake --build . --config Release
- cmake --build . --config Release --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# install ECM so we can build KImageFormats
- cd 3rdparty
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/extra-cmake-modules.git
- git rev-parse HEAD
- cd extra-cmake-modules
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DBUILD_TESTING=OFF
- cmake --build .
- cmake --build . --target install
- cd %APPVEYOR_BUILD_FOLDER%
# install AOM for libavif AV1 decoding support...
- cd 3rdparty
#- git clone -b v3.9.1 --depth 1 https://aomedia.googlesource.com/aom
#- cd aom
#- mkdir build.aom
#- cd build.aom
#- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DENABLE_DOCS=OFF -DBUILD_SHARED_LIBS=ON -DAOM_TARGET_CPU=generic -DENABLE_TESTS=OFF -DENABLE_TESTDATA=OFF -DENABLE_TOOLS=OFF -DENABLE_EXAMPLES=0
#- cmake --build . --config Release
#- cmake --build . --config Release --target install/strip
- mkdir aom
- cd aom
- curl -fsSL -o ppkg-aom.zip https://sourceforge.net/projects/pineapple-package-manager/files/packages/mingw-w64-x86_64-windows/aom-3.9.1-2.zip
- ppkg ppkg-aom.zip
- 7z x ppkg-aom.zip LICENSE -y
- cd %APPVEYOR_BUILD_FOLDER%
# install libavif for avif format support of KImageFormats
- cd %LIBAVIF%
- curl -fsSL -o libavif-v1_1_1.zip https://github.com/AOMediaCodec/libavif/archive/v1.1.1.zip
- 7z x libavif-v1_1_1.zip -y
- cd libavif-1.1.1
- mkdir build
- cd build
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_LIBYUV=ON
- cmake --build . --config Release
- cmake --build . --config Release --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# install KArchive for kra format support of KImageFormats
- cd 3rdparty
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/karchive.git
- git rev-parse HEAD
- cd karchive
- mkdir build
- cd build
- cmake .. -G "Ninja" -DWITH_LIBZSTD=OFF -DWITH_BZIP2=OFF -DWITH_LIBLZMA=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX%
- cmake --build . --config Release
- cmake --build . --config Release --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# build libexpat for libexiv2
- cd %LIBEXPAT%
- curl -fsSL -o R_2_6_2.zip https://github.com/libexpat/libexpat/archive/R_2_6_2.zip
- 7z x R_2_6_2.zip -y
- cd libexpat-R_2_6_2/expat/
- cmake -G "Ninja" . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% -DEXPAT_BUILD_EXAMPLES=OFF -DEXPAT_BUILD_TESTS=OFF -DEXPAT_BUILD_TOOLS=OFF
- cmake --build . --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# build libexiv2
- cd %LIBEXIV2%
- curl -fsSL -o v%EXIV2_VERSION%.zip https://github.com/Exiv2/exiv2/archive/v%EXIV2_VERSION%.zip
- 7z x v%EXIV2_VERSION%.zip -y
- cd exiv2-%EXIV2_VERSION%
- cmake -G "Ninja" . -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%CMAKE_INSTALL_PREFIX% %EXIV2_CMAKE_OPTIONS%
- cmake --build . --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# install KImageFormats
- cd 3rdparty
- git clone -b %KF_BRANCH% -q https://invent.kde.org/frameworks/kimageformats.git
- git rev-parse HEAD
- cd kimageformats
- mkdir build
- cd build
- cmake .. -G "Ninja" -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release -DKDE_INSTALL_QTPLUGINDIR=%QTDIR%\plugins
- cmake --build . --config Release
- cmake --build . --config Release --target install/strip
- cd %APPVEYOR_BUILD_FOLDER%
# finally...
- mkdir build
- cd build
- cmake .. -G "Ninja" %PPIC_CMAKE_OPTIONS% -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%CMAKE_INSTALL_PREFIX% -DCMAKE_INSTALL_PREFIX='%cd%'
- cmake --build . --config Release
- cmake --build . --config Release --target install/strip
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...
- cd bin
- copy %APPVEYOR_BUILD_FOLDER%\LICENSE
- copy %CMAKE_INSTALL_PREFIX%\bin\libaom.dll
- copy %CMAKE_INSTALL_PREFIX%\bin\libexpat-1.dll
- copy %CMAKE_INSTALL_PREFIX%\bin\libexiv2.dll
- copy %CMAKE_INSTALL_PREFIX%\bin\libavif.dll
- copy %CMAKE_INSTALL_PREFIX%\bin\libzlib.dll
- copy %CMAKE_INSTALL_PREFIX%\bin\libKF?Archive.dll
- windeployqt %WINDEPLOYQT_ARGS% .\ppic.exe
# copy 3rdparty licenses for the libs we vendored for windows...
- mkdir licenses
- cd licenses
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\aom\LICENSE License.aom.txt
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\karchive\LICENSES\LGPL-2.0-or-later.txt License.KArchive.txt
- copy %APPVEYOR_BUILD_FOLDER%\3rdparty\kimageformats\LICENSES\LGPL-2.1-or-later.txt License.kimageformats.txt
- copy %LIBEXPAT%\libexpat-R_2_6_2\expat\COPYING License.expat.txt
- copy %LIBAVIF%\libavif-1.1.1\LICENSE License.libavif.txt
- copy %LIBEXIV2%\exiv2-%EXIV2_VERSION%\COPYING License.exiv2.txt
# TODO: Qt, zlib
- cd ..
# for debug..
- tree /f
- cd %APPVEYOR_BUILD_FOLDER%
- xcopy %CMAKE_INSTALL_PREFIX% .\cmake-prefix-copy /E /H /C /I
artifacts:
- path: build\bin
- path: cmake-prefix-copy
@@ -1,4 +1,4 @@
IDI_ICON1 ICON DISCARDABLE "icons/app-icon.ico"
IDI_ICON1 ICON DISCARDABLE "@CMAKE_CURRENT_SOURCE_DIR@/assets/icons/app-icon.ico"
1 VERSIONINFO
BEGIN
BLOCK "StringFileInfo"
@@ -6,8 +6,9 @@ BEGIN
BLOCK "040904E4"
BEGIN
VALUE "FileDescription", "Pineapple Pictures - Image Viewer"
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2024 Gary Wang"
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2026 Gary Wang"
VALUE "ProductName", "Pineapple Pictures"
VALUE "ProductVersion", "@PROJECT_VERSION@"
END
END
BLOCK "VarFileInfo"
+29
View File
@@ -0,0 +1,29 @@
<center>
<p>
<img width='128' height='128' src=':/icons/app-icon.svg'/>
<br/>
<!-- Application name -->
<span>%1</span>
<br/>
<!-- Version info -->
<span>%2</span>
</p>
<hr/>
<p>
<!-- Upstream author and fork author -->
<span>%3</span><span>:&nbsp;</span><a href='https://github.com/BLumia'>@BLumia</a>
<br/>
<span>%4</span><span>:&nbsp;</span><a href='https://github.com/yyc12345'>@yyc12345</a>
<br/>
<!-- Icon author -->
<span>%5</span><span>:&nbsp;</span><a href='https://github.com/Lovelyblack'>@Lovelyblack</a>
</p>
<hr/>
<p>
<!-- Built Qt version info -->
<span>%6</span>
<br/>
<!-- Source code -->
<span>%7</span><span>:&nbsp;</span><a href='https://github.com/BLumia/pineapple-pictures'>GitHub</a>
</p>
</center>
+29
View File
@@ -0,0 +1,29 @@
<!-- General help -->
<h1 align='center'>%1</h1>
<ul>
<li>
<span>%2</span>
<ul>
<li>%3</li>
<li>%4</li>
</ul>
</li>
<li>%5</li>
<li>%6</li>
</ul>
<hr/>
<!-- Context menu option help -->
<h1 align='center'>%7</h1>
<!-- Context menu option list -->
<ul>
<li>
<b>%8</b><span>:</span>
<br/>
<span>%9</span>
</li>
<li>
<b>%10</b><span>:</span>
<br/>
<span>%11</span>
</li>
</ul>
+29
View File
@@ -0,0 +1,29 @@
<!-- License -->
<h1 align='center'><b>%1</b></h1>
<p>%2</p>
<hr/>
<!-- VErbatime license -->
<h1 align='center'><b>%3</b></h1>
<pre>
Expat/MIT License
Copyright &copy; 2026 BLumia
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 NONINFRINGEMENT. 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.
</pre>
@@ -1,3 +1,14 @@
<!-- Contributor title -->
<h1 align='center'>%1</h1>
<!-- Contributor list -->
<a href='https://github.com/BLumia/pineapple-pictures/graphs/contributors'>%2</a>
<!-- The message to thank these contributors -->
<p>%3</p>
<!-- Translator title -->
<h1 align='center'>%4</h1>
<!-- The message to thank these translators -->
<p>%5</p>
<ul>
<li><u>Catalan</u>: Toni Estévez</li>
<li><u>Chinese (Simplified)</u>: Percy Hong</li>
@@ -12,7 +23,10 @@
<li><u>Punjabi (Pakistan)</u>: bgo-eiu</li>
<li><u>Russian</u>: Sergey Shornikov, Artem, Andrey</li>
<li><u>Sinhala</u>: HelaBasa</li>
<li><u>Slovenian</u>: Andrej Poženel - SDT</li>
<li><u>Spanish</u>: Toni Estévez, Génesis Toxical, William(ѕ)ⁿ, gallegonovato</li>
<li><u>Tamil</u>: தமிழ்நேரம் (TamilNeram)</li>
<li><u>Turkish</u>: E-Akcaer, Oğuz Ersen, Sabri Ünal</li>
<li><u>Ukrainian</u>: Dan, volkov, Сергій</li>
<li><u>Vietnamese</u>: Loc Huynh</li>
</ul>
+15
View File
@@ -0,0 +1,15 @@
<!-- Title -->
<h1 align='center'><b>%1</b></h1>
<!-- List intro message -->
<p>%2</p>
<p>%3</p>
<ul>
<li>
<a href='https://www.qt.io/'>Qt</a><span>: </span>
<span>GPLv2 + GPLv3 + LGPLv2.1 + LGPLv3</span>
</li>
<li style="display: none;">
<a href='https://www.exiv2.org/'>Exiv2</a><span>: </span>
<span>GPLv2</span>
</li>
</ul>
+5 -1
View File
@@ -10,6 +10,10 @@
<file>icons/window-close.svg</file>
<file>icons/go-next.svg</file>
<file>icons/go-previous.svg</file>
<file>plain/translators.html</file>
<file>plain/help.html</file>
<file>plain/about.html</file>
<file>plain/special-thanks.html</file>
<file>plain/license.html</file>
<file>plain/third-party-libs.html</file>
</qresource>
</RCC>
+22
View File
@@ -30,6 +30,28 @@
<true/>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<!-- FIXME: this list can't be automatically generated by Qt's CMake API, don't know why. -->
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ca</string>
<string>de</string>
<string>es</string>
<string>fr</string>
<string>id</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>nb_NO</string>
<string>nl</string>
<string>pa_PK</string>
<string>ru</string>
<string>si</string>
<string>ta</string>
<string>tr</string>
<string>uk</string>
<string>zh_CN</string>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<!-- JPEG -->
+203 -1
View File
@@ -4,16 +4,26 @@
<name>Pineapple Pictures</name>
<name xml:lang="es">Pineapple Pictures</name>
<name xml:lang="ja">Pineapple Pictures</name>
<name xml:lang="ko">Pineapple Pictures</name>
<name xml:lang="nl">Pineapple Afbeeldingen</name>
<name xml:lang="ru">Pineapple Pictures</name>
<name xml:lang="sl">Pineapple Slike</name>
<name xml:lang="ta">அன்னாசி படங்கள்</name>
<name xml:lang="tr">Pineapple Resimler</name>
<name xml:lang="uk">Pineapple Pictures</name>
<name xml:lang="vi">Pineapple Pictures</name>
<name xml:lang="zh-CN">菠萝看图</name>
<summary>Image Viewer</summary>
<summary xml:lang="es">Visor de imágenes</summary>
<summary xml:lang="ja">画像ビューアー</summary>
<summary xml:lang="ko">이미지 뷰어</summary>
<summary xml:lang="nl">Afbeeldingsweergave</summary>
<summary xml:lang="ru">Просмотр изображений</summary>
<summary xml:lang="sl">Pregledovalnik slik</summary>
<summary xml:lang="ta">பட பார்வையாளர்</summary>
<summary xml:lang="tr">Resim Görüntüleyici</summary>
<summary xml:lang="uk">Переглядач зображень</summary>
<summary xml:lang="vi">Trình xem ảnh</summary>
<summary xml:lang="zh-CN">图像查看器</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
@@ -21,9 +31,13 @@
<p>Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support.</p>
<p xml:lang="es">Pineapple Pictures es un visor de imágenes ligero y fácil de usar que viene con una práctica miniatura de navegación al hacer zoom, y no contiene ningún soporte de gestión de imágenes.</p>
<p xml:lang="ja">Pineapple Picturesは、ズームイン時に便利なナビゲーションサムネイルを備えた軽量で使いやすい画像ビューアです。画像管理のサポートは含まれていません。</p>
<p xml:lang="ko">Pineapple Pictures는 가볍고 사용하기 쉬운 이미지 뷰어로, 확대 시 편리한 내비게이션 썸네일과 함께 제공되며 이미지 관리 지원이 포함되어 있지 않습니다.</p>
<p xml:lang="nl">Pineapple Afbeeldingen is een licht en eenvoudig te gebruiken afbeeldingsweergaveprogramma met miniatuurnavigatie na inzoomen. Het programma heeft echter geen fotobeheermogelijkheid.</p>
<p xml:lang="ru">Pineapple Pictures - это легкий и простой в использовании просмотрщик изображений, оснащенный удобной навигацией по миниатюрам при увеличении масштаба и не содержащий никакой поддержки управления изображениями.</p>
<p xml:lang="sl">Pineapple Pictures je lahek in enostaven pregledovalnik slik, ki ima pri povečavi priročno sličico za navigacijo in ne vsebuje nobene podpore za upravljanje slik.</p>
<p xml:lang="ta">அன்னாசி படங்கள் ஒரு இலகுரக மற்றும் பயன்படுத்த எளிதான பட பார்வையாளராகும், இது பெரிதாக்கும்போது ஒரு எளிமையான வழிசெலுத்தல் சிறுபடத்துடன் வருகிறது, மேலும் எந்த பட மேலாண்மை ஆதரவையும் கொண்டிருக்கவில்லை.</p>
<p xml:lang="uk">Pineapple Pictures це легкий і простий у використанні переглядач зображень, який постачається зі зручною навігаційною мініатюрою при збільшенні масштабу і не містить жодної підтримки керування зображеннями.</p>
<p xml:lang="vi">Pineapple Pictures là trình xem hình ảnh nhẹ và dễ sử dụng, đi kèm với hình thu nhỏ điều hướng tiện dụng khi phóng to và không chứa bất kỳ hỗ trợ quản lý hình ảnh nào.</p>
<p xml:lang="zh-CN">菠萝看图是一个轻量级易用的图像查看器,在图片放大时提供了方便的鸟瞰导航功能,且不包含任何图片管理功能。</p>
</description>
<developer id="net.blumia">
@@ -37,7 +51,9 @@
<launchable type="desktop-id">net.blumia.pineapple-pictures.desktop</launchable>
<url type="homepage">https://github.com/BLumia/pineapple-pictures</url>
<url type="bugtracker">https://github.com/BLumia/pineapple-pictures/issues</url>
<url type="donation">https://ko-fi.com/blumia</url>
<url type="translate">https://hosted.weblate.org/projects/pineapple-pictures/</url>
<url type="vcs-browser">https://github.com/BLumia/pineapple-pictures</url>
<provides>
<binary>ppic</binary>
</provides>
@@ -46,9 +62,13 @@
<caption>Main window when an image file is loaded</caption>
<caption xml:lang="es">Ventana principal cuando se carga un archivo de imagen</caption>
<caption xml:lang="ja">画像ファイル読み込み時のメインウィンドウ</caption>
<caption xml:lang="ko">이미지 파일이 로드될 때 기본 창</caption>
<caption xml:lang="nl">Hoofdvenster na het laden van een afbeelding</caption>
<caption xml:lang="ru">Основное окно после загрузки файла изображения</caption>
<caption xml:lang="sl">Glavno okno ob nalaganju slikovne datoteke</caption>
<caption xml:lang="ta">ஒரு படக் கோப்பு ஏற்றப்படும் போது முதன்மையான சாளரம்</caption>
<caption xml:lang="uk">Головне вікно після завантаження файлу зображення</caption>
<caption xml:lang="vi">Cửa sổ chính khi tải tệp hình ảnh</caption>
<caption xml:lang="zh-CN">加载图片后的主窗口</caption>
<image type="source" width="1503" height="640">https://pineapple-pictures.sourceforge.io/ppic-gui-static.png</image>
</screenshot>
@@ -56,9 +76,13 @@
<caption>Zooming in a raster image</caption>
<caption xml:lang="es">Ampliar una imagen rasterizada</caption>
<caption xml:lang="ja">ラスター画像の拡大</caption>
<caption xml:lang="ko">래스터 이미지 확대하기</caption>
<caption xml:lang="nl">Inzoomen op een roosterafbeelding</caption>
<caption xml:lang="ru">Масштабирование растрового изображения</caption>
<caption xml:lang="sl">Povečevanje rastrske slike</caption>
<caption xml:lang="ta">ராச்டர் படத்தில் பெரிதாக்குதல்</caption>
<caption xml:lang="uk">Масштабування растрового зображення</caption>
<caption xml:lang="vi">Phóng to hình ảnh raster</caption>
<caption xml:lang="zh-CN">放大浏览位图</caption>
<image type="source" width="771" height="553">https://pineapple-pictures.sourceforge.io/ppic-zoom-raster.png</image>
</screenshot>
@@ -66,14 +90,192 @@
<caption>Zooming in a vector image</caption>
<caption xml:lang="es">Ampliar una imagen vectorial</caption>
<caption xml:lang="ja">ベクター画像の拡大</caption>
<caption xml:lang="ko">벡터 이미지 확대하기</caption>
<caption xml:lang="nl">Inzoomen op een vectorafbeelding</caption>
<caption xml:lang="ru">Масштабирование векторного изображения</caption>
<caption xml:lang="sl">Povečevanje vektorske slike</caption>
<caption xml:lang="ta">ஒரு திசையன் படத்தில் பெரிதாக்குதல்</caption>
<caption xml:lang="uk">Масштабування векторного зображення</caption>
<caption xml:lang="vi">Phóng to hình ảnh vector</caption>
<caption xml:lang="zh-CN">放大浏览矢量图</caption>
<image type="source" width="771" height="553">https://pineapple-pictures.sourceforge.io/ppic-zoom-svg.png</image>
</screenshot>
</screenshots>
<releases>
<release type="stable" version="1.5.0" date="2026-07-04T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>Introduced an optional titlebar that display file name and allows drag and double-click to move and toggle maximize state</li>
<li>Windows: Now the application will use the system's built-in animation for open window animation</li>
</ul>
<p>This release fixes the following bug:</p>
<ul>
<li>Show empty url list should also reset window title to blank</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Update translations</li>
<li>Add vcs-browser link to AppStream metadata</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels (vistaus), VenusGirl (VenusGirl)</p>
</description>
</release>
<release type="stable" version="1.4.1" date="2026-04-05T00:00:00Z">
<description>
<p>This release includes the following changes:</p>
<ul>
<li>Update translations</li>
<li>Update Qt version to 6.10.3 for Windows and macOS pre-built binaries</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels (vistaus), Loc Huynh (hthienloc)</p>
</description>
</release>
<release type="stable" version="1.4.0" date="2026-02-11T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>Action to save image as new file with different format</li>
<li>Add Vietnamese and Slovenian translation</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Update copyright year to 2026</li>
<li>Update translations</li>
<li>Update Qt version to 6.10.2 for Windows and macOS pre-built binaries</li>
</ul>
<p>With contributions from:</p>
<p>Loc Huynh, Sabri Ünal, VenusGirl, Andrej Poženel, Andrey</p>
</description>
</release>
<release type="stable" version="1.3.0" date="2025-12-20T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>New option to limit SVG support to SVG Tiny 1.2 standard</li>
<li>Windows: ship tools and configurations that help user associate image files to this program</li>
</ul>
<p>This release fixes the following bugs:</p>
<ul>
<li>Fix window not appear at the screen that the mouse cursor is at</li>
<li>Fix Windows prebuilt binary having AVIF plugin included but not actually support it</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>CPack DEB: will now recommend "kimageformat6-plugins" instead of the KF5 version</li>
<li>Update translations</li>
<li>Update exiv2 version for Windows binary build</li>
</ul>
<p>With contributions from:</p>
<p>VenusGirl, Andrey</p>
</description>
</release>
<release type="stable" version="1.2.1" date="2025-11-22T00:00:00Z">
<description>
<p>This release fixes the following bug:</p>
<ul>
<li>Fix incorrect color when display some CMYK images.</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Now use Qt 6.10.1 for pre-built binary</li>
<li>Update README description and AppStream metainfo</li>
<li>Update translations</li>
<li>Update libavif, AOM and expat version for Windows binary build</li>
</ul>
<p>With contributions from:</p>
<p>albanobattistella</p>
</description>
</release>
<release type="stable" version="1.2.0" date="2025-09-21T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>Long image mode that detect extra wide image and auto fit by its thin side</li>
<li>Windows: Add product version to binary metadata / resource file</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Update translations</li>
<li>Update libavif version for Windows binary build</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels, albanobattistella, Sabri Ünal</p>
</description>
</release>
<release type="stable" version="1.1.1" date="2025-08-02T00:00:00Z">
<description>
<p>This release adds the following feature:</p>
<ul>
<li>Click dock icon should show window when it's hidden on macOS</li>
</ul>
<p>This release fixes the following bug:</p>
<ul>
<li>Ensure "Fit by Width" position the view to the beginning of the image</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Update translations</li>
<li>Update Exiv2 version for Windows binary build</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels, VenusGirl, தமிழ்நேரம்</p>
</description>
</release>
<release type="stable" version="1.1.0" date="2025-07-06T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>New option to disable built-in close window animation</li>
<li>New option to disable gallery looping</li>
<li>Support load m3u8 as image gallery playlist</li>
</ul>
<p>This release includes the following change:</p>
<ul>
<li>Drop Qt 5 support</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels, albanobattistella, தமிழ்நேரம்</p>
</description>
</release>
<release type="stable" version="1.0.0" date="2025-05-03T00:00:00Z">
<description>
<p>This release adds the following features:</p>
<ul>
<li>Support enforces windowed mode on start-up</li>
<li>Reload image automatically when current image gets updated</li>
</ul>
<p>This release fixes the following bug:</p>
<ul>
<li>Display correct text language on macOS</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Use native text for shortcut editor's label</li>
<li>Display native commandline message when possible</li>
<li>Merge Qt translations into app applications as well</li>
</ul>
<p>With contributions from:</p>
<p>Heimen Stoffels, albanobattistella, mmahhi</p>
</description>
</release>
<release type="stable" version="0.9.2" date="2025-03-05T00:00:00Z">
<description>
<p>This release fixes the following bug:</p>
<ul>
<li>Refer to the right exiv2 CMake module so it can be found on Linux</li>
</ul>
<p>This release includes the following changes:</p>
<ul>
<li>Convert DEP5 to REUSE.toml for better REUSE compliance</li>
<li>Update translations</li>
</ul>
<p>With contributions from:</p>
<p>Pino Toscano, TamilNeram</p>
</description>
</release>
<release type="stable" version="0.9.1" date="2025-01-25T00:00:00Z">
<description>
<p>This release adds the following features:</p>
@@ -189,7 +391,7 @@
<description>
<p>This release adds the following features:</p>
<ul>
<li>TIF and TIFF format files in the same folder will now be automatedly added to the gallery</li>
<li>TIF and TIFF format files in the same folder will now be automatically added to the gallery</li>
<li>Built-in window resizing now also supports Linux desktop. (macOS might also works as well)</li>
</ul>
<p>This release fixes the following bugs:</p>
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,51 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: 2025-12-27 02:00+0000\n"
"Last-Translator: Gary Wang <wzc782970009@gmail.com>\n"
"Language-Team: Korean <https://hosted.weblate.org/projects/pineapple-"
"pictures/appstream-metadata/ko/>\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.1\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr "Pineapple Pictures"
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr "이미지 뷰어"
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
"Pineapple Pictures는 가볍고 사용하기 쉬운 이미지 뷰어로, 확대 시 편리한 내비"
"게이션 썸네일과 함께 제공되며 이미지 관리 지원이 포함되어 있지 않습니다."
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr "이미지 파일이 로드될 때 기본 창"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr "래스터 이미지 확대하기"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr "벡터 이미지 확대하기"
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr "Gary (BLumia) Wang 외"
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: nb_NO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: pa_PK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,47 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: si\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr ""
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr ""
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr ""
@@ -0,0 +1,53 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: 2026-01-25 01:53+0000\n"
"Last-Translator: Andrej Poženel - SDT <anderlli0053@live.com>\n"
"Language-Team: Slovenian <https://hosted.weblate.org/projects/pineapple-"
"pictures/appstream-metadata/sl/>\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || "
"n%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 5.16-dev\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr "Pineapple Slike"
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr "Pregledovalnik slik"
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
"Pineapple Pictures je lahek in enostaven pregledovalnik slik, ki ima pri "
"povečavi priročno sličico za navigacijo in ne vsebuje nobene podpore za "
"upravljanje slik."
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr "Glavno okno ob nalaganju slikovne datoteke"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr "Povečevanje rastrske slike"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr "Povečevanje vektorske slike"
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr "Gary (BLumia) Wang et al."
@@ -0,0 +1,52 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: 2025-01-28 09:01+0000\n"
"Last-Translator: தமிழ்நேரம் <anishprabu.t@gmail.com>\n"
"Language-Team: Tamil <https://hosted.weblate.org/projects/pineapple-pictures/"
"appstream-metadata/ta/>\n"
"Language: ta\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10-dev\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr "அன்னாசி படங்கள்"
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr "பட பார்வையாளர்"
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
"அன்னாசி படங்கள் ஒரு இலகுரக மற்றும் பயன்படுத்த எளிதான பட பார்வையாளராகும், இது "
"பெரிதாக்கும்போது ஒரு எளிமையான வழிசெலுத்தல் சிறுபடத்துடன் வருகிறது, மேலும் எந்த பட "
"மேலாண்மை ஆதரவையும் கொண்டிருக்கவில்லை."
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr "ஒரு படக் கோப்பு ஏற்றப்படும் போது முதன்மையான சாளரம்"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr "ராச்டர் படத்தில் பெரிதாக்குதல்"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr "ஒரு திசையன் படத்தில் பெரிதாக்குதல்"
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr "கேரி (ப்ளூமியா) வாங் மற்றும் பலர்."
@@ -0,0 +1,49 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: 2025-12-20 20:00+0000\n"
"Last-Translator: Sabri Ünal <yakushabb@gmail.com>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/pineapple-"
"pictures/appstream-metadata/tr/>\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr "Pineapple Resimler"
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr "Resim Görüntüleyici"
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr ""
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr ""
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr "Gary (BLumia) Wang et al."
@@ -0,0 +1,52 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2023-08-22 18:49中国标准时间\n"
"PO-Revision-Date: 2025-12-25 17:00+0000\n"
"Last-Translator: Loc Huynh <huynhloc.contact@gmail.com>\n"
"Language-Team: Vietnamese <https://hosted.weblate.org/projects/pineapple-"
"pictures/appstream-metadata/vi/>\n"
"Language: vi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.1\n"
#. (itstool) path: component/name
#: net.blumia.pineapple-pictures.metainfo.xml:7
msgid "Pineapple Pictures"
msgstr "Pineapple Pictures"
#. (itstool) path: component/summary
#: net.blumia.pineapple-pictures.metainfo.xml:9
msgid "Image Viewer"
msgstr "Trình xem ảnh"
#. (itstool) path: description/p
#: net.blumia.pineapple-pictures.metainfo.xml:12
msgid "Pineapple Pictures is a lightweight and easy-to-use image viewer that comes with a handy navigation thumbnail when zoom-in, and doesn't contain any image management support."
msgstr ""
"Pineapple Pictures là trình xem hình ảnh nhẹ và dễ sử dụng, đi kèm với hình "
"thu nhỏ điều hướng tiện dụng khi phóng to và không chứa bất kỳ hỗ trợ quản "
"lý hình ảnh nào."
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:17
msgid "Main window when an image file is loaded"
msgstr "Cửa sổ chính khi tải tệp hình ảnh"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:22
msgid "Zooming in a raster image"
msgstr "Phóng to hình ảnh raster"
#. (itstool) path: screenshot/caption
#: net.blumia.pineapple-pictures.metainfo.xml:27
msgid "Zooming in a vector image"
msgstr "Phóng to hình ảnh vector"
#. (itstool) path: component/developer_name
#: net.blumia.pineapple-pictures.metainfo.xml:34
msgid "Gary (BLumia) Wang et al."
msgstr "Gary (BLumia) Wang et al."
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

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