diff --git a/docs/changelog.md b/docs/changelog.md
index 613054b1f..91c7f48bc 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,63 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 603](https://github.com/hydrusnetwork/hydrus/releases/tag/v603)
+
+### misc
+
+* fixed a typo that caused the 'sort files by' menu to (ironically) sort by crazy means
+* fixed a bug with the time delta widget where the ms would not set to 0 when it initialised with a value that has no milliseconds component but the minimum allowed value had a milliseconds component
+* the 'force metadata refetch' thumbnail submenu now shows actions for just the focused file, and in the media viewer it now shows these actions for the current file (previously this submenu was accidentally a stub in the media viewer since there is no concept of a 'multi-file selection' up there)
+* the 'review bandwidth usage' panel now initialises its widgets immediately after it opens, not half a second later
+* added some safety code to ensure file re-imports (and probably some other weird file import situations) integrate correctly into the similar files system
+* added some EXPERIMENTAL options just for me to `options->speed and memory`
+
+### archived file delete lock
+
+* I have had a think about the delete lock in hydrus. I have never liked this system because it interacts with some complicated file service logic and inserts awkward logical workflow exceptions. furthermore, my initial implementation has not played well with multiple local file services. it was also imperfect, since certain signals to 'delete from all local services' or some odd variants of 'delete from trash' could skip the lock, and it disallowed removal of a file from one local file service even when the file was in others. several users have asked for various exceptions, either on an ad-hoc basis of for duplicate filtering. I played around with trying to fix and implement some of this stuff this week and realised I was digging an even worse hole for myself. I have decided to KISS and scale back what the lock does down to the simple emergency case of not wanting to lose nice things. **therefore: the archive-delete lock, henceforth, will only test for deleting files from the trash, i.e. a physical delete**
+* the option UI is updated to say this, and I cleaned up and updated a bunch of hellish hacky code all around here
+* the normal manual delete files dialog now filters the 'delete physically' and 'delete physically/no record' options according to the delete-lock, no moaning or popups
+* this is obviously a workflow switch, and I apologise for the inconvenience. I know the guys who care about this do care about it. I should not have tried to make it complicated in the first place. let me know what works and what doesn't, but I will insist on keeping this whole thing KISS going forward. if you use the trash for storage, please consider making a new local file service and putting your unusual 'maybe I'll delete it' files there
+
+### trash deletion rules
+
+* while I was poking around the archived file delete lock stuff above, I saw some hacky delete logic. in several semi-automatic systems I saw code that would say 'send the file to trash, unless it is already in the trash, in which case "upgrade" to physically delete it'. I am making the formal choice to no longer do this, and this code is now amended to say just 'send the file to trash if it isn't already there. specifically--
+* the duplicates filter page will no longer allow you to set a search domain in 'trash' or 'all local files' or 'all deleted files' (or, for advanced users, 'repository updates' lol). if it is somehow given a trashed file to process and receives a duplicate action that includes a file delete (like 'this is better, and delete the other'), it no longer tries to physically delete it; it just leaves it in the trash
+* the Client API `/manage_file_relationships/set_file_relationships` call is the same; when you say to `delete_a/b`, it'll now ensure the file goes to the trash and that's it
+* the archive/delete filter has long not allowed trashed files, but it too now no longer has any special logic for trashed files that it happens to encounter (e.g. the file is trashed by other means after the filter is created); it will now never provide a 'delete from hard disk' option in the final commit dialog. the top 'delete from' item in the commit choice, which is usually the current location context, is also filtered and selected more carefully for users who use multi-location search domains
+* the manual export and export folders now explicitly, when set to delete files, now send to trash; they never delete from the trash
+* I expect I've missed some clever situation, but typically, now, files are going to be physically deleted only if the `options->files and trash` settings kick in or you the user force it manually, and of course the new archived-file delete-lock prohibits this final step no matter the source
+
+### media viewer
+
+* fixed a bug where the top-right hover window was, when the mouse is over it and the media changes from one with no URLs to one with URLs, not be able to immediately figure out how tall the URLs list should be and was giving you a height-truncated window
+* I also, after some head banging and dark art, seem to have finally and properly fixed the annoying adjust-flicker that can happen to top-right hovers with urls or note hovers where a moment after showing it may grow three pixels taller etc... the top-right and center-right hovers now appear to size perfectly every single time; at least on Windows, I cannot break them even if I try. to keep things clean, I have removed some old hacks we built up over the years, but perhaps some of these are still relevent, so let me know how things appear on different OSes
+* I improved the (re)layout after you hide/show a note with middle/right-click. it _should_ recalculate its new size immediately
+* the notes that are drawn in the background of the media viewer are now always as wide as the hover window that pops up over them. I improved the padding calculations, so they are more closely aligned with the hover, but it isn't perfect yet--however I think I will be able to make it so in future
+* fixed a variety of issues with the media viewer volume button and its slider flyout. it could stay open on media change and in some cases open up if the mouse was over where the flyout should be on a media change, or flickering into just slightly the wrong overlapping location if the mouse comes in at the wrong angle. there's still a couple weird ways you can break it (e.g. sending a 'move media' keyboard shortcut while the mouse is over the slider), but I'll leave that for another day
+* fixed an issue with the media viewer's top hover window disappearing while your mouse was over the volume slider
+* reduced some change-media flicker with the seek bar in the media viewer
+
+### misc cleanup
+
+* I replaced a bunch of `isVisible` with `not isHidden` in Qt. I was using the former for some widget and panel hide/show situations, but they are not quite the same: `isVisible` tests up the ancestor heirarchy; `isHidden` tests only the given widget's visibility bool
+* reworded the 'blacklist' explanation a bit in 'getting started with downloaders' and added a screenshot
+* updated the Linux running from source help to talk about `libgthread`, the lack of which may stop Qt6 from booting
+* I cleaned up the `/manage_file_relationships/set_relationships` Client API call a little, including fixing a slightly incorrect object type that was missed in a recent rewrite of the duplicates content pipeline. I am not sure if this was causing any bugs, but it is better now
+* brushed up some of the labels/tooltips in the `options->file viewing statistics` page (issue #1644)
+* stopped the logging of a 'custom' `REQUESTS_CA_BUNDLE` with the new `options->connection` debug checkbox if it is what we would have set anyway (this was occuring if you went `file->restart`, since the new instance shares the same process/env)
+* fixed some unit tests for the new duplicate merge delete rules
+* did some misc linting
+
+### fixed up tag filter UI
+
+* everything is in layout boxes now, some collapsible
+* fixed up some layout flags so things are aligned or expand better, and the global namespaces list shouldn't have a scrollbar any more
+
+### macOS build
+
+* updated the macOS build script to retry the hdiutil dmg-building step multiple times in the very frequent case of it, seemingly, getting lock-kekked by XProtectBehaviorService (https://github.com/actions/runner-images/issues/7522)
+
## [Version 602](https://github.com/hydrusnetwork/hydrus/releases/tag/v602)
### media viewer top hover file info line
@@ -492,38 +549,3 @@ title: Changelog
* reworked the text for the 'focus the text input when you change pages' checkbox under `options->gui pages` and added a tooltip
* reworded and changed tone of the boot error message on missing database tables if the tables are all caches and completely recoverable
* updated the twitter link and icon in `help->links` to X
-
-## [Version 593](https://github.com/hydrusnetwork/hydrus/releases/tag/v593)
-
-### misc
-
-* in a normal search page tag autocomplete input, search results will recognise exact-text-matches of their worse siblings for 'put at the top of the list' purposes. so, if you type 'lotr', and it was siblinged to 'series:lord of the rings', then 'series:lord of the rings' is now promoted to the top of the list, regardless of count, as if you had typed in that full ideal tag
-* OR predicates are now multi-line. the top line is OR:, and then each sub-tag is now listed indented below. if you construct an OR pred using shift+enter in the tag autocomplete, this new OR does start to eat up some space, but if you are making crazy 17-part OR preds, maybe you'll want to use the OR button dialog input anyway
-* when you right-click an OR predicate, the 'copy' menu now recognises this as '3 selected tags' etc.. and will copy all the involved tags and handle subtags correctly
-* the 'remove/reset for all selected' file relationship menu is no longer hidden behind advanced mode. it being buried five layers deep is enough
-* to save a button press, the manage tag siblings dialog now has a paste button for the right-side tag autocomplete input. if you paste multiple lines of content, it just takes the first
-* updated the file maintenance job descriptions for the 'try to redownload' jobs to talk about how to deal with URL downloads that 404 or produce a duplicate and brushed up a bit of that language in general
-* the new 'if a db job took more than 15 seconds, log it' thing now tests if the program was non-idle at the start or end of the db job, rather than just the end. this will catch some 'it took so long that some "wake up" stuff had time to kick in' instances
-* fixed a typo where if the 'other' hashes were unknown, the 'sha512 (unknown)' label was saying 'md5 (unknown)'
-* file import logs get a new 'advanced' menu option, tucked away a little, to 'renormalise' their contents. this is a maintenance job to clear out duplicate chaff on an existing list after the respective URL Class rules have changed to remove something in normalisation (e.g. setting a parameter to be ephemeral). I added a unit test for this also, but let me know how it works in the wild
-
-### default downloaders
-
-* fixed the source time parsing for the gelbooru 0.2.0 (rule34.xxx and others) and gelbooru 0.2.5 (gelbooru proper) page parsers
-
-### client api
-
-* fixed the 'permits everything' API Permissions update from a couple weeks ago. it was supposed to set 'permits everything' when the existing permissions structure was 'mostly full', but the logic was bad and it was setting it when the permissions were sparse. if you were hit by this and did not un-set the 'permits everything' yourself in _review services_, you will get a yes/no prompt on update asking if you want to re-run the fixed update. if the update only missed out setting "permits everything" where it should have, you'll just get a popup saying it did them. sorry for missing this, my too-brief dev machine test happened to be exactly on the case of a coin flip landing three times on its edge--I've improved my API permission tests for future
-
-### duplicate auto-resolution progress
-
-* I got started on the db module that will handle duplicates auto-resolution. this started out feeling daunting, and I wasn't totally sure how I'd do some things, but I gave it a couple iterations and managed to figure out a simple design I am very happy with. I think it is about 25-33% complete (while object design is ~50-75% and UI is 0%), so there is a decent bit to go here, but the way is coming into focus
-
-### boring code cleanup
-
-* updated my `SortedList`, which does some fast index lookup stuff, to handle more situations, optimised some remove actions, made it more compatible as a list drop-in replacement, moved it to `HydrusData`, and renamed it to `FastIndexUniqueList`
-* the autocomplete results system uses the new `FastIndexUniqueList` a bit for some cached matches and results reordering stuff
-* expanded my `TemporerIntegerTable` system, which I use to do some beardy 'executemany' SELECT statements, to support an arbitrary number of integer columns. the duplicate auto-resolution system is going to be doing mass potential pair set intersections, and this makes it simple
-* thanks to a user, the core `Globals` files get some linter magic that lets an IDE do good type checking on the core controller classes without running into circular import issues. this reduced project-wide PyCharm linter warnings from like 4,500 to 2,200 wew
-* I pulled the `ServerController` and `TestController` gubbins out of `HydrusGlobals` into their own 'Globals' files in their respective modules to ensure other module-crawlers (e.g. perhaps PyInstaller) do not get confused about what they are importing here, and to generally clean this up a bit
-* improved a daemon unit test that would sometimes fail because it was not waiting long enough for the daemon to finish. I cut some other fat and it is now four or five seconds faster too
diff --git a/docs/developer_api.md b/docs/developer_api.md
index 4917f973c..fb43aeb22 100644
--- a/docs/developer_api.md
+++ b/docs/developer_api.md
@@ -618,7 +618,7 @@ Arguments (in JSON):
Response:
: 200 and no content.
-If you specify a file service, the file will only be deleted from that location. Only local file domains are allowed (so you can't delete from a file repository or unpin from ipfs yet). It defaults to _all my files_, which will delete from all local services (i.e. force sending to trash). Sending 'all local files' on a file already in the trash will trigger a physical file delete.
+If you specify a file service, the file will only be deleted from that location. Only local file domains are allowed (so you can't delete from a file repository or unpin from ipfs yet), or the umbrella `all my files` and `all local files` domains. It defaults to `all my files`, which will delete from all local services (i.e. force sending to trash). Sending `all local files` on a file already in the trash will trigger a physical file delete.
### **POST `/add_files/undelete_files`** { id="add_files_undelete_files" }
diff --git a/docs/getting_started_downloading.md b/docs/getting_started_downloading.md
index 02420e225..d4f0d996a 100644
--- a/docs/getting_started_downloading.md
+++ b/docs/getting_started_downloading.md
@@ -147,9 +147,13 @@ This is an important dialog, although you will not need to use it much. It gover
You can see that each tag service on your client has a separate section. If you add the PTR, that will get a new box too. A new client is set to _get all tags_ for 'downloader tags' service. Things can get much more complicated. Have a play around with the options here as you figure things out. Most of the controls have tooltips or longer explainers in sub-dialogs, so don't be afraid to try things.
-It is easy to get tens of thousands of tags by downloading this way. Different sites offer different kinds and qualities of tags, and the client's downloaders (which were designed by me, the dev, or a user) may parse all or only some of them. Many users like to just get everything on offer, but others only ever want, say, `creator`, `series`, and `character` tags. If you feel brave, click that 'all tags' button, which will take you into hydrus's advanced 'tag filter', which allows you to select which of the incoming list of tags will be added.
+It is easy to get tens of thousands of tags by downloading this way. Different sites offer different kinds and qualities of tags, and the client's downloaders (which were designed by me, the dev, or a user) may parse all or only some of them. Many users like to just get everything on offer, but others only ever want, say, `creator`, `series`, and `character` tags. Once you feel comfortable with tags, try clicking that 'adding: all tags' button, which will take you into hydrus's advanced 'tag filter', which allows you to select which of the incoming tags will be added.
-The blacklist button will let you skip downloading files that have certain tags (perhaps you would like to auto-skip all images with `gore`, `scat`, or `diaper`?), again using the tag filter, while the whitelist enables you to only allow files that have at least one of a set of tags. The 'additional tags' adds some fixed personal tags to all files coming in--for instance, you might like to add 'process into favourites' to your 'my tags' for some query you really like so you can find those files again later and process them separately. That little 'cog' icon button can also do some advanced things.
+The 'additional tags' adds some fixed personal tags to all files coming in--for instance, you might like to add 'this came from the xxxxx subscription' or 'process into favourites' to your 'my tags' so you can find those files again later. That little 'cog' icon button can also do some advanced things.
+
+The blacklist button will let you skip downloading files that have certain tags, again using the tag filter, while the whitelist enables you to only allow files that have at least one of a set of tags.
+
+![](images/tag_filter_blacklist_example.png)
!!! warning
The file limit and import options on the upper panel of a gallery or watcher page, if changed, will only apply to **new** queries. If you want to change the options for an existing queue, either do so on its highlight panel below or use the 'set options to queries' button.
diff --git a/docs/getting_started_installing.md b/docs/getting_started_installing.md
index c15bd8264..ef881e839 100644
--- a/docs/getting_started_installing.md
+++ b/docs/getting_started_installing.md
@@ -40,12 +40,21 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
One user notes that launching with the environment variable `QT_QPA_PLATFORM=xcb` may help!
- !!! note "XCB Qt compatibility"
+ !!! note "Qt compatibility"
- If you run into trouble running Qt6, usually with an XCB-related error like `qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.`, try installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+ If you run into trouble running newer versions of Qt6, some users have fixed it by installing one or more of these additional packages:
+
+ * `libicu-dev`
+ * `libxcb-cursor-dev`
+ * `libgthread`
+
+ With `apt` that will be:
* `sudo apt-get install libicu-dev`
* `sudo apt-get install libxcb-cursor-dev`
+ * `sudo apt-get install libgthread2.0-0`
+
+ Or check your OS's package manager.
* Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. The build is made on Ubuntu, so if you run something else, compatibility is hit and miss.
diff --git a/docs/images/tag_filter_blacklist_example.png b/docs/images/tag_filter_blacklist_example.png
new file mode 100644
index 000000000..5fc31fde2
Binary files /dev/null and b/docs/images/tag_filter_blacklist_example.png differ
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index c2fecab2b..639443aa6 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,52 @@
+ -
+
+
+ misc
+ - fixed a typo that caused the 'sort files by' menu to (ironically) sort by crazy means
+ - fixed a bug with the time delta widget where the ms would not set to 0 when it initialised with a value that has no milliseconds component but the minimum allowed value had a milliseconds component
+ - the 'force metadata refetch' thumbnail submenu now shows actions for just the focused file, and in the media viewer it now shows these actions for the current file (previously this submenu was accidentally a stub in the media viewer since there is no concept of a 'multi-file selection' up there)
+ - the 'review bandwidth usage' panel now initialises its widgets immediately after it opens, not half a second later
+ - added some safety code to ensure file re-imports (and probably some other weird file import situations) integrate correctly into the similar files system
+ - added some EXPERIMENTAL options just for me to `options->speed and memory`
+ archived file delete lock
+ - I have had a think about the delete lock in hydrus. I have never liked this system because it interacts with some complicated file service logic and inserts awkward logical workflow exceptions. furthermore, my initial implementation has not played well with multiple local file services. it was also imperfect, since certain signals to 'delete from all local services' or some odd variants of 'delete from trash' could skip the lock, and it disallowed removal of a file from one local file service even when the file was in others. several users have asked for various exceptions, either on an ad-hoc basis of for duplicate filtering. I played around with trying to fix and implement some of this stuff this week and realised I was digging an even worse hole for myself. I have decided to KISS and scale back what the lock does down to the simple emergency case of not wanting to lose nice things. **therefore: the archive-delete lock, henceforth, will only test for deleting files from the trash, i.e. a physical delete**
+ - the option UI is updated to say this, and I cleaned up and updated a bunch of hellish hacky code all around here
+ - the normal manual delete files dialog now filters the 'delete physically' and 'delete physically/no record' options according to the delete-lock, no moaning or popups
+ - this is obviously a workflow switch, and I apologise for the inconvenience. I know the guys who care about this do care about it. I should not have tried to make it complicated in the first place. let me know what works and what doesn't, but I will insist on keeping this whole thing KISS going forward. if you use the trash for storage, please consider making a new local file service and putting your unusual 'maybe I'll delete it' files there
+ trash deletion rules
+ - while I was poking around the archived file delete lock stuff above, I saw some hacky delete logic. in several semi-automatic systems I saw code that would say 'send the file to trash, unless it is already in the trash, in which case "upgrade" to physically delete it'. I am making the formal choice to no longer do this, and this code is now amended to say just 'send the file to trash if it isn't already there. specifically--
+ - the duplicates filter page will no longer allow you to set a search domain in 'trash' or 'all local files' or 'all deleted files' (or, for advanced users, 'repository updates' lol). if it is somehow given a trashed file to process and receives a duplicate action that includes a file delete (like 'this is better, and delete the other'), it no longer tries to physically delete it; it just leaves it in the trash
+ - the Client API `/manage_file_relationships/set_file_relationships` call is the same; when you say to `delete_a/b`, it'll now ensure the file goes to the trash and that's it
+ - the archive/delete filter has long not allowed trashed files, but it too now no longer has any special logic for trashed files that it happens to encounter (e.g. the file is trashed by other means after the filter is created); it will now never provide a 'delete from hard disk' option in the final commit dialog. the top 'delete from' item in the commit choice, which is usually the current location context, is also filtered and selected more carefully for users who use multi-location search domains
+ - the manual export and export folders now explicitly, when set to delete files, now send to trash; they never delete from the trash
+ - I expect I've missed some clever situation, but typically, now, files are going to be physically deleted only if the `options->files and trash` settings kick in or you the user force it manually, and of course the new archived-file delete-lock prohibits this final step no matter the source
+ media viewer
+ - fixed a bug where the top-right hover window was, when the mouse is over it and the media changes from one with no URLs to one with URLs, not be able to immediately figure out how tall the URLs list should be and was giving you a height-truncated window
+ - I also, after some head banging and dark art, seem to have finally and properly fixed the annoying adjust-flicker that can happen to top-right hovers with urls or note hovers where a moment after showing it may grow three pixels taller etc... the top-right and center-right hovers now appear to size perfectly every single time; at least on Windows, I cannot break them even if I try. to keep things clean, I have removed some old hacks we built up over the years, but perhaps some of these are still relevent, so let me know how things appear on different OSes
+ - I improved the (re)layout after you hide/show a note with middle/right-click. it _should_ recalculate its new size immediately
+ - the notes that are drawn in the background of the media viewer are now always as wide as the hover window that pops up over them. I improved the padding calculations, so they are more closely aligned with the hover, but it isn't perfect yet--however I think I will be able to make it so in future
+ - fixed a variety of issues with the media viewer volume button and its slider flyout. it could stay open on media change and in some cases open up if the mouse was over where the flyout should be on a media change, or flickering into just slightly the wrong overlapping location if the mouse comes in at the wrong angle. there's still a couple weird ways you can break it (e.g. sending a 'move media' keyboard shortcut while the mouse is over the slider), but I'll leave that for another day
+ - fixed an issue with the media viewer's top hover window disappearing while your mouse was over the volume slider
+ - reduced some change-media flicker with the seek bar in the media viewer
+ misc cleanup
+ - I replaced a bunch of `isVisible` with `not isHidden` in Qt. I was using the former for some widget and panel hide/show situations, but they are not quite the same: `isVisible` tests up the ancestor heirarchy; `isHidden` tests only the given widget's visibility bool
+ - reworded the 'blacklist' explanation a bit in 'getting started with downloaders' and added a screenshot
+ - updated the Linux running from source help to talk about `libgthread`, the lack of which may stop Qt6 from booting
+ - I cleaned up the `/manage_file_relationships/set_relationships` Client API call a little, including fixing a slightly incorrect object type that was missed in a recent rewrite of the duplicates content pipeline. I am not sure if this was causing any bugs, but it is better now
+ - brushed up some of the labels/tooltips in the `options->file viewing statistics` page (issue #1644)
+ - stopped the logging of a 'custom' `REQUESTS_CA_BUNDLE` with the new `options->connection` debug checkbox if it is what we would have set anyway (this was occuring if you went `file->restart`, since the new instance shares the same process/env)
+ - fixed some unit tests for the new duplicate merge delete rules
+ - did some misc linting
+ fixed up tag filter UI
+ - everything is in layout boxes now, some collapsible
+ - fixed up some layout flags so things are aligned or expand better, and the global namespaces list shouldn't have a scrollbar any more
+ macOS build
+ - updated the macOS build script to retry the hdiutil dmg-building step multiple times in the very frequent case of it, seemingly, getting lock-kekked by XProtectBehaviorService (https://github.com/actions/runner-images/issues/7522)
+
+
-
diff --git a/docs/running_from_source.md b/docs/running_from_source.md
index d62c167c1..d39e954e7 100644
--- a/docs/running_from_source.md
+++ b/docs/running_from_source.md
@@ -213,10 +213,19 @@ Then run the 'setup_help' script to build the help. This isn't necessary, but it
!!! note "Qt compatibility"
- If you run into trouble running newer versions of Qt6, some users have fixed it by installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+ If you run into trouble running newer versions of Qt6, some users have fixed it by installing one or more of these additional packages:
+
+ * `libicu-dev`
+ * `libxcb-cursor-dev`
+ * `libgthread`
+
+ With `apt` that will be:
* `sudo apt-get install libicu-dev`
* `sudo apt-get install libxcb-cursor-dev`
+ * `sudo apt-get install libgthread2.0-0`
+
+ Or check your OS's package manager.
If you still have trouble with the default Qt6 version, try running setup_venv again and choose a different version. There are several to choose from, including (w)riting a custom version. Check the advanced requirements.txts files in `install_dir/static/requirements/advanced` for more info, and you can also work off this list: [PySide6](https://pypi.org/project/PySide6/#history)
@@ -337,8 +346,19 @@ If you want to set QT_API in a batch file, do this:
If you run into trouble running newer versions of Qt6 on Linux, often with an XCB-related error such as `qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.`, try installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+ If you run into trouble running newer versions of Qt6, some users have fixed it by installing one or more of these additional packages:
+
+ * `libicu-dev`
+ * `libxcb-cursor-dev`
+ * `libgthread`
+
+ With `apt` that will be:
+
* `sudo apt-get install libicu-dev`
* `sudo apt-get install libxcb-cursor-dev`
+ * `sudo apt-get install libgthread2.0-0`
+
+ Or check your OS's package manager.
If you still have trouble with the default Qt6 version, check the advanced requirements.txts in `install_dir/static/requirements/advanced`. There should be several older version examples you can explore, and you can also work off these lists: [PySide6](https://pypi.org/project/PySide6/#history) [PyQt6](https://pypi.org/project/PyQt6/#history)
diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py
index 929c12546..aa1b73c4e 100644
--- a/hydrus/client/ClientController.py
+++ b/hydrus/client/ClientController.py
@@ -2098,18 +2098,18 @@ def StartServices( *args, **kwargs ):
continue
- try:
+ if allow_non_local_connections:
- if use_https:
-
- import twisted.internet.ssl
-
- ( ssl_cert_path, ssl_key_path ) = self.db.GetSSLPaths()
-
- sslmethod = twisted.internet.ssl.SSL.TLSv1_2_METHOD
-
- context_factory = twisted.internet.ssl.DefaultOpenSSLContextFactory( ssl_key_path, ssl_cert_path, sslmethod )
-
+ ipv6_interface = '::'
+ ipv4_interface = ''
+
+ else:
+
+ ipv6_interface = '::1'
+ ipv4_interface = '127.0.0.1'
+
+
+ try:
from hydrus.client.networking.api import ClientLocalServer
@@ -2122,26 +2122,30 @@ def StartServices( *args, **kwargs ):
raise NotImplementedError( 'Unknown service type!' )
+ context_factory = None
+
+ if use_https:
+
+ import twisted.internet.ssl
+
+ ( ssl_cert_path, ssl_key_path ) = self.db.GetSSLPaths()
+
+ sslmethod = twisted.internet.ssl.SSL.TLSv1_2_METHOD
+
+ context_factory = twisted.internet.ssl.DefaultOpenSSLContextFactory( ssl_key_path, ssl_cert_path, sslmethod )
+
+
ipv6_port = None
try:
- if allow_non_local_connections:
-
- interface = '::'
-
- else:
-
- interface = '::1'
-
-
if use_https:
- ipv6_port = reactor.listenSSL( port, http_factory, context_factory, interface = interface )
+ ipv6_port = reactor.listenSSL( port, http_factory, context_factory, interface = ipv6_interface )
else:
- ipv6_port = reactor.listenTCP( port, http_factory, interface = interface )
+ ipv6_port = reactor.listenTCP( port, http_factory, interface = ipv6_interface )
except Exception as e:
@@ -2155,22 +2159,13 @@ def StartServices( *args, **kwargs ):
try:
- if allow_non_local_connections:
-
- interface = ''
-
- else:
-
- interface = '127.0.0.1'
-
-
if use_https:
- ipv4_port = reactor.listenSSL( port, http_factory, context_factory, interface = interface )
+ ipv4_port = reactor.listenSSL( port, http_factory, context_factory, interface = ipv4_interface )
else:
- ipv4_port = reactor.listenTCP( port, http_factory, interface = interface )
+ ipv4_port = reactor.listenTCP( port, http_factory, interface = ipv4_interface )
except:
diff --git a/hydrus/client/ClientEnvironment.py b/hydrus/client/ClientEnvironment.py
index b46390a96..2cef24848 100644
--- a/hydrus/client/ClientEnvironment.py
+++ b/hydrus/client/ClientEnvironment.py
@@ -7,17 +7,6 @@ def SetRequestsCABundleEnv( pem_path = None ):
# TODO: we could initialise this with a custom pem in launch args pretty easy if we wanted to!
# but tbh the user can already set it in the launch env anyway so maybe whatever
- env_var_name = 'REQUESTS_CA_BUNDLE'
-
- if env_var_name in os.environ:
-
- HydrusData.Print( f'Custom REQUESTS_CA_BUNDLE: {os.environ[env_var_name]}')
-
- return
-
-
- # could say "If CURL_CA_BUNDLE exists, use that instead of certifi"
-
if pem_path is None:
try:
@@ -28,10 +17,29 @@ def SetRequestsCABundleEnv( pem_path = None ):
except:
- HydrusData.Print( 'No certifi, so cannot set REQUESTS_CA_BUNDLE.' )
+ pass
- return
+
+
+ env_var_name = 'REQUESTS_CA_BUNDLE'
+
+ if env_var_name in os.environ:
+
+ if pem_path is None or os.environ[ env_var_name ] != pem_path:
+ HydrusData.Print( f'Custom REQUESTS_CA_BUNDLE: {os.environ[env_var_name]}')
+
+
+ return
+
+
+ # could say "If CURL_CA_BUNDLE exists, use that instead of certifi"
+
+ if pem_path is None:
+
+ HydrusData.Print( 'No certifi, so cannot set REQUESTS_CA_BUNDLE.' )
+
+ return
if os.path.exists( pem_path ):
diff --git a/hydrus/client/ClientLocation.py b/hydrus/client/ClientLocation.py
index ca70d486d..1e8ba0ada 100644
--- a/hydrus/client/ClientLocation.py
+++ b/hydrus/client/ClientLocation.py
@@ -34,7 +34,10 @@ def FilterOutRedundantMetaServices( list_of_service_keys: typing.List[ bytes ] )
return list_of_service_keys
-def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool ):
+def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool, only_all_my_files_domains_allowed: bool ):
+
+ # TODO: WOW the 'only_x' parameters here are awful!!! rewrite all this!
+ # seems like it cascades, so set up an enum instead I think!
services_manager = CG.client_controller.services_manager
@@ -49,31 +52,34 @@ def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_im
service_types_in_order.append( HC.COMBINED_LOCAL_MEDIA )
- service_types_in_order.append( HC.LOCAL_FILE_TRASH_DOMAIN )
-
- if advanced_mode:
-
- service_types_in_order.append( HC.LOCAL_FILE_UPDATE_DOMAIN )
-
-
- if advanced_mode:
-
- service_types_in_order.append( HC.COMBINED_LOCAL_FILE )
+ if not only_all_my_files_domains_allowed:
-
- if not only_local_file_domains_allowed:
+ service_types_in_order.append( HC.LOCAL_FILE_TRASH_DOMAIN )
if advanced_mode:
- service_types_in_order.append( HC.COMBINED_DELETED_FILE )
+ service_types_in_order.append( HC.LOCAL_FILE_UPDATE_DOMAIN )
- service_types_in_order.append( HC.FILE_REPOSITORY )
- service_types_in_order.append( HC.IPFS )
+ if advanced_mode:
+
+ service_types_in_order.append( HC.COMBINED_LOCAL_FILE )
+
- if all_known_files_allowed:
+ if not only_local_file_domains_allowed:
+
+ if advanced_mode:
+
+ service_types_in_order.append( HC.COMBINED_DELETED_FILE )
+
+
+ service_types_in_order.append( HC.FILE_REPOSITORY )
+ service_types_in_order.append( HC.IPFS )
- service_types_in_order.append( HC.COMBINED_FILE )
+ if all_known_files_allowed:
+
+ service_types_in_order.append( HC.COMBINED_FILE )
+
@@ -85,7 +91,7 @@ def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_im
def SortFileServiceKeysNicely( list_of_service_keys ):
- services_in_nice_order = GetPossibleFileDomainServicesInOrder( False, False, False )
+ services_in_nice_order = GetPossibleFileDomainServicesInOrder( False, False, False, False )
service_keys_in_nice_order = [ service.GetServiceKey() for service in services_in_nice_order ]
diff --git a/hydrus/client/ClientMacIntegration.py b/hydrus/client/ClientMacIntegration.py
index 919c580a1..31f0528ec 100644
--- a/hydrus/client/ClientMacIntegration.py
+++ b/hydrus/client/ClientMacIntegration.py
@@ -9,6 +9,7 @@
class HydrusQLDataSource(NSObject, protocols=[QLPreviewPanelDataSource]):
def initWithCurrentlyLooking_(self, currently_showing):
+ # noinspection PyMethodFirstArgAssignment
self = objc.super(HydrusQLDataSource, self).init()
if self is None: return None
diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py
index d3970136e..f7853f6a9 100644
--- a/hydrus/client/ClientOptions.py
+++ b/hydrus/client/ClientOptions.py
@@ -473,7 +473,11 @@ def _InitialiseDefaults( self ):
'deferred_table_delete_work_time_ms_normal' : 250,
'deferred_table_delete_rest_percentage_normal' : 1000,
'deferred_table_delete_work_time_ms_work_hard' : 5000,
- 'deferred_table_delete_rest_percentage_work_hard' : 10
+ 'deferred_table_delete_rest_percentage_work_hard' : 10,
+ 'gallery_page_status_update_time_minimum_ms' : 1000,
+ 'gallery_page_status_update_time_ratio_denominator' : 30,
+ 'watcher_page_status_update_time_minimum_ms' : 1000,
+ 'watcher_page_status_update_time_ratio_denominator' : 30
}
#
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 0427c5c9b..120f2140b 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -74,6 +74,7 @@
from hydrus.client.duplicates import ClientDuplicates
from hydrus.client.duplicates import ClientPotentialDuplicatesSearchContext
from hydrus.client.importing import ClientImportFiles
+from hydrus.client.media import ClientMediaFileFilter # don't remove this without care, it initialises serialised object early
from hydrus.client.media import ClientMediaManagers
from hydrus.client.media import ClientMediaResult
from hydrus.client.media import ClientMediaResultCache
@@ -4824,6 +4825,9 @@ def _GetTablesAndColumnsUsingDefinitions( self, content_type ):
def _GetTrashHashes( self, limit = None, minimum_age = None ):
+ # TODO: rework the filedeletelock to be a thing that kicks in _during_ the search, so the LIMIT remains valid. otherwise too many locked files means this chokes
+ # TODO: also update the report mode to talk about the lock
+
if limit is None:
limit_phrase = ''
@@ -4850,7 +4854,7 @@ def _GetTrashHashes( self, limit = None, minimum_age = None ):
hash_ids = self._STS( self._Execute( 'SELECT hash_id FROM {}{}{};'.format( current_files_table_name, age_phrase, limit_phrase ) ) )
- hash_ids = self.modules_file_delete_lock.FilterForFileDeleteLock( self.modules_services.trash_service_id, hash_ids )
+ hash_ids = self.modules_file_delete_lock.FilterForPhysicalFileDeleteLock( hash_ids )
if HG.db_report_mode:
@@ -4943,7 +4947,7 @@ def _ImportFile( self, file_import_job: ClientImportFiles.FileImportJob ):
HydrusData.ShowText( 'File import job associating perceptual_hashes' )
- self.modules_similar_files.AssociatePerceptualHashes( hash_id, perceptual_hashes )
+ self.modules_similar_files.SetPerceptualHashes( hash_id, perceptual_hashes )
if HG.file_import_report_mode:
@@ -6162,22 +6166,32 @@ def _ProcessContentUpdatePackage( self, content_update_package, publish_content_
elif action in ( HC.CONTENT_UPDATE_DELETE, HC.CONTENT_UPDATE_DELETE_FROM_SOURCE_AFTER_MIGRATE ):
- if action == HC.CONTENT_UPDATE_DELETE:
-
- actual_delete_hash_ids = self.modules_file_delete_lock.FilterForFileDeleteLock( service_id, hash_ids )
-
- else:
-
- actual_delete_hash_ids = hash_ids
-
+ actual_delete_hash_ids = hash_ids
- if len( actual_delete_hash_ids ) < len( hash_ids ):
+ if action == HC.CONTENT_UPDATE_DELETE and service_key in ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.TRASH_SERVICE_KEY ):
- hash_ids = actual_delete_hash_ids
+ local_hash_ids = self.modules_files_storage.FilterHashIds( ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ), hash_ids )
- hashes = self.modules_hashes_local_cache.GetHashes( hash_ids )
+ actually_deletable_hash_ids = self.modules_file_delete_lock.FilterForPhysicalFileDeleteLock( local_hash_ids )
- content_update.SetRow( hashes )
+ if len( actually_deletable_hash_ids ) < len( local_hash_ids ):
+
+ # ok we hit the lock on some
+ undeletable_hash_ids = set( local_hash_ids ).difference( actually_deletable_hash_ids )
+
+ media_results = self._GetMediaResults( undeletable_hash_ids, sorted = False )
+
+ ClientMediaFileFilter.ReportDeleteLockFailures( media_results )
+
+
+ if len( actually_deletable_hash_ids ) < len( hash_ids ):
+
+ hash_ids = actually_deletable_hash_ids
+
+ hashes = self.modules_hashes_local_cache.GetHashes( hash_ids )
+
+ content_update.SetRow( hashes )
+
if service_type in ( HC.LOCAL_FILE_DOMAIN, HC.COMBINED_LOCAL_MEDIA, HC.COMBINED_LOCAL_FILE ):
@@ -6204,13 +6218,6 @@ def _ProcessContentUpdatePackage( self, content_update_package, publish_content_
self._DeleteFiles( self.modules_services.combined_local_file_service_id, hash_ids )
- elif service_id == self.modules_services.combined_local_media_service_id:
-
- for s_id in self.modules_services.GetServiceIds( ( HC.LOCAL_FILE_DOMAIN, ) ):
-
- self._DeleteFiles( s_id, hash_ids, only_if_current = True )
-
-
else:
self._DeleteFiles( service_id, hash_ids )
diff --git a/hydrus/client/db/ClientDBFileDeleteLock.py b/hydrus/client/db/ClientDBFileDeleteLock.py
index 3e11d736f..d17f0abbd 100644
--- a/hydrus/client/db/ClientDBFileDeleteLock.py
+++ b/hydrus/client/db/ClientDBFileDeleteLock.py
@@ -1,8 +1,6 @@
import sqlite3
import typing
-from hydrus.core import HydrusConstants as HC
-
from hydrus.client import ClientGlobals as CG
from hydrus.client.db import ClientDBFilesInbox
from hydrus.client.db import ClientDBModule
@@ -18,19 +16,21 @@ def __init__( self, cursor: sqlite3.Cursor, modules_services: ClientDBServices.C
super().__init__( 'client file delete lock', cursor )
- def FilterForFileDeleteLock( self, service_id, hash_ids ):
+ def FilterForPhysicalFileDeleteLock( self, hash_ids: typing.Collection[ int ] ):
# TODO: like in the MediaSingleton object, eventually extend this to the metadata conditional object
+ # however the trash clearance method uses this guy, so we probably don't want to load up media results over and over bro
+ # probably figure out a table cache or something at that point
if CG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' ):
- service = self.modules_services.GetService( service_id )
-
- if service.GetServiceType() in HC.LOCAL_FILE_SERVICES:
+ if not isinstance( hash_ids, set ):
- hash_ids = set( hash_ids ).intersection( self.modules_files_inbox.inbox_hash_ids )
+ hash_ids = set( hash_ids )
+ hash_ids = hash_ids.intersection( self.modules_files_inbox.inbox_hash_ids )
+
return hash_ids
diff --git a/hydrus/client/db/ClientDBFilesDuplicates.py b/hydrus/client/db/ClientDBFilesDuplicates.py
index d3210e8b4..8c50c9e55 100644
--- a/hydrus/client/db/ClientDBFilesDuplicates.py
+++ b/hydrus/client/db/ClientDBFilesDuplicates.py
@@ -843,6 +843,10 @@ def filter_func( count ):
return count == num_relationships
+ else:
+
+ raise NotImplementedError( f'Unknown operator "{operator}"!' )
+
hash_ids = set()
diff --git a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py
index bb1c39a4d..a0e9c93be 100644
--- a/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py
+++ b/hydrus/client/db/ClientDBMappingsCacheCombinedFilesDisplay.py
@@ -244,6 +244,10 @@ def GetWithAndWithoutTagsForFilesFileCountFileService( self, status, file_servic
without_tag_ids = pending_without_tag_ids
without_tag_ids_weight = pending_without_tag_ids_weight
+ else:
+
+ raise NotImplementedError( f'Unknown status "{status}"!' )
+
if with_tag_ids_weight == 0:
diff --git a/hydrus/client/db/ClientDBMappingsCounts.py b/hydrus/client/db/ClientDBMappingsCounts.py
index 261e9d841..ea697632d 100644
--- a/hydrus/client/db/ClientDBMappingsCounts.py
+++ b/hydrus/client/db/ClientDBMappingsCounts.py
@@ -26,6 +26,10 @@ def GenerateCombinedFilesMappingsCountsCacheTableName( tag_display_type, tag_ser
prefix = FILES_COMBINED_DISPLAY_AC_CACHE_PREFIX
+ else:
+
+ raise NotImplementedError( f'Unknown tag display type "{tag_display_type}"!' )
+
suffix = str( tag_service_id )
@@ -44,6 +48,10 @@ def GenerateSpecificCountsCacheTableName( tag_display_type, file_service_id, tag
prefix = FILES_SPECIFIC_DISPLAY_AC_CACHE_PREFIX
+ else:
+
+ raise NotImplementedError( f'Unknown tag display type "{tag_display_type}"!' )
+
suffix = '{}_{}'.format( file_service_id, tag_service_id )
diff --git a/hydrus/client/db/ClientDBMaster.py b/hydrus/client/db/ClientDBMaster.py
index 1c9892e3a..bb7fb80a4 100644
--- a/hydrus/client/db/ClientDBMaster.py
+++ b/hydrus/client/db/ClientDBMaster.py
@@ -259,6 +259,10 @@ def GetHashIdFromExtraHash( self, hash_type, hash ):
result = self._Execute( 'SELECT hash_id FROM local_hashes WHERE sha512 = ?;', ( sqlite3.Binary( hash ), ) ).fetchone()
+ else:
+
+ raise NotImplementedError( f'Unknown hash type "{hash_type}"!' )
+
if result is None:
@@ -680,6 +684,10 @@ def GetTagIdsToTags( self, tag_ids = None, tags = None ) -> typing.Dict[ int, st
tag_ids_to_tags = { self.GetTagId( tag ) : tag for tag in tags }
+ else:
+
+ raise Exception( 'Called without tag parameter!' )
+
return tag_ids_to_tags
diff --git a/hydrus/client/db/ClientDBSimilarFiles.py b/hydrus/client/db/ClientDBSimilarFiles.py
index 47281025b..14ce7f6d8 100644
--- a/hydrus/client/db/ClientDBSimilarFiles.py
+++ b/hydrus/client/db/ClientDBSimilarFiles.py
@@ -627,6 +627,11 @@ def AssociatePerceptualHashes( self, hash_id, perceptual_hashes ):
# yes, replace--these files' phashes have just changed, so we want to search again with this new data
self._Execute( 'REPLACE INTO shape_search_cache ( hash_id, searched_distance ) VALUES ( ?, ? );', ( hash_id, None ) )
+ else:
+
+ # emergency backstop to ensure we do add this to the system in the case of a weird re-association gap
+ self._Execute( 'INSERT OR IGNORE INTO shape_search_cache ( hash_id, searched_distance ) VALUES ( ?, ? );', ( hash_id, None ) )
+
return perceptual_hash_ids
diff --git a/hydrus/client/duplicates/ClientDuplicates.py b/hydrus/client/duplicates/ClientDuplicates.py
index f265731aa..9edc2c3ee 100644
--- a/hydrus/client/duplicates/ClientDuplicates.py
+++ b/hydrus/client/duplicates/ClientDuplicates.py
@@ -20,7 +20,6 @@
from hydrus.client import ClientTime
from hydrus.client.importing.options import NoteImportOptions
from hydrus.client.media import ClientMediaResult
-from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
@@ -1418,29 +1417,11 @@ def worth_updating_rating( source_rating, dest_rating ):
continue
- if media_result.IsDeleteLocked():
-
- ClientMediaFileFilter.ReportDeleteLockFailures( [ media_result ] )
-
- continue
-
-
- if media_result.GetLocationsManager().IsTrashed():
-
- deletee_service_keys = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, )
-
- else:
-
- local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
-
- deletee_service_keys = media_result.GetLocationsManager().GetCurrent().intersection( local_file_service_keys )
-
-
- for deletee_service_key in deletee_service_keys:
+ if CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY in media_result.GetLocationsManager().GetCurrent():
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { media_result.GetHash() }, reason = file_deletion_reason )
- content_update_package.AddContentUpdate( deletee_service_key, content_update )
+ content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, content_update )
diff --git a/hydrus/client/exporting/ClientExportingFiles.py b/hydrus/client/exporting/ClientExportingFiles.py
index f7f79e396..ff969ae35 100644
--- a/hydrus/client/exporting/ClientExportingFiles.py
+++ b/hydrus/client/exporting/ClientExportingFiles.py
@@ -709,55 +709,26 @@ def _DoExport( self, job_status: ClientThreading.JobStatus ):
if self._delete_from_client_after_export:
- local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
+ my_files_media_results = [ media_result for media_result in media_results if CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY in media_result.GetLocationsManager().GetCurrent() ]
- service_keys_to_deletee_hashes = collections.defaultdict( list )
+ reason = 'Deleted after export to Export Folder "{}".'.format( self._path )
+
+ CHUNK_SIZE = 64
+
+ chunks_of_media_results = HydrusLists.SplitListIntoChunks( my_files_media_results, CHUNK_SIZE )
- for ( i, media_result ) in enumerate( media_results ):
+ for ( i, chunk_of_media_results ) in enumerate( chunks_of_media_results ):
if job_status.IsCancelled():
return
- job_status.SetStatusText( 'delete-prepping: {}'.format( HydrusNumbers.ValueRangeToPrettyString( i + 1, len( media_results ) ) ) )
-
- if media_result.IsDeleteLocked():
-
- continue
-
-
- hash = media_result.GetHash()
-
- deletee_service_keys = media_result.GetLocationsManager().GetCurrent().intersection( local_file_service_keys )
-
- for deletee_service_key in deletee_service_keys:
-
- service_keys_to_deletee_hashes[ deletee_service_key ].append( hash )
-
-
-
- reason = 'Deleted after export to Export Folder "{}".'.format( self._path )
-
- for ( service_key, deletee_hashes ) in service_keys_to_deletee_hashes.items():
-
- CHUNK_SIZE = 64
+ job_status.SetStatusText( 'deleting: {}'.format( HydrusNumbers.ValueRangeToPrettyString( i * CHUNK_SIZE, len( my_files_media_results ) ) ) )
- chunks_of_hashes = HydrusLists.SplitListIntoChunks( deletee_hashes, CHUNK_SIZE )
+ content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { media_result.GetHash() for media_result in chunk_of_media_results }, reason = reason )
- for ( i, chunk_of_hashes ) in enumerate( chunks_of_hashes ):
-
- if job_status.IsCancelled():
-
- return
-
-
- job_status.SetStatusText( 'deleting: {}'.format( HydrusNumbers.ValueRangeToPrettyString( i * CHUNK_SIZE, len( deletee_hashes ) ) ) )
-
- content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, chunk_of_hashes, reason = reason )
-
- CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( service_key, content_update ) )
-
+ CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, content_update ) )
diff --git a/hydrus/client/gui/ClientGUIPopupMessages.py b/hydrus/client/gui/ClientGUIPopupMessages.py
index aa356aad2..44a52aca4 100644
--- a/hydrus/client/gui/ClientGUIPopupMessages.py
+++ b/hydrus/client/gui/ClientGUIPopupMessages.py
@@ -358,17 +358,17 @@ def errback_callable( etype, value, tb ):
def ShowTB( self ):
- if self._tb_text.isVisible():
+ if self._tb_text.isHidden():
- self._show_tb_button.setText( 'show traceback' )
+ self._show_tb_button.setText( 'hide traceback' )
- self._tb_text.hide()
+ self._tb_text.show()
else:
+
+ self._show_tb_button.setText( 'show traceback' )
- self._show_tb_button.setText( 'hide traceback' )
-
- self._tb_text.show()
+ self._tb_text.hide()
self.updateGeometry()
@@ -525,7 +525,7 @@ def UpdateMessage( self ):
self._time_network_job_disappeared = HydrusTime.GetNow()
- if self._network_job_ctrl.isVisible() and HydrusTime.TimeHasPassed( self._time_network_job_disappeared + 10 ):
+ if not self._network_job_ctrl.isHidden() and HydrusTime.TimeHasPassed( self._time_network_job_disappeared + 10 ):
self._network_job_ctrl.hide()
@@ -943,11 +943,11 @@ def _DisplayingError( self ):
def _SizeAndPositionAndShow( self ):
- gui_frame = self.parentWidget()
+ gui_frame = self.window()
try:
- gui_is_hidden = not gui_frame.isVisible()
+ gui_is_hidden = gui_frame.isHidden()
going_to_bug_out_at_hide_or_show = gui_is_hidden
@@ -957,7 +957,7 @@ def _SizeAndPositionAndShow( self ):
if there_is_stuff_to_display:
- if not self.isVisible() and not going_to_bug_out_at_hide_or_show:
+ if self.isHidden() and not going_to_bug_out_at_hide_or_show:
self.show()
@@ -989,7 +989,7 @@ def _SizeAndPositionAndShow( self ):
else:
- if self.isVisible() and not going_to_bug_out_at_hide_or_show:
+ if not self.isHidden() and not going_to_bug_out_at_hide_or_show:
self.hide()
@@ -1018,9 +1018,9 @@ def _OKToAlterUI( self ):
return False
- main_gui = self.parentWidget()
+ main_gui = self.window()
- if not main_gui.isVisible():
+ if main_gui.isHidden():
return False
@@ -1209,14 +1209,7 @@ def DismissAll( self ):
def ExpandCollapse( self ):
- if self._message_panel.isVisible():
-
- self._message_panel.setVisible( False )
-
- else:
-
- self._message_panel.show()
-
+ self._message_panel.setVisible( self._message_panel.isHidden() )
self.MakeSureEverythingFits()
diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py
index 866f92585..4330deeb3 100644
--- a/hydrus/client/gui/ClientGUITags.py
+++ b/hydrus/client/gui/ClientGUITags.py
@@ -717,15 +717,19 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
#
- self._import_favourite = ClientGUICommon.BetterButton( self, 'import', self._ImportFavourite )
- self._export_favourite = ClientGUICommon.BetterButton( self, 'export', self._ExportFavourite )
- self._load_favourite = ClientGUICommon.BetterButton( self, 'load', self._LoadFavourite )
- self._save_favourite = ClientGUICommon.BetterButton( self, 'save', self._SaveFavourite )
- self._delete_favourite = ClientGUICommon.BetterButton( self, 'delete', self._DeleteFavourite )
+ self._favourites_panel = ClientGUICommon.StaticBox( self, 'favourites' )
+
+ self._import_favourite = ClientGUICommon.BetterButton( self._favourites_panel, 'import', self._ImportFavourite )
+ self._export_favourite = ClientGUICommon.BetterButton( self._favourites_panel, 'export', self._ExportFavourite )
+ self._load_favourite = ClientGUICommon.BetterButton( self._favourites_panel, 'load', self._LoadFavourite )
+ self._save_favourite = ClientGUICommon.BetterButton( self._favourites_panel, 'save', self._SaveFavourite )
+ self._delete_favourite = ClientGUICommon.BetterButton( self._favourites_panel, 'delete', self._DeleteFavourite )
#
- self._show_all_panels_button = ClientGUICommon.BetterButton( self, 'show other panels', self._ShowAllPanels )
+ self._filter_panel = ClientGUICommon.StaticBox( self, 'filter' )
+
+ self._show_all_panels_button = ClientGUICommon.BetterButton( self._filter_panel, 'show other panels', self._ShowAllPanels )
self._show_all_panels_button.setToolTip( ClientGUIFunctions.WrapToolTip( 'This shows the whitelist and advanced panels, in case you want to craft a clever blacklist with \'except\' rules.' ) )
show_the_button = self._only_show_blacklist and CG.client_controller.new_options.GetBoolean( 'advanced_mode' )
@@ -734,7 +738,7 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
#
- self._notebook = ClientGUICommon.BetterNotebook( self )
+ self._notebook = ClientGUICommon.BetterNotebook( self._filter_panel )
#
@@ -760,17 +764,21 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
#
- self._redundant_st = ClientGUICommon.BetterStaticText( self, '', ellipsize_end = True )
+ self._redundant_st = ClientGUICommon.BetterStaticText( self._filter_panel, '', ellipsize_end = True )
self._redundant_st.setVisible( False )
- self._current_filter_st = ClientGUICommon.BetterStaticText( self, 'current filter: ', ellipsize_end = True )
+ self._current_filter_st = ClientGUICommon.BetterStaticText( self._filter_panel, 'current filter: ', ellipsize_end = True )
+
+ #
+
+ self._test_panel = ClientGUICommon.StaticBox( self, 'testing', can_expand = True, start_expanded = True )
- self._test_result_st = ClientGUICommon.BetterStaticText( self, self.TEST_RESULT_DEFAULT )
+ self._test_result_st = ClientGUICommon.BetterStaticText( self._test_panel, self.TEST_RESULT_DEFAULT )
self._test_result_st.setAlignment( QC.Qt.AlignmentFlag.AlignVCenter | QC.Qt.AlignmentFlag.AlignRight )
self._test_result_st.setWordWrap( True )
- self._test_input = QW.QPlainTextEdit( self )
+ self._test_input = QW.QPlainTextEdit( self._test_panel )
#
@@ -787,13 +795,19 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
if message is not None:
- st = ClientGUICommon.BetterStaticText( self, message )
+ message_panel = ClientGUICommon.StaticBox( self, 'explanation', can_expand = True, start_expanded = True )
+
+ st = ClientGUICommon.BetterStaticText( message_panel, message )
st.setWordWrap( True )
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+ message_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ QP.AddToLayout( vbox, message_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+ #
+
hbox = QP.HBoxLayout()
if self._read_only:
@@ -808,18 +822,28 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
QP.AddToLayout( hbox, self._save_favourite, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._delete_favourite, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( vbox, hbox, CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( vbox, self._show_all_panels_button, CC.FLAGS_ON_RIGHT )
+ self._favourites_panel.Add( hbox, CC.FLAGS_ON_RIGHT )
+
+ QP.AddToLayout( vbox, self._favourites_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ #
+
+ self._filter_panel.Add( self._show_all_panels_button, CC.FLAGS_ON_RIGHT )
label = 'Click the "(un)namespaced" checkboxes to allow/disallow those tags.\nType "namespace:" to manually input a namespace that is not in the list.'
st = ClientGUICommon.BetterStaticText( self, label = label )
st.setWordWrap( True )
+ st.setAlignment( QC.Qt.AlignmentFlag.AlignCenter )
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+ self._filter_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
- QP.AddToLayout( vbox, self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( vbox, self._redundant_st, CC.FLAGS_EXPAND_PERPENDICULAR )
- QP.AddToLayout( vbox, self._current_filter_st, CC.FLAGS_EXPAND_PERPENDICULAR )
+ self._filter_panel.Add( self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
+ self._filter_panel.Add( self._redundant_st, CC.FLAGS_EXPAND_PERPENDICULAR )
+ self._filter_panel.Add( self._current_filter_st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ QP.AddToLayout( vbox, self._filter_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ #
test_text_vbox = QP.VBoxLayout()
@@ -827,7 +851,7 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
message = 'This is a fixed blacklist. It will apply rules against all test tag siblings and apply unnamespaced rules to namespaced test tags.'
- st = ClientGUICommon.BetterStaticText( self, message )
+ st = ClientGUICommon.BetterStaticText( self._test_input, message )
st.setWordWrap( True )
@@ -841,7 +865,9 @@ def __init__( self, parent, tag_filter, only_show_blacklist = False, namespaces
QP.AddToLayout( hbox, test_text_vbox, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
QP.AddToLayout( hbox, self._test_input, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
- QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
+ self._test_panel.Add( hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ QP.AddToLayout( vbox, self._test_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
@@ -1280,7 +1306,7 @@ def _InitBlacklistPanel( self ):
( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._simple_blacklist_global_checkboxes, ( 20, 3 ) )
- self._simple_blacklist_global_checkboxes.setFixedHeight( h )
+ self._simple_blacklist_global_checkboxes.setFixedHeight( h + ( self._simple_blacklist_global_checkboxes.frameWidth() * 2 ) )
self._simple_blacklist_namespace_checkboxes = ClientGUICommon.BetterCheckBoxList( self._simple_whitelist_panel )
@@ -1332,7 +1358,7 @@ def _InitBlacklistPanel( self ):
main_hbox = QP.HBoxLayout()
- QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+ QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( main_hbox, right_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._simple_blacklist_panel.Add( main_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
@@ -1369,7 +1395,7 @@ def _InitWhitelistPanel( self ):
( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._simple_whitelist_global_checkboxes, ( 20, 3 ) )
- self._simple_whitelist_global_checkboxes.setFixedHeight( h )
+ self._simple_whitelist_global_checkboxes.setFixedHeight( h + ( self._simple_whitelist_global_checkboxes.frameWidth() * 2 ) )
self._simple_whitelist_namespace_checkboxes = ClientGUICommon.BetterCheckBoxList( self._simple_whitelist_panel )
@@ -1419,7 +1445,7 @@ def _InitWhitelistPanel( self ):
main_hbox = QP.HBoxLayout()
- QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+ QP.AddToLayout( main_hbox, left_vbox, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( main_hbox, right_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._simple_whitelist_panel.Add( main_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
diff --git a/hydrus/client/gui/QLocator.py b/hydrus/client/gui/QLocator.py
index f0ba8c344..e05100d33 100644
--- a/hydrus/client/gui/QLocator.py
+++ b/hydrus/client/gui/QLocator.py
@@ -802,7 +802,13 @@ def handleItemUpdate(self, jobID: int, data) -> None:
dataItem.toggledSelectedIconPath = self.iconBasePath + dataItem.toggledSelectedIconPath
self.resultsAvailable.emit(providerIndex, jobID)
- def stopJobs(self, ids = []) -> None:
+ def stopJobs(self, ids = None) -> None:
+
+ if ids is None:
+
+ ids = []
+
+
if not len(ids):
self.currentJobs = {}
for provider in self.providers:
diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py
index 38944c689..3c9aecb90 100644
--- a/hydrus/client/gui/QtPorting.py
+++ b/hydrus/client/gui/QtPorting.py
@@ -134,8 +134,9 @@ def SplitterVisibleCount( splitter ):
if splitter.widget( i ).isVisibleTo( splitter ): count += 1
+
return count
-
+
class DirPickerCtrl( QW.QWidget ):
@@ -1361,25 +1362,27 @@ def CallAfter( fn, *args, **kwargs ):
def ClearLayout( layout, delete_widgets = False ):
while layout.count() > 0:
-
+
item = layout.itemAt( 0 )
if delete_widgets:
-
+
if item.widget():
-
+
item.widget().deleteLater()
-
+
elif item.layout():
-
+
ClearLayout( item.layout(), delete_widgets = True )
item.layout().deleteLater()
-
+
else:
-
+
spacer = item.layout().spacerItem()
-
+
del spacer
+
+
layout.removeItem( item )
@@ -2098,6 +2101,7 @@ def eventFilter( self, watched, event ):
if isValid( self._parent_widget ) and self._parent_widget.isVisible():
self._user_moved_window = True
+
elif type == QC.QEvent.Type.Resize:
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index 9e08a4c4d..123216e4d 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -41,7 +41,6 @@
from hydrus.client.gui.panels import ClientGUIScrolledPanelsCommitFiltering
from hydrus.client.gui.panels import ClientGUIScrolledPanelsEdit
from hydrus.client.media import ClientMedia
-from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.media import ClientMediaResultPrettyInfo
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientRatings
@@ -1808,13 +1807,11 @@ def _DrawNotes( self, painter: QG.QPainter, current_y: int ):
my_width = my_size.width()
my_height = my_size.height()
- max_notes_width_percentage = 20
-
+ # TODO: this sucks and it is also wrong, we actually want like half this padding in later points.
+ # maybe we'll want to merge the details canvas with the hovers one and then we can talk to the hovers directly to get framewidth and margin/padding/whatever
PADDING = 4
- max_notes_width = int( my_width * ( max_notes_width_percentage / 100 ) ) - ( PADDING * 2 )
-
- notes_width = 0
+ notes_width = int( my_width * ClientGUICanvasHoverFrames.SIDE_HOVER_PROPORTIONS ) - ( PADDING * 2 )
original_font = painter.font()
@@ -1824,6 +1821,8 @@ def _DrawNotes( self, painter: QG.QPainter, current_y: int ):
notes_font = QG.QFont( original_font )
notes_font.setBold( False )
+ # old code that tried to draw it to a smaller box
+ '''
for ( name, note ) in names_to_notes.items():
# without wrapping, let's see if we fit into a smaller box than the max possible
@@ -1845,10 +1844,11 @@ def _DrawNotes( self, painter: QG.QPainter, current_y: int ):
break
+ '''
left_x = my_width - ( notes_width + PADDING )
- current_y += PADDING * 2
+ current_y += PADDING * 3
draw_a_test_rect = False
@@ -1887,9 +1887,6 @@ def _DrawNotes( self, painter: QG.QPainter, current_y: int ):
break
- # draw a horizontal line
-
-
painter.setFont( original_font )
@@ -3782,7 +3779,7 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
-def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.LocationContext, kept: typing.Collection[ ClientMedia.MediaSingleton ], deleted: typing.Collection[ ClientMedia.MediaSingleton ], skipped: typing.Collection[ ClientMedia.MediaSingleton ] ):
+def CommitArchiveDelete( page_key: bytes, deletee_location_context: ClientLocation.LocationContext, kept: typing.Collection[ ClientMedia.MediaSingleton ], deleted: typing.Collection[ ClientMedia.MediaSingleton ], skipped: typing.Collection[ ClientMedia.MediaSingleton ] ):
kept = list( kept )
deleted = list( deleted )
@@ -3808,13 +3805,13 @@ def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.Locat
CG.client_controller.pub( 'remove_media', page_key, all_hashes )
- location_context = location_context.Duplicate()
+ deletee_location_context = deletee_location_context.Duplicate()
- location_context.FixMissingServices( ClientLocation.ValidLocalDomainsFilter )
+ deletee_location_context.FixMissingServices( ClientLocation.ValidLocalDomainsFilter )
- if location_context.IncludesCurrent():
+ if deletee_location_context.IncludesCurrent():
- deletee_file_service_keys = location_context.current_service_keys
+ deletee_file_service_keys = deletee_location_context.current_service_keys
else:
@@ -3909,7 +3906,7 @@ def TryToDoPreClose( self ):
kept = list( self._kept )
- deleted = ClientMediaFileFilter.FilterAndReportDeleteLockFailures( self._deleted )
+ deleted = list( self._deleted )
skipped = list( self._skipped )
@@ -3930,9 +3927,13 @@ def TryToDoPreClose( self ):
location_contexts_to_present_options_for = []
- if not self._location_context.IsAllLocalFiles():
+ possible_location_context_at_top = self._location_context.Duplicate()
+
+ possible_location_context_at_top.LimitToServiceTypes( CG.client_controller.services_manager.GetServiceType, ( HC.COMBINED_LOCAL_MEDIA, HC.LOCAL_FILE_DOMAIN ) )
+
+ if len( possible_location_context_at_top.current_service_keys ) > 0:
- location_contexts_to_present_options_for.append( self._location_context )
+ location_contexts_to_present_options_for.append( possible_location_context_at_top )
current_local_service_keys = HydrusLists.MassUnion( [ m.GetLocationsManager().GetCurrent() for m in deleted ] )
@@ -3955,11 +3956,6 @@ def TryToDoPreClose( self ):
- if CC.TRASH_SERVICE_KEY in current_local_service_keys or CC.LOCAL_UPDATE_SERVICE_KEY in current_local_service_keys:
-
- location_contexts_to_present_options_for.append( ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
-
-
location_contexts_to_present_options_for = HydrusData.DedupeList( location_contexts_to_present_options_for )
only_allow_all_media_files = len( location_contexts_to_present_options_for ) > 1 and CG.client_controller.new_options.GetBoolean( 'only_show_delete_from_all_local_domains_when_filtering' ) and True in ( location_context.IsAllMediaFiles() for location_context in location_contexts_to_present_options_for )
@@ -3981,10 +3977,6 @@ def TryToDoPreClose( self ):
location_label = 'all local file domains'
- elif location_context == ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ):
-
- location_label = 'my hard disk'
-
else:
location_label = location_context.ToString( CG.client_controller.services_manager.GetName )
@@ -4029,15 +4021,6 @@ def _Delete( self, media = None, reason = None, file_service_key = None ):
return False
- if self._current_media.HasDeleteLocked():
-
- message = 'This file is delete-locked! Send it back to the inbox to delete it!'
-
- ClientGUIDialogsMessage.ShowWarning( self, message )
-
- return False
-
-
self._deleted.add( self._current_media )
if self._current_media == self._GetLast():
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
index 3ec26a1ab..890e9be4a 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
@@ -434,6 +434,11 @@ def _LowerHover( self ):
+ def _MouseOverImportantDescendant( self ):
+
+ return False
+
+
def _RaiseHover( self ):
if not self._is_currently_up:
@@ -452,7 +457,7 @@ def _RaiseHover( self ):
def _ShouldBeHidden( self ):
- return self._current_media is None or not self._my_canvas.isVisible()
+ return self._current_media is None
def _ShouldBeShown( self ):
@@ -462,6 +467,7 @@ def _ShouldBeShown( self ):
def _SizeAndPosition( self ):
+ # hey the parentwidget here is the media viewer!
if self.parentWidget().isVisible():
( should_resize, my_ideal_size, my_ideal_position ) = self._GetIdealSizeAndPosition()
@@ -601,11 +607,12 @@ def DoRegularHideShow( self ):
menu_open = CGC.core().MenuIsOpen()
+ mouse_over_important_descendant = self._MouseOverImportantDescendant()
+
dialog_is_open = ClientGUIFunctions.DialogIsOpen()
mouse_is_near_animation_bar = self._my_canvas.MouseIsNearAnimationBar()
- # this used to have the flash media window test to ensure mouse over flash window hid hovers going over it
mouse_is_over_something_else_important = mouse_is_near_animation_bar
mouse_is_over_a_dominant_hover = False
@@ -621,7 +628,7 @@ def DoRegularHideShow( self ):
hide_focus_is_good = focus_is_good or current_focus_tlw is None # don't hide if focus is either gone to another problem or temporarily sperging-out due to a click-transition or similar
ready_to_show = not self._is_currently_up and in_position and not mouse_is_over_something_else_important and focus_is_good and not dialog_is_open and not menu_open and not mouse_is_over_a_dominant_hover
- ready_to_hide = self._is_currently_up and not menu_open and ( not in_position or dialog_is_open or not hide_focus_is_good or mouse_is_over_a_dominant_hover )
+ ready_to_hide = self._is_currently_up and not menu_open and not mouse_over_important_descendant and ( not in_position or dialog_is_open or not hide_focus_is_good or mouse_is_over_a_dominant_hover )
def get_logic_report_string():
@@ -791,6 +798,16 @@ def _GetIdealSizeAndPosition( self ):
return ( should_resize, ideal_size, ideal_position )
+ def _MouseOverImportantDescendant( self ):
+
+ if not self._volume_control.isHidden():
+
+ return self._volume_control.PopupIsVisible()
+
+
+ return False
+
+
def _PopulateCenterButtons( self ):
self._archive_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().archive, self._Archive )
@@ -993,7 +1010,6 @@ def _ResetText( self ):
self._title_text.hide()
-
lines = ClientMediaResultPrettyInfo.GetPrettyMediaResultInfoLines( self._current_media.GetMediaResult(), only_interesting_lines = True )
lines = [ line for line in lines if not line.IsSubmenu() ]
@@ -1396,7 +1412,7 @@ def __init__( self, parent, my_canvas, top_hover: CanvasHoverFrameTop, canvas_ke
self.setLayout( vbox )
- self._ResetData()
+ self._ResetWidgets()
CG.client_controller.sub( self, 'ProcessContentUpdatePackage', 'content_updates_gui' )
@@ -1466,7 +1482,7 @@ def _GetIdealSizeAndPosition( self ):
return ( should_resize, ideal_size, ideal_position )
- def _ResetData( self ):
+ def _ResetWidgets( self ):
if self._current_media is not None:
@@ -1517,6 +1533,11 @@ def _ResetData( self ):
# urls
+ # BE WARY TRAVELLER
+ # unusual sizeHint gubbins occurs here if one does not take care
+ # ensure you check for flicker when transitioning from a topright media with and without urls
+ # and check that it is ok when the mouse is over the hover for the transition vs mouse not over and visiting later
+
urls = self._current_media.GetLocationsManager().GetURLs()
if urls != self._last_seen_urls:
@@ -1533,11 +1554,18 @@ def _ResetData( self ):
link.setAlignment( QC.Qt.AlignmentFlag.AlignRight )
- QP.AddToLayout( self._urls_vbox, link, CC.FLAGS_EXPAND_PERPENDICULAR )
+ # very important!
+ # needed for magic hover window crazy layout reasons
+ link.setVisible( True )
+
+ QP.AddToLayout( self._urls_vbox, link, CC.FLAGS_EXPAND_BOTH_WAYS )
+ # dare not remove this
+ self.layout().activate()
+
self._SizeAndPosition()
@@ -1563,7 +1591,7 @@ def ProcessContentUpdatePackage( self, content_update_package: ClientContentUpda
if do_redraw:
- self._ResetData()
+ self._ResetWidgets()
@@ -1572,17 +1600,16 @@ def SetMedia( self, media ):
super().SetMedia( media )
- self._ResetData()
-
- # size is not immediately updated without this
- self.layout().activate()
+ self._ResetWidgets()
- self._SizeAndPosition()
+ self._position_initialised_since_last_media = False
+
class NotePanel( QW.QWidget ):
editNote = QC.Signal( str )
+ devilsBargainManualUpdateGeometry = QC.Signal()
def __init__( self, parent: "CanvasHoverFrameRightNotes", name: str, note: str, note_visible: bool ):
@@ -1614,7 +1641,7 @@ def __init__( self, parent: "CanvasHoverFrameRightNotes", name: str, note: str,
QP.AddToLayout( vbox, self._note_name, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._note_text, CC.FLAGS_EXPAND_BOTH_WAYS )
- self._note_text.setVisible( self._note_visible )
+ self._note_text.setVisible( note_visible )
self.setLayout( vbox )
@@ -1634,9 +1661,12 @@ def eventFilter( self, watched, event ):
else:
- self._note_text.setVisible( not self._note_text.isVisible() )
+ self._note_text.setVisible( self._note_text.isHidden() )
+
+ self._note_visible = not self._note_text.isHidden()
- self._note_visible = self._note_text.isVisible()
+ # a normal updateGeometry call doesn't seem to do it (and indeed whatever implicit call occurs), I believe because we have the disconnected layout nonsense
+ self.devilsBargainManualUpdateGeometry.emit()
return True
@@ -1670,7 +1700,7 @@ def heightForWidth( self, width: int ):
total_height += expected_widget_height
- if self._note_text.isVisibleTo( self ):
+ if not self._note_text.isHidden():
total_height += spacing
@@ -1691,8 +1721,10 @@ def heightForWidth( self, width: int ):
return total_height
- def IsNoteVisible( self ) -> bool:
+ def IsNoteTextVisible( self ) -> bool:
+ # through various testing, this property appears to be sometimes whack. or it may be good now but was once tangled up in a mess of hacks
+ # don't really like it, so maybe just do `not self._note_text.isHidden()` live
return self._note_visible
@@ -1801,7 +1833,7 @@ def _ResetNotes( self ):
for ( name, note_panel ) in list( self._names_to_note_panels.items() ):
- if not note_panel.IsNoteVisible():
+ if not note_panel.IsNoteTextVisible():
note_panel_names_with_hidden_notes.add( name )
@@ -1811,6 +1843,10 @@ def _ResetNotes( self ):
note_panel.deleteLater()
+ # BE CAREFUL IF YOU EDIT ANY OF THIS
+ # this is a house of cards because of the whack disconnected layout situation
+ # one minor change and you'll get tumble into flicker hell
+
self._names_to_note_panels = {}
if self._current_media is not None and self._current_media.HasNotes():
@@ -1825,14 +1861,22 @@ def _ResetNotes( self ):
note_panel = NotePanel( self, name, note, note_visible )
+ # very important
+ # magico fix as per the urls in the top-right
+ note_panel.setVisible( True )
+
QP.AddToLayout( self._vbox, note_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self._names_to_note_panels[ name ] = note_panel
note_panel.editNote.connect( self._EditNotes )
+ note_panel.devilsBargainManualUpdateGeometry.connect( self._SizeAndPosition ) # total wewmode to handle note hide/show
+ # dare not remove this
+ self.layout().activate()
+
self._SizeAndPosition()
@@ -1884,19 +1928,9 @@ def SetMedia( self, media ):
super().SetMedia( media )
- if self._is_currently_up:
-
- # magical refresh that makes the labels look correct and not be hidden???
- self._LowerHover()
- self._ResetNotes()
- self._RaiseHover()
-
- else:
-
- self._ResetNotes()
-
- self._position_initialised_since_last_media = False
-
+ self._ResetNotes()
+
+ self._position_initialised_since_last_media = False
class CanvasHoverFrameRightDuplicates( CanvasHoverFrame ):
@@ -2118,7 +2152,8 @@ def _EditMergeOptions( self, duplicate_type ):
def _EnableDisableButtons( self ):
- disabled = self._comparison_media is not None and self._comparison_media.HasDeleteLocked()
+ # old delete-lock stuff. maybe it'll be useful to bring back one day, w/e
+ disabled = False
self._this_is_better_and_delete_other.setEnabled( not disabled )
@@ -2160,10 +2195,7 @@ def _ResetComparisonStatements( self ):
show_panel = got_data
- if panel.isVisible() != show_panel:
-
- panel.setVisible( show_panel )
-
+ panel.setVisible( show_panel )
if got_data:
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
index 0f5741b9f..e7e2557dd 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
@@ -1285,10 +1285,10 @@ def SetMediaAndWindow( self, media, media_window, ):
if duration is None and isinstance(media_window, Animation):
- duration = media_window.GetDuration()
+ duration = media_window.GetDuration()
+
self._duration_ms = max( duration, 1 )
-
self._currently_in_a_drag = False
self._it_was_playing_before_drag = False
@@ -1610,6 +1610,8 @@ def _MakeMediaWindow( self ):
self._animation_bar.ClearMedia()
+ self._ShowHideControlBar()
+
media_window_changed = old_media_window != self._media_window
# this has to go after setcanvastype on the mpv window so the filters are in the correct order
@@ -1665,6 +1667,91 @@ def _SetZoom( self, zoom: float ):
self.zoomChanged.emit( self._current_zoom )
+ def _ShowHideControlBar( self ):
+
+ is_near = False
+ show_small_instead_of_hiding = None
+ force_show = False
+
+ if not ShouldHaveAnimationBar( self._media, self._show_action ):
+
+ should_show_controls = False
+
+ else:
+
+ is_near = self.MouseIsNearAnimationBar()
+ show_small_instead_of_hiding = CG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) is not None
+ force_show = self._volume_control.PopupIsVisible() or self._animation_bar.DoingADrag() or CG.client_controller.new_options.GetBoolean( 'force_animation_scanbar_show' )
+
+ should_show_controls = is_near or show_small_instead_of_hiding or force_show
+
+
+ if should_show_controls:
+
+ should_show_full = is_near or force_show
+
+ if should_show_full != self._controls_bar_show_full:
+
+ self._controls_bar_show_full = should_show_full
+
+ self._animation_bar.SetShowText( self._controls_bar_show_full )
+
+ self._volume_control.setEnabled( self._controls_bar_show_full )
+
+ self._SizeAndPositionChildren()
+
+ # TODO: investigate this
+ # ok we do seem to have a flicker here, most obvious when going from small to full size on a quick animation. we get a frame of where the top half was before. some bitmap memory issue I guess
+ # a forced repaint of the animation bar here does not fix it, so I suspect this is related to the disconnected layout nonsense I am doing
+ # TODO: if and when fixed, investigate if setGubbinsVisible is still a useful thing
+
+
+ do_layout = False
+
+ if self._controls_bar.isHidden():
+
+ self._controls_bar.setVisible( True )
+ self._controls_bar.raise_()
+
+ self._animation_bar.setGubbinsVisible( True )
+ self._animation_bar.repaint() # this is probably not needed
+
+ do_layout = True
+
+
+ should_show_volume = self.ShouldHaveVolumeControl()
+
+ volume_currently_visible = not self._volume_control.isHidden()
+
+ if volume_currently_visible != should_show_volume:
+
+ self._volume_control.setVisible( should_show_volume )
+
+ do_layout = True
+
+
+ self._controls_bar.layout()
+
+ else:
+
+ if not self._controls_bar.isHidden():
+
+ # ok, repaint here forces a clear paint event NOW, before we hide.
+ # this ensures that when we show again, we won't have the nub in the wrong place for a frame before it repaints
+ # we'll have no nub, but this is less noticeable
+
+ self._animation_bar.setGubbinsVisible( False )
+ self._animation_bar.repaint() # this is probably not needed
+
+ self._controls_bar.setVisible( False )
+
+ self._volume_control.setVisible( False )
+
+ self._controls_bar.layout() # this is probably not needed
+
+
+
+
def _SizeAndPositionChildren( self ):
if self._media is not None:
@@ -2633,74 +2720,10 @@ def ZoomSwitchMax( self, switch_base: float ):
def TIMERUIUpdate( self ):
- is_near = False
- show_small_instead_of_hiding = None
- force_show = False
-
- if not ShouldHaveAnimationBar( self._media, self._show_action ):
-
- should_show_controls = False
-
- else:
-
- is_near = self.MouseIsNearAnimationBar()
- show_small_instead_of_hiding = CG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) is not None
- force_show = self._volume_control.PopupIsVisible() or self._animation_bar.DoingADrag() or CG.client_controller.new_options.GetBoolean( 'force_animation_scanbar_show' )
-
- should_show_controls = is_near or show_small_instead_of_hiding or force_show
-
-
- if should_show_controls:
-
- should_show_full = is_near or force_show
-
- if should_show_full != self._controls_bar_show_full:
-
- self._controls_bar_show_full = should_show_full
-
- self._animation_bar.SetShowText( self._controls_bar_show_full )
-
- self._volume_control.setEnabled( self._controls_bar_show_full )
-
- self._SizeAndPositionChildren()
-
-
- if not self._controls_bar.isVisible():
-
- self._controls_bar.show()
- self._controls_bar.raise_()
-
- self._animation_bar.setGubbinsVisible( True )
- self._animation_bar.repaint()
-
-
- should_show_volume = self.ShouldHaveVolumeControl()
-
- if self._volume_control.isVisible() != should_show_volume:
-
- self._volume_control.setVisible( should_show_volume )
-
- self._controls_bar.layout()
-
-
- else:
-
- if self._controls_bar.isVisible():
-
- # ok, repaint here forces a clear paint event NOW, before we hide.
- # this ensures that when we show again, we won't have the nub in the wrong place for a frame before it repaints
- # we'll have no nub, but this is less noticeable
-
- self._animation_bar.setGubbinsVisible( False )
- self._animation_bar.repaint()
-
- self._controls_bar.hide()
-
- self._controls_bar.layout()
-
-
+ self._ShowHideControlBar()
+
class EmbedButton( QW.QWidget ):
def __init__( self, parent, background_colour_generator ):
diff --git a/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py b/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py
index e500c2af1..8435f1e7c 100644
--- a/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py
+++ b/hydrus/client/gui/duplicates/ClientGUIPotentialDuplicatesSearchContext.py
@@ -32,8 +32,8 @@ def __init__( self, parent: QW.QWidget, potential_duplicates_search_context: Cli
page_key = HydrusData.GenerateKey()
- self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_1, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
- self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_2, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
+ self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_1, allow_all_known_files = False, only_allow_local_file_domains = True, only_allow_all_my_files_domains = True, synchronised = synchronised, force_system_everything = True )
+ self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self, page_key, file_search_context_2, allow_all_known_files = False, only_allow_local_file_domains = True, only_allow_all_my_files_domains = True, synchronised = synchronised, force_system_everything = True )
self._dupe_search_type = ClientGUICommon.BetterChoice( self )
diff --git a/hydrus/client/gui/exporting/ClientGUIExport.py b/hydrus/client/gui/exporting/ClientGUIExport.py
index 0c48a102e..aacd46e7a 100644
--- a/hydrus/client/gui/exporting/ClientGUIExport.py
+++ b/hydrus/client/gui/exporting/ClientGUIExport.py
@@ -1,4 +1,3 @@
-import collections
import os
import time
import typing
@@ -17,7 +16,6 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
-from hydrus.client import ClientLocation
from hydrus.client import ClientThreading
from hydrus.client.exporting import ClientExportingFiles
from hydrus.client.gui import ClientGUIAsync
@@ -36,7 +34,6 @@
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.media import ClientMedia
-from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientMetadataMigrationExporters
from hydrus.client.metadata import ClientMetadataMigrationImporters
@@ -356,7 +353,7 @@ def __init__( self, parent, export_folder: ClientExportingFiles.ExportFolder ):
rows = []
- rows.append( ( 'delete files from client after export: ', self._delete_from_client_after_export ) )
+ rows.append( ( 'trash files in hydrus client after export: ', self._delete_from_client_after_export ) )
rows.append( ( 'EXPERIMENTAL: export symlinks', self._export_symlinks ) )
gridbox = ClientGUICommon.WrapInGrid( self._type_box, rows )
@@ -478,7 +475,7 @@ def UserIsOKToOK( self ):
if self._delete_from_client_after_export.isChecked():
- message = 'You have set this export folder to delete the files from the client after export! Are you absolutely sure this is what you want?'
+ message = 'You have set this export folder to delete the files from the client (send them to trash) after export! Are you absolutely sure this is what you want?'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -495,7 +492,7 @@ def EventDeleteFilesAfterExport( self ):
if self._delete_from_client_after_export.isChecked():
- ClientGUIDialogsMessage.ShowWarning( self, 'This will delete the exported files from your client after the export! If you do not know what this means, uncheck it!' )
+ ClientGUIDialogsMessage.ShowWarning( self, 'This will delete the exported files from your client (send them to trash) after the export! If you do not know what this means, uncheck it!' )
@@ -609,7 +606,7 @@ def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
self._examples = ClientGUICommon.ExportPatternButton( self._filenames_box )
- self._delete_files_after_export = QW.QCheckBox( 'delete files from client after export?', self )
+ self._delete_files_after_export = QW.QCheckBox( 'trash files in hydrus client after export', self )
self._delete_files_after_export.setObjectName( 'HydrusWarning' )
self._export_symlinks = QW.QCheckBox( 'EXPERIMENTAL: export symlinks', self )
@@ -794,7 +791,7 @@ def _DoExport( self, quit_afterwards = False ):
if delete_afterwards:
message += '\n' * 2
- message += 'THE FILES WILL BE DELETED FROM THE CLIENT AFTERWARDS'
+ message += 'THE FILES WILL BE SENT TO THE TRASH IN THE CLIENT AFTERWARDS'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -808,7 +805,7 @@ def _DoExport( self, quit_afterwards = False ):
elif delete_afterwards:
- message = 'THE FILES WILL BE DELETED FROM THE CLIENT AFTERWARDS'
+ message = 'THE FILES WILL BE SENT TO THE TRASH IN THE CLIENT AFTERWARDS'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
@@ -985,32 +982,19 @@ def do_it( directory, metadata_routers, delete_afterwards, export_symlinks, quit
QP.CallAfter( qt_update_label, 'deleting' )
- deletee_medias = ClientMediaFileFilter.FilterAndReportDeleteLockFailures( actually_done_ok )
+ actually_done_media_results = [ m.GetMediaResult() for m in actually_done_ok ]
- local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
+ chunks_of_deletee_media_results = HydrusLists.SplitListIntoChunks( actually_done_media_results, 64 )
- chunks_of_deletee_medias = HydrusLists.SplitListIntoChunks( list( deletee_medias ), 64 )
-
- for chunk_of_deletee_medias in chunks_of_deletee_medias:
+ for chunk_of_deletee_media_results in chunks_of_deletee_media_results:
reason = 'Deleted after manual export to "{}".'.format( directory )
- service_keys_to_hashes = collections.defaultdict( set )
+ hashes = [ media_result.GetHash() for media_result in chunk_of_deletee_media_results ]
- for media in chunk_of_deletee_medias:
-
- for service_key in media.GetLocationsManager().GetCurrent().intersection( local_file_service_keys ):
-
- service_keys_to_hashes[ service_key ].add( media.GetHash() )
-
-
+ content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, hashes, reason = reason )
- for service_key in ClientLocation.ValidLocalDomainsFilter( service_keys_to_hashes.keys() ):
-
- content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, service_keys_to_hashes[ service_key ], reason = reason )
-
- CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( service_key, content_update ) )
-
+ CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, content_update ) )
diff --git a/hydrus/client/gui/media/ClientGUIMediaControls.py b/hydrus/client/gui/media/ClientGUIMediaControls.py
index 6c0de29de..5c69b496e 100644
--- a/hydrus/client/gui/media/ClientGUIMediaControls.py
+++ b/hydrus/client/gui/media/ClientGUIMediaControls.py
@@ -102,6 +102,9 @@ def __init__( self, parent, canvas_type, direction = 'down' ):
self.setLayout( vbox )
+ # TODO: same as with much of the media controls mess, this needs to be plugged into the layout system properly
+ # we should have a custom layout here that specifies where the slider should go and do raise/show/hide while still reporting a nice small sizeHint
+
self._popup_window = self._PopupWindow( self, canvas_type, direction = direction )
@@ -109,6 +112,8 @@ def enterEvent( self, event ):
if not self.isVisible():
+ event.ignore()
+
return
@@ -121,6 +126,10 @@ def leaveEvent( self, event ):
if not self.isVisible():
+ self._popup_window.setVisible( False )
+
+ event.ignore()
+
return
@@ -129,9 +138,30 @@ def leaveEvent( self, event ):
event.ignore()
+ def moveEvent( self, event ):
+
+ super().moveEvent( event )
+
+ self._popup_window.DoShowHide()
+
+
def PopupIsVisible( self ):
- return self._popup_window.isVisible()
+ return not self._popup_window.isHidden()
+
+
+ def resizeEvent( self, event ):
+
+ super().resizeEvent( event )
+
+ CG.client_controller.CallAfterQtSafe( self, 'volume popup resize event', self._popup_window.DoShowHide )
+
+
+ def setVisible( self, *args, **kwargs ):
+
+ super().setVisible( *args, **kwargs )
+
+ self._popup_window.DoShowHide()
class _PopupWindow( QW.QFrame ):
@@ -199,10 +229,17 @@ def __init__( self, parent, canvas_type, direction = 'down' ):
CG.client_controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
- def DoShowHide( self ):
+ def DoReposition( self ):
parent = self.parentWidget()
+ if not parent.isVisible():
+
+ self.hide()
+
+ return
+
+
horizontal_offset = ( self.width() - parent.width() ) // 2
if self._direction == 'down':
@@ -218,16 +255,31 @@ def DoShowHide( self ):
self.move( pos )
- over_parent = ClientGUIFunctions.MouseIsOverWidget( parent )
+
+ def DoShowHide( self ):
+
+ self.DoReposition()
+
+ parent = self.parentWidget()
+
+ if not parent.isVisible():
+
+ self.hide()
+
+ return
+
+
+ over_parent = ClientGUIFunctions.MouseIsOverWidget( parent ) and parent.isEnabled()
over_me = ClientGUIFunctions.MouseIsOverWidget( self )
- should_show = over_parent or over_me
+ should_show = over_parent
+ should_hide = not ( over_parent or over_me )
if should_show:
self.show()
- else:
+ elif should_hide:
self.hide()
@@ -235,13 +287,11 @@ def DoShowHide( self ):
def leaveEvent( self, event ):
- if not self.isVisible():
+ if self.isVisible():
- return
+ self.DoShowHide()
- self.DoShowHide()
-
event.ignore()
diff --git a/hydrus/client/gui/media/ClientGUIMediaMenus.py b/hydrus/client/gui/media/ClientGUIMediaMenus.py
index 440c081ea..5967e2b34 100644
--- a/hydrus/client/gui/media/ClientGUIMediaMenus.py
+++ b/hydrus/client/gui/media/ClientGUIMediaMenus.py
@@ -366,6 +366,7 @@ def AddKnownURLsViewCopyMenu( win: QW.QWidget, command_processor: CAC.Applicatio
focus_urls = focus_media.GetLocationsManager().GetURLs()
+ focus_media_url_classes = set()
focus_matched_labels_and_urls = []
focus_unmatched_urls = []
focus_labels_and_urls = []
@@ -393,6 +394,8 @@ def AddKnownURLsViewCopyMenu( win: QW.QWidget, command_processor: CAC.Applicatio
focus_matched_labels_and_urls.append( ( label, url ) )
+ focus_media_url_classes.add( url_class )
+
focus_matched_labels_and_urls.sort()
@@ -526,6 +529,20 @@ def call_generator( u ):
+ if len( focus_media_url_classes ) > 0:
+
+ focus_media_url_classes = list( focus_media_url_classes )
+
+ focus_media_url_classes.sort( key = lambda url_class: url_class.GetName() )
+
+ for url_class in focus_media_url_classes:
+
+ label = 'this file\'s ' + url_class.GetName() + ' urls'
+
+ ClientGUIMenus.AppendMenuItem( urls_force_refetch_menu, label, 'Re-download these URLs with forced metadata re-fetch enabled.', ClientGUIMediaModalActions.RedownloadURLClassURLsForceRefetch, win, { focus_media }, url_class )
+
+
+
# copy this file's urls
there_are_focus_url_classes_to_action = len( focus_matched_labels_and_urls ) > 1
@@ -571,6 +588,7 @@ def call_generator( u ):
ClientGUIMenus.AppendSeparator( urls_visit_menu )
ClientGUIMenus.AppendSeparator( urls_copy_menu )
+ ClientGUIMenus.AppendSeparator( urls_force_refetch_menu )
if there_are_selection_url_classes_to_action:
@@ -587,7 +605,10 @@ def call_generator( u ):
ClientGUIMenus.AppendMenuItem( urls_copy_menu, label, 'Copy this url class for all files.', ClientGUIMediaSimpleActions.CopyMediaURLClassURLs, selected_media, url_class )
- ClientGUIMenus.AppendMenuItem( urls_force_refetch_menu, label, 'Re-download these URLs with forced metadata re-fetch enabled.', ClientGUIMediaModalActions.RedownloadURLClassURLsForceRefetch, win, selected_media, url_class )
+ if len( selected_media ) > 1:
+
+ ClientGUIMenus.AppendMenuItem( urls_force_refetch_menu, label, 'Re-download these URLs with forced metadata re-fetch enabled.', ClientGUIMediaModalActions.RedownloadURLClassURLsForceRefetch, win, selected_media, url_class )
+
diff --git a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
index cf11b1d95..b8e710cac 100644
--- a/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
+++ b/hydrus/client/gui/metadata/ClientGUIEditTimestamps.py
@@ -793,14 +793,14 @@ def _SetValueTimestampDatas( self, list_of_timestamp_data: typing.Collection[ Cl
if timestamp_data.location == CC.CANVAS_MEDIA_VIEWER:
- if self._last_viewed_media_viewer_time.isVisible():
+ if not self._last_viewed_media_viewer_time.isHidden():
self._last_viewed_media_viewer_time.SetValue( self._last_viewed_media_viewer_time.GetValue().DuplicateWithNewTimestampMS( timestamp_data.timestamp_ms ), from_user = from_user )
elif timestamp_data.location == CC.CANVAS_PREVIEW:
- if self._last_viewed_preview_viewer_time.isVisible():
+ if not self._last_viewed_preview_viewer_time.isHidden():
self._last_viewed_preview_viewer_time.SetValue( self._last_viewed_preview_viewer_time.GetValue().DuplicateWithNewTimestampMS( timestamp_data.timestamp_ms ), from_user = from_user )
diff --git a/hydrus/client/gui/metadata/ClientGUITime.py b/hydrus/client/gui/metadata/ClientGUITime.py
index 1cb8b0cd9..81bed21c2 100644
--- a/hydrus/client/gui/metadata/ClientGUITime.py
+++ b/hydrus/client/gui/metadata/ClientGUITime.py
@@ -1353,7 +1353,7 @@ def SetValue( self, value: float ):
value %= 1
- if self._show_milliseconds and value > 0:
+ if self._show_milliseconds:
self._milliseconds.setValue( int( multiplier * value * 1000 ) )
diff --git a/hydrus/client/gui/networking/ClientGUINetwork.py b/hydrus/client/gui/networking/ClientGUINetwork.py
index 1d6549f1f..48d88fd30 100644
--- a/hydrus/client/gui/networking/ClientGUINetwork.py
+++ b/hydrus/client/gui/networking/ClientGUINetwork.py
@@ -984,9 +984,9 @@ def __init__( self, parent: QW.QWidget, controller: "CG.ClientController.Control
#
- self._rules_job = CG.client_controller.CallRepeatingQtSafe( self, 0.5, 5.0, 'repeating bandwidth rules update', self._UpdateRules )
+ self._rules_job = CG.client_controller.CallRepeatingQtSafe( self, 0.0, 5.0, 'repeating bandwidth rules update', self._UpdateRules )
- self._update_job = CG.client_controller.CallRepeatingQtSafe( self, 0.5, 1.0, 'repeating bandwidth status update', self._Update )
+ self._update_job = CG.client_controller.CallRepeatingQtSafe( self, 0.0, 1.0, 'repeating bandwidth status update', self._Update )
def _EditRules( self ):
@@ -1005,7 +1005,7 @@ def _EditRules( self ):
self._controller.network_engine.bandwidth_manager.SetRules( self._network_context, self._bandwidth_rules )
- self._UpdateRules()
+ self._rules_job.Wake()
@@ -1043,7 +1043,7 @@ def _UpdateRules( self ):
if self._network_context.IsDefault() or self._network_context == ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT:
- if self._use_default_rules_button.isVisible():
+ if not self._use_default_rules_button.isHidden():
self._uses_default_rules_st.hide()
self._use_default_rules_button.hide()
@@ -1057,7 +1057,7 @@ def _UpdateRules( self ):
self._edit_rules_button.setText( 'set specific rules' )
- if self._use_default_rules_button.isVisible():
+ if not self._use_default_rules_button.isHidden():
self._use_default_rules_button.hide()
@@ -1068,7 +1068,7 @@ def _UpdateRules( self ):
self._edit_rules_button.setText( 'edit rules' )
- if not self._use_default_rules_button.isVisible():
+ if self._use_default_rules_button.isHidden():
self._use_default_rules_button.show()
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index e3e6ae296..5a2714675 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -1182,7 +1182,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
super().__init__( parent, page, controller, management_controller )
self._last_time_imports_changed = 0
- self._next_update_time = 0
+ self._next_update_time = 0.0
self._multiple_gallery_import = self._management_controller.GetVariable( 'multiple_gallery_import' )
@@ -2149,13 +2149,26 @@ def _UpdateImportOptionsSetButton( self ):
def _UpdateImportStatus( self ):
- if HydrusTime.TimeHasPassed( self._next_update_time ):
+ # TODO: Surely this can be optimised, especially with our new multi-column list tech
+ # perhaps break any sort to a ten second timer or something
+
+ if HydrusTime.TimeHasPassedFloat( self._next_update_time ):
num_items = len( self._gallery_importers_listctrl.GetData() )
- update_period = max( 1, int( ( num_items / 10 ) ** 0.33 ) )
+ min_time = CG.client_controller.new_options.GetInteger( 'gallery_page_status_update_time_minimum_ms' ) / 1000
+ denominator = CG.client_controller.new_options.GetInteger( 'gallery_page_status_update_time_ratio_denominator' )
+
+ try:
+
+ update_period = max( min_time, num_items / denominator )
+
+ except:
+
+ update_period = 1.0
+
- self._next_update_time = HydrusTime.GetNow() + update_period
+ self._next_update_time = HydrusTime.GetNowFloat() + update_period
#
@@ -2213,7 +2226,7 @@ def _UpdateImportStatus( self ):
def _UpdateImportStatusNow( self ):
- self._next_update_time = 0
+ self._next_update_time = 0.0
self._UpdateImportStatus()
@@ -2270,7 +2283,7 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
super().__init__( parent, page, controller, management_controller )
self._last_time_watchers_changed = 0
- self._next_update_time = 0
+ self._next_update_time = 0.0
self._multiple_watcher_import = self._management_controller.GetVariable( 'multiple_watcher_import' )
@@ -3297,13 +3310,26 @@ def _UpdateImportOptionsSetButton( self ):
def _UpdateImportStatus( self ):
- if HydrusTime.TimeHasPassed( self._next_update_time ):
+ # TODO: Surely this can be optimised, especially with our new multi-column list tech
+ # perhaps break any sort to a ten second timer or something
+
+ if HydrusTime.TimeHasPassedFloat( self._next_update_time ):
num_items = len( self._watchers_listctrl.GetData() )
- update_period = max( 1, int( ( num_items / 10 ) ** 0.33 ) )
+ min_time = CG.client_controller.new_options.GetInteger( 'watcher_page_status_update_time_minimum_ms' ) / 1000
+ denominator = CG.client_controller.new_options.GetInteger( 'watcher_page_status_update_time_ratio_denominator' )
+
+ try:
+
+ update_period = max( min_time, num_items / denominator )
+
+ except:
+
+ update_period = 1.0
+
- self._next_update_time = HydrusTime.GetNow() + update_period
+ self._next_update_time = HydrusTime.GetNowFloat() + update_period
#
@@ -3371,7 +3397,7 @@ def _UpdateImportStatus( self ):
def _UpdateImportStatusNow( self ):
- self._next_update_time = 0
+ self._next_update_time = 0.0
self._UpdateImportStatus()
diff --git a/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py
index f6f62800d..87f032de9 100644
--- a/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py
+++ b/hydrus/client/gui/panels/ClientGUIManageOptionsPanel.py
@@ -1303,6 +1303,7 @@ def __init__( self, parent ):
delete_lock_panel = ClientGUICommon.StaticBox( self, 'delete lock' )
self._delete_lock_for_archived_files = QW.QCheckBox( delete_lock_panel )
+ self._delete_lock_for_archived_files.setToolTip( ClientGUIFunctions.WrapToolTip( 'This will stop the client from physically deleting anything you have archived. You can still trash such files, but they cannot go further. It is a last-ditch catch to rescue accidentally deleted good files.' ) )
advanced_file_deletion_panel = ClientGUICommon.StaticBox( self, 'advanced file deletion and custom reasons' )
@@ -1389,7 +1390,7 @@ def __init__( self, parent ):
rows = []
- rows.append( ( 'Do not permit archived files to be trashed or deleted: ', self._delete_lock_for_archived_files ) )
+ rows.append( ( 'Do not permit archived files to be deleted from the trash: ', self._delete_lock_for_archived_files ) )
gridbox = ClientGUICommon.WrapInGrid( delete_lock_panel, rows )
@@ -1502,12 +1503,15 @@ def __init__( self, parent ):
self._file_viewing_statistics_active = QW.QCheckBox( self )
self._file_viewing_statistics_active_on_archive_delete_filter = QW.QCheckBox( self )
self._file_viewing_statistics_active_on_dupe_filter = QW.QCheckBox( self )
- self._file_viewing_statistics_media_min_time = ClientGUICommon.NoneableSpinCtrl( self, 2 )
+ self._file_viewing_statistics_media_min_time = ClientGUICommon.NoneableSpinCtrl( self, 2, none_phrase = 'count every view' )
+ min_tt = 'If you scroll quickly through many files, you probably do not want to count each of those loads as a view. Set a reasonable minimum here and brief looks will not be counted.'
+ self._file_viewing_statistics_media_min_time.setToolTip( ClientGUIFunctions.WrapToolTip( min_tt ) )
self._file_viewing_statistics_media_max_time = ClientGUICommon.NoneableSpinCtrl( self, 600 )
- max_tt = 'If you view a file for a very long time, the amount of viewtime recorded is clipped to this. This stops an outrageous viewtime being saved because you left something open in the background. If the media you view has duration, like a video, the max viewtime is five times its length or this, whichever is larger.'
+ max_tt = 'If you view a file for a very long time, the recorded viewtime is truncated to this. This stops an outrageous viewtime being saved because you left something open in the background. If the media you view has duration, like a video, the max viewtime is five times its length or this, whichever is larger.'
self._file_viewing_statistics_media_max_time.setToolTip( ClientGUIFunctions.WrapToolTip( max_tt ) )
- self._file_viewing_statistics_preview_min_time = ClientGUICommon.NoneableSpinCtrl( self, 5 )
+ self._file_viewing_statistics_preview_min_time = ClientGUICommon.NoneableSpinCtrl( self, 5, none_phrase = 'count every view' )
+ self._file_viewing_statistics_preview_min_time.setToolTip( ClientGUIFunctions.WrapToolTip( min_tt ) )
self._file_viewing_statistics_preview_max_time = ClientGUICommon.NoneableSpinCtrl( self, 60 )
self._file_viewing_statistics_preview_max_time.setToolTip( ClientGUIFunctions.WrapToolTip( max_tt ) )
@@ -3968,6 +3972,16 @@ def __init__( self, parent, new_options ):
#
+ pages_panel = ClientGUICommon.StaticBox( self, 'download pages update', can_expand = True, start_expanded = False )
+
+ self._gallery_page_status_update_time_minimum = ClientGUITime.TimeDeltaCtrl( pages_panel, min = 0.25, seconds = True, milliseconds = True )
+ self._gallery_page_status_update_time_ratio_denominator = ClientGUICommon.BetterSpinBox( pages_panel, min = 1 )
+
+ self._watcher_page_status_update_time_minimum = ClientGUITime.TimeDeltaCtrl( pages_panel, min = 0.25, seconds = True, milliseconds = True )
+ self._watcher_page_status_update_time_ratio_denominator = ClientGUICommon.BetterSpinBox( pages_panel, min = 1 )
+
+ #
+
buffer_panel = ClientGUICommon.StaticBox( self, 'video buffer' )
self._video_buffer_size = ClientGUIBytes.BytesControl( buffer_panel )
@@ -3987,6 +4001,12 @@ def __init__( self, parent, new_options ):
self._ideal_tile_dimension.setValue( self._new_options.GetInteger( 'ideal_tile_dimension' ) )
+ self._gallery_page_status_update_time_minimum.SetValue( self._new_options.GetInteger( 'gallery_page_status_update_time_minimum_ms' ) / 1000 )
+ self._gallery_page_status_update_time_ratio_denominator.setValue( self._new_options.GetInteger( 'gallery_page_status_update_time_ratio_denominator' ) )
+
+ self._watcher_page_status_update_time_minimum.SetValue( self._new_options.GetInteger( 'watcher_page_status_update_time_minimum_ms' ) / 1000 )
+ self._watcher_page_status_update_time_ratio_denominator.setValue( self._new_options.GetInteger( 'watcher_page_status_update_time_ratio_denominator' ) )
+
self._video_buffer_size.SetValue( self._new_options.GetInteger( 'video_buffer_size' ) )
self._media_viewer_prefetch_delay_base_ms.setValue( self._new_options.GetInteger( 'media_viewer_prefetch_delay_base_ms' ) )
@@ -4108,6 +4128,30 @@ def __init__( self, parent, new_options ):
#
+ text = 'EXPERIMENTAL, HYDEV ONLY, STAY AWAY!'
+
+ st = ClientGUICommon.BetterStaticText( pages_panel, text )
+
+ st.setWordWrap( True )
+
+ pages_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ rows = []
+
+ rows.append( ( 'EXPERIMENTAL: Minimum gallery importer update time:', self._gallery_page_status_update_time_minimum ) )
+ rows.append( ( 'EXPERIMENTAL: Gallery importer magic update time denominator:', self._gallery_page_status_update_time_ratio_denominator ) )
+
+ rows.append( ( 'EXPERIMENTAL: Minimum watcher importer update time:', self._watcher_page_status_update_time_minimum ) )
+ rows.append( ( 'EXPERIMENTAL: Watcher importer magic update time denominator:', self._watcher_page_status_update_time_ratio_denominator ) )
+
+ gridbox = ClientGUICommon.WrapInGrid( pages_panel, rows )
+
+ pages_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+
+ QP.AddToLayout( vbox, pages_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ #
+
text = 'This old option does not apply to mpv! It only applies to the native hydrus animation renderer!'
text += '\n'
text += 'Hydrus video rendering is CPU intensive.'
@@ -4267,6 +4311,12 @@ def UpdateOptions( self ):
self._new_options.SetInteger( 'image_cache_storage_limit_percentage', self._image_cache_storage_limit_percentage.value() )
self._new_options.SetInteger( 'image_cache_prefetch_limit_percentage', self._image_cache_prefetch_limit_percentage.value() )
+ self._new_options.SetInteger( 'gallery_page_status_update_time_minimum_ms', int( self._gallery_page_status_update_time_minimum.GetValue() * 1000 ) )
+ self._new_options.SetInteger( 'gallery_page_status_update_time_ratio_denominator', self._gallery_page_status_update_time_ratio_denominator.value() )
+
+ self._new_options.SetInteger( 'watcher_page_status_update_time_minimum_ms', int( self._watcher_page_status_update_time_minimum.GetValue() * 1000 ) )
+ self._new_options.SetInteger( 'watcher_page_status_update_time_ratio_denominator', self._watcher_page_status_update_time_ratio_denominator.value() )
+
self._new_options.SetInteger( 'video_buffer_size', self._video_buffer_size.GetValue() )
diff --git a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
index 05995c19f..e0509a6b6 100644
--- a/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/panels/ClientGUIScrolledPanelsEdit.py
@@ -39,7 +39,7 @@
from hydrus.client.importing.options import NoteImportOptions
from hydrus.client.importing.options import TagImportOptions
from hydrus.client.media import ClientMedia
-from hydrus.client.media import ClientMediaFileFilter
+from hydrus.client.media import ClientMediaResult
from hydrus.client.metadata import ClientContentUpdates
# TODO: ok the general plan here is to move rich panels to topical gui.xxx modules
@@ -521,7 +521,7 @@ def __init__( self, parent: QW.QWidget, media, default_reason, suggested_file_se
suggested_file_service_key = local_file_service_domains[0].GetServiceKey()
- self._media = self._FilterForDeleteLock( ClientMedia.FlattenMedia( media ), suggested_file_service_key )
+ self._media = ClientMedia.FlattenMedia( media )
self._question_is_already_resolved = len( self._media ) == 0
@@ -690,18 +690,6 @@ def __init__( self, parent: QW.QWidget, media, default_reason, suggested_file_se
QP.CallAfter( self._SetFocus )
- def _FilterForDeleteLock( self, media, suggested_file_service_key: bytes ):
-
- service = CG.client_controller.services_manager.GetService( suggested_file_service_key )
-
- if service.GetServiceType() in HC.LOCAL_FILE_SERVICES:
-
- media = ClientMediaFileFilter.FilterAndReportDeleteLockFailures( media )
-
-
- return media
-
-
def _GetExistingSharedFileDeletionReason( self ):
all_files_have_existing_file_deletion_reasons = True
@@ -789,7 +777,17 @@ def _InitialisePermittedActionChoices( self ):
possible_file_service_keys.extend( ( ( rfs.GetServiceKey(), rfs.GetServiceKey() ) for rfs in CG.client_controller.services_manager.GetServices( ( HC.FILE_REPOSITORY, ) ) ) )
- keys_to_hashes = { ( selection_file_service_key, deletee_file_service_key ) : [ m.GetHash() for m in self._media if selection_file_service_key in m.GetLocationsManager().GetCurrent() ] for ( selection_file_service_key, deletee_file_service_key ) in possible_file_service_keys }
+ def PhysicalDeleteLockOK( s_k: bytes, media_result: ClientMediaResult.MediaResult ):
+
+ if s_k in ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.TRASH_SERVICE_KEY ):
+
+ return not media_result.IsPhysicalDeleteLocked()
+
+
+ return True
+
+
+ keys_to_hashes = { ( selection_file_service_key, deletee_file_service_key ) : [ m.GetHash() for m in self._media if selection_file_service_key in m.GetLocationsManager().GetCurrent() if PhysicalDeleteLockOK( deletee_file_service_key, m.GetMediaResult() ) ] for ( selection_file_service_key, deletee_file_service_key ) in possible_file_service_keys }
trashed_key = ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
combined_key = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
@@ -809,6 +807,11 @@ def _InitialisePermittedActionChoices( self ):
for ( fsk, hashes ) in possible_file_service_keys_and_hashes:
+ if len( hashes ) == 0:
+
+ continue
+
+
num_to_delete = len( hashes )
( selection_file_service_key, deletee_file_service_key ) = fsk
@@ -964,7 +967,7 @@ def _InitialisePermittedActionChoices( self ):
if CG.client_controller.new_options.GetBoolean( 'use_advanced_file_deletion_dialog' ):
- hashes = [ m.GetHash() for m in self._media if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in m.GetLocationsManager().GetCurrent() ]
+ hashes = [ m.GetHash() for m in self._media if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in m.GetLocationsManager().GetCurrent() if PhysicalDeleteLockOK( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, m.GetMediaResult() ) ]
num_to_delete = len( hashes )
diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py
index 5e07c33ee..2537be0c0 100644
--- a/hydrus/client/gui/search/ClientGUIACDropdown.py
+++ b/hydrus/client/gui/search/ClientGUIACDropdown.py
@@ -2060,7 +2060,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
searchChanged = QC.Signal( ClientSearchFileSearchContext.FileSearchContext )
searchCancelled = QC.Signal()
- def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearchFileSearchContext.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, only_allow_local_file_domains = False, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ):
+ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearchFileSearchContext.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIMediaResultsPanelSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, only_allow_local_file_domains = False, only_allow_all_my_files_domains = False, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ):
self._page_key = page_key
@@ -2090,6 +2090,7 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
super().__init__( parent, location_context, tag_context.service_key )
self._location_context_button.SetOnlyLocalFileDomainsAllowed( only_allow_local_file_domains )
+ self._location_context_button.SetOnlyAllMyFilesDomainsAllowed( only_allow_all_my_files_domains )
self._location_context_button.SetAllKnownFilesAllowed( allow_all_known_files, True )
#
diff --git a/hydrus/client/gui/search/ClientGUILocation.py b/hydrus/client/gui/search/ClientGUILocation.py
index 03edc6e1b..bf70a4e2d 100644
--- a/hydrus/client/gui/search/ClientGUILocation.py
+++ b/hydrus/client/gui/search/ClientGUILocation.py
@@ -16,7 +16,7 @@
class EditMultipleLocationContextPanel( ClientGUIScrolledPanels.EditPanel ):
- def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext, all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool ):
+ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext, all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool, only_all_my_files_domains_allowed: bool ):
super().__init__( parent )
@@ -24,10 +24,11 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio
self._all_known_files_allowed = all_known_files_allowed
self._only_importable_domains_allowed = only_importable_domains_allowed
self._only_local_file_domains_allowed = only_local_file_domains_allowed
+ self._only_all_my_files_domains_allowed = only_all_my_files_domains_allowed
self._location_list = ClientGUICommon.BetterCheckBoxList( self )
- services = ClientLocation.GetPossibleFileDomainServicesInOrder( all_known_files_allowed, only_importable_domains_allowed, only_local_file_domains_allowed )
+ services = ClientLocation.GetPossibleFileDomainServicesInOrder( all_known_files_allowed, only_importable_domains_allowed, only_local_file_domains_allowed, only_all_my_files_domains_allowed )
for service in services:
@@ -41,7 +42,7 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio
advanced_mode = CG.client_controller.new_options.GetBoolean( 'advanced_mode' )
- if advanced_mode and not ( only_local_file_domains_allowed or only_importable_domains_allowed ):
+ if advanced_mode and not ( only_local_file_domains_allowed or only_importable_domains_allowed or only_all_my_files_domains_allowed ):
for service in services:
@@ -143,13 +144,14 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio
self._all_known_files_allowed_only_in_advanced_mode = False
self._only_importable_domains_allowed = False
self._only_local_file_domains_allowed = False
+ self._only_all_my_files_domains_allowed = False
self.SetValue( location_context, force_label = True )
def _EditLocation( self ):
- services = ClientLocation.GetPossibleFileDomainServicesInOrder( self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed )
+ services = ClientLocation.GetPossibleFileDomainServicesInOrder( self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed, self._only_all_my_files_domains_allowed )
menu = ClientGUIMenus.GenerateMenu( self )
@@ -223,7 +225,7 @@ def _EditMultipleLocationContext( self ):
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit multiple location' ) as dlg:
- panel = EditMultipleLocationContextPanel( dlg, self._location_context, self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed )
+ panel = EditMultipleLocationContextPanel( dlg, self._location_context, self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed, self._only_all_my_files_domains_allowed )
dlg.SetPanel( panel )
@@ -260,6 +262,11 @@ def GetValue( self ) -> ClientLocation.LocationContext:
return self._location_context
+ def SetOnlyAllMyFilesDomainsAllowed( self, only_all_my_files_domains_allowed: bool ):
+
+ self._only_all_my_files_domains_allowed = only_all_my_files_domains_allowed
+
+
def SetOnlyImportableDomainsAllowed( self, only_importable_domains_allowed: bool ):
self._only_importable_domains_allowed = only_importable_domains_allowed
diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py
index 05a4eebf5..c194c6b05 100644
--- a/hydrus/client/media/ClientMedia.py
+++ b/hydrus/client/media/ClientMedia.py
@@ -398,11 +398,6 @@ def HasAudio( self ) -> bool:
raise NotImplementedError()
- def HasDeleteLocked( self ) -> bool:
-
- raise NotImplementedError()
-
-
def HasDuration( self ) -> bool:
raise NotImplementedError()
@@ -1757,11 +1752,6 @@ def HasAudio( self ):
return self._has_audio
- def HasDeleteLocked( self ):
-
- return True in ( media.HasDeleteLocked() for media in self._sorted_media )
-
-
def HasDuration( self ):
return self._duration is not None
@@ -1978,13 +1968,11 @@ def HasAudio( self ):
return self._media_result.HasAudio()
- def HasDeleteLocked( self ):
+ def IsPhysicalDeleteLocked( self ):
- return self._media_result.IsDeleteLocked()
+ return self._media_result.IsPhysicalDeleteLocked()
- IsDeleteLocked = HasDeleteLocked
-
def HasDuration( self ):
duration = self._media_result.GetDurationMS()
diff --git a/hydrus/client/media/ClientMediaFileFilter.py b/hydrus/client/media/ClientMediaFileFilter.py
index 5664700cf..7d39a81b4 100644
--- a/hydrus/client/media/ClientMediaFileFilter.py
+++ b/hydrus/client/media/ClientMediaFileFilter.py
@@ -467,24 +467,6 @@ def ToStringWithCount( self, media_list: ClientMedia.MediaList, filter_counts: d
FileFilter( FILE_FILTER_REMOTE ) : FileFilter( FILE_FILTER_LOCAL )
} )
-def FilterAndReportDeleteLockFailures( medias: typing.Collection[ ClientMedia.Media ] ):
-
- # TODO: update this system with some texts like 'file was archived' so user can know how to fix the situation
-
- deletee_medias = [ media for media in medias if not media.HasDeleteLocked() ]
-
- if len( deletee_medias ) < len( medias ):
-
- locked_medias = [ media for media in medias if media.HasDeleteLocked() ]
-
- locked_media_results = [ media_singleton.GetMediaResult() for media_singleton in ClientMedia.FlattenMedia( locked_medias ) ]
-
- ReportDeleteLockFailures( locked_media_results )
-
-
- return deletee_medias
-
-
def ReportDeleteLockFailures( media_results: typing.Collection[ ClientMediaResult.MediaResult ] ):
HydrusData.Print( 'Hey, we had a delete-lock problem. Here is the stack, which hydev may care to see:' )
diff --git a/hydrus/client/media/ClientMediaResult.py b/hydrus/client/media/ClientMediaResult.py
index 5c1f31666..1694ff072 100644
--- a/hydrus/client/media/ClientMediaResult.py
+++ b/hydrus/client/media/ClientMediaResult.py
@@ -157,7 +157,7 @@ def HasNotes( self ):
return self._notes_manager.GetNumNotes() > 0
- def IsDeleteLocked( self ):
+ def IsPhysicalDeleteLocked( self ):
# TODO: ultimately replace this with metadata conditionals for whatever the user likes, 'don't delete anything rated 5 stars', whatever
diff --git a/hydrus/client/networking/api/ClientLocalServerResourcesAddFiles.py b/hydrus/client/networking/api/ClientLocalServerResourcesAddFiles.py
index c45fbfafa..71c9f85a1 100644
--- a/hydrus/client/networking/api/ClientLocalServerResourcesAddFiles.py
+++ b/hydrus/client/networking/api/ClientLocalServerResourcesAddFiles.py
@@ -183,11 +183,11 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
location_context.LimitToServiceTypes( CG.client_controller.services_manager.GetServiceType, ( HC.COMBINED_LOCAL_FILE, HC.COMBINED_LOCAL_MEDIA, HC.LOCAL_FILE_DOMAIN ) )
- if CG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' ):
+ if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in location_context.current_service_keys:
media_results = CG.client_controller.Read( 'media_results', hashes )
- undeletable_media_results = [ m for m in media_results if m.IsDeleteLocked() ]
+ undeletable_media_results = [ m for m in media_results if m.IsPhysicalDeleteLocked() ]
if len( undeletable_media_results ) > 0:
diff --git a/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py b/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
index 0b6cb6362..5a2460ff7 100644
--- a/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
+++ b/hydrus/client/networking/api/ClientLocalServerResourcesManageFileRelationships.py
@@ -8,8 +8,6 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
-from hydrus.client.media import ClientMedia
-from hydrus.client.media import ClientMediaFileFilter
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.networking.api import ClientLocalServerCore
from hydrus.client.networking.api import ClientLocalServerResources
@@ -208,8 +206,8 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
content_update_packages = []
- first_media_result = ClientMedia.MediaSingleton( hashes_to_media_results[ hash_a ] )
- second_media_result = ClientMedia.MediaSingleton( hashes_to_media_results[ hash_b ] )
+ first_media_result = hashes_to_media_results[ hash_a ]
+ second_media_result = hashes_to_media_results[ hash_b ]
file_deletion_reason = 'From Client API (duplicates processing).'
@@ -237,29 +235,11 @@ def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
for media_result in deletee_media_results:
- if media_result.IsDeleteLocked():
-
- ClientMediaFileFilter.ReportDeleteLockFailures( [ media_result ] )
-
- continue
-
-
- if media_result.GetLocationsManager().IsTrashed():
-
- deletee_service_keys = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, )
-
- else:
-
- local_file_service_keys = CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
-
- deletee_service_keys = media_result.GetLocationsManager().GetCurrent().intersection( local_file_service_keys )
-
-
- for deletee_service_key in deletee_service_keys:
+ if CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY in media_result.GetLocationsManager().GetCurrent():
content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { media_result.GetHash() }, reason = file_deletion_reason )
- content_update_package.AddContentUpdate( deletee_service_key, content_update )
+ content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, content_update )
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 62650eb6f..84dc45309 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,7 +105,7 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 602
+SOFTWARE_VERSION = 603
CLIENT_API_VERSION = 76
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/files/images/HydrusImageHandling.py b/hydrus/core/files/images/HydrusImageHandling.py
index f0812f2f0..d920fbcc0 100644
--- a/hydrus/core/files/images/HydrusImageHandling.py
+++ b/hydrus/core/files/images/HydrusImageHandling.py
@@ -342,10 +342,15 @@ def GeneratePILImageFromNumPyImage( numpy_image: numpy.array ) -> PILImage.Image
return pil_image
-def GenerateFileBytesNumPy( numpy_image, ext: str = '.png', params: list[int] = [] ) -> bytes:
+def GenerateFileBytesNumPy( numpy_image, ext: str = '.png', params: typing.Optional[ typing.List[ int ] ] = None ) -> bytes:
+
+ if params is None:
+
+ params = []
+
if len( numpy_image.shape ) == 2:
-
+
convert = cv2.COLOR_GRAY2RGB
else:
diff --git a/hydrus/external/LogicExpressionQueryParser.py b/hydrus/external/LogicExpressionQueryParser.py
index 0f05ec4fb..14932e56a 100644
--- a/hydrus/external/LogicExpressionQueryParser.py
+++ b/hydrus/external/LogicExpressionQueryParser.py
@@ -96,9 +96,17 @@ def precedence(token):
#A simple class representing a node in a logical expression tree
class Node:
- def __init__(self, op, children = []):
+ def __init__(self, op, children = None):
+
+ if children is None:
+
+ children = []
+
+
self.op = op
self.children = children[:]
+
+
def __str__(self): #pretty string form, for debug purposes
if self.op == "not":
return "not ({})".format(str(self.children[0]) if type(self.children[0]) != str else self.children[0])
diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py
index dba6f0afd..8739572ea 100644
--- a/hydrus/test/TestClientAPI.py
+++ b/hydrus/test/TestClientAPI.py
@@ -1390,6 +1390,38 @@ def _test_add_files_other_actions( self, connection, set_up_permissions ):
self.assertIn( not_existing_service_hex, text ) # error message should be complaining about it
+ # test file lock, 200 response
+
+ locked_hash = list( hashes )[0]
+
+ media_result = HF.GetFakeMediaResult( locked_hash )
+
+ media_result.GetLocationsManager().inbox = False
+
+ TG.test_controller.new_options.SetBoolean( 'delete_lock_for_archived_files', True )
+
+ TG.test_controller.ClearWrites( 'content_updates' )
+
+ TG.test_controller.SetRead( 'media_results', [ media_result ] )
+
+ path = '/add_files/delete_files'
+
+ body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_key' : CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY.hex() }
+
+ body = json.dumps( body_dict )
+
+ connection.request( 'POST', path, body = body, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ self.assertEqual( response.status, 200 )
+
+ CG.client_controller.new_options.SetBoolean( 'delete_lock_for_archived_files', False )
+
+ TG.test_controller.ClearReads( 'media_results' )
+
# test file lock, 409 response
locked_hash = list( hashes )[0]
@@ -1406,7 +1438,7 @@ def _test_add_files_other_actions( self, connection, set_up_permissions ):
path = '/add_files/delete_files'
- body_dict = { 'hashes' : [ h.hex() for h in hashes ] }
+ body_dict = { 'hashes' : [ h.hex() for h in hashes ], 'file_service_key' : CC.COMBINED_LOCAL_FILE_SERVICE_KEY.hex() }
body = json.dumps( body_dict )
@@ -4836,7 +4868,7 @@ def _test_manage_duplicates( self, connection, set_up_permissions ):
self.assertTrue( len( content_update_packages ) == 1 )
- expected_content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { bytes.fromhex( r_dict[ 'hash_b' ] ) }, reason = 'From Client API (duplicates processing).' ) )
+ expected_content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { bytes.fromhex( r_dict[ 'hash_b' ] ) }, reason = 'From Client API (duplicates processing).' ) )
HF.compare_content_update_packages( self, content_update_packages[0], expected_content_update_package )
diff --git a/hydrus/test/TestHydrusSerialisable.py b/hydrus/test/TestHydrusSerialisable.py
index b7cb3228c..5ab15ef43 100644
--- a/hydrus/test/TestHydrusSerialisable.py
+++ b/hydrus/test/TestHydrusSerialisable.py
@@ -197,7 +197,7 @@ def test( obj, dupe_obj ):
local_times_manager = ClientMediaManagers.TimesManager()
- current_to_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 123000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 123000 }
+ current_to_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 123000, CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY : 123000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 123000 }
local_times_manager.SetImportedTimestampsMS( current_to_timestamps_ms )
@@ -213,8 +213,8 @@ def test( obj, dupe_obj ):
deleted_times_manager = ClientMediaManagers.TimesManager()
- deleted_to_previously_imported_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 10000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 118000 }
- deleted_to_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 120000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 123000 }
+ deleted_to_previously_imported_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 10000, CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY : 118000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 118000 }
+ deleted_to_timestamps_ms = { CC.LOCAL_FILE_SERVICE_KEY : 120000, CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY : 120000, CC.COMBINED_LOCAL_FILE_SERVICE_KEY : 123000 }
deleted_times_manager.SetDeletedTimestampsMS( deleted_to_timestamps_ms )
deleted_times_manager.SetPreviouslyImportedTimestampsMS( deleted_to_previously_imported_timestamps_ms )
@@ -309,7 +309,7 @@ def test( obj, dupe_obj ):
content_update_package = ClientContentUpdates.ContentUpdatePackage()
- content_update_package.AddContentUpdate( CC.LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { local_hash_empty }, reason = file_deletion_reason ) )
+ content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { local_hash_empty }, reason = file_deletion_reason ) )
HF.compare_content_update_packages( self, result, content_update_package )
@@ -319,15 +319,15 @@ def test( obj, dupe_obj ):
content_update_package = ClientContentUpdates.ContentUpdatePackage()
- content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { trashed_hash_empty }, reason = file_deletion_reason ) )
-
HF.compare_content_update_packages( self, result, content_update_package )
#
result = duplicate_content_merge_options_delete_and_move.ProcessPairIntoContentUpdatePackage( local_media_result_has_values, deleted_media_result_empty, delete_second = True, file_deletion_reason = file_deletion_reason )
- HF.compare_content_update_packages( self, result, ClientContentUpdates.ContentUpdatePackage() )
+ content_update_package = ClientContentUpdates.ContentUpdatePackage()
+
+ HF.compare_content_update_packages( self, result, content_update_package )
#
@@ -339,7 +339,7 @@ def test( obj, dupe_obj ):
content_update_package.AddContentUpdate( TC.LOCAL_RATING_LIKE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( None, { other_local_hash_has_values } ) ) )
content_update_package.AddContentUpdate( TC.LOCAL_RATING_NUMERICAL_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( None, { other_local_hash_has_values } ) ) )
content_update_package.AddContentUpdates( TC.LOCAL_RATING_INCDEC_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 12, { local_hash_has_values } ) ), ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 0, { other_local_hash_has_values } ) ) ] )
- content_update_package.AddContentUpdate( CC.LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { other_local_hash_has_values }, reason = file_deletion_reason ) )
+ content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { other_local_hash_has_values }, reason = file_deletion_reason ) )
HF.compare_content_update_packages( self, result, content_update_package )
@@ -353,7 +353,7 @@ def test( obj, dupe_obj ):
content_update_package.AddContentUpdates( TC.LOCAL_RATING_LIKE_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 1.0, { local_hash_empty } ) ), ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( None, { other_local_hash_has_values } ) ) ] )
content_update_package.AddContentUpdates( TC.LOCAL_RATING_NUMERICAL_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 0.8, { local_hash_empty } ) ), ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( None, { other_local_hash_has_values } ) ) ])
content_update_package.AddContentUpdates( TC.LOCAL_RATING_INCDEC_SERVICE_KEY, [ ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 6, { local_hash_empty } ) ), ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( 0, { other_local_hash_has_values } ) ) ] )
- content_update_package.AddContentUpdate( CC.LOCAL_FILE_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { other_local_hash_has_values }, reason = file_deletion_reason ) )
+ content_update_package.AddContentUpdate( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, { other_local_hash_has_values }, reason = file_deletion_reason ) )
HF.compare_content_update_packages( self, result, content_update_package )