diff --git a/docs/changelog.md b/docs/changelog.md
index 92d35c748..1f2881be2 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,44 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 577](https://github.com/hydrusnetwork/hydrus/releases/tag/v577)
+
+### explorer integration
+
+* thanks to a user, we have some new OS-file-explorer integration
+* two additional options are added to the "open" menu for Windows users, "in another program" opens the Windows dialog to select which program to use and "properties" opens the Windows file properties dialog for the file
+* the 'media' shortcut set gets the new 'open file properties' and 'open with...' commands to plug into these new features
+* the "open in file browser" media menu command now more reliably selects the file in Windows and is now available for most Linux file managers--full list [here](https://github.com/damonlynch/showinfilemanager#supported-file-managers).
+* the "open files' locations" file import log menu command is similarly more reliable, and can sometimes select multiple files when launched on a selection
+* this requires a new external library, so users who run from source will want to rebuild their venvs this week to get this functionality
+
+### misc
+
+* the manage times single-time edit dialog's paste button can now eat any datstring you can think of. try pasting 'yesterday 3am' into it, it'll work!
+* split the increasingly cluttered 'media' options panel into 'media playback' (options governing how media is rendered) and 'media viewer' (options governing the viewer itself like drags and slideshows)
+* added to the new 'media viewer' panel are five checkboxes to turn off the background text in the full media viewer--for the taglist, the top hover, the top-right hover, the notes hover, and the bottom-right index string. if you want, you can have a completely blank background now
+* gave the _help->about_ window a pass. I broke the cluttered first tab into two, and the layout all over is a bit clearer
+* the _help->advanced mode_ option is now available under a new _options->advanced_ tab. this thing covers several dozen things across the program, all insufficiently documented, so the plan is to blow it out into all its granular constituent components on this page!
+* fixed it so an invalid `ApplicationCommand` will still render a string. if you got some jank `ToString()` errors in a shortcuts dialog recently, please try again and let me know what you get. you'll probably want to go into the actual shortcut with the error string and try and see if you can fix what it has set--again, let me know the details please!
+* updated the 'installing and updating' help page to talk clearly about the different versions that have special update instructions, and generally gave the language a pass
+
+### some url encoding
+
+* fixed an issue in url encoding-normalisation where urls were not retaining their parameters if their names had certain decoded characters (particularly, this was stuff like the decoded square brackets in `fields[post]=123`). a new unit test will catch this in future
+* url classes and parsers are now careful to encode their example urls any time they are asked for (outside of their respective edit dialogs' "example url(s)" fields, so if you want to work with a human-looking URL in UI, that's fine). this ensures the automatic url-parser linking system works if the parser and url classes have a mish-mash of encoded and non-encoded example URLs. it also fixes some stuff like the multi-column list in the manage url classes dialog when the url class has a decoded example url. this was basically just an ingestion point that I missed in the previous work
+* the edit parser dialog makes sure to properly encode the URL when you do a test pull
+
+### orphan table tech
+
+* the _database->db maintenance->clear orphan tables_ command, which could previously only clear out the repository update/processing-tracking tables, can now nuke: the core file list tables in client.db; the core mappings tables in client.mappings.db; the display and storage mappings caches in client.caches.db; the display and storage autocomplete count caches; the ideal and actual tag parent lookup tables in client.caches.db; the ideal and actual tag sibling lookup tables in client.caches.db; and the various tag search tables (except the fts4 stuff) in client.caches.db
+* when this job fires, it now sends orphan tables to the deferred delete system (previously it dropped them immediately, which for a big mappings table is a no-go)
+
+### boring cleanup
+
+* cleaned a bunch of db table code for the new orphan table stuff
+* deleted the old 'yaml_dumps' table and all associated methods, which are all now unused
+* added a couple help labels to the "colours" and "style" pages to better explain what is actually going on here
+
## [Version 576](https://github.com/hydrusnetwork/hydrus/releases/tag/v576)
### file access latency
@@ -384,30 +422,3 @@ title: Changelog
* cleaned up some misc URL Class code
## Version 567 was cancelled, its changes folded into 568.
-
-## [Version 566](https://github.com/hydrusnetwork/hydrus/releases/tag/v566)
-
-### incremental tagging
-
-* when you boot a 'manage tags' dialog on multiple files, a new `±` button now lets you do 'incremental tagging'. this is where you, let's say for twenty files, tag them from page:1->page:20. this has been a long time in the works, but now we have thumbnail reorganisation tech, it is now sensible to do.
-* the dialog lets you set a namespace (or none), start point (e.g. you can start tagging at page:19 if you are doing the second chapter etc...), the step (you can count by +2 every file, instead of +1, or even -1 to decrement), the subtag prefix (so you can say 'page:insert-4' or something), and the subtag suffix (for, say, 'page:2 (wip)')
-* the last namespace is remembered between dialog opens, and if the first file in the selection has a number tag in that namespace, that is the number the 'start' will initialise with. a bit of overlap/prep may save time here!
-* the prefix and suffix are remembered between dialog opens
-* a status text gives you a live preview of what you will be adding and says whether any of the files already have exactly those tags or have different tags under the same namespace (which would be possible conflicts, suggesting you are not lined up correct)
-
-### misc
-
-* added import support for .docx, .xlsx, and .pptx files (the Microsoft Open XML Formats). they get icons, not much else. they are secretly zips, so **on update, you will be asked if you want to scan your existing zips for these formats**
-* when you move a window to another screen in a maximised state (e.g. on Windows you can do this with win+shift+arrow), the system that remembers window coordinates will now register and save this. the 'restore' window size is preserved from whatever it was on the previous screen while the 'restore' position will try to stay the same on the new monitor (e.g. if it was at (200, 400) on the old monitor, it will try to do the same on the new) as long as the window fits, otherwise it is moved to (20,20) on the new screen
-* the 'edit string converter' panel no longer requires you to enter an example text that can be converted. you can see the error on the dialog, so if you don't want to fix it, or you just need to nip in and out testing things, it is now up to you
-* if the database takes a long time to update, the 'just woke up from sleep' state should no longer trigger. the system thought the long weird early delay was the computer going to sleep
-* the system that gives a popup and then a dialog when you have 165+ (and then 500+ or so) pages open is now removed. this was always a wx thing primarily, and Qt is much happier about having a whole load of UI elements. the main problem here is now memory blot and UI-update lag. this is now in the user's hands alone, no more bothering from me (unless it becomes a new problem, and I'll figure out a better warning test/system)
-
-### boring code cleanup
-
-* neatened how some manage tags ui is initialised. there's a hair of a chance this fixes the 'the manage tags dialog taglist is cut off sometimes' bug
-* neatened how some pending content updates are held in manage tags
-* manage tags dialogs now receive their media list in the same order as the underlying thumbnail selection, ha ha ha
-* untangled some of the presentation import options. stuff like 'is new or in inbox' gets slightly better description labels and cleaner actual logic code
-* fixed some type issues, some typo'd pubsubs, and other misc linting
-* tried last week's aborted github build update again. the build is now Node 20 compatible
diff --git a/docs/getting_started_installing.md b/docs/getting_started_installing.md
index d157bb9c0..ebef79156 100644
--- a/docs/getting_started_installing.md
+++ b/docs/getting_started_installing.md
@@ -133,31 +133,26 @@ To run the client:
Although I put out a new version every week, you can update far less often if you prefer. The client keeps to itself, so if it does exactly what you want and a new version does nothing you care about, you can just leave it. Other users enjoy updating every week, simply because it makes for a nice schedule. Others like to stay a week or two behind what is current, just in case I mess up and cause a temporary bug in something they like.
-A user has written a longer and more formal guide to updating, and information on the 334->335 step (python2 to python3) [here](update_guide.rtf).
-
-??? note "The 526->527 step was also important."
- 527 changed the program executable name from 'client' to 'hydrus_client'. There was also a library update that caused a dll conflict with previous installs.
-
- If you need to update from 526 or before, then:
-
- * If you use the Windows installer, install as normal. Your start menu 'hydrus client' shortcut should be overwritten with one to the new executable, but if you use a custom shortcut, you will need to update that too.
- * If you use one of the normal extract builds, you will have to do a 'clean install', as below. You also need to update your program shortcuts.
- * If you use the macOS app, there are no special instructions. Update as normal.
- * If you run from source, `git pull` as normal. If you haven't already, feel free to run setup_venv again to get the new OpenCV. Update your launch scripts to point at the new `hydrus_client.py` boot scripts.
-
The update process:
-* If the client is running, close it!
-* If you maintain a backup, run it now!
-* If you use the installer, just download the new installer and run it. It should detect where the last install was and overwrite everything automatically.
-* If you extract, then just extract the new version right on top of your current install and overwrite manually. *It is wise to extract it straight from the archive to your install folder.*
-* Start your client or server. It may take a few minutes to update its database. I will say in the release post if it is likely to take longer.
+* If the client is running, close it!
+* If you maintain a backup, run it now!
+* Update your install:
+ 1. **If you use the installer**, just download the new installer and run it. It should detect where the last install was and overwrite everything automatically.
+ 2. **If you use the extract**, then just extract the new version right on top of your current install and overwrite manually. *It is wise to extract it straight from the archive to your install folder.*
+ 3. **If you use the macOS App**, just drag and drop from the dmg to your Applications as normal.
+ 4. **If you run from source**, then run `git pull` as normal.
+* Start your client or server. It may take a few minutes to update its database. I will say in the release post if it is likely to take longer.
+
+A user has written a longer and more formal guide to updating [here](update_guide.rtf).
??? warning "Be extremely careful making test runs of the Extract release"
**Do not test-run the extract before copying it over your install!** Running the program anywhere will create database files in the /db/ dir, and if you then copy that once-run folder on top of your real install, you will overwrite your real database! Of course it doesn't really matter, because you made a full backup before you started, right? :^)
If you need to perform tests of an update, make sure you have a good backup before you start and then remember to delete any functional test extracts before extracting from the original archive once more for the actual 'install'.
+**Several older versions, like 334, 526, and 570 have [special update instructions](#big_updates).**
+
Unless the update specifically disables or reconfigures something, all your files and tags and settings will be remembered after the update.
Releases typically need to update your database to their version. New releases can retroactively perform older database updates, so if the new version is v255 but your database is on v250, you generally only need to get the v255 release, and it'll do all the intervening v250->v251, v251->v252, etc... update steps in order as soon as you boot it. If you need to update from a release more than, say, ten versions older than current, see below. You might also like to skim the release posts or [changelog](changelog.md) to see what is new.
@@ -166,7 +161,7 @@ Clients and servers of different versions can usually connect to one another, bu
## Clean installs
-**This is usually only relevant if you know you have a dll conflict or otherwise update and cannot boot at all. It usually only applies to Windows or Linux users who manually update using the 'Extract' releases.**
+**This is usually only relevant if you use the extract release and have a dll conflict or otherwise update and cannot boot at all. A handful of hydrus updates through its history have needed this.**
Very rarely, hydrus needs a clean install. This can be due to a special update like when we moved from 32-bit to 64-bit or needing to otherwise 'reset' a custom install situation. The problem is usually that a library file has been renamed in a new version and hydrus has trouble figuring out whether to use the older one (from a previous version) or the newer.
@@ -181,17 +176,58 @@ However, you need to be careful not to delete your database! It sounds silly, bu
After that, you'll have a 'clean' version of hydrus that only has the latest version's dlls. If hydrus still will not boot, I recommend you roll back to your last working backup and let me, hydrus dev, know what your error is.
-*Note that macOS App users will not ever have to do a clean install because every App is self-contained and non-merging with previous Apps. Source users similarly do not have to worry about this issue, although if they update their system python, they'll want to recreate their venv. Windows Installer users basically get a clean install every time, so they don't have to worry about this.*
+*Note that macOS App users will not ever have to do a clean install because every App is self-contained and non-merging with previous Apps. Source users similarly do not have to worry about this issue, although if they update their system python, they'll want to recreate their venv. Windows Installer users basically get a clean install every time, so they shouldn't have to worry about this.*
+
+## Big updates { id="big_updates" }
+
+If you have not updated in some time--say twenty versions or more--doing it all in one jump, like v290->v330, may not work. I am doing a lot of unusual stuff with hydrus, change my code at a fast pace, and do not have a ton of testing in place. Hydrus update code often falls to [bitrot](https://en.wikipedia.org/wiki/Software_rot), and so some underlying truth I assumed for the v299->v300 code may not still apply six months later. If you try to update more than 50 versions at once (i.e. trying to perform more than a year of updates in one go), the client will give you a polite error rather than even try.
-## Big updates
+As a result, if you get a failure on trying to do a big update, try cutting the distance in half--try v290->v310 first, and boot it. If the database updates correctly and the program boots, then shut down and move on to v310->v330. If the update does not work, cut down the gap and try v290->v300, and so on. **Again, it is very important you make a backup before starting a process like this so you can roll back and try a different version if things go wrong.**
-If you have not updated in some time--say twenty versions or more--doing it all in one jump, like v250->v290, is likely not going to work. I am doing a lot of unusual stuff with hydrus, change my code at a fast pace, and do not have a ton of testing in place. Hydrus update code often falls to [bitrot](https://en.wikipedia.org/wiki/Software_rot), and so some underlying truth I assumed for the v255->v256 code may not still apply six months later. If you try to update more than 50 versions at once (i.e. trying to perform more than a year of updates in one go), the client will give you a polite error rather than even try.
+If you narrow the gap down to just one version and still get an error, please let me know. If the problem is ever quick to appear and ugly/serious-looking, and perhaps talking about a "bootloader" or "dll" issue, then try doing a clean install as above. I am very interested in these sorts of problems and will be happy to help figure out a fix with you (and everyone else who might be affected).
+
+_All that said, and while updating is complex and every client is different, various user reports over the years suggest this route works and is efficient: 204 > 238 > 246 > 291 > 328 > 335 (clean install) > 376 > 421 > 466 (clean install) > 474 > 480 > 521 (maybe clean install) > 527 (special clean install) > 535 > 558 > 571 (clean install)_
+
+??? note "334->335"
+ We moved from python 2 to python 3.
+
+ If you need to update from 334 or before to 335 or later, then:
+
+ * If you use the Windows installer, install as normal.
+ * If you use one of the normal extract builds, you will have to do a 'clean install', as above.
+ * If you use the macOS app, there are no special instructions. Update as normal.
+ * If you run from source, there are no special instructions. Update as normal.
+
-As a result, if you get a failure on trying to do a big update, try cutting the distance in half--try v270 first, and then if that works, try v270->v290. If it doesn't, try v260, and so on.
+??? note "427->428"
+ Some new dlls cause a potential conflict.
+
+ If you need to update from 427 or before to 428 or later, then:
+
+ * If you use the Windows installer, install as normal.
+ * If you use one of the normal extract builds, you will have to do a 'clean install', as above.
+ * If you use the macOS app, there are no special instructions. Update as normal.
+ * If you run from source, there are no special instructions. Update as normal.
-If you narrow the gap down to just one version and still get an error, please let me know. I am very interested in these sorts of problems and will be happy to help figure out a fix with you (and everyone else who might be affected).
+??? note "526->527"
+ 527 changed the program executable name from 'client' to 'hydrus_client'. There was also a library update that caused a dll conflict with previous installs.
+
+ If you need to update from 526 or before to 527 or later, then:
+
+ * If you use the Windows installer, install as normal. Your start menu 'hydrus client' shortcut should be overwritten with one to the new executable, but if you use a custom shortcut, you will need to update that too.
+ * If you use one of the normal extract builds, you will have to do a 'clean install', as above.
+ * If you use the macOS app, there are no special instructions. Update as normal.
+ * If you run from source, `git pull` as normal. If you haven't already, feel free to run setup_venv again to get the new OpenCV. Update your launch scripts to point at the new `hydrus_client.py` boot scripts.
-_All that said, and while updating is complex and every client is different, various user reports over the years suggest this route works and is efficient: 204 > 238 > 246 > 291 > 328 > 335 > 376 > 421 > 466 > 474 > 480 > 521 > 527 (clean install) > 535 > 558 > 571 (clean install)_
+??? note "570->571"
+ 571 updated the python version, which caused a dll conflict with previous installs.
+
+ If you need to update from 570 or before to 571 or later, then:
+
+ * If you use the Windows installer, install as normal.
+ * If you use one of the normal extract builds, you will have to do a 'clean install', as above.
+ * If you use the macOS app, there are no special instructions. Update as normal.
+ * If you run from source, there are no special instructions. Update as normal.
## Backing up
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index 79bebbd3f..0c3fddbb1 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,37 @@
+ -
+
+
+ explorer integration
+ - thanks to a user, we have some new OS-file-explorer integration
+ - two additional options are added to the "open" menu for Windows users, "in another program" opens the Windows dialog to select which program to use and "properties" opens the Windows file properties dialog for the file
+ - the 'media' shortcut set gets the new 'open file properties' and 'open with...' commands to plug into these new features
+ - the "open in file browser" media menu command now more reliably selects the file in Windows and is now available for most Linux file managers--full list [here](https://github.com/damonlynch/showinfilemanager#supported-file-managers).
+ - the "open files' locations" file import log menu command is similarly more reliable, and can sometimes select multiple files when launched on a selection
+ - this requires a new external library, so users who run from source will want to rebuild their venvs this week to get this functionality
+ misc
+ - the manage times single-time edit dialog's paste button can now eat any datstring you can think of. try pasting 'yesterday 3am' into it, it'll work!
+ - split the increasingly cluttered 'media' options panel into 'media playback' (options governing how media is rendered) and 'media viewer' (options governing the viewer itself like drags and slideshows)
+ - added to the new 'media viewer' panel are five checkboxes to turn off the background text in the full media viewer--for the taglist, the top hover, the top-right hover, the notes hover, and the bottom-right index string. if you want, you can have a completely blank background now
+ - gave the _help->about_ window a pass. I broke the cluttered first tab into two, and the layout all over is a bit clearer
+ - the _help->advanced mode_ option is now available under a new _options->advanced_ tab. this thing covers several dozen things across the program, all insufficiently documented, so the plan is to blow it out into all its granular constituent components on this page!
+ - fixed it so an invalid `ApplicationCommand` will still render a string. if you got some jank `ToString()` errors in a shortcuts dialog recently, please try again and let me know what you get. you'll probably want to go into the actual shortcut with the error string and try and see if you can fix what it has set--again, let me know the details please!
+ - updated the 'installing and updating' help page to talk clearly about the different versions that have special update instructions, and generally gave the language a pass
+ some url encoding
+ - fixed an issue in url encoding-normalisation where urls were not retaining their parameters if their names had certain decoded characters (particularly, this was stuff like the decoded square brackets in `fields[post]=123`). a new unit test will catch this in future
+ - url classes and parsers are now careful to encode their example urls any time they are asked for (outside of their respective edit dialogs' "example url(s)" fields, so if you want to work with a human-looking URL in UI, that's fine). this ensures the automatic url-parser linking system works if the parser and url classes have a mish-mash of encoded and non-encoded example URLs. it also fixes some stuff like the multi-column list in the manage url classes dialog when the url class has a decoded example url. this was basically just an ingestion point that I missed in the previous work
+ - the edit parser dialog makes sure to properly encode the URL when you do a test pull
+ orphan table tech
+ - the _database->db maintenance->clear orphan tables_ command, which could previously only clear out the repository update/processing-tracking tables, can now nuke: the core file list tables in client.db; the core mappings tables in client.mappings.db; the display and storage mappings caches in client.caches.db; the display and storage autocomplete count caches; the ideal and actual tag parent lookup tables in client.caches.db; the ideal and actual tag sibling lookup tables in client.caches.db; and the various tag search tables (except the fts4 stuff) in client.caches.db
+ - when this job fires, it now sends orphan tables to the deferred delete system (previously it dropped them immediately, which for a big mappings table is a no-go)
+ boring cleanup
+ - cleaned a bunch of db table code for the new orphan table stuff
+ - deleted the old 'yaml_dumps' table and all associated methods, which are all now unused
+ - added a couple help labels to the "colours" and "style" pages to better explain what is actually going on here
+
+
-
diff --git a/hydrus/client/ClientApplicationCommand.py b/hydrus/client/ClientApplicationCommand.py
index b9033637c..c74642852 100644
--- a/hydrus/client/ClientApplicationCommand.py
+++ b/hydrus/client/ClientApplicationCommand.py
@@ -387,7 +387,7 @@
SIMPLE_MAC_QUICKLOOK : 'open quick look for selected file (macOS only)',
SIMPLE_COPY_URLS : 'copy file known urls',
SIMPLE_NATIVE_OPEN_FILE_PROPERTIES: 'open file properties',
- SIMPLE_NATIVE_OPEN_FILE_WITH_DIALOG: 'open with'
+ SIMPLE_NATIVE_OPEN_FILE_WITH_DIALOG: 'open with' + HC.UNICODE_ELLIPSIS
}
legacy_simple_str_to_enum_lookup = {
@@ -905,195 +905,202 @@ def IsContentCommand( self ):
def ToString( self ):
- if self._command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
-
- action = self.GetSimpleAction()
+ try:
- s = simple_enum_to_str_lookup[ action ]
-
- if action == SIMPLE_SHOW_DUPLICATES:
-
- duplicate_type = self.GetSimpleData()
-
- s = f'{s} {HC.duplicate_type_string_lookup[ duplicate_type ]}'
-
- elif action == SIMPLE_MEDIA_SEEK_DELTA:
-
- ( direction, ms ) = self.GetSimpleData()
-
- direction_s = 'back' if direction == -1 else 'forwards'
+ if self._command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
- ms_s = HydrusTime.TimeDeltaToPrettyTimeDelta( ms / 1000 )
+ action = self.GetSimpleAction()
- s = f'{s} ({direction_s} {ms_s})'
+ s = simple_enum_to_str_lookup[ action ]
- elif action == SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
-
- hamming_distance = self.GetSimpleData()
-
- if hamming_distance in CC.hamming_string_lookup:
+ if action == SIMPLE_SHOW_DUPLICATES:
- s = f'{s} ({hamming_distance} - {CC.hamming_string_lookup[ hamming_distance ]})'
+ duplicate_type = self.GetSimpleData()
- else:
+ s = f'{s} {HC.duplicate_type_string_lookup[ duplicate_type ]}'
- s = f'{s} ({hamming_distance})'
+ elif action == SIMPLE_MEDIA_SEEK_DELTA:
-
- elif action == SIMPLE_COPY_FILE_HASHES:
-
- ( file_command_target, hash_type ) = self.GetSimpleData()
-
- s = f'{s} ({hash_type}, {file_command_target_enum_to_str_lookup[ file_command_target ]})'
-
- elif action == SIMPLE_COPY_FILE_BITMAP:
-
- bitmap_type = self.GetSimpleData()
-
- s = f'{s} ({bitmap_type_enum_to_str_lookup[ bitmap_type ]})'
-
- elif action in ( SIMPLE_COPY_FILES, SIMPLE_COPY_FILE_PATHS, SIMPLE_COPY_FILE_ID ):
-
- file_command_target = self.GetSimpleData()
-
- s = f'{s} ({file_command_target_enum_to_str_lookup[ file_command_target ]})'
-
- elif action == SIMPLE_COPY_FILE_SERVICE_FILENAMES:
-
- hacky_ipfs_dict = self.GetSimpleData()
-
- try:
+ ( direction, ms ) = self.GetSimpleData()
- file_command_target_string = file_command_target_enum_to_str_lookup[ hacky_ipfs_dict[ 'file_command_target' ] ]
+ direction_s = 'back' if direction == -1 else 'forwards'
- except:
+ ms_s = HydrusTime.TimeDeltaToPrettyTimeDelta( ms / 1000 )
- file_command_target_string = 'unknown'
+ s = f'{s} ({direction_s} {ms_s})'
-
- try:
+ elif action == SIMPLE_OPEN_SIMILAR_LOOKING_FILES:
- ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ]
+ hamming_distance = self.GetSimpleData()
- name = CG.client_controller.services_manager.GetName( ipfs_service_key )
+ if hamming_distance in CC.hamming_string_lookup:
+
+ s = f'{s} ({hamming_distance} - {CC.hamming_string_lookup[ hamming_distance ]})'
+
+ else:
+
+ s = f'{s} ({hamming_distance})'
+
- except:
+ elif action == SIMPLE_COPY_FILE_HASHES:
- name = 'unknown service'
+ ( file_command_target, hash_type ) = self.GetSimpleData()
-
- s = f'{s} ({name}, {file_command_target_string})'
-
- elif action == SIMPLE_MOVE_THUMBNAIL_FOCUS:
-
- ( move_direction, selection_status ) = self.GetSimpleData()
-
- s = f'{s} ({selection_status_enum_to_str_lookup[selection_status]} {move_enum_to_str_lookup[move_direction]})'
-
- elif action == SIMPLE_SELECT_FILES:
-
- file_filter = self.GetSimpleData()
-
- s = f'{s} ({file_filter.ToString()})'
-
- elif action == SIMPLE_REARRANGE_THUMBNAILS:
-
- ( rearrange_type, rearrange_data ) = self.GetSimpleData()
-
- if rearrange_type == REARRANGE_THUMBNAILS_TYPE_COMMAND:
+ s = f'{s} ({hash_type}, {file_command_target_enum_to_str_lookup[ file_command_target ]})'
- s = f'{s} ({move_enum_to_str_lookup[ rearrange_data ]})'
+ elif action == SIMPLE_COPY_FILE_BITMAP:
- elif rearrange_type == REARRANGE_THUMBNAILS_TYPE_FIXED:
+ bitmap_type = self.GetSimpleData()
- s = f'{s} (to index {HydrusData.ToHumanInt(rearrange_data)})'
+ s = f'{s} ({bitmap_type_enum_to_str_lookup[ bitmap_type ]})'
-
-
- return s
-
- elif self._command_type == APPLICATION_COMMAND_TYPE_CONTENT:
-
- ( service_key, content_type, action, value ) = self._data
-
- components = []
-
- components.append( HC.content_update_string_lookup[ action ] )
- components.append( HC.content_type_string_lookup[ content_type ] )
-
- value_string = ''
-
- if content_type == HC.CONTENT_TYPE_RATINGS:
-
- if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
+ elif action in ( SIMPLE_COPY_FILES, SIMPLE_COPY_FILE_PATHS, SIMPLE_COPY_FILE_ID ):
+
+ file_command_target = self.GetSimpleData()
+
+ s = f'{s} ({file_command_target_enum_to_str_lookup[ file_command_target ]})'
+
+ elif action == SIMPLE_COPY_FILE_SERVICE_FILENAMES:
- value_string = 'uncertain rating, "{}"'.format( value )
+ hacky_ipfs_dict = self.GetSimpleData()
- if CG.client_controller is not None and CG.client_controller.IsBooted():
+ try:
- try:
-
- service = CG.client_controller.services_manager.GetService( service_key )
-
- value_string = service.ConvertNoneableRatingToString( value )
-
- except HydrusExceptions.DataMissing:
-
- pass
-
+ file_command_target_string = file_command_target_enum_to_str_lookup[ hacky_ipfs_dict[ 'file_command_target' ] ]
+
+ except:
+
+ file_command_target_string = 'unknown'
- else:
+ try:
+
+ ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ]
+
+ name = CG.client_controller.services_manager.GetName( ipfs_service_key )
+
+ except:
+
+ name = 'unknown service'
+
- value_string = '' # only 1 up/down allowed atm
+ s = f'{s} ({name}, {file_command_target_string})'
-
- elif content_type == HC.CONTENT_TYPE_FILES and action == HC.CONTENT_UPDATE_MOVE and value is not None:
-
- try:
+ elif action == SIMPLE_MOVE_THUMBNAIL_FOCUS:
- from_name = CG.client_controller.services_manager.GetName( value )
+ ( move_direction, selection_status ) = self.GetSimpleData()
- value_string = '(from {})'.format( from_name )
+ s = f'{s} ({selection_status_enum_to_str_lookup[selection_status]} {move_enum_to_str_lookup[move_direction]})'
- except:
+ elif action == SIMPLE_SELECT_FILES:
- value_string = ''
+ file_filter = self.GetSimpleData()
+
+ s = f'{s} ({file_filter.ToString()})'
+
+ elif action == SIMPLE_REARRANGE_THUMBNAILS:
+
+ ( rearrange_type, rearrange_data ) = self.GetSimpleData()
+
+ if rearrange_type == REARRANGE_THUMBNAILS_TYPE_COMMAND:
+
+ s = f'{s} ({move_enum_to_str_lookup[ rearrange_data ]})'
+
+ elif rearrange_type == REARRANGE_THUMBNAILS_TYPE_FIXED:
+
+ s = f'{s} (to index {HydrusData.ToHumanInt(rearrange_data)})'
+
- elif value is not None:
-
- value_string = '"{}"'.format( value )
+ return s
-
- if len( value_string ) > 0:
+ elif self._command_type == APPLICATION_COMMAND_TYPE_CONTENT:
- components.append( value_string )
+ ( service_key, content_type, action, value ) = self._data
-
- if content_type == HC.CONTENT_TYPE_FILES:
+ components = []
- components.append( 'to' )
+ components.append( HC.content_update_string_lookup[ action ] )
+ components.append( HC.content_type_string_lookup[ content_type ] )
- else:
+ value_string = ''
- components.append( 'for' )
+ if content_type == HC.CONTENT_TYPE_RATINGS:
+
+ if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
+
+ value_string = 'uncertain rating, "{}"'.format( value )
+
+ if CG.client_controller is not None and CG.client_controller.IsBooted():
+
+ try:
+
+ service = CG.client_controller.services_manager.GetService( service_key )
+
+ value_string = service.ConvertNoneableRatingToString( value )
+
+ except HydrusExceptions.DataMissing:
+
+ pass
+
+
+
+ else:
+
+ value_string = '' # only 1 up/down allowed atm
+
+
+ elif content_type == HC.CONTENT_TYPE_FILES and action == HC.CONTENT_UPDATE_MOVE and value is not None:
+
+ try:
+
+ from_name = CG.client_controller.services_manager.GetName( value )
+
+ value_string = '(from {})'.format( from_name )
+
+ except:
+
+ value_string = ''
+
+
+ elif value is not None:
+
+ value_string = '"{}"'.format( value )
+
-
- services_manager = CG.client_controller.services_manager
-
- if services_manager.ServiceExists( service_key ):
+ if len( value_string ) > 0:
+
+ components.append( value_string )
+
- service = services_manager.GetService( service_key )
+ if content_type == HC.CONTENT_TYPE_FILES:
+
+ components.append( 'to' )
+
+ else:
+
+ components.append( 'for' )
+
- components.append( service.GetName() )
+ services_manager = CG.client_controller.services_manager
- else:
+ if services_manager.ServiceExists( service_key ):
+
+ service = services_manager.GetService( service_key )
+
+ components.append( service.GetName() )
+
+ else:
+
+ components.append( 'unknown service!' )
+
- components.append( 'unknown service!' )
+ return ' '.join( components )
- return ' '.join( components )
+ except:
+
+ return 'Unknown Application Command: ' + repr( ( self._command_type, self._data ) )
diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py
index 7ac911875..157a3d340 100644
--- a/hydrus/client/ClientOptions.py
+++ b/hydrus/client/ClientOptions.py
@@ -243,6 +243,11 @@ def _InitialiseDefaults( self ):
'use_custom_sibling_connector_colour' : False,
'hide_uninteresting_local_import_time' : True,
'hide_uninteresting_modified_time' : True,
+ 'draw_tags_hover_in_media_viewer_background' : True,
+ 'draw_top_hover_in_media_viewer_background' : True,
+ 'draw_top_right_hover_in_media_viewer_background' : True,
+ 'draw_notes_hover_in_media_viewer_background' : True,
+ 'draw_bottom_right_index_in_media_viewer_background' : True,
'allow_blurhash_fallback' : True,
'fade_thumbnails' : True,
'slideshow_always_play_duration_media_once_through' : False,
diff --git a/hydrus/client/ClientParsing.py b/hydrus/client/ClientParsing.py
index ea2766175..c9d8e9df8 100644
--- a/hydrus/client/ClientParsing.py
+++ b/hydrus/client/ClientParsing.py
@@ -2599,9 +2599,16 @@ def GetExampleParsingContext( self ):
return self._example_parsing_context
- def GetExampleURLs( self ):
+ def GetExampleURLs( self, encoded = True ):
- return self._example_urls
+ if encoded:
+
+ return [ ClientNetworkingFunctions.EnsureURLIsEncoded( url ) for url in self._example_urls ]
+
+ else:
+
+ return list( self._example_urls )
+
def GetNamespaces( self ):
diff --git a/hydrus/client/ClientPaths.py b/hydrus/client/ClientPaths.py
index e9b408000..3614e07dd 100644
--- a/hydrus/client/ClientPaths.py
+++ b/hydrus/client/ClientPaths.py
@@ -2,6 +2,7 @@
import webbrowser
from hydrus.core import HydrusConstants as HC
+from hydrus.core import HydrusData
from hydrus.core import HydrusPaths
from hydrus.client import ClientGlobals as CG
@@ -19,7 +20,15 @@
if HC.PLATFORM_WINDOWS:
- from hydrus.client import ClientWindowsIntegration
+ try:
+
+ from hydrus.client import ClientWindowsIntegration
+
+ except Exception as e:
+
+ HydrusData.Print( 'Could not import ClientWindowsIntegration--maybe you need PyWin32 in your venv?' )
+ HydrusData.PrintException( e, do_wait = False )
+
CAN_OPEN_FILE_LOCATION = HC.PLATFORM_WINDOWS or HC.PLATFORM_MACOS or ( HC.PLATFORM_LINUX and SHOW_IN_FILE_MANAGER_OK )
diff --git a/hydrus/client/ClientServices.py b/hydrus/client/ClientServices.py
index f280cd316..3b5747dac 100644
--- a/hydrus/client/ClientServices.py
+++ b/hydrus/client/ClientServices.py
@@ -231,7 +231,7 @@ def _SetDirty( self ):
CG.client_controller.pub( 'service_updated', self )
- def CheckFunctional( self ) -> bool:
+ def CheckFunctional( self ):
with self._lock:
diff --git a/hydrus/client/ClientStrings.py b/hydrus/client/ClientStrings.py
index 0413d4f2e..92046faf5 100644
--- a/hydrus/client/ClientStrings.py
+++ b/hydrus/client/ClientStrings.py
@@ -984,11 +984,11 @@ def ToString( self, simple = False, with_type = False ) -> str:
result = 'selecting everything'
- elif self._index_end is None:
+ elif self._index_start is not None and self._index_end is None:
result = 'selecting the {} string and onwards'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_start ) )
- elif self._index_start is None:
+ elif self._index_start is None and self._index_end is not None:
result = 'selecting up to and including the {} string'.format( HydrusData.ConvertIndexToPrettyOrdinalString( self._index_end - 1 ) )
diff --git a/hydrus/client/ClientVideoHandling.py b/hydrus/client/ClientVideoHandling.py
index 00b3da983..dbcb7e79d 100644
--- a/hydrus/client/ClientVideoHandling.py
+++ b/hydrus/client/ClientVideoHandling.py
@@ -116,7 +116,9 @@ def _MoveRendererOnOneFrame( self ):
def _RenderCurrentFrameAndResizeIt( self ) -> numpy.array:
- if self._cannot_seek_to_or_beyond_this_index is not None and self._current_render_index >= self._cannot_seek_to_or_beyond_this_index:
+ we_are_in_the_dangerzone = self._cannot_seek_to_or_beyond_this_index is not None and self._current_render_index >= self._cannot_seek_to_or_beyond_this_index
+
+ if we_are_in_the_dangerzone:
numpy_image = self._GetRecoveryFrame()
diff --git a/hydrus/client/ClientWindowsIntegration.py b/hydrus/client/ClientWindowsIntegration.py
index 97e0c0c68..fb42f8c48 100644
--- a/hydrus/client/ClientWindowsIntegration.py
+++ b/hydrus/client/ClientWindowsIntegration.py
@@ -1,3 +1,4 @@
+# noinspection PyUnresolvedReferences
from win32com.shell import shell, shellcon
def OpenFileProperties( path: str ):
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index cb7caf1ff..6486c2d82 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -10403,6 +10403,25 @@ def ask_what_to_do_zip_docx_scan():
+ if version == 575:
+
+ try:
+
+ if self._TableExists( 'yaml_dumps' ):
+
+ self._Execute( 'DROP TABLE yaml_dumps;' )
+
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Trying to delete an old table failed! Please let hydrus dev know!'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/db/ClientDBFilesStorage.py b/hydrus/client/db/ClientDBFilesStorage.py
index 37a12511c..45d77a47e 100644
--- a/hydrus/client/db/ClientDBFilesStorage.py
+++ b/hydrus/client/db/ClientDBFilesStorage.py
@@ -17,17 +17,22 @@
from hydrus.client.db import ClientDBModule
from hydrus.client.db import ClientDBServices
+FILES_CURRENT_PREFIX = 'current_files_'
+FILES_DELETED_PREFIX = 'deleted_files_'
+FILES_PENDING_PREFIX = 'pending_files_'
+FILES_PETITIONED_PREFIX = 'petitioned_files_'
+
def GenerateFilesTableNames( service_id: int ) -> typing.Tuple[ str, str, str, str ]:
suffix = str( service_id )
- current_files_table_name = 'main.current_files_{}'.format( suffix )
+ current_files_table_name = f'main.{FILES_CURRENT_PREFIX}{suffix}'
- deleted_files_table_name = 'main.deleted_files_{}'.format( suffix )
+ deleted_files_table_name = f'main.{FILES_DELETED_PREFIX}{suffix}'
- pending_files_table_name = 'main.pending_files_{}'.format( suffix )
+ pending_files_table_name = f'main.{FILES_PENDING_PREFIX}{suffix}'
- petitioned_files_table_name = 'main.petitioned_files_{}'.format( suffix )
+ petitioned_files_table_name = f'main.{FILES_PETITIONED_PREFIX}{suffix}'
return ( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name )
@@ -298,6 +303,16 @@ def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_FILE_SERVICES )
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ FILES_CURRENT_PREFIX,
+ FILES_DELETED_PREFIX,
+ FILES_PENDING_PREFIX,
+ FILES_PETITIONED_PREFIX
+ }
+
+
def _GetTimestampMS( self, service_id: int, timestamp_type: int, hash_id: int ) -> typing.Optional[ int ]:
( current_files_table_name, deleted_files_table_name, pending_files_table_name, petitioned_files_table_name ) = GenerateFilesTableNames( service_id )
diff --git a/hydrus/client/db/ClientDBMaintenance.py b/hydrus/client/db/ClientDBMaintenance.py
index dbc0db0a6..41c528a61 100644
--- a/hydrus/client/db/ClientDBMaintenance.py
+++ b/hydrus/client/db/ClientDBMaintenance.py
@@ -406,10 +406,7 @@ def ClearOrphanTables( self ):
table_names = self._STS( self._Execute( 'SELECT name FROM {}.sqlite_master WHERE type = ?;'.format( db_name ), ( 'table', ) ) )
- if db_name != 'main':
-
- table_names = { f'{db_name}.{table_name}' for table_name in table_names }
-
+ table_names = { f'{db_name}.{table_name}' for table_name in table_names }
all_table_names.update( table_names )
@@ -430,9 +427,9 @@ def ClearOrphanTables( self ):
for table_name in all_surplus_table_names:
- HydrusData.ShowText( f'Dropping {table_name}' )
+ HydrusData.ShowText( f'Cleared orphan table "{table_name}"' )
- self._Execute( f'DROP table {table_name};' )
+ self.DeferredDropTable( table_name )
diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
index 34ebabbc8..99b6d843d 100644
--- a/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
+++ b/hydrus/client/db/ClientDBMappingsCacheSpecificDisplay.py
@@ -104,6 +104,14 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
return table_dict
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ ClientDBMappingsStorage.SPECIFIC_DISPLAY_MAPPINGS_CURRENT_PREFIX,
+ ClientDBMappingsStorage.SPECIFIC_DISPLAY_MAPPINGS_PENDING_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
index 94cdfb22e..f486aad23 100644
--- a/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
+++ b/hydrus/client/db/ClientDBMappingsCacheSpecificStorage.py
@@ -171,6 +171,15 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
return table_dict
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ ClientDBMappingsStorage.SPECIFIC_MAPPINGS_CURRENT_PREFIX,
+ ClientDBMappingsStorage.SPECIFIC_MAPPINGS_DELETED_PREFIX,
+ ClientDBMappingsStorage.SPECIFIC_MAPPINGS_PENDING_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/db/ClientDBMappingsCounts.py b/hydrus/client/db/ClientDBMappingsCounts.py
index 996fd4480..9e1da0ee0 100644
--- a/hydrus/client/db/ClientDBMappingsCounts.py
+++ b/hydrus/client/db/ClientDBMappingsCounts.py
@@ -11,40 +11,47 @@
from hydrus.client.db import ClientDBServices
from hydrus.client.metadata import ClientTags
+FILES_COMBINED_AC_CACHE_PREFIX = 'combined_files_ac_cache_'
+FILES_COMBINED_DISPLAY_AC_CACHE_PREFIX = 'combined_files_display_ac_cache_'
+FILES_SPECIFIC_AC_CACHE_PREFIX = 'specific_ac_cache_'
+FILES_SPECIFIC_DISPLAY_AC_CACHE_PREFIX = 'specific_display_ac_cache_'
+
def GenerateCombinedFilesMappingsCountsCacheTableName( tag_display_type, tag_service_id ):
if tag_display_type == ClientTags.TAG_DISPLAY_STORAGE:
- name = 'combined_files_ac_cache'
+ prefix = FILES_COMBINED_AC_CACHE_PREFIX
elif tag_display_type == ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL:
- name = 'combined_files_display_ac_cache'
+ prefix = FILES_COMBINED_DISPLAY_AC_CACHE_PREFIX
suffix = str( tag_service_id )
- combined_counts_cache_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ combined_counts_cache_table_name = f'external_caches.{prefix}{suffix}'
return combined_counts_cache_table_name
+
def GenerateSpecificCountsCacheTableName( tag_display_type, file_service_id, tag_service_id ):
if tag_display_type == ClientTags.TAG_DISPLAY_STORAGE:
- name = 'specific_ac_cache'
+ prefix = FILES_SPECIFIC_AC_CACHE_PREFIX
elif tag_display_type == ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL:
- name = 'specific_display_ac_cache'
+ prefix = FILES_SPECIFIC_DISPLAY_AC_CACHE_PREFIX
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- specific_counts_cache_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ specific_counts_cache_table_name = f'external_caches.{prefix}{suffix}'
return specific_counts_cache_table_name
+
class ClientDBMappingsCounts( ClientDBModule.ClientDBModule ):
CAN_REPOPULATE_ALL_MISSING_DATA = True
@@ -96,6 +103,16 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
return table_dict
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ FILES_COMBINED_AC_CACHE_PREFIX,
+ FILES_COMBINED_DISPLAY_AC_CACHE_PREFIX,
+ FILES_SPECIFIC_AC_CACHE_PREFIX,
+ FILES_SPECIFIC_DISPLAY_AC_CACHE_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/db/ClientDBMappingsStorage.py b/hydrus/client/db/ClientDBMappingsStorage.py
index 0daf1c54b..26e379640 100644
--- a/hydrus/client/db/ClientDBMappingsStorage.py
+++ b/hydrus/client/db/ClientDBMappingsStorage.py
@@ -25,41 +25,53 @@ def DoingAFileJoinTagSearchIsFaster( estimated_file_row_count, estimated_tag_row
return estimated_file_row_count * ( file_lookup_speed_ratio + temp_table_overhead ) < estimated_tag_row_count
+MAPPINGS_CURRENT_PREFIX = 'current_mappings_'
+MAPPINGS_DELETED_PREFIX = 'deleted_mappings_'
+MAPPINGS_PENDING_PREFIX = 'pending_mappings_'
+MAPPINGS_PETITIONED_PREFIX = 'petitioned_mappings_'
+
def GenerateMappingsTableNames( service_id: int ) -> typing.Tuple[ str, str, str, str ]:
suffix = str( service_id )
- current_mappings_table_name = 'external_mappings.current_mappings_{}'.format( suffix )
+ current_mappings_table_name = f'external_mappings.{MAPPINGS_CURRENT_PREFIX}{suffix}'
- deleted_mappings_table_name = 'external_mappings.deleted_mappings_{}'.format( suffix )
+ deleted_mappings_table_name = f'external_mappings.{MAPPINGS_DELETED_PREFIX}{suffix}'
- pending_mappings_table_name = 'external_mappings.pending_mappings_{}'.format( suffix )
+ pending_mappings_table_name = f'external_mappings.{MAPPINGS_PENDING_PREFIX}{suffix}'
- petitioned_mappings_table_name = 'external_mappings.petitioned_mappings_{}'.format( suffix )
+ petitioned_mappings_table_name = f'external_mappings.{MAPPINGS_PETITIONED_PREFIX}{suffix}'
return ( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name )
+SPECIFIC_DISPLAY_MAPPINGS_CURRENT_PREFIX = 'specific_display_current_mappings_cache_'
+SPECIFIC_DISPLAY_MAPPINGS_PENDING_PREFIX = 'specific_display_pending_mappings_cache_'
+
def GenerateSpecificDisplayMappingsCacheTableNames( file_service_id, tag_service_id ):
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- cache_display_current_mappings_table_name = 'external_caches.specific_display_current_mappings_cache_{}'.format( suffix )
+ cache_display_current_mappings_table_name = f'external_caches.{SPECIFIC_DISPLAY_MAPPINGS_CURRENT_PREFIX}{suffix}'
- cache_display_pending_mappings_table_name = 'external_caches.specific_display_pending_mappings_cache_{}'.format( suffix )
+ cache_display_pending_mappings_table_name = f'external_caches.{SPECIFIC_DISPLAY_MAPPINGS_PENDING_PREFIX}{suffix}'
return ( cache_display_current_mappings_table_name, cache_display_pending_mappings_table_name )
+SPECIFIC_MAPPINGS_CURRENT_PREFIX = 'specific_current_mappings_cache_'
+SPECIFIC_MAPPINGS_DELETED_PREFIX = 'specific_deleted_mappings_cache_'
+SPECIFIC_MAPPINGS_PENDING_PREFIX = 'specific_pending_mappings_cache_'
+
def GenerateSpecificMappingsCacheTableNames( file_service_id, tag_service_id ):
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- cache_current_mappings_table_name = 'external_caches.specific_current_mappings_cache_{}'.format( suffix )
+ cache_current_mappings_table_name = f'external_caches.{SPECIFIC_MAPPINGS_CURRENT_PREFIX}{suffix}'
- cache_deleted_mappings_table_name = 'external_caches.specific_deleted_mappings_cache_{}'.format( suffix )
+ cache_deleted_mappings_table_name = f'external_caches.{SPECIFIC_MAPPINGS_DELETED_PREFIX}{suffix}'
- cache_pending_mappings_table_name = 'external_caches.specific_pending_mappings_cache_{}'.format( suffix )
+ cache_pending_mappings_table_name = f'external_caches.{SPECIFIC_MAPPINGS_PENDING_PREFIX}{suffix}'
return ( cache_current_mappings_table_name, cache_deleted_mappings_table_name, cache_pending_mappings_table_name )
@@ -116,6 +128,16 @@ def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ MAPPINGS_CURRENT_PREFIX,
+ MAPPINGS_DELETED_PREFIX,
+ MAPPINGS_PENDING_PREFIX,
+ MAPPINGS_PETITIONED_PREFIX
+ }
+
+
def ClearMappingsTables( self, service_id: int ):
( current_mappings_table_name, deleted_mappings_table_name, pending_mappings_table_name, petitioned_mappings_table_name ) = GenerateMappingsTableNames( service_id )
diff --git a/hydrus/client/db/ClientDBSerialisable.py b/hydrus/client/db/ClientDBSerialisable.py
index 8ac59aa31..39659cd99 100644
--- a/hydrus/client/db/ClientDBSerialisable.py
+++ b/hydrus/client/db/ClientDBSerialisable.py
@@ -161,8 +161,7 @@ def _GetCriticalTableNames( self ) -> typing.Collection[ str ]:
return {
'main.json_dict',
- 'main.json_dumps',
- 'main.yaml_dumps'
+ 'main.json_dumps'
}
@@ -172,8 +171,7 @@ def _GetInitialTableGenerationDict( self ) -> dict:
'main.json_dict' : ( 'CREATE TABLE IF NOT EXISTS {} ( name TEXT PRIMARY KEY, dump BLOB_BYTES );', 400 ),
'main.json_dumps' : ( 'CREATE TABLE IF NOT EXISTS {} ( dump_type INTEGER PRIMARY KEY, version INTEGER, dump BLOB_BYTES );', 400 ),
'main.json_dumps_named' : ( 'CREATE TABLE IF NOT EXISTS {} ( dump_type INTEGER, dump_name TEXT, version INTEGER, timestamp_ms INTEGER, dump BLOB_BYTES, PRIMARY KEY ( dump_type, dump_name, timestamp_ms ) );', 400 ),
- 'main.json_dumps_hashed' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash BLOB_BYTES PRIMARY KEY, dump_type INTEGER, version INTEGER, dump BLOB_BYTES );', 442 ),
- 'main.yaml_dumps' : ( 'CREATE TABLE IF NOT EXISTS {} ( dump_type INTEGER, dump_name TEXT, dump TEXT_YAML, PRIMARY KEY ( dump_type, dump_name ) );', 400 )
+ 'main.json_dumps_hashed' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash BLOB_BYTES PRIMARY KEY, dump_type INTEGER, version INTEGER, dump BLOB_BYTES );', 442 )
}
@@ -198,18 +196,6 @@ def DeleteJSONDumpNamed( self, dump_type, dump_name = None, timestamp_ms = None
- def DeleteYAMLDump( self, dump_type, dump_name = None ):
-
- if dump_name is None:
-
- self._Execute( 'DELETE FROM yaml_dumps WHERE dump_type = ?;', ( dump_type, ) )
-
- else:
-
- self._Execute( 'DELETE FROM yaml_dumps WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) )
-
-
-
def GetAllExpectedHashedJSONHashes( self ) -> typing.Collection[ bytes ]:
all_expected_hashes = set()
@@ -495,51 +481,6 @@ def GetTablesAndColumnsThatUseDefinitions( self, content_type: int ) -> typing.L
return []
- def GetYAMLDump( self, dump_type, dump_name = None ):
-
- if dump_name is None:
-
- result = { dump_name : data for ( dump_name, data ) in self._Execute( 'SELECT dump_name, dump FROM yaml_dumps WHERE dump_type = ?;', ( dump_type, ) ) }
-
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU:
-
- result = { bytes.fromhex( dump_name ) : data for ( dump_name, data ) in list(result.items()) }
-
-
- else:
-
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU: dump_name = dump_name.hex()
-
- result = self._Execute( 'SELECT dump FROM yaml_dumps WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) ).fetchone()
-
- if result is None:
-
- if result is None:
-
- raise HydrusExceptions.DataMissing( dump_name + ' was not found!' )
-
-
- else:
-
- ( result, ) = result
-
-
-
- return result
-
-
- def GetYAMLDumpNames( self, dump_type ):
-
- names = [ name for ( name, ) in self._Execute( 'SELECT dump_name FROM yaml_dumps WHERE dump_type = ?;', ( dump_type, ) ) ]
-
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU:
-
- names = [ bytes.fromhex( name ) for name in names ]
-
-
- return names
-
-
def HaveHashedJSONDump( self, hash ):
result = self._Execute( 'SELECT 1 FROM json_dumps_hashed WHERE hash = ?;', ( sqlite3.Binary( hash ), ) ).fetchone()
@@ -900,24 +841,3 @@ def SetJSONSimple( self, name, value ):
- def SetYAMLDump( self, dump_type, dump_name, data ):
-
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU:
-
- dump_name = dump_name.hex()
-
-
- self._Execute( 'DELETE FROM yaml_dumps WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) )
-
- try:
-
- self._Execute( 'INSERT INTO yaml_dumps ( dump_type, dump_name, dump ) VALUES ( ?, ?, ? );', ( dump_type, dump_name, data ) )
-
- except:
-
- HydrusData.Print( ( dump_type, dump_name, data ) )
-
- raise
-
-
-
diff --git a/hydrus/client/db/ClientDBTagParents.py b/hydrus/client/db/ClientDBTagParents.py
index 94aadb5ff..c3f9b6967 100644
--- a/hydrus/client/db/ClientDBTagParents.py
+++ b/hydrus/client/db/ClientDBTagParents.py
@@ -28,13 +28,20 @@ def GenerateTagParentsLookupCacheTableName( display_type: int, service_id: int )
return cache_actual_tag_parents_lookup_table_name
+
+TAG_PARENTS_IDEAL_PREFIX = 'ideal_tag_parents_lookup_cache_'
+TAG_PARENTS_ACTUAL_PREFIX = 'actual_tag_parents_lookup_cache_'
+
def GenerateTagParentsLookupCacheTableNames( service_id ):
- cache_ideal_tag_parents_lookup_table_name = 'external_caches.ideal_tag_parents_lookup_cache_{}'.format( service_id )
- cache_actual_tag_parents_lookup_table_name = 'external_caches.actual_tag_parents_lookup_cache_{}'.format( service_id )
+ suffix = service_id
+
+ cache_ideal_tag_parents_lookup_table_name = f'external_caches.{TAG_PARENTS_IDEAL_PREFIX}{suffix}'
+ cache_actual_tag_parents_lookup_table_name = f'external_caches.{TAG_PARENTS_ACTUAL_PREFIX}{suffix}'
return ( cache_ideal_tag_parents_lookup_table_name, cache_actual_tag_parents_lookup_table_name )
+
class ClientDBTagParents( ClientDBModule.ClientDBModule ):
CAN_REPOPULATE_ALL_MISSING_DATA = True
@@ -112,6 +119,14 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
}
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ TAG_PARENTS_IDEAL_PREFIX,
+ TAG_PARENTS_ACTUAL_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/db/ClientDBTagSearch.py b/hydrus/client/db/ClientDBTagSearch.py
index 1c6a8b6be..b5d5a762a 100644
--- a/hydrus/client/db/ClientDBTagSearch.py
+++ b/hydrus/client/db/ClientDBTagSearch.py
@@ -41,91 +41,84 @@ def ConvertWildcardToSQLiteLikeParameter( wildcard ):
return like_param
+COMBINED_INTEGER_SUBTAGS_PREFIX = 'combined_files_integer_subtags_cache_'
+COMBINED_SUBTAGS_FTS4_PREFIX = 'combined_files_subtags_fts4_cache_'
+COMBINED_SUBTAGS_SEARCHABLE_MAP_PREFIX = 'combined_files_subtags_searchable_map_cache_'
+COMBINED_TAGS_PREFIX = 'combined_files_tags_cache_'
+
+SPECIFIC_INTEGER_SUBTAGS_PREFIX = 'specific_integer_subtags_cache_'
+SPECIFIC_SUBTAGS_FTS4_PREFIX = 'specific_subtags_fts4_cache_'
+SPECIFIC_SUBTAGS_SEARCHABLE_MAP_PREFIX = 'specific_subtags_searchable_map_cache_'
+SPECIFIC_TAGS_PREFIX = 'specific_tags_cache_'
+
def GenerateCombinedFilesIntegerSubtagsTableName( tag_service_id ):
- name = 'combined_files_integer_subtags_cache'
+ suffix = tag_service_id
- integer_subtags_table_name = 'external_caches.{}_{}'.format( name, tag_service_id )
+ integer_subtags_table_name = f'external_caches.{COMBINED_INTEGER_SUBTAGS_PREFIX}{suffix}'
return integer_subtags_table_name
def GenerateCombinedFilesSubtagsFTS4TableName( tag_service_id ):
- name = 'combined_files_subtags_fts4_cache'
+ suffix = tag_service_id
- subtags_fts4_table_name = 'external_caches.{}_{}'.format( name, tag_service_id )
+ subtags_fts4_table_name = f'external_caches.{COMBINED_SUBTAGS_FTS4_PREFIX}{suffix}'
return subtags_fts4_table_name
def GenerateCombinedFilesSubtagsSearchableMapTableName( tag_service_id ):
- name = 'combined_files_subtags_searchable_map_cache'
+ suffix = tag_service_id
- subtags_searchable_map_table_name = 'external_caches.{}_{}'.format( name, tag_service_id )
+ subtags_searchable_map_table_name = f'external_caches.{COMBINED_SUBTAGS_SEARCHABLE_MAP_PREFIX}{suffix}'
return subtags_searchable_map_table_name
def GenerateCombinedFilesTagsTableName( tag_service_id ):
- name = 'combined_files_tags_cache'
-
- tags_table_name = 'external_caches.{}_{}'.format( name, tag_service_id )
-
- return tags_table_name
-
-
-def GenerateCombinedTagsTagsTableName( file_service_id ):
-
- name = 'combined_tags_tags_cache'
+ suffix = tag_service_id
- tags_table_name = 'external_caches.{}_{}'.format( name, file_service_id )
+ tags_table_name = f'external_caches.{COMBINED_TAGS_PREFIX}{suffix}'
return tags_table_name
def GenerateSpecificIntegerSubtagsTableName( file_service_id, tag_service_id ):
- name = 'specific_integer_subtags_cache'
-
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- integer_subtags_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ integer_subtags_table_name = f'external_caches.{SPECIFIC_INTEGER_SUBTAGS_PREFIX}{suffix}'
return integer_subtags_table_name
def GenerateSpecificSubtagsFTS4TableName( file_service_id, tag_service_id ):
- name = 'specific_subtags_fts4_cache'
-
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- subtags_fts4_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ subtags_fts4_table_name = f'external_caches.{SPECIFIC_SUBTAGS_FTS4_PREFIX}{suffix}'
return subtags_fts4_table_name
def GenerateSpecificSubtagsSearchableMapTableName( file_service_id, tag_service_id ):
- name = 'specific_subtags_searchable_map_cache'
-
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- subtags_searchable_map_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ subtags_searchable_map_table_name = f'external_caches.{SPECIFIC_SUBTAGS_SEARCHABLE_MAP_PREFIX}{suffix}'
return subtags_searchable_map_table_name
def GenerateSpecificTagsTableName( file_service_id, tag_service_id ):
- name = 'specific_tags_cache'
-
suffix = '{}_{}'.format( file_service_id, tag_service_id )
- tags_table_name = 'external_caches.{}_{}'.format( name, suffix )
+ tags_table_name = f'external_caches.{SPECIFIC_TAGS_PREFIX}{suffix}'
return tags_table_name
@@ -248,6 +241,19 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
return table_dict
+ def _GetServiceTablePrefixes( self ):
+
+ # do not add the fts4 guys to this, since that already uses a bunch of different suffixes for its own virtual sub-tables and it all gets wrapped up false-positive in our tests!
+ return {
+ COMBINED_TAGS_PREFIX,
+ COMBINED_SUBTAGS_SEARCHABLE_MAP_PREFIX,
+ COMBINED_INTEGER_SUBTAGS_PREFIX,
+ SPECIFIC_TAGS_PREFIX,
+ SPECIFIC_SUBTAGS_SEARCHABLE_MAP_PREFIX,
+ SPECIFIC_INTEGER_SUBTAGS_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/db/ClientDBTagSiblings.py b/hydrus/client/db/ClientDBTagSiblings.py
index 1b05a0c5e..83761b0db 100644
--- a/hydrus/client/db/ClientDBTagSiblings.py
+++ b/hydrus/client/db/ClientDBTagSiblings.py
@@ -31,10 +31,15 @@ def GenerateTagSiblingsLookupCacheTableName( display_type: int, service_id: int
+TAG_SIBLINGS_IDEAL_PREFIX = 'ideal_tag_siblings_lookup_cache_'
+TAG_SIBLINGS_ACTUAL_PREFIX = 'actual_tag_siblings_lookup_cache_'
+
def GenerateTagSiblingsLookupCacheTableNames( service_id ):
- cache_ideal_tag_siblings_lookup_table_name = 'external_caches.ideal_tag_siblings_lookup_cache_{}'.format( service_id )
- cache_actual_tag_siblings_lookup_table_name = 'external_caches.actual_tag_siblings_lookup_cache_{}'.format( service_id )
+ suffix = service_id
+
+ cache_ideal_tag_siblings_lookup_table_name = f'external_caches.{TAG_SIBLINGS_IDEAL_PREFIX}{suffix}'
+ cache_actual_tag_siblings_lookup_table_name = f'external_caches.{TAG_SIBLINGS_ACTUAL_PREFIX}{suffix}'
return ( cache_ideal_tag_siblings_lookup_table_name, cache_actual_tag_siblings_lookup_table_name )
@@ -128,6 +133,14 @@ def _GetServiceTableGenerationDict( self, service_id ) -> dict:
}
+ def _GetServiceTablePrefixes( self ):
+
+ return {
+ TAG_SIBLINGS_IDEAL_PREFIX,
+ TAG_SIBLINGS_ACTUAL_PREFIX
+ }
+
+
def _GetServiceIdsWeGenerateDynamicTablesFor( self ):
return self.modules_services.GetServiceIds( HC.REAL_TAG_SERVICES )
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 9d337979e..88ca5809d 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -702,7 +702,7 @@ def __init__( self, controller: ClientControllerInterface.ClientControllerInterf
def _AboutWindow( self ):
name = 'hydrus client'
- version = '{}, using network version {}'.format( HC.SOFTWARE_VERSION, HC.NETWORK_VERSION )
+ version = 'v{}, using network version {}'.format( HC.SOFTWARE_VERSION, HC.NETWORK_VERSION )
library_version_lines = []
@@ -742,7 +742,6 @@ def _AboutWindow( self ):
library_version_lines.append( 'OpenCV: {}'.format( cv2.__version__ ) )
library_version_lines.append( 'openssl: {}'.format( ssl.OPENSSL_VERSION ) )
library_version_lines.append( 'Pillow: {}'.format( PIL.__version__ ) )
- library_version_lines.append( 'Pillow-HEIF: {}'.format( HydrusImageHandling.HEIF_OK ) )
qt_string = 'Qt: Unknown'
@@ -798,15 +797,44 @@ def _AboutWindow( self ):
library_version_lines.append( qt_string )
- library_version_lines.append( 'QtCharts ok: {}'.format( ClientGUICharts.QT_CHARTS_OK ) )
+ library_version_lines.append( 'sqlite: {}'.format( sqlite3.sqlite_version ) )
+
+ library_version_lines.append( '' )
+
+ library_version_lines.append( 'install dir: {}'.format( HC.BASE_DIR ) )
+ library_version_lines.append( 'db dir: {}'.format( CG.client_controller.db_dir ) )
+ library_version_lines.append( 'temp dir: {}'.format( HydrusTemp.GetCurrentTempDir() ) )
+
+ import locale
+
+ l_string = locale.getlocale()[0]
+ qtl_string = QC.QLocale().name()
+
+ library_version_lines.append( 'locale: {}/{}'.format( l_string, qtl_string ) )
+
+ library_version_lines.append( '' )
+
+ library_version_lines.append( 'db cache size per file: {}MB'.format( HG.db_cache_size ) )
+ library_version_lines.append( 'db journal mode: {}'.format( HG.db_journal_mode ) )
+ library_version_lines.append( 'db synchronous mode: {}'.format( HG.db_synchronous ) )
+ library_version_lines.append( 'db transaction commit period: {}'.format( HydrusTime.TimeDeltaToPrettyTimeDelta( HG.db_cache_size ) ) )
+ library_version_lines.append( 'db using memory for temp?: {}'.format( HG.no_db_temp_files ) )
+
+ description_versions = 'This is the media management application of the hydrus software suite.' + '\n' * 2 + '\n'.join( library_version_lines )
+
+ #
+
+ availability_lines = []
+
+ availability_lines.append( 'QtCharts: {}'.format( ClientGUICharts.QT_CHARTS_OK ) )
if QtInit.WE_ARE_QT5:
- library_version_lines.append( 'QtPdf not available on Qt5' )
+ availability_lines.append( 'QtPdf not available on Qt5' )
else:
- library_version_lines.append( 'QtPdf ok: {}'.format( ClientPDFHandling.PDF_OK ) )
+ availability_lines.append( 'QtPdf: {}'.format( ClientPDFHandling.PDF_OK ) )
if not ClientPDFHandling.PDF_OK:
@@ -816,7 +844,7 @@ def _AboutWindow( self ):
- library_version_lines.append( 'sqlite: {}'.format( sqlite3.sqlite_version ) )
+ availability_lines.append( 'Pillow-HEIF: {}'.format( HydrusImageHandling.HEIF_OK ) )
CBOR_AVAILABLE = False
@@ -830,9 +858,9 @@ def _AboutWindow( self ):
pass
- library_version_lines.append( 'cbor2 present: {}'.format( str( CBOR_AVAILABLE ) ) )
+ availability_lines.append( 'cbor2 present: {}'.format( str( CBOR_AVAILABLE ) ) )
- library_version_lines.append( 'chardet present: {}'.format( str( HydrusText.CHARDET_OK ) ) )
+ availability_lines.append( 'chardet present: {}'.format( str( HydrusText.CHARDET_OK ) ) )
from hydrus.client.networking import ClientNetworkingJobs
@@ -840,48 +868,32 @@ def _AboutWindow( self ):
try:
- library_version_lines.append( 'cloudscraper present: {}'.format( ClientNetworkingJobs.cloudscraper.__version__ ) )
+ availability_lines.append( 'cloudscraper present: {}'.format( ClientNetworkingJobs.cloudscraper.__version__ ) )
except:
- library_version_lines.append( 'cloudscraper present: unknown version' )
+ availability_lines.append( 'cloudscraper present: unknown version' )
else:
- library_version_lines.append( 'cloudscraper present: {}'.format( 'False' ) )
+ availability_lines.append( 'cloudscraper present: {}'.format( 'False' ) )
- library_version_lines.append( 'cryptography present: {}'.format( HydrusEncryption.CRYPTO_OK ) )
- library_version_lines.append( 'dateparser present: {}'.format( ClientTime.DATEPARSER_OK ) )
- library_version_lines.append( 'dateutil present: {}'.format( ClientTime.DATEUTIL_OK ) )
- library_version_lines.append( 'html5lib present: {}'.format( ClientParsing.HTML5LIB_IS_OK ) )
- library_version_lines.append( 'lxml present: {}'.format( ClientParsing.LXML_IS_OK ) )
- library_version_lines.append( 'lz4 present: {}'.format( HydrusCompression.LZ4_OK ) )
- library_version_lines.append( 'olefile present: {}'.format( HydrusOLEHandling.OLEFILE_OK ) )
- library_version_lines.append( 'pympler present: {}'.format( HydrusMemory.PYMPLER_OK ) )
- library_version_lines.append( 'pyopenssl present: {}'.format( HydrusEncryption.OPENSSL_OK ) )
- library_version_lines.append( 'psd_tools present: {}'.format( HydrusPSDHandling.PSD_TOOLS_OK ) )
- library_version_lines.append( 'speedcopy (experimental test) present: {}'.format( HydrusFileHandling.SPEEDCOPY_OK ) )
- library_version_lines.append( 'install dir: {}'.format( HC.BASE_DIR ) )
- library_version_lines.append( 'db dir: {}'.format( CG.client_controller.db_dir ) )
- library_version_lines.append( 'temp dir: {}'.format( HydrusTemp.GetCurrentTempDir() ) )
- library_version_lines.append( 'db cache size per file: {}MB'.format( HG.db_cache_size ) )
- library_version_lines.append( 'db journal mode: {}'.format( HG.db_journal_mode ) )
- library_version_lines.append( 'db synchronous mode: {}'.format( HG.db_synchronous ) )
- library_version_lines.append( 'db transaction commit period: {}'.format( HydrusTime.TimeDeltaToPrettyTimeDelta( HG.db_cache_size ) ) )
- library_version_lines.append( 'db using memory for temp?: {}'.format( HG.no_db_temp_files ) )
-
- import locale
+ availability_lines.append( 'cryptography present: {}'.format( HydrusEncryption.CRYPTO_OK ) )
+ availability_lines.append( 'dateparser present: {}'.format( ClientTime.DATEPARSER_OK ) )
+ availability_lines.append( 'dateutil present: {}'.format( ClientTime.DATEUTIL_OK ) )
+ availability_lines.append( 'html5lib present: {}'.format( ClientParsing.HTML5LIB_IS_OK ) )
+ availability_lines.append( 'lxml present: {}'.format( ClientParsing.LXML_IS_OK ) )
+ availability_lines.append( 'lz4 present: {}'.format( HydrusCompression.LZ4_OK ) )
+ availability_lines.append( 'olefile present: {}'.format( HydrusOLEHandling.OLEFILE_OK ) )
+ availability_lines.append( 'pympler present: {}'.format( HydrusMemory.PYMPLER_OK ) )
+ availability_lines.append( 'pyopenssl present: {}'.format( HydrusEncryption.OPENSSL_OK ) )
+ availability_lines.append( 'psd_tools present: {}'.format( HydrusPSDHandling.PSD_TOOLS_OK ) )
+ availability_lines.append( 'show-in-file-manager present: {}'.format( ClientPaths.SHOW_IN_FILE_MANAGER_OK ) )
+ availability_lines.append( 'speedcopy (experimental test) present: {}'.format( HydrusFileHandling.SPEEDCOPY_OK ) )
- l_string = locale.getlocale()[0]
- qtl_string = QC.QLocale().name()
-
- library_version_lines.append( 'locale: {}/{}'.format( l_string, qtl_string ) )
-
- description = 'This is the media management application of the hydrus software suite.'
-
- description += '\n' * 2 + '\n'.join( library_version_lines )
+ description_availability = '\n'.join( availability_lines )
#
@@ -903,7 +915,7 @@ def _AboutWindow( self ):
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'about hydrus' )
- panel = ClientGUIScrolledPanelsReview.AboutPanel( frame, name, version, description, license, developers, site )
+ panel = ClientGUIScrolledPanelsReview.AboutPanel( frame, name, version, description_versions, description_availability, license, developers, site )
frame.SetPanel( panel )
@@ -1253,7 +1265,7 @@ def _ClearOrphanFileRecords( self ):
def _ClearOrphanHashedSerialisables( self ):
- text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO'
+ text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO. MAKE A BACKUP BEFORE YOU RUN IT'
text += '\n' * 2
text += 'This force-runs a routine that regularly removes some spare data from the database. You most likely do not need to run it.'
@@ -1285,7 +1297,7 @@ def do_it():
def _ClearOrphanTables( self ):
- text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO'
+ text = 'DO NOT RUN THIS UNLESS YOU KNOW YOU NEED TO. MAKE A BACKUP BEFORE YOU RUN IT'
text += '\n' * 2
text += 'This will instruct the database to review its service tables and delete any orphans. This will typically do nothing, but hydrus dev may tell you to run this, just to check. Be sure you have a recent backup before you run this--if it deletes something important by accident, you will want to roll back!'
text += '\n' * 2
@@ -3371,7 +3383,7 @@ def _InitialiseMenuInfoHelp( self ):
current_value = check_manager.GetCurrentValue()
func = check_manager.Invert
- ClientGUIMenus.AppendMenuCheckItem( menu, 'advanced mode', 'Turn on advanced menu options and buttons.', current_value, func )
+ self._menu_item_help_advanced_mode = ClientGUIMenus.AppendMenuCheckItem( menu, 'advanced mode', 'Turn on advanced menu options and buttons.', current_value, func )
ClientGUIMenus.AppendSeparator( menu )
@@ -4421,6 +4433,7 @@ def _ManageOptions( self ):
CG.client_controller.ReinitGlobalSettings()
self._menu_item_help_darkmode.setChecked( CG.client_controller.new_options.GetString( 'current_colourset' ) == 'darkmode' )
+ self._menu_item_help_advanced_mode.setChecked( self._new_options.GetBoolean( 'advanced_mode' ) )
self._UpdateSystemTrayIcon()
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index e11f55674..d77b2a1f7 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -1316,7 +1316,7 @@ def __init__( self, parent: QW.QWidget, url_class: ClientNetworkingURLClass.URLC
parameters = url_class.GetParameters()
api_lookup_converter = url_class.GetAPILookupConverter()
( send_referral_url, referral_url_converter ) = url_class.GetReferralURLInfo()
- example_url = url_class.GetExampleURL()
+ example_url = url_class.GetExampleURL( encoded = False )
self._notebook = ClientGUICommon.BetterNotebook( self )
diff --git a/hydrus/client/gui/ClientGUILogin.py b/hydrus/client/gui/ClientGUILogin.py
index f9ad3a973..0251c1ff6 100644
--- a/hydrus/client/gui/ClientGUILogin.py
+++ b/hydrus/client/gui/ClientGUILogin.py
@@ -1647,7 +1647,7 @@ def clean_up( final_result ):
self._currently_testing = False
- def do_it( login_script, domain, credentials, network_job_presentation_context_factory ):
+ def do_it( login_script: ClientNetworkingLogin.LoginScriptDomain, domain, credentials, network_job_presentation_context_factory ):
login_result = 'login did not finish'
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
index 74ab97fb0..9dbd6dba5 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
@@ -2504,7 +2504,7 @@ def __init__( self, parent: QW.QWidget, ordered_medias: typing.List[ ClientMedia
self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy timestamps to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste timestamps from another timestamps dialog.' ) )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste timestamps from another timestamps dialog.\n\nCannot be simple strings, this needs to be rich data from another dialog. It also cannot create new web domain entries if the new file does not share entries which what was copied!' ) )
#
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index 44451a3fc..c96ebcac9 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -74,7 +74,8 @@ def __init__( self, parent ):
self._listbook.AddPage( 'file viewing statistics', 'file viewing statistics', self._FileViewingStatisticsPanel( self._listbook ) )
self._listbook.AddPage( 'speed and memory', 'speed and memory', self._SpeedAndMemoryPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'maintenance and processing', 'maintenance and processing', self._MaintenanceAndProcessingPanel( self._listbook ) )
- self._listbook.AddPage( 'media', 'media', self._MediaPanel( self._listbook ) )
+ self._listbook.AddPage( 'media viewer', 'media viewer', self._MediaViewerPanel( self._listbook ) )
+ self._listbook.AddPage( 'media playback', 'media playback', self._MediaPlaybackPanel( self._listbook ) )
self._listbook.AddPage( 'audio', 'audio', self._AudioPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'system tray', 'system tray', self._SystemTrayPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'search', 'search', self._SearchPanel( self._listbook, self._new_options ) )
@@ -92,6 +93,7 @@ def __init__( self, parent ):
self._listbook.AddPage( 'thumbnails', 'thumbnails', self._ThumbnailsPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'system', 'system', self._SystemPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'notes', 'notes', self._NotesPanel( self._listbook, self._new_options ) )
+ self._listbook.AddPage( '𝖆𝖉𝖛𝖆𝖓𝖈𝖊𝖉', 'advanced', self._AdvancedPanel( self._listbook, self._new_options ), do_sort = False )
#
@@ -102,6 +104,40 @@ def __init__( self, parent ):
self.widget().setLayout( vbox )
+ class _AdvancedPanel( QW.QWidget ):
+
+ def __init__( self, parent, new_options ):
+
+ QW.QWidget.__init__( self, parent )
+
+ self._new_options = new_options
+
+ # https://github.com/hydrusnetwork/hydrus/issues/1558
+
+ self._advanced_mode = QW.QCheckBox( self )
+ self._advanced_mode.setToolTip( ClientGUIFunctions.WrapToolTip( 'This controls a variety of different features across the program, too many to list neatly. The plan is to blow this single option out into many granular options on this pgae.' ) )
+
+ self._advanced_mode.setChecked( self._new_options.GetBoolean( 'advanced_mode' ) )
+
+ vbox = QP.VBoxLayout()
+
+ rows = []
+
+ rows.append( ( 'Advanced mode: ', self._advanced_mode ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( self, rows )
+
+ QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+
+ self.setLayout( vbox )
+
+
+ def UpdateOptions( self ):
+
+ self._new_options.SetBoolean( 'advanced_mode', self._advanced_mode.isChecked() )
+
+
+
class _AudioPanel( QW.QWidget ):
def __init__( self, parent, new_options ):
@@ -164,6 +200,14 @@ def __init__( self, parent ):
self._new_options = CG.client_controller.new_options
+ help_text = 'Hey, this page is pretty old. We want to eventually move its capabilities to the more flexible "style" page, but for now, several custom widgets have hardcoded colours set here.'
+ help_text += '\n' * 2
+ help_text += 'In a similar way, the "darkmode" here only changes these colours, it does not change the stylesheet. Please bear with the awkwardness of these two systems, we do plan to improve them, thank you!'
+
+ self._help_label = ClientGUICommon.BetterStaticText( self, label = help_text )
+
+ self._help_label.setObjectName( 'HydrusWarning' )
+
coloursets_panel = ClientGUICommon.StaticBox( self, 'coloursets' )
self._current_colourset = ClientGUICommon.BetterChoice( coloursets_panel )
@@ -255,6 +299,7 @@ def __init__( self, parent ):
vbox = QP.VBoxLayout()
+ QP.AddToLayout( vbox, self._help_label, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, coloursets_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.addStretch( 1 )
@@ -711,7 +756,7 @@ def __init__( self, parent, new_options ):
self._duplicate_background_switch_intensity_b.setToolTip( ClientGUIFunctions.WrapToolTip( 'This changes the background colour when you are looking at B. Making it different to the A value helps to highlight switches between the two.' ) )
self._draw_transparency_checkerboard_media_canvas_duplicates = QW.QCheckBox( colours_panel )
- self._draw_transparency_checkerboard_media_canvas_duplicates.setToolTip( ClientGUIFunctions.WrapToolTip( 'Same as the setting in _media_, but only for the duplicate filter. Only applies if that _media_ setting is unchecked.' ) )
+ self._draw_transparency_checkerboard_media_canvas_duplicates.setToolTip( ClientGUIFunctions.WrapToolTip( 'Same as the setting in _media playback_, but only for the duplicate filter. Only applies if that _media_ setting is unchecked.' ) )
#
@@ -2389,7 +2434,7 @@ def UpdateOptions( self ):
- class _MediaPanel( QW.QWidget ):
+ class _MediaViewerPanel( QW.QWidget ):
def __init__( self, parent ):
@@ -2399,73 +2444,41 @@ def __init__( self, parent ):
#
- animations_panel = ClientGUICommon.StaticBox( self, 'animations' )
-
- self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( animations_panel, min=1, max=255 )
- self._animated_scanbar_hide_height = ClientGUICommon.NoneableSpinCtrl( animations_panel, none_phrase = 'no, hide it', min = 1, max = 255, unit = 'px' )
- self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( animations_panel, min=1, max=63 )
-
- self._animation_start_position = ClientGUICommon.BetterSpinBox( animations_panel, min=0, max=100 )
-
- self._always_loop_animations = QW.QCheckBox( animations_panel )
- self._always_loop_animations.setToolTip( ClientGUIFunctions.WrapToolTip( 'Some GIFS and APNGs have metadata specifying how many times they should be played, usually 1. Uncheck this to obey that number.' ) )
-
- #
-
- system_panel = ClientGUICommon.StaticBox( self, 'system' )
-
- self._mpv_conf_path = QP.FilePickerCtrl( system_panel, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
-
- self._use_system_ffmpeg = QW.QCheckBox( system_panel )
- self._use_system_ffmpeg.setToolTip( ClientGUIFunctions.WrapToolTip( 'Check this to always default to the system ffmpeg in your path, rather than using the static ffmpeg in hydrus\'s bin directory. (requires restart)' ) )
-
- self._load_images_with_pil = QW.QCheckBox( system_panel )
- self._load_images_with_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'We are expecting to drop CV and move to PIL exclusively. This used to be a test option but is now default true and may soon be retired.' ) )
-
- self._do_icc_profile_normalisation = QW.QCheckBox( system_panel )
- self._do_icc_profile_normalisation.setToolTip( ClientGUIFunctions.WrapToolTip( 'Should PIL attempt to load ICC Profiles and normalise the colours of an image? This is usually fine, but when it janks out due to an additional OS/GPU ICC Profile, we can turn it off here.' ) )
-
- self._enable_truncated_images_pil = QW.QCheckBox( system_panel )
- self._enable_truncated_images_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'Should PIL be allowed to load broken images that are missing some data? This is usually fine, but some years ago we had stability problems when this was mixed with OpenCV. Now it is default on, but if you need to, you can disable it here.' ) )
-
- #
+ media_viewer_panel = ClientGUICommon.StaticBox( self, 'mouse and animations' )
- media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer' )
+ self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( media_viewer_panel, min=1, max=255 )
+ self._animated_scanbar_hide_height = ClientGUICommon.NoneableSpinCtrl( media_viewer_panel, none_phrase = 'no, hide it', min = 1, max = 255, unit = 'px' )
+ self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( media_viewer_panel, min=1, max=63 )
self._media_viewer_cursor_autohide_time_ms = ClientGUICommon.NoneableSpinCtrl( media_viewer_panel, none_phrase = 'do not autohide', min = 100, max = 100000, unit = 'ms' )
- self._media_zooms = QW.QLineEdit( media_viewer_panel )
- self._media_zooms.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will be what the program steps through as you zoom a media up and down.' ) )
- self._media_zooms.textChanged.connect( self.EventZoomsChanged )
-
- from hydrus.client.gui.canvas import ClientGUICanvasMedia
-
- self._media_viewer_zoom_center = ClientGUICommon.BetterChoice( media_viewer_panel )
-
- for zoom_centerpoint_type in ClientGUICanvasMedia.ZOOM_CENTERPOINT_TYPES:
-
- self._media_viewer_zoom_center.addItem( ClientGUICanvasMedia.zoom_centerpoints_str_lookup[ zoom_centerpoint_type ], zoom_centerpoint_type )
-
+ self._anchor_and_hide_canvas_drags = QW.QCheckBox( media_viewer_panel )
+ self._touchscreen_canvas_drags_unanchor = QW.QCheckBox( media_viewer_panel )
- tt = 'When you zoom in or out, there is a centerpoint about which the image zooms. This point \'stays still\' while the image expands or shrinks around it. Different centerpoints give different feels, especially if you drag images around a bit before zooming.'
+ #
- self._media_viewer_zoom_center.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+ media_canvas_panel = ClientGUICommon.StaticBox( self, 'hover windows and background' )
- self._draw_transparency_checkerboard_media_canvas = QW.QCheckBox( media_viewer_panel )
- self._draw_transparency_checkerboard_media_canvas.setToolTip( ClientGUIFunctions.WrapToolTip( 'If unchecked, will fill in with the normal background colour. Does not apply to MPV.' ) )
+ self._draw_tags_hover_in_media_viewer_background = QW.QCheckBox( media_canvas_panel )
+ self._draw_tags_hover_in_media_viewer_background.setToolTip( 'Draw the left list of tags in the background of the media viewer.' )
+ self._draw_top_hover_in_media_viewer_background = QW.QCheckBox( media_canvas_panel )
+ self._draw_top_hover_in_media_viewer_background.setToolTip( 'Draw the center-top file metadata in the background of the media viewer.' )
+ self._draw_top_right_hover_in_media_viewer_background = QW.QCheckBox( media_canvas_panel )
+ self._draw_top_right_hover_in_media_viewer_background.setToolTip( 'Draw the top-right ratings, inbox and URL information in the background of the media viewer.' )
+ self._draw_notes_hover_in_media_viewer_background = QW.QCheckBox( media_canvas_panel )
+ self._draw_notes_hover_in_media_viewer_background.setToolTip( 'Draw the right list of notes in the background of the media viewer.' )
+ self._draw_bottom_right_index_in_media_viewer_background = QW.QCheckBox( media_canvas_panel )
+ self._draw_bottom_right_index_in_media_viewer_background.setToolTip( 'Draw the bottom-right index string in the background of the media viewer.' )
- self._hide_uninteresting_local_import_time = QW.QCheckBox( media_viewer_panel )
+ self._hide_uninteresting_local_import_time = QW.QCheckBox( media_canvas_panel )
self._hide_uninteresting_local_import_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the file was imported at a similar time to when it was added to its current services (i.e. the number of seconds since both events differs by less than 10%), hide the import time in the top of the media viewer.' ) )
- self._hide_uninteresting_modified_time = QW.QCheckBox( media_viewer_panel )
+ self._hide_uninteresting_modified_time = QW.QCheckBox( media_canvas_panel )
self._hide_uninteresting_modified_time.setToolTip( ClientGUIFunctions.WrapToolTip( 'If the file has a modified time similar to its import time (i.e. the number of seconds since both events differs by less than 10%), hide the modified time in the top of the media viewer.' ) )
- self._anchor_and_hide_canvas_drags = QW.QCheckBox( media_viewer_panel )
- self._touchscreen_canvas_drags_unanchor = QW.QCheckBox( media_viewer_panel )
-
#
- slideshow_panel = ClientGUICommon.StaticBox( media_viewer_panel, 'slideshows' )
+ slideshow_panel = ClientGUICommon.StaticBox( self, 'slideshows' )
self._slideshow_durations = QW.QLineEdit( slideshow_panel )
self._slideshow_durations.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will end up in the slideshow menu in the media viewer.' ) )
@@ -2493,39 +2506,23 @@ def __init__( self, parent ):
#
- filetype_handling_panel = ClientGUICommon.StaticBox( media_viewer_panel, 'media viewer filetype handling' )
-
- media_viewer_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( filetype_handling_panel )
-
- self._media_viewer_options = ClientGUIListCtrl.BetterListCtrl( media_viewer_list_panel, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, 20, data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
-
- media_viewer_list_panel.SetListCtrl( self._media_viewer_options )
-
- media_viewer_list_panel.AddButton( 'add', self.AddMediaViewerOptions, enabled_check_func = self._CanAddMediaViewOption )
- media_viewer_list_panel.AddButton( 'edit', self.EditMediaViewerOptions, enabled_only_on_selection = True )
- media_viewer_list_panel.AddDeleteButton( enabled_check_func = self._CanDeleteMediaViewOptions )
+ self._animated_scanbar_height.setValue( self._new_options.GetInteger( 'animated_scanbar_height' ) )
+ self._animated_scanbar_nub_width.setValue( self._new_options.GetInteger( 'animated_scanbar_nub_width' ) )
- #
+ self._animated_scanbar_hide_height.SetValue( 5 )
+ self._animated_scanbar_hide_height.SetValue( self._new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) )
- self._animation_start_position.setValue( int( HC.options['animation_start_position'] * 100.0 ) )
+ self._draw_tags_hover_in_media_viewer_background.setChecked( self._new_options.GetBoolean( 'draw_tags_hover_in_media_viewer_background' ) )
+ self._draw_top_hover_in_media_viewer_background.setChecked( self._new_options.GetBoolean( 'draw_top_hover_in_media_viewer_background' ) )
+ self._draw_top_right_hover_in_media_viewer_background.setChecked( self._new_options.GetBoolean( 'draw_top_right_hover_in_media_viewer_background' ) )
+ self._draw_notes_hover_in_media_viewer_background.setChecked( self._new_options.GetBoolean( 'draw_notes_hover_in_media_viewer_background' ) )
+ self._draw_bottom_right_index_in_media_viewer_background.setChecked( self._new_options.GetBoolean( 'draw_bottom_right_index_in_media_viewer_background' ) )
self._hide_uninteresting_local_import_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_local_import_time' ) )
self._hide_uninteresting_modified_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_modified_time' ) )
- self._load_images_with_pil.setChecked( self._new_options.GetBoolean( 'load_images_with_pil' ) )
- self._enable_truncated_images_pil.setChecked( self._new_options.GetBoolean( 'enable_truncated_images_pil' ) )
- self._do_icc_profile_normalisation.setChecked( self._new_options.GetBoolean( 'do_icc_profile_normalisation' ) )
- self._use_system_ffmpeg.setChecked( self._new_options.GetBoolean( 'use_system_ffmpeg' ) )
- self._always_loop_animations.setChecked( self._new_options.GetBoolean( 'always_loop_gifs' ) )
- self._draw_transparency_checkerboard_media_canvas.setChecked( self._new_options.GetBoolean( 'draw_transparency_checkerboard_media_canvas' ) )
+
self._media_viewer_cursor_autohide_time_ms.SetValue( self._new_options.GetNoneableInteger( 'media_viewer_cursor_autohide_time_ms' ) )
self._anchor_and_hide_canvas_drags.setChecked( self._new_options.GetBoolean( 'anchor_and_hide_canvas_drags' ) )
self._touchscreen_canvas_drags_unanchor.setChecked( self._new_options.GetBoolean( 'touchscreen_canvas_drags_unanchor' ) )
- self._animated_scanbar_height.setValue( self._new_options.GetInteger( 'animated_scanbar_height' ) )
- self._animated_scanbar_nub_width.setValue( self._new_options.GetInteger( 'animated_scanbar_nub_width' ) )
-
- self._animated_scanbar_hide_height.SetValue( 5 )
- self._animated_scanbar_hide_height.SetValue( self._new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) )
-
- self._media_viewer_zoom_center.SetValue( self._new_options.GetInteger( 'media_viewer_zoom_center' ) )
slideshow_durations = self._new_options.GetSlideshowDurations()
@@ -2537,39 +2534,34 @@ def __init__( self, parent ):
self._slideshow_short_duration_cutoff_percentage.SetValue( self._new_options.GetNoneableInteger( 'slideshow_short_duration_cutoff_percentage' ) )
self._slideshow_long_duration_overspill_percentage.SetValue( self._new_options.GetNoneableInteger( 'slideshow_long_duration_overspill_percentage' ) )
- media_zooms = self._new_options.GetMediaZooms()
-
- self._media_zooms.setText( ','.join( ( str( media_zoom ) for media_zoom in media_zooms ) ) )
-
- all_media_view_options = self._new_options.GetMediaViewOptions()
-
- for ( mime, view_options ) in all_media_view_options.items():
-
- data = QP.ListsToTuples( [ mime ] + list( view_options ) )
-
- self._media_viewer_options.AddDatas( ( data, ) )
-
+ #
- self._media_viewer_options.Sort()
+ rows = []
- #
+ rows.append( ( 'Time until mouse cursor autohides on media viewer:', self._media_viewer_cursor_autohide_time_ms ) )
+ rows.append( ( 'Animation scanbar height:', self._animated_scanbar_height ) )
+ rows.append( ( 'Animation scanbar height when mouse away:', self._animated_scanbar_hide_height ) )
+ rows.append( ( 'Animation scanbar nub width:', self._animated_scanbar_nub_width ) )
+ rows.append( ( 'RECOMMEND WINDOWS ONLY: Hide and anchor mouse cursor on media viewer drags:', self._anchor_and_hide_canvas_drags ) )
+ rows.append( ( 'RECOMMEND WINDOWS ONLY: If set to hide and anchor, undo on apparent touchscreen drag:', self._touchscreen_canvas_drags_unanchor ) )
- vbox = QP.VBoxLayout()
+ media_viewer_gridbox = ClientGUICommon.WrapInGrid( media_viewer_panel, rows )
- #
+ media_viewer_panel.Add( media_viewer_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
rows = []
- rows.append( ( 'Time until mouse cursor autohides on media viewer:', self._media_viewer_cursor_autohide_time_ms ) )
- rows.append( ( 'Media zooms:', self._media_zooms ) )
- rows.append( ( 'Centerpoint for media zooming:', self._media_viewer_zoom_center ) )
- rows.append( ( 'Draw image transparency as checkerboard:', self._draw_transparency_checkerboard_media_canvas ) )
+ rows.append( ( 'Duplicate tags hover-window information in the background of the viewer:', self._draw_tags_hover_in_media_viewer_background ) )
+ rows.append( ( 'Duplicate top hover-window information in the background of the viewer:', self._draw_top_hover_in_media_viewer_background ) )
+ rows.append( ( 'Duplicate top-right hover-window information in the background of the viewer:', self._draw_top_right_hover_in_media_viewer_background ) )
+ rows.append( ( 'Duplicate notes hover-window information in the background of the viewer:', self._draw_notes_hover_in_media_viewer_background ) )
+ rows.append( ( 'Draw bottom-right index string in the background of the viewer:', self._draw_bottom_right_index_in_media_viewer_background ) )
rows.append( ( 'Hide uninteresting import times:', self._hide_uninteresting_local_import_time ) )
rows.append( ( 'Hide uninteresting modified times:', self._hide_uninteresting_modified_time ) )
- rows.append( ( 'RECOMMEND WINDOWS ONLY: Hide and anchor mouse cursor on media viewer drags:', self._anchor_and_hide_canvas_drags ) )
- rows.append( ( 'RECOMMEND WINDOWS ONLY: If set to hide and anchor, undo on apparent touchscreen drag:', self._touchscreen_canvas_drags_unanchor ) )
- media_viewer_gridbox = ClientGUICommon.WrapInGrid( media_viewer_panel, rows )
+ media_canvas_gridbox = ClientGUICommon.WrapInGrid( media_canvas_panel, rows )
+
+ media_canvas_panel.Add( media_canvas_gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
rows = []
@@ -2584,29 +2576,204 @@ def __init__( self, parent ):
slideshow_panel.Add( slideshow_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- filetype_handling_panel.Add( media_viewer_list_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+ #
- media_viewer_panel.Add( media_viewer_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- media_viewer_panel.Add( slideshow_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- media_viewer_panel.Add( filetype_handling_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+ vbox = QP.VBoxLayout()
+
+ QP.AddToLayout( vbox, media_viewer_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, media_canvas_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+ QP.AddToLayout( vbox, slideshow_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ vbox.addStretch( 1 )
+
+ self.setLayout( vbox )
+
+
+ def EventSlideshowChanged( self, text ):
+
+ try:
+
+ slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
+
+ self._slideshow_durations.setObjectName( '' )
+
+ except ValueError:
+
+ self._slideshow_durations.setObjectName( 'HydrusInvalid' )
+
+
+ self._slideshow_durations.style().polish( self._slideshow_durations )
+
+ self._slideshow_durations.update()
+
+ always_once_through = self._slideshow_always_play_duration_media_once_through.isChecked()
+
+ self._slideshow_long_duration_overspill_percentage.setEnabled( not always_once_through )
+
+
+ def UpdateOptions( self ):
+
+ self._new_options.SetBoolean( 'draw_tags_hover_in_media_viewer_background', self._draw_tags_hover_in_media_viewer_background.isChecked() )
+ self._new_options.SetBoolean( 'draw_top_hover_in_media_viewer_background', self._draw_top_hover_in_media_viewer_background.isChecked() )
+ self._new_options.SetBoolean( 'draw_top_right_hover_in_media_viewer_background', self._draw_top_right_hover_in_media_viewer_background.isChecked() )
+ self._new_options.SetBoolean( 'draw_notes_hover_in_media_viewer_background', self._draw_notes_hover_in_media_viewer_background.isChecked() )
+ self._new_options.SetBoolean( 'draw_bottom_right_index_in_media_viewer_background', self._draw_bottom_right_index_in_media_viewer_background.isChecked() )
+ self._new_options.SetBoolean( 'hide_uninteresting_local_import_time', self._hide_uninteresting_local_import_time.isChecked() )
+ self._new_options.SetBoolean( 'hide_uninteresting_modified_time', self._hide_uninteresting_modified_time.isChecked() )
+
+ self._new_options.SetBoolean( 'anchor_and_hide_canvas_drags', self._anchor_and_hide_canvas_drags.isChecked() )
+ self._new_options.SetBoolean( 'touchscreen_canvas_drags_unanchor', self._touchscreen_canvas_drags_unanchor.isChecked() )
+
+ self._new_options.SetNoneableInteger( 'media_viewer_cursor_autohide_time_ms', self._media_viewer_cursor_autohide_time_ms.GetValue() )
+
+ self._new_options.SetInteger( 'animated_scanbar_height', self._animated_scanbar_height.value() )
+ self._new_options.SetInteger( 'animated_scanbar_nub_width', self._animated_scanbar_nub_width.value() )
+
+ self._new_options.SetNoneableInteger( 'animated_scanbar_hide_height', self._animated_scanbar_hide_height.GetValue() )
+
+ try:
+
+ slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
+
+ slideshow_durations = [ slideshow_duration for slideshow_duration in slideshow_durations if slideshow_duration > 0.0 ]
+
+ if len( slideshow_durations ) > 0:
+
+ self._new_options.SetSlideshowDurations( slideshow_durations )
+
+
+ except ValueError:
+
+ HydrusData.ShowText( 'Could not parse those slideshow durations, so they were not saved!' )
+
+
+ self._new_options.SetBoolean( 'slideshow_always_play_duration_media_once_through', self._slideshow_always_play_duration_media_once_through.isChecked() )
+ self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_percentage', self._slideshow_short_duration_loop_percentage.GetValue() )
+ self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_seconds', self._slideshow_short_duration_loop_seconds.GetValue() )
+ self._new_options.SetNoneableInteger( 'slideshow_short_duration_cutoff_percentage', self._slideshow_short_duration_cutoff_percentage.GetValue() )
+ self._new_options.SetNoneableInteger( 'slideshow_long_duration_overspill_percentage', self._slideshow_long_duration_overspill_percentage.GetValue() )
+
+
+
+ class _MediaPlaybackPanel( QW.QWidget ):
+
+ def __init__( self, parent ):
+
+ QW.QWidget.__init__( self, parent )
+
+ self._new_options = CG.client_controller.new_options
+
+ #
+
+ media_panel = ClientGUICommon.StaticBox( self, 'media' )
+
+ self._animation_start_position = ClientGUICommon.BetterSpinBox( media_panel, min=0, max=100 )
+
+ self._always_loop_animations = QW.QCheckBox( media_panel )
+ self._always_loop_animations.setToolTip( ClientGUIFunctions.WrapToolTip( 'Some GIFS and APNGs have metadata specifying how many times they should be played, usually 1. Uncheck this to obey that number.' ) )
+
+ self._draw_transparency_checkerboard_media_canvas = QW.QCheckBox( media_panel )
+ self._draw_transparency_checkerboard_media_canvas.setToolTip( ClientGUIFunctions.WrapToolTip( 'If unchecked, will fill in with the normal background colour. Does not apply to MPV.' ) )
+
+ self._media_zooms = QW.QLineEdit( media_panel )
+ self._media_zooms.setToolTip( ClientGUIFunctions.WrapToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will be what the program steps through as you zoom a media up and down.' ) )
+ self._media_zooms.textChanged.connect( self.EventZoomsChanged )
+
+ from hydrus.client.gui.canvas import ClientGUICanvasMedia
+
+ self._media_viewer_zoom_center = ClientGUICommon.BetterChoice( media_panel )
+
+ for zoom_centerpoint_type in ClientGUICanvasMedia.ZOOM_CENTERPOINT_TYPES:
+
+ self._media_viewer_zoom_center.addItem( ClientGUICanvasMedia.zoom_centerpoints_str_lookup[ zoom_centerpoint_type ], zoom_centerpoint_type )
+
+
+ tt = 'When you zoom in or out, there is a centerpoint about which the image zooms. This point \'stays still\' while the image expands or shrinks around it. Different centerpoints give different feels, especially if you drag images around a bit before zooming.'
+
+ self._media_viewer_zoom_center.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
+
+ #
+
+ system_panel = ClientGUICommon.StaticBox( self, 'system' )
+
+ self._mpv_conf_path = QP.FilePickerCtrl( system_panel, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
+
+ self._use_system_ffmpeg = QW.QCheckBox( system_panel )
+ self._use_system_ffmpeg.setToolTip( ClientGUIFunctions.WrapToolTip( 'FFMPEG is used for file import metadata parsing and the native animation viewer. Check this to always default to the system ffmpeg in your path, rather than using the static ffmpeg in hydrus\'s bin directory. (requires restart)' ) )
+
+ self._load_images_with_pil = QW.QCheckBox( system_panel )
+ self._load_images_with_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'We are expecting to drop CV and move to PIL exclusively. This used to be a test option but is now default true and may soon be retired.' ) )
+
+ self._do_icc_profile_normalisation = QW.QCheckBox( system_panel )
+ self._do_icc_profile_normalisation.setToolTip( ClientGUIFunctions.WrapToolTip( 'Should PIL attempt to load ICC Profiles and normalise the colours of an image? This is usually fine, but when it janks out due to an additional OS/GPU ICC Profile, we can turn it off here.' ) )
+
+ self._enable_truncated_images_pil = QW.QCheckBox( system_panel )
+ self._enable_truncated_images_pil.setToolTip( ClientGUIFunctions.WrapToolTip( 'Should PIL be allowed to load broken images that are missing some data? This is usually fine, but some years ago we had stability problems when this was mixed with OpenCV. Now it is default on, but if you need to, you can disable it here.' ) )
+
+ #
+
+ filetype_handling_panel = ClientGUICommon.StaticBox( media_panel, 'per-filetype handling' )
+
+ media_viewer_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( filetype_handling_panel )
+
+ self._filetype_handling_listctrl = ClientGUIListCtrl.BetterListCtrl( media_viewer_list_panel, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, 20, data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
+
+ media_viewer_list_panel.SetListCtrl( self._filetype_handling_listctrl )
+
+ media_viewer_list_panel.AddButton( 'add', self.AddMediaViewerOptions, enabled_check_func = self._CanAddMediaViewOption )
+ media_viewer_list_panel.AddButton( 'edit', self.EditMediaViewerOptions, enabled_only_on_selection = True )
+ media_viewer_list_panel.AddDeleteButton( enabled_check_func = self._CanDeleteMediaViewOptions )
+
+ #
+
+ self._animation_start_position.setValue( int( HC.options['animation_start_position'] * 100.0 ) )
+ self._always_loop_animations.setChecked( self._new_options.GetBoolean( 'always_loop_gifs' ) )
+ self._draw_transparency_checkerboard_media_canvas.setChecked( self._new_options.GetBoolean( 'draw_transparency_checkerboard_media_canvas' ) )
+
+ media_zooms = self._new_options.GetMediaZooms()
+
+ self._media_zooms.setText( ','.join( ( str( media_zoom ) for media_zoom in media_zooms ) ) )
+
+ self._media_viewer_zoom_center.SetValue( self._new_options.GetInteger( 'media_viewer_zoom_center' ) )
+
+ self._load_images_with_pil.setChecked( self._new_options.GetBoolean( 'load_images_with_pil' ) )
+ self._enable_truncated_images_pil.setChecked( self._new_options.GetBoolean( 'enable_truncated_images_pil' ) )
+ self._do_icc_profile_normalisation.setChecked( self._new_options.GetBoolean( 'do_icc_profile_normalisation' ) )
+ self._use_system_ffmpeg.setChecked( self._new_options.GetBoolean( 'use_system_ffmpeg' ) )
- QP.AddToLayout( vbox, media_viewer_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+ all_media_view_options = self._new_options.GetMediaViewOptions()
+
+ for ( mime, view_options ) in all_media_view_options.items():
+
+ data = QP.ListsToTuples( [ mime ] + list( view_options ) )
+
+ self._filetype_handling_listctrl.AddDatas( ( data, ) )
+
+
+ self._filetype_handling_listctrl.Sort()
+
+ #
+
+ vbox = QP.VBoxLayout()
+
+ #
+
+ filetype_handling_panel.Add( media_viewer_list_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
#
rows = []
- rows.append( ( 'Animation scanbar height:', self._animated_scanbar_height ) )
- rows.append( ( 'Animation scanbar height when mouse away:', self._animated_scanbar_hide_height ) )
- rows.append( ( 'Animation scanbar nub width:', self._animated_scanbar_nub_width ) )
+ rows.append( ( 'Centerpoint for media zooming:', self._media_viewer_zoom_center ) )
+ rows.append( ( 'Media zooms:', self._media_zooms ) )
rows.append( ( 'Start animations this % in:', self._animation_start_position ) )
rows.append( ( 'Always Loop GIFs/APNGs:', self._always_loop_animations ) )
+ rows.append( ( 'Draw image transparency as checkerboard:', self._draw_transparency_checkerboard_media_canvas ) )
- gridbox = ClientGUICommon.WrapInGrid( animations_panel, rows )
-
- animations_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ gridbox = ClientGUICommon.WrapInGrid( media_panel, rows )
- QP.AddToLayout( vbox, animations_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+ media_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ media_panel.Add( filetype_handling_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
#
@@ -2622,6 +2789,7 @@ def __init__( self, parent ):
system_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+ QP.AddToLayout( vbox, media_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, system_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
#
@@ -2640,7 +2808,7 @@ def _CanDeleteMediaViewOptions( self ):
selected_mimes = set()
- for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData( only_selected = True ):
+ for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._filetype_handling_listctrl.GetData( only_selected = True ):
selected_mimes.add( mime )
@@ -2659,7 +2827,7 @@ def _GetCopyOfGeneralMediaViewOptions( self, desired_mime ):
general_mime_type = HC.mimes_to_general_mimetypes[ desired_mime ]
- for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
+ for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._filetype_handling_listctrl.GetData():
if mime == general_mime_type:
@@ -2676,7 +2844,7 @@ def _GetUnsetMediaViewFiletypes( self ):
set_mimes = set()
- for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
+ for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._filetype_handling_listctrl.GetData():
set_mimes.add( mime )
@@ -2781,14 +2949,14 @@ def AddMediaViewerOptions( self ):
new_data = panel.GetValue()
- self._media_viewer_options.AddDatas( ( new_data, ) )
+ self._filetype_handling_listctrl.AddDatas( ( new_data, ) )
def EditMediaViewerOptions( self ):
- for data in self._media_viewer_options.GetData( only_selected = True ):
+ for data in self._filetype_handling_listctrl.GetData( only_selected = True ):
title = 'edit media view options information'
@@ -2802,68 +2970,58 @@ def EditMediaViewerOptions( self ):
new_data = panel.GetValue()
- self._media_viewer_options.ReplaceData( data, new_data )
+ self._filetype_handling_listctrl.ReplaceData( data, new_data )
- def EventSlideshowChanged( self, text ):
+ def EventZoomsChanged( self, text ):
try:
- slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
+ media_zooms = [ float( media_zoom ) for media_zoom in self._media_zooms.text().split( ',' ) ]
- self._slideshow_durations.setObjectName( '' )
+ self._media_zooms.setObjectName( '' )
except ValueError:
- self._slideshow_durations.setObjectName( 'HydrusInvalid' )
+ self._media_zooms.setObjectName( 'HydrusInvalid' )
- self._slideshow_durations.style().polish( self._slideshow_durations )
-
- self._slideshow_durations.update()
-
- always_once_through = self._slideshow_always_play_duration_media_once_through.isChecked()
+ self._media_zooms.style().polish( self._media_zooms )
- self._slideshow_long_duration_overspill_percentage.setEnabled( not always_once_through )
+ self._media_zooms.update()
- def EventZoomsChanged( self, text ):
+ def UpdateOptions( self ):
+
+ HC.options[ 'animation_start_position' ] = self._animation_start_position.value() / 100.0
+ self._new_options.SetBoolean( 'always_loop_gifs', self._always_loop_animations.isChecked() )
+ self._new_options.SetBoolean( 'draw_transparency_checkerboard_media_canvas', self._draw_transparency_checkerboard_media_canvas.isChecked() )
try:
media_zooms = [ float( media_zoom ) for media_zoom in self._media_zooms.text().split( ',' ) ]
- self._media_zooms.setObjectName( '' )
+ media_zooms = [ media_zoom for media_zoom in media_zooms if media_zoom > 0.0 ]
+
+ if len( media_zooms ) > 0:
+
+ self._new_options.SetMediaZooms( media_zooms )
+
except ValueError:
- self._media_zooms.setObjectName( 'HydrusInvalid' )
+ HydrusData.ShowText( 'Could not parse those zooms, so they were not saved!' )
- self._media_zooms.style().polish( self._media_zooms )
-
- self._media_zooms.update()
-
-
- def UpdateOptions( self ):
-
- HC.options[ 'animation_start_position' ] = self._animation_start_position.value() / 100.0
+ self._new_options.SetInteger( 'media_viewer_zoom_center', self._media_viewer_zoom_center.GetValue() )
- self._new_options.SetBoolean( 'hide_uninteresting_local_import_time', self._hide_uninteresting_local_import_time.isChecked() )
- self._new_options.SetBoolean( 'hide_uninteresting_modified_time', self._hide_uninteresting_modified_time.isChecked() )
self._new_options.SetBoolean( 'load_images_with_pil', self._load_images_with_pil.isChecked() )
self._new_options.SetBoolean( 'enable_truncated_images_pil', self._enable_truncated_images_pil.isChecked() )
self._new_options.SetBoolean( 'do_icc_profile_normalisation', self._do_icc_profile_normalisation.isChecked() )
self._new_options.SetBoolean( 'use_system_ffmpeg', self._use_system_ffmpeg.isChecked() )
- self._new_options.SetBoolean( 'always_loop_gifs', self._always_loop_animations.isChecked() )
- self._new_options.SetBoolean( 'draw_transparency_checkerboard_media_canvas', self._draw_transparency_checkerboard_media_canvas.isChecked() )
- self._new_options.SetBoolean( 'anchor_and_hide_canvas_drags', self._anchor_and_hide_canvas_drags.isChecked() )
- self._new_options.SetBoolean( 'touchscreen_canvas_drags_unanchor', self._touchscreen_canvas_drags_unanchor.isChecked() )
-
- self._new_options.SetNoneableInteger( 'media_viewer_cursor_autohide_time_ms', self._media_viewer_cursor_autohide_time_ms.GetValue() )
mpv_conf_path = self._mpv_conf_path.GetPath()
@@ -2882,54 +3040,9 @@ def UpdateOptions( self ):
- self._new_options.SetInteger( 'animated_scanbar_height', self._animated_scanbar_height.value() )
- self._new_options.SetInteger( 'animated_scanbar_nub_width', self._animated_scanbar_nub_width.value() )
-
- self._new_options.SetNoneableInteger( 'animated_scanbar_hide_height', self._animated_scanbar_hide_height.GetValue() )
-
- self._new_options.SetInteger( 'media_viewer_zoom_center', self._media_viewer_zoom_center.GetValue() )
-
- try:
-
- slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
-
- slideshow_durations = [ slideshow_duration for slideshow_duration in slideshow_durations if slideshow_duration > 0.0 ]
-
- if len( slideshow_durations ) > 0:
-
- self._new_options.SetSlideshowDurations( slideshow_durations )
-
-
- except ValueError:
-
- HydrusData.ShowText( 'Could not parse those slideshow durations, so they were not saved!' )
-
-
- self._new_options.SetBoolean( 'slideshow_always_play_duration_media_once_through', self._slideshow_always_play_duration_media_once_through.isChecked() )
- self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_percentage', self._slideshow_short_duration_loop_percentage.GetValue() )
- self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_seconds', self._slideshow_short_duration_loop_seconds.GetValue() )
- self._new_options.SetNoneableInteger( 'slideshow_short_duration_cutoff_percentage', self._slideshow_short_duration_cutoff_percentage.GetValue() )
- self._new_options.SetNoneableInteger( 'slideshow_long_duration_overspill_percentage', self._slideshow_long_duration_overspill_percentage.GetValue() )
-
- try:
-
- media_zooms = [ float( media_zoom ) for media_zoom in self._media_zooms.text().split( ',' ) ]
-
- media_zooms = [ media_zoom for media_zoom in media_zooms if media_zoom > 0.0 ]
-
- if len( media_zooms ) > 0:
-
- self._new_options.SetMediaZooms( media_zooms )
-
-
- except ValueError:
-
- HydrusData.ShowText( 'Could not parse those zooms, so they were not saved!' )
-
-
mimes_to_media_view_options = {}
- for data in self._media_viewer_options.GetData():
+ for data in self._filetype_handling_listctrl.GetData():
data = list( data )
@@ -3819,6 +3932,12 @@ def __init__( self, parent, new_options ):
#
+ help_text = 'Hey, there are several colours, mostly for custom widgets, not set here. Check the "colours" page out!'
+
+ self._help_label = ClientGUICommon.BetterStaticText( self, label = help_text )
+
+ self._help_label.setObjectName( 'HydrusWarning' )
+
self._qt_style_name = ClientGUICommon.BetterChoice( self )
self._qt_stylesheet_name = ClientGUICommon.BetterChoice( self )
@@ -3861,6 +3980,8 @@ def __init__( self, parent, new_options ):
#
+ QP.AddToLayout( vbox, self._help_label, CC.FLAGS_EXPAND_PERPENDICULAR )
+
text = 'The current styles are what your Qt has available, the stylesheets are what .css and .qss files are currently in install_dir/static/qss.'
text += '\n' * 2
text += 'Note that there are several colours not handled by this yet. Check out the "colours" page of this options to change them.'
@@ -4117,7 +4238,7 @@ def __init__( self, parent, new_options ):
self._num_to_show_in_ac_dropdown_children_tab = ClientGUICommon.NoneableSpinCtrl( children_panel, none_phrase = 'show all', min = 1 )
tt = 'The "children" tab will show children of the current tag context (usually the list of tags above the autocomplete), ordered by file count. This can quickly get spammy, so I recommend you cull it to a reasonable size.'
self._num_to_show_in_ac_dropdown_children_tab.setToolTip( tt )
- self._num_to_show_in_ac_dropdown_children_tab.SetValue( 20 ) # init default
+ self._num_to_show_in_ac_dropdown_children_tab.SetValue( 40 ) # init default
#
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
index 7c33b7c92..20f481cf5 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
@@ -66,7 +66,7 @@
class AboutPanel( ClientGUIScrolledPanels.ReviewPanel ):
- def __init__( self, parent, name, version, description, license_text, developers, site ):
+ def __init__( self, parent, name, version, description_versions, description_availability, license_text, developers, site ):
ClientGUIScrolledPanels.ReviewPanel.__init__( self, parent )
@@ -80,14 +80,21 @@ def __init__( self, parent, name, version, description, license_text, developers
version_label = ClientGUICommon.BetterStaticText( self, version )
+ url_label = ClientGUICommon.BetterHyperLink( self, site, site )
+
tabwidget = QW.QTabWidget( self )
- desc_panel = QW.QWidget( self )
+ #
- desc_label = ClientGUICommon.BetterStaticText( self, description )
+ desc_label = ClientGUICommon.BetterStaticText( self, description_versions )
desc_label.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
- url_label = ClientGUICommon.BetterHyperLink( self, site, site )
+ #
+
+ availability_label = ClientGUICommon.BetterStaticText( self, description_availability )
+ availability_label.setAlignment( QC.Qt.AlignHCenter | QC.Qt.AlignVCenter )
+
+ #
credits = QW.QTextEdit( self )
credits.setPlainText( 'Created by ' + ', '.join( developers ) )
@@ -98,23 +105,22 @@ def __init__( self, parent, name, version, description, license_text, developers
license_textedit.setPlainText( license_text )
license_textedit.setReadOnly( True )
- tabwidget.addTab( desc_panel, 'Description' )
+ text_width = ClientGUIFunctions.ConvertTextToPixelWidth( license_textedit, 64 )
+
+ license_textedit.setFixedWidth( text_width )
+
+ tabwidget.addTab( desc_label, 'Description' )
+ tabwidget.addTab( availability_label, 'Optional Libraries' )
tabwidget.addTab( credits, 'Credits' )
tabwidget.addTab( license_textedit, 'License' )
tabwidget.setCurrentIndex( 0 )
- desc_layout = QP.VBoxLayout()
-
- QP.AddToLayout( desc_layout, desc_label, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( desc_layout, url_label, CC.FLAGS_CENTER_PERPENDICULAR )
-
- desc_panel.setLayout( desc_layout )
-
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, icon_label, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( vbox, name_label, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( vbox, version_label, CC.FLAGS_CENTER_PERPENDICULAR )
+ QP.AddToLayout( vbox, url_label, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( vbox, tabwidget, CC.FLAGS_CENTER_PERPENDICULAR )
self.widget().setLayout( vbox )
diff --git a/hydrus/client/gui/ClientGUITime.py b/hydrus/client/gui/ClientGUITime.py
index d42dc6133..e417d6edf 100644
--- a/hydrus/client/gui/ClientGUITime.py
+++ b/hydrus/client/gui/ClientGUITime.py
@@ -707,7 +707,7 @@ def __init__( self, parent, time_allowed = True, seconds_allowed = False, millis
self._copy_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Copy timestamp to the clipboard.' ) )
self._paste_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().paste, self._Paste )
- self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste timestamp from another datetime widget.' ) )
+ self._paste_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'Paste a timestamp. Needs to be a simple string but can handle pretty much anything.' ) )
#
@@ -828,21 +828,57 @@ def _Paste( self ):
return
- try:
-
- timestamp = json.loads( raw_text )
+ timestamp = None
+ timestamp_set = False
+
+ if not timestamp_set:
- if isinstance( timestamp, str ):
+ try:
- try:
-
- timestamp = float( timestamp )
-
- except ValueError:
+ timestamp = json.loads( raw_text )
+
+ timestamp_set = True
+
+ if isinstance( timestamp, str ):
- raise Exception( 'Does not look like a number!' )
+ try:
+
+ timestamp = float( timestamp )
+
+ except ValueError:
+
+ raise Exception( 'Does not look like a number!' )
+
+ except:
+
+ pass
+
+
+
+ if not timestamp_set:
+
+ try:
+
+ timestamp = ClientTime.ParseDate( raw_text )
+
+ timestamp_set = True
+
+ except:
+
+ pass
+
+
+
+ if not timestamp_set:
+
+ ClientGUIDialogsMessage.ShowWarning( self, f'Sorry, I did not understand that! I am looking for a simple timestamp integer or parseable datestring, but I got:\n\n{raw_text}' )
+
+ return
+
+
+ try:
looks_good = timestamp is None or isinstance( timestamp, ( int, float ) )
@@ -866,7 +902,7 @@ def _Paste( self ):
except Exception as e:
- ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'A simple integer timestamp', e )
+ ClientGUIDialogsQuick.PresentClipboardParseError( self, raw_text, 'A parseable timestamp string', e )
return
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index be9b5d2a8..aafdb89f1 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -902,6 +902,7 @@ def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
if it_worked:
self._MediaFocusWentToExternalProgram()
+
elif action == CAC.SIMPLE_OPEN_SELECTION_IN_NEW_PAGE:
@@ -1593,15 +1594,37 @@ def _DrawBackgroundDetails( self, painter: QG.QPainter ):
else:
- self._DrawTags( painter )
+ new_options = CG.client_controller.new_options
+
+ if new_options.GetBoolean( 'draw_tags_hover_in_media_viewer_background' ):
+
+ self._DrawTags( painter )
+
- self._DrawTopMiddle( painter )
+ if new_options.GetBoolean( 'draw_top_hover_in_media_viewer_background' ):
+
+ self._DrawTopMiddle( painter )
+
- current_y = self._DrawTopRight( painter )
+ if new_options.GetBoolean( 'draw_top_right_hover_in_media_viewer_background' ):
+
+ current_y = self._DrawTopRight( painter )
+
+ else:
+
+ # ah this is actually wrong, bleargh
+ current_y = 0
+
- self._DrawNotes( painter, current_y )
+ if new_options.GetBoolean( 'draw_notes_hover_in_media_viewer_background' ):
+
+ self._DrawNotes( painter, current_y )
+
- self._DrawIndexAndZoom( painter )
+ if new_options.GetBoolean( 'draw_bottom_right_index_in_media_viewer_background' ):
+
+ self._DrawIndexAndZoom( painter )
+
diff --git a/hydrus/client/gui/lists/ClientGUIListBook.py b/hydrus/client/gui/lists/ClientGUIListBook.py
index 5d2304d2a..f6311b381 100644
--- a/hydrus/client/gui/lists/ClientGUIListBook.py
+++ b/hydrus/client/gui/lists/ClientGUIListBook.py
@@ -85,7 +85,7 @@ def _ShowPage( self, key ):
self.update()
- def AddPage( self, display_name, key, page, select = False ):
+ def AddPage( self, display_name, key, page, select = False, do_sort = True ):
if self.HasKey( key ):
@@ -108,7 +108,10 @@ def AddPage( self, display_name, key, page, select = False ):
self._list_box.Append( display_name, key, select = select )
- self._list_box.sortItems()
+ if do_sort:
+
+ self._list_box.sortItems()
+
if select:
diff --git a/hydrus/client/gui/media/ClientGUIMediaMenus.py b/hydrus/client/gui/media/ClientGUIMediaMenus.py
index 5d58bb445..0f8a19586 100644
--- a/hydrus/client/gui/media/ClientGUIMediaMenus.py
+++ b/hydrus/client/gui/media/ClientGUIMediaMenus.py
@@ -687,11 +687,12 @@ def AddOpenMenu( win: QW.QWidget, menu: QW.QMenu, focused_media: typing.Optional
if focused_media.GetLocationsManager().IsLocal():
- show_windows_native_options = advanced_mode and HC.PLATFORM_WINDOWS
+ show_windows_native_options = HC.PLATFORM_WINDOWS
if show_windows_native_options:
ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}in another program', 'Choose which program to open this file with.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_NATIVE_OPEN_FILE_WITH_DIALOG ) )
+
show_open_in_explorer = advanced_mode and ClientPaths.CAN_OPEN_FILE_LOCATION
@@ -699,10 +700,11 @@ def AddOpenMenu( win: QW.QWidget, menu: QW.QMenu, focused_media: typing.Optional
ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}in file browser', 'Show this file in your OS\'s file browser.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_FILE_IN_FILE_EXPLORER ) )
+
if show_windows_native_options:
-
ClientGUIMenus.AppendMenuItem( open_menu, f'{prefix}properties', 'Open your OS\'s properties window for this file.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_NATIVE_OPEN_FILE_PROPERTIES ) )
+
diff --git a/hydrus/client/gui/parsing/ClientGUIParsing.py b/hydrus/client/gui/parsing/ClientGUIParsing.py
index 743bd7aa8..9f0234f35 100644
--- a/hydrus/client/gui/parsing/ClientGUIParsing.py
+++ b/hydrus/client/gui/parsing/ClientGUIParsing.py
@@ -1281,7 +1281,7 @@ def __init__( self, parent, parser: ClientParsing.PageParser, formula = None, te
( sub_page_parsers, content_parsers ) = parser.GetContentParsers()
- example_urls = parser.GetExampleURLs()
+ example_urls = parser.GetExampleURLs( encoded = False )
if len( example_urls ) > 0:
@@ -1576,7 +1576,7 @@ def qt_tidy_up( example_data, example_bytes, error ):
QP.CallAfter( qt_tidy_up, example_data, example_bytes, error )
- url = self._test_url.text()
+ url = ClientNetworkingFunctions.EnsureURLIsEncoded( self._test_url.text() )
referral_url = self._test_referral_url.text()
if referral_url == '':
diff --git a/hydrus/client/networking/ClientNetworkingFunctions.py b/hydrus/client/networking/ClientNetworkingFunctions.py
index 5f9671960..fac22874e 100644
--- a/hydrus/client/networking/ClientNetworkingFunctions.py
+++ b/hydrus/client/networking/ClientNetworkingFunctions.py
@@ -514,6 +514,8 @@ def EnsureURLIsEncoded( url: str, keep_fragment = True ) -> str:
path_components = ConvertPathTextToList( p.path )
( query_dict, single_value_parameters, param_order ) = ConvertQueryTextToDict( p.query )
+ param_order = [ ensure_param_component_is_encoded( param ) if param is not None else None for param in param_order ]
+
path_components = [ ensure_path_component_is_encoded( path_component ) for path_component in path_components ]
query_dict = { ensure_param_component_is_encoded( name ) : ensure_param_component_is_encoded( value ) for ( name, value ) in query_dict.items() }
single_value_parameters = [ ensure_param_component_is_encoded( single_value_parameter ) for single_value_parameter in single_value_parameters ]
diff --git a/hydrus/client/networking/ClientNetworkingURLClass.py b/hydrus/client/networking/ClientNetworkingURLClass.py
index 38007d30f..48dd94485 100644
--- a/hydrus/client/networking/ClientNetworkingURLClass.py
+++ b/hydrus/client/networking/ClientNetworkingURLClass.py
@@ -1068,9 +1068,16 @@ def GetDomain( self ):
return self._netloc
- def GetExampleURL( self ):
+ def GetExampleURL( self, encoded = True ):
- return self._example_url
+ if encoded:
+
+ return ClientNetworkingFunctions.EnsureURLIsEncoded( self._example_url )
+
+ else:
+
+ return self._example_url
+
def GetGalleryIndexValues( self ):
@@ -1441,6 +1448,8 @@ def ShouldAssociateWithFiles( self ):
def Test( self, url ):
+ url = ClientNetworkingFunctions.EnsureURLIsEncoded( url )
+
p = ClientNetworkingFunctions.ParseURL( url )
if self._match_subdomains:
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 5cc233361..5c38cd29c 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,7 +105,7 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 576
+SOFTWARE_VERSION = 577
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/HydrusController.py b/hydrus/core/HydrusController.py
index ae01076c7..4651dfcf9 100644
--- a/hydrus/core/HydrusController.py
+++ b/hydrus/core/HydrusController.py
@@ -129,7 +129,7 @@ def _GetCallToThreadLongRunning( self ):
- def _GetPubsubValidCallable( self ) -> bool:
+ def _GetPubsubValidCallable( self ):
return lambda o: True
diff --git a/hydrus/core/HydrusDBModule.py b/hydrus/core/HydrusDBModule.py
index 9b0e3808d..d4029a04e 100644
--- a/hydrus/core/HydrusDBModule.py
+++ b/hydrus/core/HydrusDBModule.py
@@ -122,7 +122,7 @@ def _GetServicesTableGenerationDict( self ) -> dict:
return table_generation_dict
- def _GetServiceTablePrefixes( self ) -> typing.Collection:
+ def _GetServiceTablePrefixes( self ) -> typing.Collection[ str ]:
return set()
@@ -186,7 +186,7 @@ def GetExpectedInitialTableNames( self ) -> typing.Collection[ str ]:
return list( table_generation_dict.keys() )
- def GetSurplusServiceTableNames( self, all_table_names ) -> set:
+ def GetSurplusServiceTableNames( self, all_table_names ) -> typing.Set[ str ]:
prefixes = self._GetServiceTablePrefixes()
@@ -195,10 +195,17 @@ def GetSurplusServiceTableNames( self, all_table_names ) -> set:
return set()
- all_service_table_names = { table_name for table_name in all_table_names if True in ( table_name.startswith( prefix ) or '.{}'.format( prefix ) in table_name for prefix in prefixes ) }
+ # careful about the flattening here. after adding more tables here, I ran into issues with db tables either being main.current_files_x or just current_files_x and got (dangerous!!) false positives on the test
+ # so, to failsafe, let's merge everything down to just the table name, no db schema, and then we'll catch all possible collisions no matter what the calls are actually giving us here
+
+ all_service_table_names = { name if '.' not in name else name.split( '.', 1 )[1] for name in all_table_names }
+
+ all_service_table_names = { table_name for table_name in all_service_table_names if True in ( table_name.startswith( prefix ) for prefix in prefixes ) }
good_service_table_names = self.GetExpectedServiceTableNames()
+ good_service_table_names = { name if '.' not in name else name.split( '.', 1 )[1] for name in good_service_table_names }
+
surplus_table_names = all_service_table_names.difference( good_service_table_names )
return surplus_table_names
diff --git a/hydrus/test/TestClientNetworking.py b/hydrus/test/TestClientNetworking.py
index 05b8b4ecd..b7c1cc0a1 100644
--- a/hydrus/test/TestClientNetworking.py
+++ b/hydrus/test/TestClientNetworking.py
@@ -364,6 +364,13 @@ def test_encoding( self ):
self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url_with_mix ), encoded_url_with_mix )
self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url_with_mix ), encoded_url_with_mix )
+ # double-check we don't auto-alphabetise params in this early stage! we screwed this up before and broke that option
+ human_url_with_brackets = 'https://weouthere.site/post?yo=1&name[id]=wew'
+ encoded_url_with_brackets = 'https://weouthere.site/post?yo=1&name%5Bid%5D=wew'
+
+ self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( human_url_with_brackets ), encoded_url_with_brackets )
+ self.assertEqual( ClientNetworkingFunctions.EnsureURLIsEncoded( encoded_url_with_brackets ), encoded_url_with_brackets )
+
def test_defaults( self ):