diff --git a/db/extract_options.sql b/db/extract_options.sql
deleted file mode 100644
index b1348a18..00000000
--- a/db/extract_options.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-.open client.db
-.out my_options.sql
-.print .open client.db\r\n
-.print delete from options;\r\n
-.print delete from json_dumps where dump_type = 22;\r\n
-.mode insert options
-select * from options;
-.mode insert json_dumps
-select * from json_dumps where dump_type = 22;
\ No newline at end of file
diff --git a/db/extract_subscriptions.sql b/db/extract_subscriptions.sql
deleted file mode 100644
index 87ff6c45..00000000
--- a/db/extract_subscriptions.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-.print The subscriptions will lose their tag import options, so make sure to check them once they are imported back in.
-.open client.db
-.out my_subscriptions.sql
-.print .open client.db\r\n
-.mode insert json_dumps_named
-select * from json_dumps_named where dump_type = 3;
\ No newline at end of file
diff --git a/db/extract_version.bat b/db/extract_version.bat
new file mode 100644
index 00000000..947fca16
--- /dev/null
+++ b/db/extract_version.bat
@@ -0,0 +1,3 @@
+@ECHO off
+sqlite3 < extract_version.sql
+SET /P gumpf=Hit Enter to exit!
\ No newline at end of file
diff --git a/db/extract_version.sql b/db/extract_version.sql
new file mode 100644
index 00000000..5c3802df
--- /dev/null
+++ b/db/extract_version.sql
@@ -0,0 +1,2 @@
+.open --readonly client.db
+SELECT "This database is version " || version FROM version;
\ No newline at end of file
diff --git a/db/using the extract scripts.txt b/db/using the extract scripts.txt
deleted file mode 100644
index 2cef5d53..00000000
--- a/db/using the extract scripts.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-If your db is completely broken and you need to extract some important data, please check out the emergency extract scripts. To use them, put your old database, the sqlite3 executable, and the script in the same folder and feed the script into sqlite3, like so:
-
-sqlite3 < extract_subscriptions.sql
-
-This will connect to the database and copy your subscriptions to the new file my_subscriptions.sql, which you can then move and import to a new db folder in the same way:
-
-sqlite3 < my_subscriptions.sql
-
-Some things are difficult to copy over at this basic level. Your tag options and anything else service-specific will be lost or reset back to default.
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
index a8ab4c24..bb9a5a45 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -3,6 +3,49 @@
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 479](https://github.com/hydrusnetwork/hydrus/releases/tag/v479)
+
+### misc
+* when shift-selecting some thumbnails, you can now reverse the direction of the select and what you just selected will be deselected, basically a full undo (issue #1105)
+* when ctrl-selecting thumbnails, if you add to the selection, the file you click is now focused and always previewed (previously this only happened if there was no focused file already). this is related to the shift-select logic above, but it may be annoying when making a big ctrl-selection of videos etc.. so let me know and I can make this more clever if needed
+* added file sort 'file->hash', which sorts pseudorandomly but repeatably. it sounds not super clever, but it will be useful for certain comparison operations across clients
+* when you hit 'copy->hash' on a file right-click, it now shows the sha256 hash for quick review
+* in the duplicate filter, the zoom locking tech now works betterâ„¢ when one of the pair is portrait and the other landscape. it now tries to select either width or height to lock both when going AB and BA. it also chooses the 'better' of width or height by choosing the zoom that'll change the size less radically. previously, it could do width on AB and height on BA, which lead to a variety of odd situations. there are probably still some issues here, most likely when one of the files almost exactly fills the whole canvas, so let me know how you get on
+* webps with transparency should now load correct! previously they were going crazy in the transparent area. all webps are scheduled a thumbnail regen this week
+* when import folders run, the count on their progress bar now ignores previous failed and ignored entries. it should always start 0, like 0/100, rather than 20/120 etc...
+* when import folders run, any imports where the status type is set to 'leave the file alone' is now still scanned at the end of a job. if the path does not exist any more, it is removed from the import list
+* fixed a typo bug in the recent delete code cleanup that meant 'delete files after export' after a manual export was only working on the last file in the selection. sorry for the trouble!
+* the delete files dialog now starts with keyboard focus on the action radiobox (it was defaulting to ok button since I added the recent panel disable tech)
+* if a network job has a connection error or serverside bandwidth block and then waits before retrying, it now checks if all network jobs have just been paused and will not reattempt the connection if so (issue #1095)
+* fixed a bug in thumbnail fallback rendering
+* fixed another problem with cloudscraper's new method names. it should work for users still on an old version
+* wrote a little 'extract version' sql and bat file for the db folder that simply pull the version from the client.db file in the same directory. I removed the extract options/subscriptions sql scripts since they are super old and out of date, but this general system may return in future
+
+### file history chart
+* added 'archive' line to the file history chart. this isn't exactly (current_count - inbox_count), but it pretty much is
+* added a 'show deleted' checkbox to the file history chart. it will recalculate the y axis range on click, so if you have loads of deleted files, you can now hide them to see current better
+* improved the way data is aggregated in the file history chart. diagonal lines should be reduced during any periods of client import-inactivity, and spikes should show better
+* also bumped the number of steps up to 8,000, so it should look nice maximised on a 4k
+* the file history chart now remembers its last size and position--it has an entry under options->gui
+
+### client api
+* thanks to a user, the Client API now accepts any file_id, file_ids, hash, or hashes as arguments in any place where you need to specify a file or files
+* like 'return_hashes', the 'search_files' command in the Client API now takes an optional 'return_file_ids' parameter, default true, to turn off the file ids if you only want hashes
+* added 'only_return_basic_information' parameter, default false, to 'get_metadata' call, which is fast for first-time requests (it is slim but not well cached) and just delivers the basics like resolution and file size
+* added unit tests and updated the help to reflect the above
+* client api version is now 29
+
+### help
+* split up the 'more files' help section into 'powerful searching' and 'exporting files', both still under the 'next steps' section
+* moved the semi-advanced 'OR' section from 'tags' to 'searching'
+* brushed up misc help
+* a couple of users added some misc help updates too, thank you!
+
+### misc boring cleanup
+* cleaned up an old wx label patch
+* cleaned up an old wx system colour patch
+* cleaned up some misc initialisation code
+
## [Version 478](https://github.com/hydrusnetwork/hydrus/releases/tag/v478)
### misc
diff --git a/docs/developer_api.md b/docs/developer_api.md
index f509a0ae..986be2ea 100644
--- a/docs/developer_api.md
+++ b/docs/developer_api.md
@@ -1242,7 +1242,8 @@ Arguments (in percent-encoded JSON):
* `tag_service_key`: (optional, selective, hexadecimal, the tag domain on which to search)
* `file_sort_type`: (optional, integer, the results sort method)
* `file_sort_asc`: true or false (optional, the results sort order)
- * `return_hashes`: true or false (optional, default false, returns hex hashes in addition to file ids, hashes and file ids are in the same order)
+ * `return_file_ids`: true or false (optional, default true, returns file id results)
+ * `return_hashes`: true or false (optional, default false, returns hex hash results)
* _`system_inbox`: true or false (obsolete, use tags)_
* _`system_archive`: true or false (obsolete, use tags)_
@@ -1394,7 +1395,9 @@ Response:
}
```
- File ids are internal and specific to an individual client. For a client, a file with hash H always has the same file id N, but two clients will have different ideas about which N goes with which H. They are a bit faster than hashes to retrieve and search with _en masse_, which is why they are exposed here.
+ You can of course also specify `return_hashes=true&return_file_ids=false` just to get the hashes. The order of both lists is the same.
+
+ File ids are internal and specific to an individual client. For a client, a file with hash H always has the same file id N, but two clients will have different ideas about which N goes with which H. IDs are a bit faster to retrieve than hashes and search with _en masse_, which is why they are exposed here.
This search does **not** apply the implicit limit that most clients set to all searches (usually 10,000), so if you do system:everything on a client with millions of files, expect to get boshed. Even with a system:limit included, complicated queries with large result sets may take several seconds to respond. Just like the client itself.
@@ -1414,6 +1417,7 @@ Arguments (in percent-encoded JSON):
* `hash`: (selective, a hexadecimal SHA256 hash)
* `hashes`: (selective, a list of hexadecimal SHA256 hashes)
* `only_return_identifiers`: true or false (optional, defaulting to false)
+ * `only_return_basic_information`: true or false (optional, defaulting to false)
* `detailed_url_information`: true or false (optional, defaulting to false)
* `hide_service_names_tags`: true or false (optional, defaulting to false)
* `include_notes`: true or false (optional, defaulting to false)
@@ -1559,6 +1563,38 @@ Response:
]
}
```
+```json title="And where only_return_basic_information is true"
+{
+ "metadata": [
+ {
+ "file_id": 123,
+ "hash": "4c77267f93415de0bc33b7725b8c331a809a924084bee03ab2f5fae1c6019eb2",
+ "size": 63405,
+ "mime": "image/jpg",
+ "ext": ".jpg",
+ "width": 640,
+ "height": 480,
+ "duration": null,
+ "has_audio": false,
+ "num_frames": null,
+ "num_words": null,
+ },
+ {
+ "file_id": 4567,
+ "hash": "3e7cb9044fe81bda0d7a84b5cb781cba4e255e4871cba6ae8ecd8207850d5b82",
+ "size": 199713,
+ "mime": "video/webm",
+ "ext": ".webm",
+ "width": 1920,
+ "height": 1080,
+ "duration": 4040,
+ "has_audio": true,
+ "num_frames": 102,
+ "num_words": null,
+ }
+ ]
+}
+```
Size is in bytes. Duration is in milliseconds, and may be an int or a float.
@@ -1578,6 +1614,8 @@ The tag structure is duplicated for both `name` and `key`. The use of `name` is
While `service_XXX_to_statuses_to_tags` represent the actual tags stored on the database for a file, the service_XXX_to_statuses_to_display_tags
structures reflect how tags appear in the UI, after siblings are collapsed and parents are added. If you want to edit a file's tags, start with `service_keys_to_statuses_to_tags`. If you want to render to the user, use `service_keys_to_statuses_to_displayed_tags`.
+If you set `only_return_basic_information=true`, this will be much faster for first-time requests than the full metadata result, but it will be slower for repeat requests. The full metadata object is cached after first fetch, the limited file info object is not.
+
If you add `hide_service_names_tags=true`, the `service_names_to_statuses_to_tags` and `service_names_to_statuses_to_display_tags` Objects will not be included. Use this to save data/CPU on large queries.
If you add `detailed_url_information=true`, a new entry, `detailed_known_urls`, will be added for each file, with a list of the same structure as /`add_urls/get_url_info`. This may be an expensive request if you are querying thousands of files at once.
diff --git a/docs/getting_started_exporting.md b/docs/getting_started_exporting.md
new file mode 100644
index 00000000..68b13278
--- /dev/null
+++ b/docs/getting_started_exporting.md
@@ -0,0 +1,49 @@
+---
+title: exporting files
+---
+
+# exporting files
+
+## exporting { id="exporting" }
+
+There are many ways to export files from the client:
+
+* **drag and drop**
+
+ Just dragging from the thumbnail view will export (copy) all the selected files to wherever you drop them. You can also start a drag and drop for single files from the media viewer using this arrow button on the top hover window:
+
+ ![](images/media_viewer_dnd.png)
+
+ If you want to drag and drop to discord, check the special BUGFIX option under _options->gui_.
+
+ By default, the files will be named by their ugly hexadecimal [hash](faq.md#hashes), which is how they are stored inside the database. Once you learn filename patterns (practise with manual exports, as below!), you will be able to change this in the options if you wish.
+
+ If you use a drag and drop to open a file inside an image editing program, remember to hit 'save as' and give it a new filename in a new location! The client does not expect files inside its db directory to ever change.
+
+* **share->copy->files**
+
+ This will copy the files themselves to your clipboard. You can then paste them wherever you like, just as with normal files. They will have their hashes for filenames.
+
+ This is a very quick operation. It can also be triggered by hitting Ctrl+C.
+
+* **share->copy->image (bitmap)**
+
+ This copies a file's rendered image data to your clipboard. This is useful for pasting into an image editor, but do not use it to upload images to the internet.
+
+* **share->copy->hashes**
+
+ This will copy the files' unique identifiers to your clipboard, in hexadecimal.
+
+ You will not have to do this often. It is best when you want to identify a number of files to someone else without having to send them the actual files.
+
+* **export dialog**
+
+ Right clicking some files and selecting _share->export->files_ will open this dialog:
+
+ ![](images/export.png)
+
+ Which lets you export the selected files with custom filenames. It will initialise trying to export the files named by their hashes, but once you are comfortable with tags, you'll be able to generate much cleverer and prettier filenames.
+
+* **export folders**
+
+ You can set up a regularly repeating export under _file->import and export folders_. This is an advanced operation, so best left until you know the client a bit better, but it is very useful if you want to regularly export some of your collection to a revolving wallpaper directory or similar.
diff --git a/docs/getting_started_more_files.md b/docs/getting_started_searching.md
similarity index 69%
rename from docs/getting_started_more_files.md
rename to docs/getting_started_searching.md
index d1ac2fa6..a7c09678 100644
--- a/docs/getting_started_more_files.md
+++ b/docs/getting_started_searching.md
@@ -1,8 +1,39 @@
---
-title: more files
+title: powerful searching
---
-# more getting started with files
+# powerful searching
+
+## the dropdown controls
+
+Let's look at the tag autocomplete dropdown again:
+
+![](images/ac_dropdown.png)
+
+* **favourite searches star**
+
+ Once you get experience with the client, have a play with this. Rather than leaving common search pages open, save them in here and load them up as needed. You will keep your client lightweight and save time.
+
+* **include current/pending tags**
+
+ Turn these on and off to control whether tag _search predicates_ apply to tags the exist, or limit just to those pending to be uploaded to a tag repository. Just searching 'pending' tags is useful if you want to scan what you have pending to go up to the PTR--just turn off 'current' tags and search `system:num tags > 0`.
+
+* **searching immediately**
+
+ This controls whether a change to the search tags will instantly run the new search and get new results. Turning this off is helpful if you want to add, remove, or replace several heavy search terms in a row without getting UI lag.
+
+* **OR**
+
+ You only see this if you have 'advanced mode' on. It lets you enter some pretty complicated tags!
+
+* **file/tag domains**
+
+ By default, you will search in 'my files' and 'all known tags' domain. This is the intersection of your local media files (on your hard disk) and the union of all known tag searches. If you search for `character:samus aran`, then you will get file results from your 'my files' domain that have `character:samus aran` in any tag service. For most purposes, this search domain is fine, but as you use the client more, you may want to access different search domains.
+
+ For instance, if you change the file domain to 'trash', then you will instead get files that are in your trash. Setting the tag domain to 'my tags' will ignore other tag services (e.g. the PTR) for all tag search predicates, so a `system:num_tags` or a `character:samus aran` will only look 'my tags'.
+
+ Turning on 'advanced mode' gives access to more search domains. Some of them are subtly complicated and only useful for clever jobs--most of the time, you still want 'my files' and 'all known tags'.
+
## searching with wildcards { id="wildcards" }
@@ -24,36 +55,27 @@ This is particularly useful if you have a number of files with commonly structur
In this case, selecting the `title:cool pic*` predicate will return all three images in the same search, where you can conveniently give them some more-easily searched tags like `series:cool pic` and `page:1`, `page:2`, `page:3`.
-## more searching
+## OR searching
-Let's look at the tag autocomplete dropdown again:
+Searches find files that match every search 'predicate' in the list (it is an **AND** search), which makes it difficult to search for files that include one **OR** another tag. More recently, simple OR search support was added. All you have to do is hold down Shift when you enter/double-click a tag in the autocomplete entry area. Instead of sending the tag up to the active search list up top, it will instead start an under-construction 'OR chain' in the tag results below:
-![](images/ac_dropdown.png)
+![](images/or_under_construction.png)
-* **favourite searches star**
-
- Once you get experience with the client, have a play with this. Rather than leaving common search pages open, save them in here and load them up as needed. You will keep your client lightweight and save time.
-
-* **include current/pending tags**
-
- Turn these on and off to control whether tag _search predicates_ apply to tags the exist, or limit just to those pending to be uploaded to a tag repository. Just searching 'pending' tags is useful if you want to scan what you have pending to go up to the PTR--just turn off 'current' tags and search `system:num tags > 0`.
-
-* **searching immediately**
-
- This controls whether a change to the search tags will instantly run the new search and get new results. Turning this off is helpful if you want to add, remove, or replace several heavy search terms in a row without getting UI lag.
-
-* **OR**
-
- You only see this if you have 'advanced mode' on. It is an experimental module. Have a play with it--it lets you enter some pretty complicated tags!
-
-* **file/tag domains**
-
- By default, you will search in 'my files' and 'all known tags' domain. This is the intersection of your local media files (on your hard disk) and the union of all known tag searches. If you search for `character:samus aran`, then you will get file results from your 'my files' domain that have `character:samus aran` in any tag service. For most purposes, this search domain is fine, but as you use the client more, you may want to access different search domains.
-
- For instance, if you change the file domain to 'trash', then you will instead get files that are in your trash. Setting the tag domain to 'my tags' will ignore other tag services (e.g. the PTR) for all tag search predicates, so a `system:num_tags` or a `character:samus aran` will only look 'my tags'.
-
- Turning on 'advanced mode' gives access to more search domains. Some of them are subtly complicated and only useful for clever jobs--most of the time, you still want 'my files' and 'all known tags'.
-
+You can keep searching for and entering new tags. Holding down Shift on new tags will extend the OR chain, and entering them as normal will 'cap' the chain and send it to the complete and active search predicates above.
+
+![](images/or_done.png)
+
+Any file that has one or more of those OR sub-tags will match.
+
+If you enter an OR tag incorrectly, you can either cancel or 'rewind' the under-construction search predicate with these new buttons that will appear:
+
+![](images/or_buttons.png)
+
+You can also cancel an under-construction OR by hitting Esc on an empty input. You can add any sort of search term to an OR search predicate, including system predicates. Some unusual sub-predicates (typically a `-tag`, or a very broad system predicate) can run very slowly, but they will run much faster if you include non-OR search predicates in the search:
+
+![](images/or_mixed.png)
+
+This search will return all files that have the tag `fanfic` and one or more of `medium:text`, a positive value for the like/dislike rating 'read later', or PDF mime.
## sorting with system limit
@@ -62,35 +84,3 @@ If you add system:limit to a search, the client will consider what that page's f
If you change the sort, hydrus will not refresh the search, it'll just re-sort the n files you have. Hit F5 to refresh the search with a new sort.
Not all sorts are supported. Anything complicated like tag sort will result in a random sample instead.
-
-## exporting and uploading { id="intro" }
-
-There are many ways to export files from the client:
-
-* **drag and drop**
-
- Just dragging from the thumbnail view will export (copy) all the selected files to wherever you drop them.
-
- The files will be named by their ugly hexadecimal [hash](faq.md#hashes), which is how they are stored inside the database.
-
- If you use this to open a file inside an image editing program, remember to go 'save as' and give it a new filename! The client does not expect files inside its db directory to change.
-
-* **export dialog**
-
- Right clicking some files and selecting _share->export->files_ will open this dialog:
-
- ![](images/export.png)
-
- Which lets you export the selected files with custom filenames. It will initialise trying to export the files named by their hashes, but once you are comfortable with tags, you'll be able to generate much cleverer and prettier filenames.
-
-* **share->copy->files**
-
- This will copy the files themselves to your clipboard. You can then paste them wherever you like, just as with normal files. They will have their hashes for filenames.
-
- This is a very quick operation. It can also be triggered by hitting Ctrl+C.
-
-* **share->copy->hashes**
-
- This will copy the files' unique identifiers to your clipboard, in hexadecimal.
-
- You will not have to do this often. It is best when you want to identify a number of files to someone else without having to send them the actual files.
diff --git a/docs/getting_started_subscriptions.md b/docs/getting_started_subscriptions.md
index cc665169..8c66c500 100644
--- a/docs/getting_started_subscriptions.md
+++ b/docs/getting_started_subscriptions.md
@@ -2,7 +2,7 @@
title: subscriptions
---
-# getting started with subscriptions
+# subscriptions
Do not try to create a subscription until you are comfortable with a normal gallery download page! Go [here](getting_started_downloading.md).
@@ -125,4 +125,4 @@ The second case is a safety stopgap for hydrus. If a site decides to have `/post
## I put character queries in my artist sub, and now things are all mixed up { id="merging_and_separating" }
-On the main subscription dialog, there are 'merge' and 'separate' buttons. These are powerful, but they will walk you through the process of pulling queries out of a sub and merging them back into a different one. Only subs that use the same download source can be merged. Give them a go, and if it all goes wrong, just hit the cancel button on the dialog.
\ No newline at end of file
+On the main subscription dialog, there are 'merge' and 'separate' buttons. These are powerful, but they will walk you through the process of pulling queries out of a sub and merging them back into a different one. Only subs that use the same download source can be merged. Give them a go, and if it all goes wrong, just hit the cancel button on the dialog.
diff --git a/docs/getting_started_tags.md b/docs/getting_started_tags.md
index 7c20a56b..97f1d811 100644
--- a/docs/getting_started_tags.md
+++ b/docs/getting_started_tags.md
@@ -41,28 +41,6 @@ If you add more tags or system predicates to a search, you will limit the result
You can also exclude a tag by prefixing it with a hyphen (e.g. `-heresy`).
-## OR searching
-
-Searches find files that match every search 'predicate' in the list (it is an **AND** search), which makes it difficult to search for files that include one **OR** another tag. More recently, simple OR search support was added. All you have to do is hold down Shift when you enter/double-click a tag in the autocomplete entry area. Instead of sending the tag up to the active search list up top, it will instead start an under-construction 'OR chain' in the tag results below:
-
-![](images/or_under_construction.png)
-
-You can keep searching for and entering new tags. Holding down Shift on new tags will extend the OR chain, and entering them as normal will 'cap' the chain and send it to the complete and active search predicates above.
-
-![](images/or_done.png)
-
-Any file that has one or more of those OR sub-tags will match.
-
-If you enter an OR tag incorrectly, you can either cancel or 'rewind' the under-construction search predicate with these new buttons that will appear:
-
-![](images/or_buttons.png)
-
-You can also cancel an under-construction OR by hitting Esc on an empty input. You can add any sort of search term to an OR search predicate, including system predicates. Some unusual sub-predicates (typically a `-tag`, or a very broad system predicate) can run very slowly, but they will run much faster if you include non-OR search predicates in the search:
-
-![](images/or_mixed.png)
-
-This search will return all files that have the tag `fanfic` and one or more of `medium:text`, a positive value for the like/dislike rating 'read later', or PDF mime.
-
## tag repositories
It can take a long time to tag even small numbers of files well, so I created _tag repositories_ so people can share the work.
@@ -93,4 +71,4 @@ I recommend you not spam tags to the public tag repo until you get a rough feel
You can connect to more than one tag repository if you like. When you are in the _manage tags_ dialog, pressing the up or down arrow keys on an empty input switches between your services.
-[FAQ: why can my friend not see what I just uploaded?](faq.md#delays)
\ No newline at end of file
+[FAQ: why can my friend not see what I just uploaded?](faq.md#delays)
diff --git a/docs/images/media_viewer_dnd.png b/docs/images/media_viewer_dnd.png
new file mode 100644
index 00000000..25453ee8
Binary files /dev/null and b/docs/images/media_viewer_dnd.png differ
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index d47b91cf..123672f9 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -33,6 +33,49 @@
+
+
+ - misc:
+ - when shift-selecting some thumbnails, you can now reverse the direction of the select and what you just selected will be deselected, basically a full undo (issue #1105)
+ - when ctrl-selecting thumbnails, if you add to the selection, the file you click is now focused and always previewed (previously this only happened if there was no focused file already). this is related to the shift-select logic above, but it may be annoying when making a big ctrl-selection of videos etc.. so let me know and I can make this more clever if needed
+ - added file sort 'file->hash', which sorts pseudorandomly but repeatably. it sounds not super clever, but it will be useful for certain comparison operations across clients
+ - when you hit 'copy->hash' on a file right-click, it now shows the sha256 hash for quick review
+ - in the duplicate filter, the zoom locking tech now works betterâ„¢ when one of the pair is portrait and the other landscape. it now tries to select either width or height to lock both when going AB and BA. it also chooses the 'better' of width or height by choosing the zoom that'll change the size less radically. previously, it could do width on AB and height on BA, which lead to a variety of odd situations. there are probably still some issues here, most likely when one of the files almost exactly fills the whole canvas, so let me know how you get on
+ - webps with transparency should now load correct! previously they were going crazy in the transparent area. all webps are scheduled a thumbnail regen this week
+ - when import folders run, the count on their progress bar now ignores previous failed and ignored entries. it should always start 0, like 0/100, rather than 20/120 etc...
+ - when import folders run, any imports where the status type is set to 'leave the file alone' is now still scanned at the end of a job. if the path does not exist any more, it is removed from the import list
+ - fixed a typo bug in the recent delete code cleanup that meant 'delete files after export' after a manual export was only working on the last file in the selection. sorry for the trouble!
+ - the delete files dialog now starts with keyboard focus on the action radiobox (it was defaulting to ok button since I added the recent panel disable tech)
+ - if a network job has a connection error or serverside bandwidth block and then waits before retrying, it now checks if all network jobs have just been paused and will not reattempt the connection if so (issue #1095)
+ - fixed a bug in thumbnail fallback rendering
+ - fixed another problem with cloudscraper's new method names. it should work for users still on an old version
+ - wrote a little 'extract version' sql and bat file for the db folder that simply pull the version from the client.db file in the same directory. I removed the extract options/subscriptions sql scripts since they are super old and out of date, but this general system may return in future
+ - .
+ - file history chart:
+ - added 'archive' line to the file history chart. this isn't exactly (current_count - inbox_count), but it pretty much is
+ - added a 'show deleted' checkbox to the file history chart. it will recalculate the y axis range on click, so if you have loads of deleted files, you can now hide them to see current better
+ - improved the way data is aggregated in the file history chart. diagonal lines should be reduced during any periods of client import-inactivity, and spikes should show better
+ - also bumped the number of steps up to 8,000, so it should look nice maximised on a 4k
+ - the file history chart now remembers its last size and position--it has an entry under options->gui
+ - .
+ - client api:
+ - thanks to a user, the Client API now accepts any file_id, file_ids, hash, or hashes as arguments in any place where you need to specify a file or files
+ - like 'return_hashes', the 'search_files' command in the Client API now takes an optional 'return_file_ids' parameter, default true, to turn off the file ids if you only want hashes
+ - added 'only_return_basic_information' parameter, default false, to 'get_metadata' call, which is fast for first-time requests (it is slim but not well cached) and just delivers the basics like resolution and file size
+ - added unit tests and updated the help to reflect the above
+ - client api version is now 29
+ - .
+ - help:
+ - split up the 'more files' help section into 'powerful searching' and 'exporting files', both still under the 'next steps' section
+ - moved the semi-advanced 'OR' section from 'tags' to 'searching'
+ - brushed up misc help
+ - a couple of users added some misc help updates too, thank you!
+ - .
+ - misc boring cleanup:
+ - cleaned up an old wx label patch
+ - cleaned up an old wx system colour patch
+ - cleaned up some misc initialisation code
+
- misc:
diff --git a/hydrus/client/ClientCaches.py b/hydrus/client/ClientCaches.py
index 5bb7e753..18ab8a04 100644
--- a/hydrus/client/ClientCaches.py
+++ b/hydrus/client/ClientCaches.py
@@ -654,8 +654,9 @@ class ThumbnailCache( object ):
thumbnail_mime = HC.IMAGE_JPEG
# we don't actually know this, it comes down to detailed stuff, but since this is png vs jpeg it isn't a huge deal down in the guts of image loading
+ # only really matters with transparency, so anything that can be transparent we'll prime with a png thing
# ain't like I am encoding EXIF rotation in my jpeg thumbs
- if mime in ( HC.IMAGE_APNG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_ICON ):
+ if mime in ( HC.IMAGE_APNG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_ICON, HC.IMAGE_WEBP ):
thumbnail_mime = HC.IMAGE_PNG
diff --git a/hydrus/client/ClientConstants.py b/hydrus/client/ClientConstants.py
index 62f1ac53..c816ad9a 100644
--- a/hydrus/client/ClientConstants.py
+++ b/hydrus/client/ClientConstants.py
@@ -323,6 +323,7 @@ SORT_FILES_BY_NUM_FRAMES = 16
SORT_FILES_BY_NUM_COLLECTION_FILES = 17
SORT_FILES_BY_LAST_VIEWED_TIME = 18
SORT_FILES_BY_ARCHIVED_TIMESTAMP = 19
+SORT_FILES_BY_HASH = 20
SYSTEM_SORT_TYPES = {
SORT_FILES_BY_NUM_COLLECTION_FILES,
@@ -344,7 +345,8 @@ SYSTEM_SORT_TYPES = {
SORT_FILES_BY_IMPORT_TIME,
SORT_FILES_BY_FILE_MODIFIED_TIMESTAMP,
SORT_FILES_BY_LAST_VIEWED_TIME,
- SORT_FILES_BY_ARCHIVED_TIMESTAMP
+ SORT_FILES_BY_ARCHIVED_TIMESTAMP,
+ SORT_FILES_BY_HASH
}
system_sort_type_submetatype_string_lookup = {
@@ -358,6 +360,7 @@ system_sort_type_submetatype_string_lookup = {
SORT_FILES_BY_NUM_FRAMES : 'duration',
SORT_FILES_BY_APPROX_BITRATE : 'file',
SORT_FILES_BY_FILESIZE : 'file',
+ SORT_FILES_BY_HASH : 'file',
SORT_FILES_BY_MIME : 'file',
SORT_FILES_BY_HAS_AUDIO : 'file',
SORT_FILES_BY_RANDOM : None,
@@ -388,6 +391,7 @@ sort_type_basic_string_lookup = {
SORT_FILES_BY_ARCHIVED_TIMESTAMP : 'archived time',
SORT_FILES_BY_LAST_VIEWED_TIME : 'last viewed time',
SORT_FILES_BY_RANDOM : 'random',
+ SORT_FILES_BY_HASH : 'hash',
SORT_FILES_BY_NUM_TAGS : 'number of tags',
SORT_FILES_BY_MEDIA_VIEWS : 'media views',
SORT_FILES_BY_MEDIA_VIEWTIME : 'media viewtime'
diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py
index df0d9b52..7abaf177 100644
--- a/hydrus/client/ClientOptions.py
+++ b/hydrus/client/ClientOptions.py
@@ -631,6 +631,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'frame_locations' ][ 'review_services' ] = ( False, True, None, None, ( -1, -1 ), 'topleft', False, False )
self._dictionary[ 'frame_locations' ][ 'deeply_nested_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'topleft', False, False )
self._dictionary[ 'frame_locations' ][ 'regular_center_dialog' ] = ( False, False, None, None, ( -1, -1 ), 'center', False, False )
+ self._dictionary[ 'frame_locations' ][ 'file_history_chart' ] = ( True, True, ( 960, 720 ), None, ( -1, -1 ), 'topleft', False, False )
#
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 355c51eb..aa2c17ff 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -3999,13 +3999,15 @@ class DB( HydrusDB.HydrusDB ):
since_deleted = self._STL( self._Execute( 'SELECT original_timestamp FROM {} WHERE original_timestamp IS NOT NULL;'.format( deleted_files_table_name ) ) )
- current_timestamps.extend( since_deleted )
+ all_known_import_timestamps = list( current_timestamps )
- current_timestamps.sort()
+ all_known_import_timestamps.extend( since_deleted )
+
+ all_known_import_timestamps.sort()
deleted_timestamps = self._STL( self._Execute( 'SELECT timestamp FROM {} WHERE timestamp IS NOT NULL ORDER BY timestamp ASC;'.format( deleted_files_table_name ) ) )
- combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in current_timestamps ]
+ combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in all_known_import_timestamps ]
combined_timestamps_with_delta.extend( ( ( timestamp, -1 ) for timestamp in deleted_timestamps ) )
combined_timestamps_with_delta.sort()
@@ -4028,11 +4030,11 @@ class DB( HydrusDB.HydrusDB ):
for ( timestamp, delta ) in combined_timestamps_with_delta:
- if timestamp > step_timestamp + step_gap:
+ while timestamp > step_timestamp + step_gap:
current_file_history.append( ( step_timestamp, total_current_files ) )
- step_timestamp = timestamp
+ step_timestamp += step_gap
total_current_files += delta
@@ -4062,11 +4064,11 @@ class DB( HydrusDB.HydrusDB ):
for deleted_timestamp in deleted_timestamps:
- if deleted_timestamp > step_timestamp + step_gap:
+ while deleted_timestamp > step_timestamp + step_gap:
deleted_file_history.append( ( step_timestamp, total_deleted_files ) )
- step_timestamp = deleted_timestamp
+ step_timestamp += step_gap
total_deleted_files += 1
@@ -4080,11 +4082,19 @@ class DB( HydrusDB.HydrusDB ):
# working backwards in time (which reverses increment/decrement):
# an archive increments
# a file import decrements
- # note that we archive right before we delete a file, so file deletes shouldn't change anything. all deletes are on archived files, so the increment will already be counted
+ # note that we archive right before we delete a file, so file deletes shouldn't change anything for inbox count. all deletes are on archived files, so the increment will already be counted
+ # UPDATE: and now we add archived, which is mostly the same deal but we subtract from current files to start and don't care about file imports since they are always inbox but do care about file deletes
inbox_file_history = []
+ archive_file_history = []
( total_inbox_files, ) = self._Execute( 'SELECT COUNT( * ) FROM file_inbox;' ).fetchone()
+ total_current_files = len( current_timestamps )
+
+ total_update_files = self.modules_files_storage.GetCurrentFilesCount( self.modules_services.local_update_service_id, HC.CONTENT_STATUS_CURRENT )
+ total_trash_files = self.modules_files_storage.GetCurrentFilesCount( self.modules_services.trash_service_id, HC.CONTENT_STATUS_CURRENT )
+
+ total_archive_files = ( total_current_files - total_update_files - total_trash_files ) - total_inbox_files
# note also that we do not scrub archived time on a file delete, so this upcoming fetch is for all files ever. this is useful, so don't undo it m8
archive_timestamps = self._STL( self._Execute( 'SELECT archived_timestamp FROM archive_timestamps ORDER BY archived_timestamp ASC;' ) )
@@ -4093,46 +4103,122 @@ class DB( HydrusDB.HydrusDB ):
first_archive_time = archive_timestamps[0]
- combined_timestamps_with_delta = [ ( timestamp, 1 ) for timestamp in archive_timestamps ]
- combined_timestamps_with_delta.extend( ( ( timestamp, -1 ) for timestamp in current_timestamps if timestamp >= first_archive_time ) )
+ combined_timestamps_with_deltas = [ ( timestamp, 1, -1 ) for timestamp in archive_timestamps ]
+ combined_timestamps_with_deltas.extend( ( ( timestamp, -1, 0 ) for timestamp in all_known_import_timestamps if timestamp >= first_archive_time ) )
+ combined_timestamps_with_deltas.extend( ( ( timestamp, 0, 1 ) for timestamp in deleted_timestamps if timestamp >= first_archive_time ) )
- combined_timestamps_with_delta.sort( reverse = True )
+ combined_timestamps_with_deltas.sort( reverse = True )
- if len( combined_timestamps_with_delta ) > 0:
+ if len( combined_timestamps_with_deltas ) > 0:
- if len( combined_timestamps_with_delta ) < 2:
+ if len( combined_timestamps_with_deltas ) < 2:
step_gap = 1
else:
# reversed, so first minus last
- step_gap = max( ( combined_timestamps_with_delta[0][0] - combined_timestamps_with_delta[-1][0] ) // num_steps, 1 )
+ step_gap = max( ( combined_timestamps_with_deltas[0][0] - combined_timestamps_with_deltas[-1][0] ) // num_steps, 1 )
- step_timestamp = combined_timestamps_with_delta[0][0]
+ step_timestamp = combined_timestamps_with_deltas[0][0]
- for ( archived_timestamp, delta ) in combined_timestamps_with_delta:
+ for ( archived_timestamp, inbox_delta, archive_delta ) in combined_timestamps_with_deltas:
- if archived_timestamp < step_timestamp - step_gap:
+ while archived_timestamp < step_timestamp - step_gap:
inbox_file_history.append( ( archived_timestamp, total_inbox_files ) )
+ archive_file_history.append( ( archived_timestamp, total_archive_files ) )
- step_timestamp = archived_timestamp
+ step_timestamp -= step_gap
- total_inbox_files += delta
+ total_inbox_files += inbox_delta
+ total_archive_files += archive_delta
inbox_file_history.reverse()
+ archive_file_history.reverse()
file_history[ 'inbox' ] = inbox_file_history
+ file_history[ 'archive' ] = archive_file_history
return file_history
+ def _GetFileInfoManagers( self, hash_ids: typing.Collection[ int ], sorted = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
+
+ ( cached_media_results, missing_hash_ids ) = self._weakref_media_result_cache.GetMediaResultsAndMissing( hash_ids )
+
+ file_info_managers = [ media_result.GetFileInfoManager() for media_result in cached_media_results ]
+
+ if len( missing_hash_ids ) > 0:
+
+ missing_hash_ids_to_hashes = self.modules_hashes_local_cache.GetHashIdsToHashes( hash_ids = missing_hash_ids )
+
+ with self._MakeTemporaryIntegerTable( missing_hash_ids, 'hash_id' ) as temp_table_name:
+
+ # temp hashes to metadata
+ hash_ids_to_info = { hash_id : ClientMediaManagers.FileInfoManager( hash_id, missing_hash_ids_to_hashes[ hash_id ], size, mime, width, height, duration, num_frames, has_audio, num_words ) for ( hash_id, size, mime, width, height, duration, num_frames, has_audio, num_words ) in self._Execute( 'SELECT * FROM {} CROSS JOIN files_info USING ( hash_id );'.format( temp_table_name ) ) }
+
+
+ # build it
+
+ for hash_id in missing_hash_ids:
+
+ if hash_id in hash_ids_to_info:
+
+ file_info_manager = hash_ids_to_info[ hash_id ]
+
+ else:
+
+ hash = missing_hash_ids_to_hashes[ hash_id ]
+
+ file_info_manager = ClientMediaManagers.FileInfoManager( hash_id, hash )
+
+
+ file_info_managers.append( file_info_manager )
+
+
+
+ if sorted:
+
+ if len( hash_ids ) > len( file_info_managers ):
+
+ hash_ids = HydrusData.DedupeList( hash_ids )
+
+
+ hash_ids_to_file_info_managers = { file_info_manager.hash_id : file_info_manager for file_info_manager in file_info_managers }
+
+ file_info_managers = [ hash_ids_to_file_info_managers[ hash_id ] for hash_id in hash_ids if hash_id in hash_ids_to_file_info_managers ]
+
+
+ return file_info_managers
+
+
+ def _GetFileInfoManagersFromHashes( self, hashes: typing.Collection[ bytes ], sorted: bool = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
+
+ query_hash_ids = set( self.modules_hashes_local_cache.GetHashIds( hashes ) )
+
+ file_info_managers = self._GetFileInfoManagers( query_hash_ids )
+
+ if sorted:
+
+ if len( hashes ) > len( query_hash_ids ):
+
+ hashes = HydrusData.DedupeList( hashes )
+
+
+ hashes_to_file_info_managers = { file_info_manager.hash : file_info_manager for file_info_manager in file_info_managers }
+
+ file_info_managers = [ hashes_to_file_info_managers[ hash ] for hash in hashes if hash in hashes_to_file_info_managers ]
+
+
+ return file_info_managers
+
+
def _GetFileNotes( self, hash ):
hash_id = self.modules_hashes_local_cache.GetHashId( hash )
@@ -10365,6 +10451,8 @@ class DB( HydrusDB.HydrusDB ):
elif action == 'file_duplicate_info': result = self.modules_files_duplicates.DuplicatesGetFileDuplicateInfo( *args, **kwargs )
elif action == 'file_hashes': result = self.modules_hashes.GetFileHashes( *args, **kwargs )
elif action == 'file_history': result = self._GetFileHistory( *args, **kwargs )
+ elif action == 'file_info_managers': result = self._GetFileInfoManagersFromHashes( *args, **kwargs )
+ elif action == 'file_info_managers_from_ids': result = self._GetFileInfoManagers( *args, **kwargs )
elif action == 'file_maintenance_get_job': result = self.modules_files_maintenance_queue.GetJob( *args, **kwargs )
elif action == 'file_maintenance_get_job_counts': result = self.modules_files_maintenance_queue.GetJobCounts( *args, **kwargs )
elif action == 'file_query_ids': result = self._GetHashIdsFromQuery( *args, **kwargs )
@@ -12199,6 +12287,16 @@ class DB( HydrusDB.HydrusDB ):
did_sort = True
+ elif sort_data == CC.SORT_FILES_BY_HASH:
+
+ hash_ids_to_hashes = self.modules_hashes_local_cache.GetHashIdsToHashes( hash_ids = hash_ids )
+
+ hash_ids_to_hex_hashes = { hash_id : hash.hex() for ( hash_id, hash ) in hash_ids_to_hashes }
+
+ hash_ids = sorted( hash_ids, key = lambda hash_id: hash_ids_to_hex_hashes[ hash_id ] )
+
+ did_sort = True
+
if query is not None:
@@ -14183,6 +14281,29 @@ class DB( HydrusDB.HydrusDB ):
self.pub_initial_message( message )
+
+ if version == 478:
+
+ try:
+
+ # transparent webp regen
+
+ table_join = self.modules_files_storage.GetTableJoinLimitedByFileDomain( self.modules_services.combined_local_file_service_id, 'files_info', HC.CONTENT_STATUS_CURRENT )
+
+ hash_ids = self._STL( self._Execute( 'SELECT hash_id FROM {} WHERE mime = ?;'.format( table_join ), ( HC.IMAGE_WEBP, ) ) )
+
+ self.modules_files_maintenance_queue.AddJobs( hash_ids, ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL )
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Some webp regen scheduling failed to set! This is not super important, but hydev would be interested in seeing the error that was printed to the log.'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index d0ff0545..65a65fbf 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -1740,7 +1740,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
service = self._controller.services_manager.GetService( service_key )
- with QP.BusyCursor(): response = service.Request( HC.GET, 'ip', { 'hash' : hash } )
+ with ClientGUICommon.BusyCursor(): response = service.Request( HC.GET, 'ip', { 'hash' : hash } )
ip = response[ 'ip' ]
timestamp = response[ 'timestamp' ]
@@ -2276,7 +2276,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
HG.client_controller.pub( 'message', job_key )
- num_steps = 2000
+ num_steps = 7680
file_history = HG.client_controller.Read( 'file_history', num_steps )
@@ -2289,7 +2289,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
job_key.Delete()
- frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'file history' )
+ frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'file history', frame_key = 'file_history_chart' )
panel = ClientGUIScrolledPanelsReview.ReviewFileHistory( frame, file_history )
diff --git a/hydrus/client/gui/ClientGUIApplicationCommand.py b/hydrus/client/gui/ClientGUIApplicationCommand.py
index 498643d6..41caa4f2 100644
--- a/hydrus/client/gui/ClientGUIApplicationCommand.py
+++ b/hydrus/client/gui/ClientGUIApplicationCommand.py
@@ -365,8 +365,8 @@ class SimpleSubPanel( QW.QWidget ):
self._seek_direction = ClientGUICommon.BetterRadioBox( self._seek_panel, choices = choices )
- self._seek_duration_s = QP.MakeQSpinBox( self._seek_panel, max=3599, width = 60 )
- self._seek_duration_ms = QP.MakeQSpinBox( self._seek_panel, max=999, width = 60 )
+ self._seek_duration_s = ClientGUICommon.BetterSpinBox( self._seek_panel, max=3599, width = 60 )
+ self._seek_duration_ms = ClientGUICommon.BetterSpinBox( self._seek_panel, max=999, width = 60 )
#
diff --git a/hydrus/client/gui/ClientGUICharts.py b/hydrus/client/gui/ClientGUICharts.py
index fde3b996..7e04245f 100644
--- a/hydrus/client/gui/ClientGUICharts.py
+++ b/hydrus/client/gui/ClientGUICharts.py
@@ -76,79 +76,140 @@ try:
QCh.QtCharts.QChartView.__init__( self, parent )
+ self._file_history = file_history
+ self._show_deleted = True
+
# this lad takes ms timestamp, not s, so * 1000
# note you have to give this floats for the ms or it throws a type problem of big number to C long
- current_files_series = QCh.QtCharts.QLineSeries()
+ self._current_files_series = QCh.QtCharts.QLineSeries()
- current_files_series.setName( 'files in storage' )
+ self._current_files_series.setName( 'files in storage' )
- max_num_files = 0
+ self._max_num_files_current = 0
- for ( timestamp, num_files ) in file_history[ 'current' ]:
+ for ( timestamp, num_files ) in self._file_history[ 'current' ]:
- current_files_series.append( timestamp * 1000.0, num_files )
+ self._current_files_series.append( timestamp * 1000.0, num_files )
- max_num_files = max( max_num_files, num_files )
+ self._max_num_files_current = max( self._max_num_files_current, num_files )
- deleted_files_series = QCh.QtCharts.QLineSeries()
+ #
- deleted_files_series.setName( 'deleted' )
+ self._deleted_files_series = QCh.QtCharts.QLineSeries()
- for ( timestamp, num_files ) in file_history[ 'deleted' ]:
+ self._deleted_files_series.setName( 'deleted' )
+
+ self._max_num_files_deleted = 0
+
+ for ( timestamp, num_files ) in self._file_history[ 'deleted' ]:
- deleted_files_series.append( timestamp * 1000.0, num_files )
+ self._deleted_files_series.append( timestamp * 1000.0, num_files )
- max_num_files = max( max_num_files, num_files )
+ self._max_num_files_deleted = max( self._max_num_files_deleted, num_files )
- inbox_files_series = QCh.QtCharts.QLineSeries()
+ #
- inbox_files_series.setName( 'inbox' )
+ self._inbox_files_series = QCh.QtCharts.QLineSeries()
- for ( timestamp, num_files ) in file_history[ 'inbox' ]:
+ self._inbox_files_series.setName( 'inbox' )
+
+ self._max_num_files_inbox = 0
+
+ for ( timestamp, num_files ) in self._file_history[ 'inbox' ]:
- inbox_files_series.append( timestamp * 1000.0, num_files )
+ self._inbox_files_series.append( timestamp * 1000.0, num_files )
- max_num_files = max( max_num_files, num_files )
+ self._max_num_files_inbox = max( self._max_num_files_inbox, num_files )
+
+
+ #
+
+ self._archive_files_series = QCh.QtCharts.QLineSeries()
+
+ self._archive_files_series.setName( 'archive' )
+
+ self._max_num_files_archive = 0
+
+ for ( timestamp, num_files ) in self._file_history[ 'archive' ]:
+
+ self._archive_files_series.append( timestamp * 1000.0, num_files )
+
+ self._max_num_files_archive = max( self._max_num_files_archive, num_files )
# takes ms since epoch
- x_datetime_axis = QCh.QtCharts.QDateTimeAxis()
+ self._x_datetime_axis = QCh.QtCharts.QDateTimeAxis()
- x_datetime_axis.setTickCount( 25 )
- x_datetime_axis.setLabelsAngle( 90 )
+ self._x_datetime_axis.setTickCount( 25 )
+ self._x_datetime_axis.setLabelsAngle( 90 )
- x_datetime_axis.setFormat( 'yyyy-MM-dd' )
+ self._x_datetime_axis.setFormat( 'yyyy-MM-dd' )
- y_value_axis = QCh.QtCharts.QValueAxis()
+ self._y_value_axis = QCh.QtCharts.QValueAxis()
- y_value_axis.setLabelFormat( '%\'i' )
+ self._y_value_axis.setLabelFormat( '%\'i' )
- chart = QCh.QtCharts.QChart()
+ self._chart = QCh.QtCharts.QChart()
- chart.addSeries( current_files_series )
- chart.addSeries( inbox_files_series )
- chart.addSeries( deleted_files_series )
+ self._chart.addSeries( self._current_files_series )
+ self._chart.addSeries( self._inbox_files_series )
+ self._chart.addSeries( self._archive_files_series )
+ self._chart.addSeries( self._deleted_files_series )
- chart.addAxis( x_datetime_axis, QC.Qt.AlignBottom )
- chart.addAxis( y_value_axis, QC.Qt.AlignLeft )
+ self._chart.addAxis( self._x_datetime_axis, QC.Qt.AlignBottom )
+ self._chart.addAxis( self._y_value_axis, QC.Qt.AlignLeft )
- current_files_series.attachAxis( x_datetime_axis )
- current_files_series.attachAxis( y_value_axis )
+ self._current_files_series.attachAxis( self._x_datetime_axis )
+ self._current_files_series.attachAxis( self._y_value_axis )
- deleted_files_series.attachAxis( x_datetime_axis )
- deleted_files_series.attachAxis( y_value_axis )
+ self._deleted_files_series.attachAxis( self._x_datetime_axis )
+ self._deleted_files_series.attachAxis( self._y_value_axis )
- inbox_files_series.attachAxis( x_datetime_axis )
- inbox_files_series.attachAxis( y_value_axis )
+ self._inbox_files_series.attachAxis( self._x_datetime_axis )
+ self._inbox_files_series.attachAxis( self._y_value_axis )
- y_value_axis.setRange( 0, max_num_files )
+ self._archive_files_series.attachAxis( self._x_datetime_axis )
+ self._archive_files_series.attachAxis( self._y_value_axis )
- y_value_axis.applyNiceNumbers()
+ self._CalculateYRange()
- self.setChart( chart )
+ self.setChart( self._chart )
+
+
+ def _CalculateYRange( self ):
+
+ max_num_files = max( self._max_num_files_current, self._max_num_files_inbox, self._max_num_files_archive )
+
+ if self._show_deleted:
+
+ max_num_files = max( self._max_num_files_deleted, max_num_files )
+
+
+ self._y_value_axis.setRange( 0, max_num_files )
+
+ self._y_value_axis.applyNiceNumbers()
+
+
+ def FlipDeletedVisible( self ):
+
+ self._show_deleted = not self._show_deleted
+
+ if self._show_deleted:
+
+ self._chart.addSeries( self._deleted_files_series )
+
+ self._deleted_files_series.attachAxis( self._x_datetime_axis )
+ self._deleted_files_series.attachAxis( self._y_value_axis )
+
+ else:
+
+ self._chart.removeSeries( self._deleted_files_series )
+
+
+ self._CalculateYRange()
diff --git a/hydrus/client/gui/ClientGUIDialogs.py b/hydrus/client/gui/ClientGUIDialogs.py
index e4fc506d..ad0de908 100644
--- a/hydrus/client/gui/ClientGUIDialogs.py
+++ b/hydrus/client/gui/ClientGUIDialogs.py
@@ -101,7 +101,12 @@ class DialogChooseNewServiceMethod( Dialog ):
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._register, CC.FLAGS_EXPAND_PERPENDICULAR )
- QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment('-or-', self, QC.Qt.AlignHCenter | QC.Qt.AlignVCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ st = ClientGUICommon.BetterStaticText( self, '-or-' )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._setup, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
@@ -135,7 +140,7 @@ class DialogGenerateNewAccounts( Dialog ):
self._service_key = service_key
- self._num = QP.MakeQSpinBox( self, min=1, max=10000, width = 80 )
+ self._num = ClientGUICommon.BetterSpinBox( self, min=1, max=10000, width = 80 )
self._account_types = ClientGUICommon.BetterChoice( self )
@@ -586,15 +591,15 @@ class DialogInputUPnPMapping( Dialog ):
Dialog.__init__( self, parent, 'configure upnp mapping' )
- self._external_port = QP.MakeQSpinBox( self, min=0, max=65535 )
+ self._external_port = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
self._protocol_type = ClientGUICommon.BetterChoice( self )
self._protocol_type.addItem( 'TCP', 'TCP' )
self._protocol_type.addItem( 'UDP', 'UDP' )
- self._internal_port = QP.MakeQSpinBox( self, min=0, max=65535 )
+ self._internal_port = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
self._description = QW.QLineEdit( self )
- self._duration = QP.MakeQSpinBox( self, min=0, max=86400 )
+ self._duration = ClientGUICommon.BetterSpinBox( self, min=0, max=86400 )
self._ok = ClientGUICommon.BetterButton( self, 'OK', self.done, QW.QDialog.Accepted )
self._ok.setObjectName( 'HydrusAccept' )
diff --git a/hydrus/client/gui/ClientGUIDownloaders.py b/hydrus/client/gui/ClientGUIDownloaders.py
index 41aef7d9..cfdaba6d 100644
--- a/hydrus/client/gui/ClientGUIDownloaders.py
+++ b/hydrus/client/gui/ClientGUIDownloaders.py
@@ -1091,7 +1091,7 @@ class EditURLClassPanel( ClientGUIScrolledPanels.EditPanel ):
self._next_gallery_page_choice = ClientGUICommon.BetterChoice( self._next_gallery_page_panel )
- self._next_gallery_page_delta = QP.MakeQSpinBox( self._next_gallery_page_panel, min=1, max=65536 )
+ self._next_gallery_page_delta = ClientGUICommon.BetterSpinBox( self._next_gallery_page_panel, min=1, max=65536 )
self._next_gallery_page_url = QW.QLineEdit( self._next_gallery_page_panel )
self._next_gallery_page_url.setReadOnly( True )
diff --git a/hydrus/client/gui/ClientGUIExport.py b/hydrus/client/gui/ClientGUIExport.py
index 3ed65ea8..c3bcb71e 100644
--- a/hydrus/client/gui/ClientGUIExport.py
+++ b/hydrus/client/gui/ClientGUIExport.py
@@ -920,7 +920,7 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
for service_key in media.GetLocationsManager().GetCurrent():
- service_keys_to_hashes[ service_key ].add( hash )
+ service_keys_to_hashes[ service_key ].add( media.GetHash() )
diff --git a/hydrus/client/gui/ClientGUIImport.py b/hydrus/client/gui/ClientGUIImport.py
index e235a881..6e131039 100644
--- a/hydrus/client/gui/ClientGUIImport.py
+++ b/hydrus/client/gui/ClientGUIImport.py
@@ -267,11 +267,11 @@ class FilenameTaggingOptionsPanel( QW.QWidget ):
self._num_panel = ClientGUICommon.StaticBox( self, '#' )
- self._num_base = QP.MakeQSpinBox( self._num_panel, min=-10000000, max=10000000, width = 60 )
+ self._num_base = ClientGUICommon.BetterSpinBox( self._num_panel, min=-10000000, max=10000000, width = 60 )
self._num_base.setValue( 1 )
self._num_base.valueChanged.connect( self.tagsChanged )
- self._num_step = QP.MakeQSpinBox( self._num_panel, min=-1000000, max=1000000, width = 60 )
+ self._num_step = ClientGUICommon.BetterSpinBox( self._num_panel, min=-1000000, max=1000000, width = 60 )
self._num_step.setValue( 1 )
self._num_step.valueChanged.connect( self.tagsChanged )
@@ -1112,7 +1112,7 @@ class EditImportFolderPanel( ClientGUIScrolledPanels.EditPanel ):
rows = []
- rows.append( ( 'mimes to import: ', self._mimes ) )
+ rows.append( ( 'file types to import: ', self._mimes ) )
mimes_gridbox = ClientGUICommon.WrapInGrid( self._file_box, rows, expand_text = True )
diff --git a/hydrus/client/gui/ClientGUIParsing.py b/hydrus/client/gui/ClientGUIParsing.py
index f2419cf0..b8a99631 100644
--- a/hydrus/client/gui/ClientGUIParsing.py
+++ b/hydrus/client/gui/ClientGUIParsing.py
@@ -1021,7 +1021,7 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._tag_index = ClientGUICommon.NoneableSpinCtrl( self, 'index to fetch', none_phrase = 'get all', min = -65536, max = 65535 )
self._tag_index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
- self._tag_depth = QP.MakeQSpinBox( self, min=1, max=255 )
+ self._tag_depth = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
self._should_test_tag_string = QW.QCheckBox( self )
@@ -1452,7 +1452,7 @@ class EditJSONParsingRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._string_match = ClientGUIStringPanels.EditStringMatchPanel( self, string_match )
- self._index = QP.MakeQSpinBox( self, min=-65536, max=65535 )
+ self._index = ClientGUICommon.BetterSpinBox( self, min=-65536, max=65535 )
self._index.setToolTip( 'You can make this negative to do negative indexing, i.e. "Select the second from last item".' )
#
@@ -1825,7 +1825,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._url_type.addItem( 'GALLERY parsers only: next gallery page (not queued if no post/file urls found)', HC.URL_TYPE_NEXT )
self._url_type.addItem( 'GALLERY parsers only: sub-gallery page (is queued even if no post/file urls found--be careful, only use if you know you need it)', HC.URL_TYPE_SUB_GALLERY )
- self._file_priority = QP.MakeQSpinBox( self._urls_panel, min=0, max=100 )
+ self._file_priority = ClientGUICommon.BetterSpinBox( self._urls_panel, min=0, max=100 )
self._file_priority.setValue( 50 )
self._mappings_panel = QW.QWidget( self._content_panel )
@@ -1856,7 +1856,7 @@ class EditContentParserPanel( ClientGUIScrolledPanels.EditPanel ):
self._title_panel = QW.QWidget( self._content_panel )
- self._title_priority = QP.MakeQSpinBox( self._title_panel, min=0, max=100 )
+ self._title_priority = ClientGUICommon.BetterSpinBox( self._title_panel, min=0, max=100 )
self._title_priority.setValue( 50 )
self._veto_panel = QW.QWidget( self._content_panel )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
index 39495911..738f1943 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
@@ -22,9 +22,18 @@ class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.Resizing
vbox = QP.VBoxLayout()
- QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( label, self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._commit, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( '-or-', self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ st = ClientGUICommon.BetterStaticText( self, '-or-' )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._back, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
@@ -59,9 +68,18 @@ class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPane
vbox = QP.VBoxLayout()
- QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( label, self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- QP.AddToLayout( vbox, QP.MakeQLabelWithAlignment( '-or-', self, QC.Qt.AlignVCenter | QC.Qt.AlignHCenter ), CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ st = ClientGUICommon.BetterStaticText( self, '-or-' )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._back, CC.FLAGS_EXPAND_PERPENDICULAR )
self.widget().setLayout( vbox )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
index c6f23140..a635a00c 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsEdit.py
@@ -531,6 +531,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
self.widget().setLayout( vbox )
+ QP.CallAfter( self._SetFocus )
+
def _FilterForDeleteLock( self, media, suggested_file_service_key: bytes ):
@@ -723,6 +725,18 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
+ def _SetFocus( self ):
+
+ if self._action_radio.isVisible():
+
+ self._action_radio.setFocus( QC.Qt.OtherFocusReason )
+
+ elif self._reason_panel.isVisible() and self._reason_panel.isEnabled():
+
+ self._reason_radio.setFocus( QC.Qt.OtherFocusReason )
+
+
+
def _UpdateControls( self ):
( file_service_key, hashes, description ) = self._action_radio.GetValue()
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index 1e378570..0e53d2d5 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -303,19 +303,19 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
max_network_jobs_per_domain_max = 5
- self._network_timeout = QP.MakeQSpinBox( general, min = network_timeout_min, max = network_timeout_max )
+ self._network_timeout = ClientGUICommon.BetterSpinBox( general, min = network_timeout_min, max = network_timeout_max )
self._network_timeout.setToolTip( 'If a network connection cannot be made in this duration or, if once started, it experiences uninterrupted inactivity for six times this duration, it will be abandoned.' )
- self._connection_error_wait_time = QP.MakeQSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
+ self._connection_error_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._connection_error_wait_time.setToolTip( 'If a network connection times out as above, it will wait increasing multiples of this base time before retrying.' )
- self._serverside_bandwidth_wait_time = QP.MakeQSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
+ self._serverside_bandwidth_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
self._serverside_bandwidth_wait_time.setToolTip( 'If a server returns a failure status code indicating it is short on bandwidth, the network job will wait increasing multiples of this base time before retrying.' )
self._domain_network_infrastructure_error_velocity = ClientGUITime.VelocityCtrl( general, 0, 100, 30, hours = True, minutes = True, seconds = True, per_phrase = 'within', unit = 'errors' )
- self._max_network_jobs = QP.MakeQSpinBox( general, min = 1, max = max_network_jobs_max )
- self._max_network_jobs_per_domain = QP.MakeQSpinBox( general, min = 1, max = max_network_jobs_per_domain_max )
+ self._max_network_jobs = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_max )
+ self._max_network_jobs_per_domain = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_per_domain_max )
#
@@ -453,7 +453,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._default_gug = ClientGUIImport.GUGKeyAndNameSelector( gallery_downloader, gug_key_and_name )
- self._gallery_page_wait_period_pages = QP.MakeQSpinBox( gallery_downloader, min=1, max=120 )
+ self._gallery_page_wait_period_pages = ClientGUICommon.BetterSpinBox( gallery_downloader, min=1, max=120 )
self._gallery_file_limit = ClientGUICommon.NoneableSpinCtrl( gallery_downloader, none_phrase = 'no limit', min = 1, max = 1000000 )
self._highlight_new_query = QW.QCheckBox( gallery_downloader )
@@ -462,8 +462,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
subscriptions = ClientGUICommon.StaticBox( self, 'subscriptions' )
- self._gallery_page_wait_period_subscriptions = QP.MakeQSpinBox( subscriptions, min=1, max=30 )
- self._max_simultaneous_subscriptions = QP.MakeQSpinBox( subscriptions, min=1, max=100 )
+ self._gallery_page_wait_period_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=30 )
+ self._max_simultaneous_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=100 )
self._subscription_file_error_cancel_threshold = ClientGUICommon.NoneableSpinCtrl( subscriptions, min = 1, max = 1000000, unit = 'errors' )
self._subscription_file_error_cancel_threshold.setToolTip( 'This is a simple patch and will be replaced with a better "retry network errors later" system at some point, but is useful to increase if you have subs to unreliable websites.' )
@@ -479,7 +479,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
watchers = ClientGUICommon.StaticBox( self, 'watchers' )
- self._watcher_page_wait_period = QP.MakeQSpinBox( watchers, min=1, max=120 )
+ self._watcher_page_wait_period = ClientGUICommon.BetterSpinBox( watchers, min=1, max=120 )
self._highlight_new_watcher = QW.QCheckBox( watchers )
checker_options = self._new_options.GetDefaultWatcherCheckerOptions()
@@ -659,19 +659,19 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
weights_panel = ClientGUICommon.StaticBox( self, 'duplicate filter comparison score weights' )
- self._duplicate_comparison_score_higher_jpeg_quality = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_much_higher_jpeg_quality = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_higher_filesize = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_much_higher_filesize = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_higher_resolution = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_much_higher_resolution = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_more_tags = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_older = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
- self._duplicate_comparison_score_nicer_ratio = QP.MakeQSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_much_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_much_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_much_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_more_tags = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_older = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
+ self._duplicate_comparison_score_nicer_ratio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
self._duplicate_comparison_score_nicer_ratio.setToolTip( 'For instance, 16:9 vs 640:357.')
- self._duplicate_filter_max_batch_size = QP.MakeQSpinBox( self, min = 10, max = 1024 )
+ self._duplicate_filter_max_batch_size = ClientGUICommon.BetterSpinBox( self, min = 10, max = 1024 )
#
@@ -1200,7 +1200,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
tt = 'In many places across the program (typically import status lists), the client will state a timestamp as "5 days ago". If you would prefer a standard ISO string, like "2018-03-01 12:40:23", check this.'
self._always_show_iso_time.setToolTip( tt )
- self._human_bytes_sig_figs = QP.MakeQSpinBox( self._misc_panel, min = 1, max = 6 )
+ self._human_bytes_sig_figs = ClientGUICommon.BetterSpinBox( self._misc_panel, min = 1, max = 6 )
self._human_bytes_sig_figs.setToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.')
self._discord_dnd_fix = QW.QCheckBox( self._misc_panel )
@@ -1384,13 +1384,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._default_gui_session = QW.QComboBox( self._sessions_panel )
- self._last_session_save_period_minutes = QP.MakeQSpinBox( self._sessions_panel, min = 1, max = 1440 )
+ self._last_session_save_period_minutes = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 1440 )
self._only_save_last_session_during_idle = QW.QCheckBox( self._sessions_panel )
self._only_save_last_session_during_idle.setToolTip( 'This is useful if you usually have a very large session (200,000+ files/import items open) and a client that is always on.' )
- self._number_of_gui_session_backups = QP.MakeQSpinBox( self._sessions_panel, min = 1, max = 32 )
+ self._number_of_gui_session_backups = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 32 )
self._number_of_gui_session_backups.setToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' )
@@ -1416,7 +1416,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._notebook_tab_alignment.addItem( CC.directions_alignment_string_lookup[ value ], value )
- self._total_pages_warning = QP.MakeQSpinBox( self._pages_panel, min=5, max=65565 )
+ self._total_pages_warning = ClientGUICommon.BetterSpinBox( self._pages_panel, min=5, max=65565 )
tt = 'If you have a gigantic session, or you have very page-spammy subscriptions, you can try boosting this, but be warned it may lead to resource limit crashes. The best solution to a large session is to make it smaller!'
@@ -1433,7 +1433,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._page_names_panel = ClientGUICommon.StaticBox( self._pages_panel, 'page tab names' )
- self._max_page_name_chars = QP.MakeQSpinBox( self._page_names_panel, min=1, max=256 )
+ self._max_page_name_chars = ClientGUICommon.BetterSpinBox( self._page_names_panel, min=1, max=256 )
self._elide_page_tab_names = QW.QCheckBox( self._page_names_panel )
self._page_file_count_display = ClientGUICommon.BetterChoice( self._page_names_panel )
@@ -1686,7 +1686,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._idle_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore normal browsing' )
self._idle_mouse_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore mouse movements' )
self._idle_mode_client_api_timeout = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore client api' )
- self._system_busy_cpu_percent = QP.MakeQSpinBox( self._idle_panel, min = 5, max = 99 )
+ self._system_busy_cpu_percent = ClientGUICommon.BetterSpinBox( self._idle_panel, min = 5, max = 99 )
self._system_busy_cpu_count = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, min = 1, max = 64, unit = 'cores', none_phrase = 'ignore cpu usage' )
#
@@ -1700,7 +1700,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._idle_shutdown.currentIndexChanged.connect( self._EnableDisableIdleShutdown )
- self._idle_shutdown_max_minutes = QP.MakeQSpinBox( self._shutdown_panel, min=1, max=1440 )
+ self._idle_shutdown_max_minutes = ClientGUICommon.BetterSpinBox( self._shutdown_panel, min=1, max=1440 )
self._shutdown_work_period = ClientGUITime.TimeDeltaButton( self._shutdown_panel, min = 60, days = True, hours = True, minutes = True )
#
@@ -1921,7 +1921,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = HG.client_controller.new_options
- self._animation_start_position = QP.MakeQSpinBox( self, min=0, max=100 )
+ self._animation_start_position = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
self._disable_cv_for_gifs = QW.QCheckBox( self )
self._disable_cv_for_gifs.setToolTip( 'OpenCV is good at rendering gifs, but if you have problems with it and your graphics card, check this and the less reliable and slower PIL will be used instead. EDIT: OpenCV is much better these days--this is mostly not needed.' )
@@ -1958,8 +1958,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._mpv_conf_path = QP.FilePickerCtrl( self, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
- self._animated_scanbar_height = QP.MakeQSpinBox( self, min=1, max=255 )
- self._animated_scanbar_nub_width = QP.MakeQSpinBox( self, min=1, max=63 )
+ self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
+ self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( self, min=1, max=63 )
self._media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer filetype handling' )
@@ -2320,7 +2320,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._popup_panel = ClientGUICommon.StaticBox( self, 'popup window toaster' )
- self._popup_message_character_width = QP.MakeQSpinBox( self._popup_panel, min = 16, max = 256 )
+ self._popup_message_character_width = ClientGUICommon.BetterSpinBox( self._popup_panel, min = 16, max = 256 )
self._popup_message_force_min_width = QW.QCheckBox( self._popup_panel )
@@ -2442,11 +2442,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
tt = 'The autocomplete dropdown can either \'float\' on top of dialogs like _manage tags_, or if that does not work well for you (it can sometimes annoyingly overlap the ok/cancel buttons), it can embed into the parent dialog panel.'
self._autocomplete_float_frames.setToolTip( tt )
- self._ac_read_list_height_num_chars = QP.MakeQSpinBox( self._autocomplete_panel, min = 1, max = 128 )
+ self._ac_read_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._autocomplete_panel, min = 1, max = 128 )
tt = 'Read autocompletes are those in search pages, where you are looking through existing tags to find your files.'
self._ac_read_list_height_num_chars.setToolTip( tt )
- self._ac_write_list_height_num_chars = QP.MakeQSpinBox( self._autocomplete_panel, min = 1, max = 128 )
+ self._ac_write_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._autocomplete_panel, min = 1, max = 128 )
tt = 'Write autocompletes are those in most dialogs, where you are adding new tags to files.'
self._ac_write_list_height_num_chars.setToolTip( tt )
@@ -2664,7 +2664,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
thumbnail_cache_panel = ClientGUICommon.StaticBox( self, 'thumbnail cache' )
- self._thumbnail_cache_size = QP.MakeQSpinBox( thumbnail_cache_panel, min=5, max=3000 )
+ self._thumbnail_cache_size = ClientGUICommon.BetterSpinBox( thumbnail_cache_panel, min=5, max=3000 )
self._thumbnail_cache_size.valueChanged.connect( self.EventThumbnailsUpdate )
self._estimated_number_thumbnails = QW.QLabel( '', thumbnail_cache_panel )
@@ -2674,7 +2674,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
image_cache_panel = ClientGUICommon.StaticBox( self, 'image cache' )
- self._fullscreen_cache_size = QP.MakeQSpinBox( image_cache_panel, min=25, max=8192 )
+ self._fullscreen_cache_size = ClientGUICommon.BetterSpinBox( image_cache_panel, min=25, max=8192 )
self._fullscreen_cache_size.valueChanged.connect( self.EventImageCacheUpdate )
self._estimated_number_fullscreens = QW.QLabel( '', image_cache_panel )
@@ -2682,18 +2682,18 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._image_cache_timeout = ClientGUITime.TimeDeltaButton( image_cache_panel, min = 300, days = True, hours = True, minutes = True )
self._image_cache_timeout.setToolTip( 'The amount of time after which a rendered image in the cache will naturally be removed, if it is not shunted out due to a new member exceeding the size limit.' )
- self._media_viewer_prefetch_delay_base_ms = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 2000 )
+ self._media_viewer_prefetch_delay_base_ms = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 2000 )
tt = 'How long to wait, after the current image is rendered, to start rendering neighbours. Does not matter so much any more, but if you have CPU lag, you can try boosting it a bit.'
self._media_viewer_prefetch_delay_base_ms.setToolTip( tt )
- self._media_viewer_prefetch_num_previous = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 5 )
- self._media_viewer_prefetch_num_next = QP.MakeQSpinBox( image_cache_panel, min = 0, max = 5 )
+ self._media_viewer_prefetch_num_previous = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 5 )
+ self._media_viewer_prefetch_num_next = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 5 )
- self._image_cache_storage_limit_percentage = QP.MakeQSpinBox( image_cache_panel, min = 20, max = 50 )
+ self._image_cache_storage_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 20, max = 50 )
self._image_cache_storage_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
- self._image_cache_prefetch_limit_percentage = QP.MakeQSpinBox( image_cache_panel, min = 5, max = 20 )
+ self._image_cache_prefetch_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 5, max = 20 )
self._image_cache_prefetch_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
@@ -2707,14 +2707,14 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._image_tile_cache_timeout = ClientGUITime.TimeDeltaButton( image_tile_cache_panel, min = 300, hours = True, minutes = True )
self._image_tile_cache_timeout.setToolTip( 'The amount of time after which a rendered image tile in the cache will naturally be removed, if it is not shunted out due to a new member exceeding the size limit.' )
- self._ideal_tile_dimension = QP.MakeQSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
+ self._ideal_tile_dimension = ClientGUICommon.BetterSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
self._ideal_tile_dimension.setToolTip( 'This is the square size the system will aim for. Smaller tiles are more memory efficient but prone to warping and other artifacts. Extreme values may waste CPU.' )
#
buffer_panel = ClientGUICommon.StaticBox( self, 'video buffer' )
- self._video_buffer_size_mb = QP.MakeQSpinBox( buffer_panel, min=48, max=16*1024 )
+ self._video_buffer_size_mb = ClientGUICommon.BetterSpinBox( buffer_panel, min=48, max= 16 * 1024 )
self._video_buffer_size_mb.valueChanged.connect( self.EventVideoBufferUpdate )
self._estimated_number_video_frames = QW.QLabel( '', buffer_panel )
@@ -3138,7 +3138,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
sleep_panel = ClientGUICommon.StaticBox( self, 'system sleep' )
- self._wake_delay_period = QP.MakeQSpinBox( sleep_panel, min = 0, max = 60 )
+ self._wake_delay_period = ClientGUICommon.BetterSpinBox( sleep_panel, min = 0, max = 60 )
tt = 'It sometimes takes a few seconds for your network adapter to reconnect after a wake. This adds a grace period after a detected wake-from-sleep to allow your OS to sort that out before Hydrus starts making requests.'
@@ -3554,7 +3554,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
suggested_tags_panel = ClientGUICommon.StaticBox( self, 'suggested tags' )
- self._suggested_tags_width = QP.MakeQSpinBox( suggested_tags_panel, min=20, max=65535 )
+ self._suggested_tags_width = ClientGUICommon.BetterSpinBox( suggested_tags_panel, min=20, max=65535 )
self._suggested_tags_layout = ClientGUICommon.BetterChoice( suggested_tags_panel )
@@ -3594,9 +3594,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._show_related_tags = QW.QCheckBox( suggested_tags_related_panel )
- self._related_tags_search_1_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
- self._related_tags_search_2_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
- self._related_tags_search_3_duration_ms = QP.MakeQSpinBox( suggested_tags_related_panel, min=50, max=60000 )
+ self._related_tags_search_1_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
+ self._related_tags_search_2_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
+ self._related_tags_search_3_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
#
@@ -3793,15 +3793,15 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options = new_options
- self._thumbnail_width = QP.MakeQSpinBox( self, min=20, max=2048 )
- self._thumbnail_height = QP.MakeQSpinBox( self, min=20, max=2048 )
+ self._thumbnail_width = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
+ self._thumbnail_height = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
- self._thumbnail_border = QP.MakeQSpinBox( self, min=0, max=20 )
- self._thumbnail_margin = QP.MakeQSpinBox( self, min=0, max=20 )
+ self._thumbnail_border = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
+ self._thumbnail_margin = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
self._thumbnail_scale_type = ClientGUICommon.BetterChoice( self )
- self._video_thumbnail_percentage_in = QP.MakeQSpinBox( self, min=0, max=100 )
+ self._video_thumbnail_percentage_in = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
for t in ( HydrusImageHandling.THUMBNAIL_SCALE_DOWN_ONLY, HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, HydrusImageHandling.THUMBNAIL_SCALE_TO_FILL ):
@@ -3810,7 +3810,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._thumbnail_scroll_rate = QW.QLineEdit( self )
- self._thumbnail_visibility_scroll_percent = QP.MakeQSpinBox( self, min=1, max=99 )
+ self._thumbnail_visibility_scroll_percent = ClientGUICommon.BetterSpinBox( self, min=1, max=99 )
self._thumbnail_visibility_scroll_percent.setToolTip( 'Lower numbers will cause fewer scrolls, higher numbers more.' )
self._media_background_bmp_path = QP.FilePickerCtrl( self )
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
index 83bd25f3..bdf20e34 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsReview.py
@@ -2296,11 +2296,11 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ):
file_history_chart = ClientGUICharts.FileHistory( self, file_history )
- file_history_chart.setMinimumSize( 640, 480 )
+ file_history_chart.setMinimumSize( 720, 480 )
vbox = QP.VBoxLayout()
- label = 'Please note that delete and inbox time tracking are new so you may not have full data for them.'
+ label = 'Please note that delete and inbox time tracking are new so you may not have full data for them. Also, files in storage includes trash and any repository updates, so inbox and archive may not add up to 100% of it.'
st = ClientGUICommon.BetterStaticText( self, label = label )
@@ -2309,6 +2309,14 @@ class ReviewFileHistory( ClientGUIScrolledPanels.ReviewPanel ):
QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+ flip_deleted = QW.QCheckBox( 'show deleted', self )
+
+ flip_deleted.setChecked( True )
+
+ flip_deleted.clicked.connect( file_history_chart.FlipDeletedVisible )
+
+ QP.AddToLayout( vbox, flip_deleted, CC.FLAGS_CENTER )
+
QP.AddToLayout( vbox, file_history_chart, CC.FLAGS_EXPAND_BOTH_WAYS )
self.widget().setLayout( vbox )
diff --git a/hydrus/client/gui/ClientGUISerialisable.py b/hydrus/client/gui/ClientGUISerialisable.py
index ea1d4070..77892d31 100644
--- a/hydrus/client/gui/ClientGUISerialisable.py
+++ b/hydrus/client/gui/ClientGUISerialisable.py
@@ -34,7 +34,7 @@ class PNGExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._text = QW.QLineEdit( self )
- self._width = QP.MakeQSpinBox( self, min=100, max=4096 )
+ self._width = ClientGUICommon.BetterSpinBox( self, min=100, max=4096 )
self._export = ClientGUICommon.BetterButton( self, 'export', self.Export )
@@ -190,7 +190,7 @@ class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
self._directory_picker.setMinimumWidth( dp_width )
- self._width = QP.MakeQSpinBox( self, min=100, max=4096 )
+ self._width = ClientGUICommon.BetterSpinBox( self, min=100, max=4096 )
self._export = ClientGUICommon.BetterButton( self, 'export', self.Export )
diff --git a/hydrus/client/gui/ClientGUIStringPanels.py b/hydrus/client/gui/ClientGUIStringPanels.py
index ec8ba1ab..10ac61b6 100644
--- a/hydrus/client/gui/ClientGUIStringPanels.py
+++ b/hydrus/client/gui/ClientGUIStringPanels.py
@@ -685,14 +685,14 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
self._data_text = QW.QLineEdit( self._control_panel )
- self._data_number = QP.MakeQSpinBox( self._control_panel, min=0, max=65535 )
+ self._data_number = ClientGUICommon.BetterSpinBox( self._control_panel, min=0, max=65535 )
self._data_encoding = ClientGUICommon.BetterChoice( self._control_panel )
self._data_decoding = ClientGUICommon.BetterChoice( self._control_panel )
self._data_regex_repl = QW.QLineEdit( self._control_panel )
self._data_date_link = ClientGUICommon.BetterHyperLink( self._control_panel, 'link to date info', 'https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior' )
self._data_timezone_decode = ClientGUICommon.BetterChoice( self._control_panel )
self._data_timezone_encode = ClientGUICommon.BetterChoice( self._control_panel )
- self._data_timezone_offset = QP.MakeQSpinBox( self._control_panel, min=-86400, max=86400 )
+ self._data_timezone_offset = ClientGUICommon.BetterSpinBox( self._control_panel, min=-86400, max=86400 )
for e in ( 'hex', 'base64', 'url percent encoding', 'unicode escape characters', 'html entities' ):
@@ -1318,7 +1318,7 @@ class EditStringSlicerPanel( ClientGUIScrolledPanels.EditPanel ):
self._single_panel = QW.QWidget( self._controls_panel )
- self._index_single = QP.MakeQSpinBox( self._single_panel, min = -65536, max = 65536 )
+ self._index_single = ClientGUICommon.BetterSpinBox( self._single_panel, min = -65536, max = 65536 )
self._range_panel = QW.QWidget( self._controls_panel )
diff --git a/hydrus/client/gui/ClientGUISubscriptions.py b/hydrus/client/gui/ClientGUISubscriptions.py
index 93b74ac4..6001c863 100644
--- a/hydrus/client/gui/ClientGUISubscriptions.py
+++ b/hydrus/client/gui/ClientGUISubscriptions.py
@@ -197,10 +197,10 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
limits_max = 1000
- self._initial_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
+ self._initial_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._initial_file_limit.setToolTip( 'The first sync will add no more than this many URLs.' )
- self._periodic_file_limit = QP.MakeQSpinBox( self._file_limits_panel, min=1, max=limits_max )
+ self._periodic_file_limit = ClientGUICommon.BetterSpinBox( self._file_limits_panel, min=1, max=limits_max )
self._periodic_file_limit.setToolTip( 'Normal syncs will add no more than this many URLs, stopping early if they find several URLs the query has seen before.' )
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
diff --git a/hydrus/client/gui/ClientGUITime.py b/hydrus/client/gui/ClientGUITime.py
index 17c4561a..9b202faf 100644
--- a/hydrus/client/gui/ClientGUITime.py
+++ b/hydrus/client/gui/ClientGUITime.py
@@ -72,7 +72,7 @@ class EditCheckerOptions( ClientGUIScrolledPanels.EditPanel ):
self._reactive_check_panel = ClientGUICommon.StaticBox( self, 'reactive checking' )
- self._intended_files_per_check = QP.MakeQSpinBox( self._reactive_check_panel, min=1, max=1000 )
+ self._intended_files_per_check = ClientGUICommon.BetterSpinBox( self._reactive_check_panel, min=1, max=1000 )
self._intended_files_per_check.setToolTip( 'How many new files you want the checker to find on each check. If a source is producing about 2 files a day, and this is set to 6, you will probably get a check every three days. You probably want this to be a low number, like 1-4.' )
self._never_faster_than = TimeDeltaCtrl( self._reactive_check_panel, min = never_faster_than_min, days = True, hours = True, minutes = True, seconds = True )
@@ -348,7 +348,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_days:
- self._days = QP.MakeQSpinBox( self, min=0, max=3653, width = 50 )
+ self._days = ClientGUICommon.BetterSpinBox( self, min=0, max=3653, width = 50 )
self._days.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._days, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -357,7 +357,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_hours:
- self._hours = QP.MakeQSpinBox( self, min=0, max=23, width = 45 )
+ self._hours = ClientGUICommon.BetterSpinBox( self, min=0, max=23, width = 45 )
self._hours.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._hours, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -366,7 +366,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_minutes:
- self._minutes = QP.MakeQSpinBox( self, min=0, max=59, width = 45 )
+ self._minutes = ClientGUICommon.BetterSpinBox( self, min=0, max=59, width = 45 )
self._minutes.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._minutes, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -375,7 +375,7 @@ class TimeDeltaCtrl( QW.QWidget ):
if self._show_seconds:
- self._seconds = QP.MakeQSpinBox( self, min=0, max=59, width = 45 )
+ self._seconds = ClientGUICommon.BetterSpinBox( self, min=0, max=59, width = 45 )
self._seconds.valueChanged.connect( self.EventChange )
QP.AddToLayout( hbox, self._seconds, CC.FLAGS_CENTER_PERPENDICULAR )
@@ -549,7 +549,7 @@ class VelocityCtrl( QW.QWidget ):
QW.QWidget.__init__( self, parent )
- self._num = QP.MakeQSpinBox( self, min=min_unit_value, max=max_unit_value, width = 60 )
+ self._num = ClientGUICommon.BetterSpinBox( self, min=min_unit_value, max=max_unit_value, width = 60 )
self._times = TimeDeltaCtrl( self, min = min_time_delta, days = days, hours = hours, minutes = minutes, seconds = seconds )
diff --git a/hydrus/client/gui/QtPorting.py b/hydrus/client/gui/QtPorting.py
index 00fc49a9..cefdf686 100644
--- a/hydrus/client/gui/QtPorting.py
+++ b/hydrus/client/gui/QtPorting.py
@@ -26,7 +26,6 @@ from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
import math
-import typing
from collections import defaultdict
@@ -941,16 +940,6 @@ def SplitHorizontally( splitter: QW.QSplitter, w1, w2, vpos ):
splitter.setSizes( [ vpos, total_sum - vpos ] )
-
-def MakeQLabelWithAlignment( label, parent, align ):
-
- res = QW.QLabel( label, parent )
-
- res.setAlignment( align )
-
- return res
-
-
class GridLayout( QW.QGridLayout ):
def __init__( self, cols = 1, spacing = 2 ):
@@ -1197,17 +1186,6 @@ def AddShortcut( widget, modifier, key, callable, *args ):
shortcut.activated.connect( lambda: callable( *args ) )
-class BusyCursor:
-
- def __enter__( self ):
-
- QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
-
- def __exit__( self, exc_type, exc_val, exc_tb ):
-
- QW.QApplication.restoreOverrideCursor()
-
-
def GetBackgroundColour( widget ):
return widget.palette().color( QG.QPalette.Window )
@@ -1325,10 +1303,6 @@ def Unsplit( splitter, widget ):
widget.setVisible( False )
-def GetSystemColour( colour ):
-
- return QG.QPalette().color( colour )
-
def CenterOnWindow( parent, window ):
parent_window = parent.window()
@@ -1394,21 +1368,6 @@ def ListWidgetSetSelection( widget, idxs ):
-def MakeQSpinBox( parent = None, initial = None, min = None, max = None, width = None ):
-
- spinbox = QW.QSpinBox( parent )
-
- if min is not None: spinbox.setMinimum( min )
-
- if max is not None: spinbox.setMaximum( max )
-
- if initial is not None: spinbox.setValue( initial )
-
- if width is not None: spinbox.setMinimumWidth( width )
-
- return spinbox
-
-
def SetInitialSize( widget, size ):
if hasattr( widget, 'SetInitialSize' ):
@@ -1701,6 +1660,18 @@ class RadioBox( QW.QFrame ):
self._choices[-1].setChecked( True )
+ def _GetCurrentChoiceWidget( self ):
+
+ for choice in self._choices:
+
+ if choice.isChecked():
+
+ return choice
+
+
+
+ return None
+
def GetCurrentIndex( self ):
for i in range( len( self._choices ) ):
@@ -1731,6 +1702,20 @@ class RadioBox( QW.QFrame ):
return None
+ def setFocus( self, reason ):
+
+ item = self._GetCurrentChoiceWidget()
+
+ if item is not None:
+
+ item.setFocus( reason )
+
+ else:
+
+ QW.QFrame.setFocus( self, reason )
+
+
+
def SetValue( self, data ):
pass
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index eba4df64..d346a59f 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -740,6 +740,7 @@ class Canvas( QW.QWidget ):
( previous_width, previous_height ) = CalculateMediaSize( previous_media, self._current_zoom )
+ ( previous_media_100_width, previous_media_100_height ) = previous_media.GetResolution()
( current_media_100_width, current_media_100_height ) = self._current_media.GetResolution()
width_locked_zoom = previous_width / current_media_100_width
@@ -748,30 +749,43 @@ class Canvas( QW.QWidget ):
width_locked_size = CalculateMediaContainerSize( self._current_media, width_locked_zoom, media_show_action )
height_locked_size = CalculateMediaContainerSize( self._current_media, height_locked_zoom, media_show_action )
- # if we have both landscape, we'll go height, otherwise default width
- if previous_width > previous_height and current_media_100_width > current_media_100_height:
+ # if landscape, go height, portrait, go width
+ if previous_media_100_width > previous_media_100_height and current_media_100_width > current_media_100_height:
lock_height = True
- else:
+ elif previous_media_100_width < previous_media_100_height and current_media_100_width < current_media_100_height:
lock_height = False
+ else:
+
+ # for weird stuff, we'll choose the smaller of the two ratios
+
+ width_difference = max( previous_media_100_width, current_media_100_width ) / min( previous_media_100_width, current_media_100_width )
+ height_difference = max( previous_media_100_height, current_media_100_height ) / min( previous_media_100_height, current_media_100_height )
+
+ lock_height = height_difference <= width_difference
+
- if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.02:
+ # however we don't want to accidentally zoom in if the media we are switching to is larger. it'll spill over the bottom of the canvas
+ # therefore let's have a little safety check
+
+ if previous_current_zoom == previous_default_zoom and previous_current_zoom <= previous_canvas_zoom * 1.05:
# we were looking at the default zoom, near or at canvas edge(s), probably hadn't zoomed before switching comparison
# we want to make sure our comparison does not spill over the canvas edge
- width_a_concern = self._media_container.width() >= self.width() * 0.95
height_a_concern = self._media_container.height() >= self.height() * 0.95
# locking by width will spill over bottom of screen
- if height_a_concern and width_locked_size.height() > self._media_container.height():
+ if height_a_concern and width_locked_size.height() >= self._media_container.height():
lock_height = True
+ width_a_concern = self._media_container.width() >= self.width() * 0.95
+
# locking by height will spill over right of screen
if width_a_concern and height_locked_size.width() > self._media_container.width():
@@ -1997,7 +2011,7 @@ class CanvasPanel( Canvas ):
copy_hash_menu = QW.QMenu( copy_menu )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash.', self._CopyHashToClipboard, 'sha512' )
@@ -4514,7 +4528,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
copy_hash_menu = QW.QMenu( copy_menu )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash to your clipboard.', self._CopyHashToClipboard, 'sha512' )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
index 971c54fb..f1c08af0 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasHoverFrames.py
@@ -1282,7 +1282,9 @@ class CanvasHoverFrameTopRight( CanvasHoverFrame ):
# repo strings
- self._file_repos = QP.MakeQLabelWithAlignment( '', self, QC.Qt.AlignRight | QC.Qt.AlignVCenter )
+ self._file_repos = ClientGUICommon.BetterStaticText( self, '' )
+
+ self._file_repos.setAlignment( QC.Qt.AlignRight | QC.Qt.AlignVCenter )
# urls
diff --git a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
index 74323342..ebe11618 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvasMedia.py
@@ -1027,7 +1027,7 @@ class MediaContainer( QW.QWidget ):
self._controls_bar = QW.QWidget( self )
- QP.SetBackgroundColour( self._controls_bar, QP.GetSystemColour( QG.QPalette.Shadow ) )
+ QP.SetBackgroundColour( self._controls_bar, QG.QPalette().color( QG.QPalette.Shadow ) )
self._animation_bar = AnimationBar( self._controls_bar )
self._volume_control = ClientGUIMediaControls.VolumeControl( self._controls_bar, self._canvas_type, direction = 'up' )
@@ -1595,11 +1595,11 @@ class EmbedButton( QW.QWidget ):
painter.setTransform( QG.QTransform().scale( 1.0, 1.0 ) )
- painter.setBrush( QG.QBrush( QP.GetSystemColour( QG.QPalette.Button ) ) )
+ painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.Button ) ) )
painter.drawEllipse( QC.QPointF( center_x, center_y ), radius, radius )
- painter.setBrush( QG.QBrush( QP.GetSystemColour( QG.QPalette.Window ) ) )
+ painter.setBrush( QG.QBrush( QG.QPalette().color( QG.QPalette.Window ) ) )
# play symbol is a an equilateral triangle
@@ -1623,7 +1623,7 @@ class EmbedButton( QW.QWidget ):
#
- painter.setPen( QG.QPen( QP.GetSystemColour( QG.QPalette.Shadow ) ) )
+ painter.setPen( QG.QPen( QG.QPalette().color( QG.QPalette.Shadow ) ) )
painter.setBrush( QC.Qt.NoBrush )
diff --git a/hydrus/client/gui/pages/ClientGUIManagement.py b/hydrus/client/gui/pages/ClientGUIManagement.py
index fa429281..a2355bb9 100644
--- a/hydrus/client/gui/pages/ClientGUIManagement.py
+++ b/hydrus/client/gui/pages/ClientGUIManagement.py
@@ -1174,7 +1174,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._search_distance_button = ClientGUIMenuButton.MenuButton( self._searching_panel, 'similarity', menu_items )
- self._search_distance_spinctrl = QP.MakeQSpinBox( self._searching_panel, min=0, max=64, width = 50 )
+ self._search_distance_spinctrl = ClientGUICommon.BetterSpinBox( self._searching_panel, min=0, max=64, width = 50 )
self._search_distance_spinctrl.setSingleStep( 2 )
self._num_searched = ClientGUICommon.TextAndGauge( self._searching_panel )
@@ -1214,7 +1214,7 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
self._pixel_dupes_preference.addItem( CC.similar_files_pixel_dupes_string_lookup[ p ], p )
- self._max_hamming_distance = QP.MakeQSpinBox( self._filtering_panel, min = 0, max = 64 )
+ self._max_hamming_distance = ClientGUICommon.BetterSpinBox( self._filtering_panel, min = 0, max = 64 )
self._max_hamming_distance.setSingleStep( 2 )
self._num_potential_duplicates = ClientGUICommon.BetterStaticText( self._filtering_panel, ellipsize_end = True )
@@ -4655,7 +4655,7 @@ class ManagementPanelPetitions( ManagementPanel ):
location_context = self._management_controller.GetVariable( 'location_context' )
- with QP.BusyCursor():
+ with ClientGUICommon.BusyCursor():
media_results = self._controller.Read( 'media_results', hashes )
diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py
index 1462d8a5..8f40c8dc 100644
--- a/hydrus/client/gui/pages/ClientGUIResults.py
+++ b/hydrus/client/gui/pages/ClientGUIResults.py
@@ -79,7 +79,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._focused_media = None
self._next_best_media_after_focused_media_removed = None
- self._shift_focused_media = None
+ self._shift_select_started_with_this_media = None
+ self._media_added_in_current_shift_select = set()
self._empty_page_status_override = None
@@ -406,6 +407,12 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
+ def _EndShiftSelect( self ):
+
+ self._shift_select_started_with_this_media = None
+ self._media_added_in_current_shift_select = set()
+
+
def _ExportFiles( self, do_export_and_then_quit = False ):
if len( self._selected_media ) > 0:
@@ -844,7 +851,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._Select( ClientMedia.FileFilter( ClientMedia.FILE_FILTER_NONE ) )
self._SetFocusedMedia( None )
- self._shift_focused_media = None
+ self._EndShiftSelect()
+
else:
@@ -860,32 +868,42 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._SetFocusedMedia( None )
- self._shift_focused_media = None
+ self._EndShiftSelect()
else:
self._DeselectSelect( (), ( media, ) )
- if self._focused_media is None: self._SetFocusedMedia( media )
+ self._SetFocusedMedia( media )
- self._shift_focused_media = media
+ self._StartShiftSelect( media )
- elif shift and self._shift_focused_media is not None:
+ elif shift and self._shift_select_started_with_this_media is not None:
- start_index = self._sorted_media.index( self._shift_focused_media )
+ start_index = self._sorted_media.index( self._shift_select_started_with_this_media )
end_index = self._sorted_media.index( media )
- if start_index < end_index: media_to_select = set( self._sorted_media[ start_index : end_index + 1 ] )
- else: media_to_select = set( self._sorted_media[ end_index : start_index + 1 ] )
+ if start_index < end_index:
+
+ media_from_start_of_shift_to_end = set( self._sorted_media[ start_index : end_index + 1 ] )
+
+ else:
+
+ media_from_start_of_shift_to_end = set( self._sorted_media[ end_index : start_index + 1 ] )
+
- self._DeselectSelect( (), media_to_select )
+ media_to_deselect = [ m for m in self._media_added_in_current_shift_select if m not in media_from_start_of_shift_to_end ]
+ media_to_select = [ m for m in media_from_start_of_shift_to_end if not m.IsSelected() ]
+
+ self._media_added_in_current_shift_select.difference_update( media_to_deselect )
+ self._media_added_in_current_shift_select.update( media_to_select )
+
+ self._DeselectSelect( media_to_deselect, media_to_select )
self._SetFocusedMedia( media )
- self._shift_focused_media = media
-
else:
if not media.IsSelected():
@@ -898,7 +916,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._SetFocusedMedia( media )
- self._shift_focused_media = media
+ self._StartShiftSelect( media )
@@ -1343,9 +1361,9 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
move_focus = self._focused_media in media_to_deselect or self._focused_media is None
- if move_focus or self._shift_focused_media in media_to_deselect:
+ if move_focus or self._shift_select_started_with_this_media in media_to_deselect:
- self._shift_focused_media = None
+ self._EndShiftSelect()
self._DeselectSelect( media_to_deselect, media_to_select )
@@ -1749,6 +1767,12 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
+ def _StartShiftSelect( self, media ):
+
+ self._shift_select_started_with_this_media = media
+ self._media_added_in_current_shift_select = set()
+
+
def _Undelete( self ):
media = self._GetSelectedFlatMedia()
@@ -2876,7 +2900,7 @@ class MediaPanelThumbnails( MediaPanel ):
self._selected_media.difference_update( singleton_media )
self._selected_media.difference_update( collected_media )
- self._shift_focused_media = None
+ self._EndShiftSelect()
self._RecalculateVirtualSize()
@@ -4103,10 +4127,15 @@ class MediaPanelThumbnails( MediaPanel ):
copy_hash_menu = QW.QMenu( copy_menu )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected file\'s MD5 hash to the clipboard.', self._CopyHashToClipboard, 'md5' )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected file\'s SHA1 hash to the clipboard.', self._CopyHashToClipboard, 'sha1' )
- ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected file\'s SHA512 hash to the clipboard.', self._CopyHashToClipboard, 'sha512' )
+ if self._HasFocusSingleton():
+
+ focus_singleton = self._GetFocusSingleton()
+
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( focus_singleton.GetHash().hex() ), 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected file\'s MD5 hash to the clipboard.', self._CopyHashToClipboard, 'md5' )
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected file\'s SHA1 hash to the clipboard.', self._CopyHashToClipboard, 'sha1' )
+ ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected file\'s SHA512 hash to the clipboard.', self._CopyHashToClipboard, 'sha512' )
+
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
diff --git a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
index 6e10328f..e056217d 100644
--- a/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
+++ b/hydrus/client/gui/search/ClientGUIPredicatesSingle.py
@@ -241,10 +241,10 @@ class PanelPredicateSystemAgeDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
- self._years = QP.MakeQSpinBox( self, max=30, width = 60 )
- self._months = QP.MakeQSpinBox( self, max=60, width = 60 )
- self._days = QP.MakeQSpinBox( self, max=90, width = 60 )
- self._hours = QP.MakeQSpinBox( self, max=24, width = 60 )
+ self._years = ClientGUICommon.BetterSpinBox( self, max=30, width = 60 )
+ self._months = ClientGUICommon.BetterSpinBox( self, max=60, width = 60 )
+ self._days = ClientGUICommon.BetterSpinBox( self, max=90, width = 60 )
+ self._hours = ClientGUICommon.BetterSpinBox( self, max=24, width = 60 )
#
@@ -360,10 +360,10 @@ class PanelPredicateSystemLastViewedDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
- self._years = QP.MakeQSpinBox( self, max=30 )
- self._months = QP.MakeQSpinBox( self, max=60 )
- self._days = QP.MakeQSpinBox( self, max=90 )
- self._hours = QP.MakeQSpinBox( self, max=24 )
+ self._years = ClientGUICommon.BetterSpinBox( self, max=30 )
+ self._months = ClientGUICommon.BetterSpinBox( self, max=60 )
+ self._days = ClientGUICommon.BetterSpinBox( self, max=90 )
+ self._hours = ClientGUICommon.BetterSpinBox( self, max=24 )
#
@@ -479,10 +479,10 @@ class PanelPredicateSystemModifiedDelta( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'>'] )
- self._years = QP.MakeQSpinBox( self, max=30 )
- self._months = QP.MakeQSpinBox( self, max=60 )
- self._days = QP.MakeQSpinBox( self, max=90 )
- self._hours = QP.MakeQSpinBox( self, max=24 )
+ self._years = ClientGUICommon.BetterSpinBox( self, max=30 )
+ self._months = ClientGUICommon.BetterSpinBox( self, max=60 )
+ self._days = ClientGUICommon.BetterSpinBox( self, max=90 )
+ self._hours = ClientGUICommon.BetterSpinBox( self, max=24 )
#
@@ -539,7 +539,7 @@ class PanelPredicateSystemDuplicateRelationships( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
- self._num = QP.MakeQSpinBox( self, min=0, max=65535 )
+ self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=65535 )
choices = [ ( HC.duplicate_type_string_lookup[ status ], status ) for status in ( HC.DUPLICATE_MEMBER, HC.DUPLICATE_ALTERNATE, HC.DUPLICATE_FALSE_POSITIVE, HC.DUPLICATE_POTENTIAL ) ]
@@ -595,8 +595,8 @@ class PanelPredicateSystemDuration( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
- self._duration_s = QP.MakeQSpinBox( self, max=3599, width = 60 )
- self._duration_ms = QP.MakeQSpinBox( self, max=999, width = 60 )
+ self._duration_s = ClientGUICommon.BetterSpinBox( self, max=3599, width = 60 )
+ self._duration_ms = ClientGUICommon.BetterSpinBox( self, max=999, width = 60 )
#
@@ -720,7 +720,7 @@ class PanelPredicateSystemFileViewingStatsViews( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=','>'] )
- self._num = QP.MakeQSpinBox( self, min=0, max=1000000 )
+ self._num = ClientGUICommon.BetterSpinBox( self, min=0, max=1000000 )
#
@@ -861,7 +861,7 @@ class PanelPredicateSystemFramerate( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
- self._framerate = QP.MakeQSpinBox( self, min = 1, max = 3600, width = 60 )
+ self._framerate = ClientGUICommon.BetterSpinBox( self, min = 1, max = 3600, width = 60 )
#
@@ -1042,7 +1042,7 @@ class PanelPredicateSystemHeight( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
- self._height = QP.MakeQSpinBox( self, max=200000, width = 60 )
+ self._height = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
#
@@ -1392,7 +1392,7 @@ class PanelPredicateSystemLimit( PanelPredicateSystemSingle ):
PanelPredicateSystemSingle.__init__( self, parent )
- self._limit = QP.MakeQSpinBox( self, max=1000000, width = 60 )
+ self._limit = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
#
@@ -1485,7 +1485,7 @@ class PanelPredicateSystemNumPixels( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=[ '<', CC.UNICODE_ALMOST_EQUAL_TO, '=', CC.UNICODE_NOT_EQUAL_TO, '>' ] )
- self._num_pixels = QP.MakeQSpinBox( self, max=1048576, width = 60 )
+ self._num_pixels = ClientGUICommon.BetterSpinBox( self, max=1048576, width = 60 )
self._unit = QP.RadioBox( self, choices=['pixels','kilopixels','megapixels'] )
@@ -1541,7 +1541,7 @@ class PanelPredicateSystemNumFrames( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
- self._num_frames = QP.MakeQSpinBox( self, min = 0, max = 1000000, width = 80 )
+ self._num_frames = ClientGUICommon.BetterSpinBox( self, min = 0, max = 1000000, width = 80 )
#
@@ -1591,7 +1591,7 @@ class PanelPredicateSystemNumTags( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=','>'] )
- self._num_tags = QP.MakeQSpinBox( self, max=2000, width = 60 )
+ self._num_tags = ClientGUICommon.BetterSpinBox( self, max=2000, width = 60 )
#
@@ -1666,7 +1666,7 @@ class PanelPredicateSystemNumNotes( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = [ '<', '=', '>' ] )
- self._num_notes = QP.MakeQSpinBox( self, max = 256, width = 60 )
+ self._num_notes = ClientGUICommon.BetterSpinBox( self, max = 256, width = 60 )
#
@@ -1714,7 +1714,7 @@ class PanelPredicateSystemNumWords( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
- self._num_words = QP.MakeQSpinBox( self, max=1000000, width = 60 )
+ self._num_words = ClientGUICommon.BetterSpinBox( self, max=1000000, width = 60 )
#
@@ -1762,9 +1762,9 @@ class PanelPredicateSystemRatio( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['=','wider than','taller than',CC.UNICODE_ALMOST_EQUAL_TO,CC.UNICODE_NOT_EQUAL_TO] )
- self._width = QP.MakeQSpinBox( self, max=50000, width = 60 )
+ self._width = ClientGUICommon.BetterSpinBox( self, max=50000, width = 60 )
- self._height = QP.MakeQSpinBox( self, max=50000, width = 60 )
+ self._height = ClientGUICommon.BetterSpinBox( self, max=50000, width = 60 )
#
@@ -1821,7 +1821,7 @@ class PanelPredicateSystemSimilarTo( PanelPredicateSystemSingle ):
self._hashes.setMinimumSize( QC.QSize( init_width, init_height ) )
- self._max_hamming = QP.MakeQSpinBox( self, max=256, width = 60 )
+ self._max_hamming = ClientGUICommon.BetterSpinBox( self, max=256, width = 60 )
#
@@ -1933,7 +1933,7 @@ class PanelPredicateSystemTagAsNumber( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices = choices )
- self._num = QP.MakeQSpinBox( self, min=-(2**31), max=(2**31)-1 )
+ self._num = ClientGUICommon.BetterSpinBox( self, min=-(2 ** 31), max= (2 ** 31) - 1 )
#
@@ -1983,7 +1983,7 @@ class PanelPredicateSystemWidth( PanelPredicateSystemSingle ):
self._sign = QP.RadioBox( self, choices=['<',CC.UNICODE_ALMOST_EQUAL_TO,'=',CC.UNICODE_NOT_EQUAL_TO,'>'] )
- self._width = QP.MakeQSpinBox( self, max=200000, width = 60 )
+ self._width = ClientGUICommon.BetterSpinBox( self, max=200000, width = 60 )
#
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index e8797afa..4e1d6043 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -417,7 +417,7 @@ class EditServiceRemoteSubPanel( ClientGUICommon.StaticBox ):
credentials = dictionary[ 'credentials' ]
self._host = QW.QLineEdit( self )
- self._port = QP.MakeQSpinBox( self, min=1, max=65535, width = 80 )
+ self._port = ClientGUICommon.BetterSpinBox( self, min=1, max=65535, width = 80 )
self._test_address_button = ClientGUICommon.BetterButton( self, 'test address', self._TestAddress )
@@ -1292,7 +1292,7 @@ class EditServiceRatingsNumericalSubPanel( ClientGUICommon.StaticBox ):
ClientGUICommon.StaticBox.__init__( self, parent, 'numerical ratings' )
- self._num_stars = QP.MakeQSpinBox( self, min=1, max=20 )
+ self._num_stars = ClientGUICommon.BetterSpinBox( self, min=1, max=20 )
self._allow_zero = QW.QCheckBox( self )
#
diff --git a/hydrus/client/gui/services/ClientGUIServersideServices.py b/hydrus/client/gui/services/ClientGUIServersideServices.py
index df4f4850..fee698d0 100644
--- a/hydrus/client/gui/services/ClientGUIServersideServices.py
+++ b/hydrus/client/gui/services/ClientGUIServersideServices.py
@@ -86,7 +86,7 @@ class EditServersideService( ClientGUIScrolledPanels.EditPanel ):
ClientGUICommon.StaticBox.__init__( self, parent, 'basic information' )
self._name = QW.QLineEdit( self )
- self._port = QP.MakeQSpinBox( self, min=1, max=65535 )
+ self._port = ClientGUICommon.BetterSpinBox( self, min=1, max=65535 )
self._upnp_port = ClientGUICommon.NoneableSpinCtrl( self, 'external upnp port', none_phrase = 'do not forward port', min = 1, max = 65535 )
self._bandwidth_tracker_st = ClientGUICommon.BetterStaticText( self )
diff --git a/hydrus/client/gui/widgets/ClientGUICommon.py b/hydrus/client/gui/widgets/ClientGUICommon.py
index 1e1d45de..ff30dc24 100644
--- a/hydrus/client/gui/widgets/ClientGUICommon.py
+++ b/hydrus/client/gui/widgets/ClientGUICommon.py
@@ -2,7 +2,7 @@ import os
import re
import typing
-from qtpy import QtCore as QC
+from qtpy import QtCore as QC, QtWidgets as QW
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
@@ -559,6 +559,33 @@ class BetterNotebook( QW.QTabWidget ):
self._ShiftSelection( 1 )
+class BetterSpinBox( QW.QSpinBox ):
+
+ def __init__( self, parent: QW.QWidget, initial = None, min = None, max = None, width = None ):
+
+ QW.QSpinBox.__init__( self, parent )
+
+ if min is not None:
+
+ self.setMinimum( min )
+
+
+ if max is not None:
+
+ self.setMaximum( max )
+
+
+ if initial is not None:
+
+ self.setValue( initial )
+
+
+ if width is not None:
+
+ self.setMinimumWidth( width )
+
+
+
class ButtonWithMenuArrow( QW.QToolButton ):
def __init__( self, parent: QW.QWidget, action: QW.QAction ):
@@ -765,6 +792,17 @@ class BufferedWindowIcon( BufferedWindow ):
+class BusyCursor( object ):
+
+ def __enter__( self ):
+
+ QW.QApplication.setOverrideCursor( QC.Qt.WaitCursor )
+
+ def __exit__( self, exc_type, exc_val, exc_tb ):
+
+ QW.QApplication.restoreOverrideCursor()
+
+
class CheckboxManager( object ):
def GetCurrentValue( self ):
@@ -868,7 +906,7 @@ class AlphaColourControl( QW.QWidget ):
self._colour_picker = BetterColourControl( self )
- self._alpha_selector = QP.MakeQSpinBox( self, min=0, max=255 )
+ self._alpha_selector = BetterSpinBox( self, min=0, max=255 )
hbox = QP.HBoxLayout( spacing = 5 )
@@ -1409,7 +1447,7 @@ class NoneableSpinCtrl( QW.QWidget ):
self._checkbox.stateChanged.connect( self.EventCheckBox )
self._checkbox.setText( none_phrase )
- self._one = QP.MakeQSpinBox( self, min=min, max=max )
+ self._one = BetterSpinBox( self, min=min, max=max )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._one, len( str( max ) ) + 5 )
@@ -1417,7 +1455,7 @@ class NoneableSpinCtrl( QW.QWidget ):
if num_dimensions == 2:
- self._two = QP.MakeQSpinBox( self, initial=0, min=min, max=max )
+ self._two = BetterSpinBox( self, initial=0, min=min, max=max )
self._two.valueChanged.connect( self._HandleValueChanged )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self._two, len( str( max ) ) + 5 )
@@ -1931,4 +1969,3 @@ class TextAndGauge( QW.QWidget ):
self._gauge.SetRange( range )
self._gauge.SetValue( value )
-
diff --git a/hydrus/client/gui/widgets/ClientGUIControls.py b/hydrus/client/gui/widgets/ClientGUIControls.py
index d8145fbb..5a4c4df7 100644
--- a/hydrus/client/gui/widgets/ClientGUIControls.py
+++ b/hydrus/client/gui/widgets/ClientGUIControls.py
@@ -194,7 +194,7 @@ class BandwidthRulesCtrl( ClientGUICommon.StaticBox ):
self._bandwidth_type.currentIndexChanged.connect( self._UpdateEnabled )
self._max_allowed_bytes = BytesControl( self )
- self._max_allowed_requests = QP.MakeQSpinBox( self, min=1, max=1048576 )
+ self._max_allowed_requests = ClientGUICommon.BetterSpinBox( self, min=1, max=1048576 )
self._time_delta = ClientGUITime.TimeDeltaButton( self, min = 1, days = True, hours = True, minutes = True, seconds = True, monthly_allowed = True )
@@ -272,7 +272,7 @@ class BytesControl( QW.QWidget ):
QW.QWidget.__init__( self, parent )
- self._spin = QP.MakeQSpinBox( self, min=0, max=1048576 )
+ self._spin = ClientGUICommon.BetterSpinBox( self, min=0, max=1048576 )
self._unit = ClientGUICommon.BetterChoice( self )
diff --git a/hydrus/client/importing/ClientImportLocal.py b/hydrus/client/importing/ClientImportLocal.py
index 9490d22b..f7366fa8 100644
--- a/hydrus/client/importing/ClientImportLocal.py
+++ b/hydrus/client/importing/ClientImportLocal.py
@@ -4,7 +4,6 @@ import time
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
-from hydrus.core import HydrusExceptions
from hydrus.core import HydrusFileHandling
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
@@ -554,7 +553,24 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
elif status == CC.IMPORT_FOLDER_IGNORE:
- pass
+ file_seeds = self._file_seed_cache.GetFileSeeds( status )
+
+ for file_seed in file_seeds:
+
+ path = file_seed.file_seed_data
+
+ try:
+
+ if not os.path.exists( path ):
+
+ self._file_seed_cache.RemoveFileSeeds( ( file_seed, ) )
+
+
+ except Exception as e:
+
+ raise Exception( 'Tried to check existence of "{}", but could not.'.format( path ) )
+
+
@@ -621,9 +637,9 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
i = 0
- num_total = len( self._file_seed_cache )
- num_total_unknown = self._file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
- num_total_done = num_total - num_total_unknown
+ # don't want to start at 23/100 because of carrying over failed results or whatever
+ # num_to_do is num currently unknown
+ num_total = self._file_seed_cache.GetFileSeedCount( CC.STATUS_UNKNOWN )
while True:
@@ -647,7 +663,7 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
time_to_save = HydrusData.GetNow() + 600
- gauge_num_done = num_total_done + num_files_imported + 1
+ gauge_num_done = num_files_imported + 1
job_key.SetVariable( 'popup_text_1', 'importing file ' + HydrusData.ConvertValueRangeToPrettyString( gauge_num_done, num_total ) )
job_key.SetVariable( 'popup_gauge_1', ( gauge_num_done, num_total ) )
@@ -658,6 +674,8 @@ class ImportFolder( HydrusSerialisable.SerialisableBaseNamed ):
if file_seed.status in CC.SUCCESSFUL_IMPORT_STATES:
+ hash = None
+
if file_seed.HasHash():
hash = file_seed.GetHash()
diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py
index 6f1baead..bb62cff2 100644
--- a/hydrus/client/media/ClientMedia.py
+++ b/hydrus/client/media/ClientMedia.py
@@ -2840,7 +2840,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
if sort_metatype == 'system':
- if sort_data in ( CC.SORT_FILES_BY_MIME, CC.SORT_FILES_BY_RANDOM ):
+ if sort_data in ( CC.SORT_FILES_BY_MIME, CC.SORT_FILES_BY_RANDOM, CC.SORT_FILES_BY_HASH ):
return False
@@ -2884,6 +2884,13 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
return random.random()
+ elif sort_data == CC.SORT_FILES_BY_HASH:
+
+ def sort_key( x ):
+
+ return x.GetHash().hex()
+
+
elif sort_data == CC.SORT_FILES_BY_APPROX_BITRATE:
def sort_key( x ):
@@ -3179,6 +3186,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
sort_string_lookup[ CC.SORT_FILES_BY_ARCHIVED_TIMESTAMP ] = ( 'oldest first', 'newest first', CC.SORT_DESC )
sort_string_lookup[ CC.SORT_FILES_BY_MIME ] = ( 'filetype', 'filetype', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_RANDOM ] = ( 'random', 'random', CC.SORT_ASC )
+ sort_string_lookup[ CC.SORT_FILES_BY_HASH ] = ( 'hash', 'hash', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_WIDTH ] = ( 'slimmest first', 'widest first', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_HEIGHT ] = ( 'shortest first', 'tallest first', CC.SORT_ASC )
sort_string_lookup[ CC.SORT_FILES_BY_RATIO ] = ( 'tallest first', 'widest first', CC.SORT_ASC )
diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py
index b746d1be..7252c3af 100644
--- a/hydrus/client/networking/ClientLocalServerResources.py
+++ b/hydrus/client/networking/ClientLocalServerResources.py
@@ -55,7 +55,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'file_service_key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name', 'reason' }
-CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'include_notes', 'notes', 'note_names' }
+CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@@ -2108,20 +2108,30 @@ class HydrusResourceClientAPIRestrictedGetFilesSearchFiles( HydrusResourceClient
return_hashes = request.parsed_request_args.GetValue( 'return_hashes', bool )
+ return_file_ids = True
+
+ if 'return_file_ids' in request.parsed_request_args:
+
+ return_file_ids = request.parsed_request_args.GetValue( 'return_file_ids', bool )
+
+
hash_ids = HG.client_controller.Read( 'file_query_ids', file_search_context, sort_by = sort_by, apply_implicit_limit = False )
request.client_api_permissions.SetLastSearchResults( hash_ids )
+ body_dict = {}
+
if return_hashes:
hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = hash_ids )
# maintain sort
- body_dict = { 'hashes' : [ hash_ids_to_hashes[ hash_id ].hex() for hash_id in hash_ids ], 'file_ids' : list( hash_ids ) }
+ body_dict[ 'hashes' ] = [ hash_ids_to_hashes[ hash_id ].hex() for hash_id in hash_ids ]
- else:
+
+ if return_file_ids:
- body_dict = { 'file_ids' : list( hash_ids ) }
+ body_dict[ 'file_ids' ] = list( hash_ids )
body = Dumps( body_dict, request.preferred_mime )
@@ -2190,6 +2200,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):
only_return_identifiers = request.parsed_request_args.GetValue( 'only_return_identifiers', bool, default_value = False )
+ only_return_basic_information = request.parsed_request_args.GetValue( 'only_return_basic_information', bool, default_value = False )
hide_service_names_tags = request.parsed_request_args.GetValue( 'hide_service_names_tags', bool, default_value = False )
detailed_url_information = request.parsed_request_args.GetValue( 'detailed_url_information', bool, default_value = False )
include_notes = request.parsed_request_args.GetValue( 'include_notes', bool, default_value = False )
@@ -2212,6 +2223,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = file_ids )
+ elif only_return_basic_information:
+
+ file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', file_ids, sorted = True )
+
else:
media_results = HG.client_controller.Read( 'media_results_from_ids', file_ids, sorted = True )
@@ -2235,6 +2250,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hashes = hashes )
+ elif only_return_basic_information:
+
+ file_info_managers = HG.client_controller.Read( 'file_info_managers', hashes, sorted = True )
+
else:
media_results = HG.client_controller.Read( 'media_results', hashes, sorted = True )
@@ -2266,6 +2285,27 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
metadata.append( metadata_row )
+ elif only_return_basic_information:
+
+ for file_info_manager in file_info_managers:
+
+ metadata_row = {
+ 'file_id' : file_info_manager.hash_id,
+ 'hash' : file_info_manager.hash.hex(),
+ 'size' : file_info_manager.size,
+ 'mime' : HC.mime_mimetype_string_lookup[ file_info_manager.mime ],
+ 'ext' : HC.mime_ext_lookup[ file_info_manager.mime ],
+ 'width' : file_info_manager.width,
+ 'height' : file_info_manager.height,
+ 'duration' : file_info_manager.duration,
+ 'num_frames' : file_info_manager.num_frames,
+ 'num_words' : file_info_manager.num_words,
+ 'has_audio' : file_info_manager.has_audio
+ }
+
+ metadata.append( metadata_row )
+
+
else:
services_manager = HG.client_controller.services_manager
diff --git a/hydrus/client/networking/ClientNetworking.py b/hydrus/client/networking/ClientNetworking.py
index d9b1ca95..89d0c3d5 100644
--- a/hydrus/client/networking/ClientNetworking.py
+++ b/hydrus/client/networking/ClientNetworking.py
@@ -19,13 +19,13 @@ JOB_STATUS_AWAITING_LOGIN = 2
JOB_STATUS_AWAITING_SLOT = 3
JOB_STATUS_RUNNING = 4
-job_status_str_lookup = {}
-
-job_status_str_lookup[ JOB_STATUS_AWAITING_VALIDITY ] = 'waiting for validation'
-job_status_str_lookup[ JOB_STATUS_AWAITING_BANDWIDTH ] = 'waiting for bandwidth'
-job_status_str_lookup[ JOB_STATUS_AWAITING_LOGIN ] = 'waiting for login'
-job_status_str_lookup[ JOB_STATUS_AWAITING_SLOT ] = 'waiting for free work slot'
-job_status_str_lookup[ JOB_STATUS_RUNNING ] = 'running'
+job_status_str_lookup = {
+ JOB_STATUS_AWAITING_VALIDITY : 'waiting for validation',
+ JOB_STATUS_AWAITING_BANDWIDTH : 'waiting for bandwidth',
+ JOB_STATUS_AWAITING_LOGIN : 'waiting for login',
+ JOB_STATUS_AWAITING_SLOT : 'waiting for free work slot',
+ JOB_STATUS_RUNNING : 'running'
+}
class NetworkEngine( object ):
@@ -49,6 +49,9 @@ class NetworkEngine( object ):
self._lock = threading.Lock()
+ self.MAX_JOBS = 1
+ self.MAX_JOBS_PER_DOMAIN = 1
+
self.RefreshOptions()
self._new_work_to_do = threading.Event()
diff --git a/hydrus/client/networking/ClientNetworkingBandwidth.py b/hydrus/client/networking/ClientNetworkingBandwidth.py
index 89bed08d..9d743331 100644
--- a/hydrus/client/networking/ClientNetworkingBandwidth.py
+++ b/hydrus/client/networking/ClientNetworkingBandwidth.py
@@ -626,6 +626,10 @@ class NetworkBandwidthManager( HydrusSerialisable.SerialisableBase ):
delay = HG.client_controller.new_options.GetInteger( 'watcher_page_wait_period' )
+ else:
+
+ raise NotImplementedError( 'Unknown query type' )
+
next_timestamp = timestamps_dict[ second_level_domain ] + delay
@@ -640,8 +644,6 @@ class NetworkBandwidthManager( HydrusSerialisable.SerialisableBase ):
return ( False, next_timestamp )
- raise NotImplementedError( 'Unknown query type' )
-
def TryToStartRequest( self, network_contexts ):
diff --git a/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py b/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py
index 5900e8f4..612ff0ff 100644
--- a/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py
+++ b/hydrus/client/networking/ClientNetworkingBandwidthLegacy.py
@@ -1,9 +1,6 @@
import collections
-import threading
-from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
-from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core.networking import HydrusNetworking
diff --git a/hydrus/client/networking/ClientNetworkingDomain.py b/hydrus/client/networking/ClientNetworkingDomain.py
index cf74289a..05df77f5 100644
--- a/hydrus/client/networking/ClientNetworkingDomain.py
+++ b/hydrus/client/networking/ClientNetworkingDomain.py
@@ -2,7 +2,6 @@ import collections
import os
import threading
import time
-import typing
import urllib.parse
from hydrus.core import HydrusConstants as HC
@@ -22,11 +21,11 @@ VALID_DENIED = 0
VALID_APPROVED = 1
VALID_UNKNOWN = 2
-valid_str_lookup = {}
-
-valid_str_lookup[ VALID_DENIED ] = 'denied'
-valid_str_lookup[ VALID_APPROVED ] = 'approved'
-valid_str_lookup[ VALID_UNKNOWN ] = 'unknown'
+valid_str_lookup = {
+ VALID_DENIED : 'denied',
+ VALID_APPROVED : 'approved',
+ VALID_UNKNOWN : 'unknown'
+}
class NetworkDomainManager( HydrusSerialisable.SerialisableBase ):
@@ -2137,4 +2136,3 @@ class DomainValidationPopupProcess( object ):
self._is_done = True
-
\ No newline at end of file
diff --git a/hydrus/client/networking/ClientNetworkingFunctions.py b/hydrus/client/networking/ClientNetworkingFunctions.py
index c945ee2b..ccd449f8 100644
--- a/hydrus/client/networking/ClientNetworkingFunctions.py
+++ b/hydrus/client/networking/ClientNetworkingFunctions.py
@@ -30,7 +30,7 @@ def AddCookieToSession( session, name, value, domain, path, expires, secure = Fa
def ConvertDomainIntoAllApplicableDomains( domain, discard_www = True ):
# is an ip address or localhost, possibly with a port
- if '.' not in domain or re.search( r'^[\d\.:]+$', domain ) is not None:
+ if '.' not in domain or re.search( r'^[\d.:]+$', domain ) is not None:
return [ domain ]
@@ -420,6 +420,7 @@ def ParseURL( url: str ) -> urllib.parse.ParseResult:
return urllib.parse.urlparse( url )
+
OH_NO_NO_NETLOC_CHARACTERS = '?#'
OH_NO_NO_NETLOC_CHARACTERS_UNICODE_TRANSLATE = { ord( char ) : '_' for char in OH_NO_NO_NETLOC_CHARACTERS }
@@ -476,4 +477,3 @@ def UnicodeNormaliseURL( url: str ):
return url
-
\ No newline at end of file
diff --git a/hydrus/client/networking/ClientNetworkingJobs.py b/hydrus/client/networking/ClientNetworkingJobs.py
index 49f35015..be9faa02 100644
--- a/hydrus/client/networking/ClientNetworkingJobs.py
+++ b/hydrus/client/networking/ClientNetworkingJobs.py
@@ -160,7 +160,9 @@ class NetworkJob( object ):
self._method = method
self._url = url
+ self._current_connection_attempt_number = 1
self._max_connection_attempts_allowed = 5
+ self._we_tried_cloudflare_once = False
self._domain = ClientNetworkingFunctions.ConvertURLIntoDomain( self._url )
self._second_level_domain = ClientNetworkingFunctions.ConvertURLIntoSecondLevelDomain( self._url )
@@ -185,9 +187,6 @@ class NetworkJob( object ):
self._files = None
self._for_login = False
- self._current_connection_attempt_number = 1
- self._we_tried_cloudflare_once = False
-
self._additional_headers = {}
self._creation_time = HydrusData.GetNow()
@@ -288,9 +287,7 @@ class NetworkJob( object ):
def _GenerateNetworkContexts( self ):
- network_contexts = []
-
- network_contexts.append( ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT )
+ network_contexts = [ ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT ]
domains = ClientNetworkingFunctions.ConvertDomainIntoAllApplicableDomains( self._domain )
@@ -761,15 +758,38 @@ class NetworkJob( object ):
# cloudscraper refactored a bit around 1.2.60, so we now have some different paths to what we want
+ old_module = None
+ new_module = None
+
+ if hasattr( cloudscraper, 'CloudScraper' ):
+
+ old_module = getattr( cloudscraper, 'CloudScraper' )
+
+
+ if hasattr( cloudscraper, 'cloudflare' ):
+
+ m = getattr( cloudscraper, 'cloudflare' )
+
+ if hasattr( m, 'Cloudflare' ):
+
+ new_module = getattr( m, 'Cloudflare' )
+
+
+
possible_paths = [
- ( cloudscraper.CloudScraper, 'is_Firewall_Blocked' ),
- ( cloudscraper.cloudflare.Cloudflare, 'is_Firewall_Blocked' )
+ ( old_module, 'is_Firewall_Blocked' ),
+ ( new_module, 'is_Firewall_Blocked' )
]
is_firewall = False
for ( m, method_name ) in possible_paths:
+ if m is None:
+
+ continue
+
+
if hasattr( m, method_name ):
is_firewall = getattr( m, method_name )( response )
@@ -782,15 +802,20 @@ class NetworkJob( object ):
possible_paths = [
- ( cloudscraper.CloudScraper, 'is_reCaptcha_Challenge' ),
- ( cloudscraper.CloudScraper, 'is_Captcha_Challenge' ),
- ( cloudscraper.cloudflare.Cloudflare, 'is_Captcha_Challenge' )
+ ( old_module, 'is_reCaptcha_Challenge' ),
+ ( old_module, 'is_Captcha_Challenge' ),
+ ( new_module, 'is_Captcha_Challenge' )
]
is_captcha = False
for ( m, method_name ) in possible_paths:
+ if m is None:
+
+ continue
+
+
if hasattr( m, method_name ):
is_captcha = getattr( m, method_name )( response )
@@ -803,15 +828,20 @@ class NetworkJob( object ):
possible_paths = [
- ( cloudscraper.CloudScraper, 'is_IUAM_Challenge' ),
- ( cloudscraper.cloudflare.Cloudflare, 'is_IUAM_Challenge' ),
- ( cloudscraper.cloudflare.Cloudflare, 'is_New_IUAM_Challenge' )
+ ( old_module, 'is_IUAM_Challenge' ),
+ ( new_module, 'is_IUAM_Challenge' ),
+ ( new_module, 'is_New_IUAM_Challenge' )
]
is_iuam = False
for ( m, method_name ) in possible_paths:
+ if m is None:
+
+ continue
+
+
if hasattr( m, method_name ):
is_iuam = getattr( m, method_name )( response )
@@ -892,7 +922,7 @@ class NetworkJob( object ):
- def _WaitOnConnectionError( self, status_text ):
+ def _WaitOnConnectionError( self, status_text: str ):
connection_error_wait_time = HG.client_controller.new_options.GetInteger( 'connection_error_wait_time' )
@@ -902,7 +932,22 @@ class NetworkJob( object ):
with self._lock:
- self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
+ self._status_text = '{} - retrying in {}'.format( status_text, ClientData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
+
+
+ time.sleep( 1 )
+
+
+ self._WaitOnNetworkTrafficPaused( status_text )
+
+
+ def _WaitOnNetworkTrafficPaused( self, status_text: str ):
+
+ while HG.client_controller.new_options.GetBoolean( 'pause_all_new_network_traffic' ) and not self._IsCancelled():
+
+ with self._lock:
+
+ self._status_text = '{} - now waiting because all network traffic is paused'.format( status_text )
time.sleep( 1 )
@@ -917,7 +962,7 @@ class NetworkJob( object ):
- def _WaitOnServersideBandwidth( self, status_text ):
+ def _WaitOnServersideBandwidth( self, status_text: str ):
# 429 or 509 response from server. basically means 'I'm under big load mate'
# a future version of this could def talk to domain manager and add a temp delay so other network jobs can be informed
@@ -930,12 +975,14 @@ class NetworkJob( object ):
with self._lock:
- self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
+ self._status_text = '{} - retrying in {}'.format( status_text, ClientData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
time.sleep( 1 )
+ self._WaitOnNetworkTrafficPaused( status_text )
+
def AddAdditionalHeader( self, key, value ):
@@ -1884,6 +1931,43 @@ class NetworkJobSubscription( NetworkJob ):
return network_contexts
+def CheckHydrusVersion( service_type, response ):
+
+ service_string = HC.service_string_lookup[ service_type ]
+
+ headers = response.headers
+
+ if 'server' in headers and service_string in headers[ 'server' ]:
+
+ server_header = headers[ 'server' ]
+
+ elif 'hydrus-server' in headers and service_string in headers[ 'hydrus-server' ]:
+
+ server_header = headers[ 'hydrus-server' ]
+
+ else:
+
+ raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
+
+
+ ( service_string_gumpf, network_version ) = server_header.split( '/' )
+
+ network_version = int( network_version )
+
+ if network_version != HC.NETWORK_VERSION:
+
+ if network_version > HC.NETWORK_VERSION:
+
+ message = 'Your client is out of date; please download the latest release.'
+
+ else:
+
+ message = 'The server is out of date; please ask its admin to update to the latest release.'
+
+
+ raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! The server\'s network version was ' + str( network_version ) + ', whereas your client\'s is ' + str( HC.NETWORK_VERSION ) + '! ' + message )
+
+
class NetworkJobHydrus( NetworkJob ):
WILLING_TO_WAIT_ON_INVALID_LOGIN = False
@@ -1896,50 +1980,12 @@ class NetworkJobHydrus( NetworkJob ):
NetworkJob.__init__( self, method, url, body = body, referral_url = referral_url, temp_path = temp_path )
- def _CheckHydrusVersion( self, service_type, response ):
-
- service_string = HC.service_string_lookup[ service_type ]
-
- headers = response.headers
-
- if 'server' in headers and service_string in headers[ 'server' ]:
-
- server_header = headers[ 'server' ]
-
- elif 'hydrus-server' in headers and service_string in headers[ 'hydrus-server' ]:
-
- server_header = headers[ 'hydrus-server' ]
-
- else:
-
- raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
-
-
- ( service_string_gumpf, network_version ) = server_header.split( '/' )
-
- network_version = int( network_version )
-
- if network_version != HC.NETWORK_VERSION:
-
- if network_version > HC.NETWORK_VERSION:
-
- message = 'Your client is out of date; please download the latest release.'
-
- else:
-
- message = 'The server is out of date; please ask its admin to update to the latest release.'
-
-
- raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! The server\'s network version was ' + str( network_version ) + ', whereas your client\'s is ' + str( HC.NETWORK_VERSION ) + '! ' + message )
-
-
-
def _GenerateNetworkContexts( self ):
- network_contexts = []
-
- network_contexts.append( ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT )
- network_contexts.append( ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_HYDRUS, self._service_key ) )
+ network_contexts = [
+ ClientNetworkingContexts.GLOBAL_NETWORK_CONTEXT,
+ ClientNetworkingContexts.NetworkContext( CC.NETWORK_CONTEXT_HYDRUS, self._service_key )
+ ]
return network_contexts
@@ -1987,7 +2033,7 @@ class NetworkJobHydrus( NetworkJob ):
if response.ok and service_type in HC.RESTRICTED_SERVICES:
- self._CheckHydrusVersion( service_type, response )
+ CheckHydrusVersion( service_type, response )
return response
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index 567f3253..d2fb18f9 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -79,8 +79,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 478
-CLIENT_API_VERSION = 28
+SOFTWARE_VERSION = 479
+CLIENT_API_VERSION = 29
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/HydrusImageHandling.py b/hydrus/core/HydrusImageHandling.py
index 8eeef878..4abaf987 100644
--- a/hydrus/core/HydrusImageHandling.py
+++ b/hydrus/core/HydrusImageHandling.py
@@ -97,7 +97,7 @@ try:
# this preserves colour info but does EXIF reorientation and flipping
CV_IMREAD_FLAGS_JPEG = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR
# this seems to allow weirdass tiffs to load as non greyscale, although the LAB conversion 'whitepoint' or whatever can be wrong
- CV_IMREAD_FLAGS_WEIRD = cv2.IMREAD_ANYDEPTH | cv2.IMREAD_ANYCOLOR
+ CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_JPEG_QUALITY, 92 ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2.IMWRITE_PNG_COMPRESSION, 9 ]
@@ -310,7 +310,7 @@ def GenerateNumPyImage( path, mime, force_pil = False ) -> numpy.array:
HydrusData.ShowText( 'Loading with OpenCV' )
- if mime == HC.IMAGE_JPEG:
+ if mime in ( HC.IMAGE_JPEG, HC.IMAGE_TIFF ):
flags = CV_IMREAD_FLAGS_JPEG
@@ -450,7 +450,7 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
pil_image = GeneratePILImage( path )
- if clip_rect is None:
+ if clip_rect is not None:
pil_image = ClipPILImage( pil_image, clip_rect )
diff --git a/hydrus/test/TestClientAPI.py b/hydrus/test/TestClientAPI.py
index 41561551..167d08a0 100644
--- a/hydrus/test/TestClientAPI.py
+++ b/hydrus/test/TestClientAPI.py
@@ -2237,6 +2237,69 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( set( d[ 'hashes' ] ), expected_hashes_set )
+ self.assertIn( 'file_ids', d )
+
+ [ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
+
+ ( file_search_context, ) = args
+
+ self.assertEqual( file_search_context.GetLocationContext().current_service_keys, { CC.LOCAL_FILE_SERVICE_KEY } )
+ self.assertEqual( file_search_context.GetTagSearchContext().service_key, CC.COMBINED_TAG_SERVICE_KEY )
+ self.assertEqual( set( file_search_context.GetPredicates() ), { ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, tag ) for tag in tags } )
+
+ self.assertIn( 'sort_by', kwargs )
+
+ sort_by = kwargs[ 'sort_by' ]
+
+ self.assertEqual( sort_by.sort_type, ( 'system', CC.SORT_FILES_BY_IMPORT_TIME ) )
+ self.assertEqual( sort_by.sort_order, CC.SORT_DESC )
+
+ self.assertIn( 'apply_implicit_limit', kwargs )
+
+ self.assertEqual( kwargs[ 'apply_implicit_limit' ], False )
+
+ [ ( args, kwargs ) ] = HG.test_controller.GetRead( 'hash_ids_to_hashes' )
+
+ hash_ids = kwargs[ 'hash_ids' ]
+
+ self.assertEqual( set( hash_ids ), sample_hash_ids )
+
+ self.assertEqual( set( hash_ids ), set( d[ 'file_ids' ] ) )
+
+ # search files and only get hashes
+
+ HG.test_controller.ClearReads( 'file_query_ids' )
+
+ sample_hash_ids = set( random.sample( hash_ids, 3 ) )
+
+ hash_ids_to_hashes = { hash_id : os.urandom( 32 ) for hash_id in sample_hash_ids }
+
+ HG.test_controller.SetRead( 'file_query_ids', set( sample_hash_ids ) )
+
+ HG.test_controller.SetRead( 'hash_ids_to_hashes', hash_ids_to_hashes )
+
+ tags = [ 'kino', 'green' ]
+
+ path = '/get_files/search_files?tags={}&return_hashes=true&return_file_ids=false'.format( urllib.parse.quote( json.dumps( tags ) ) )
+
+ connection.request( 'GET', path, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ text = str( data, 'utf-8' )
+
+ self.assertEqual( response.status, 200 )
+
+ d = json.loads( text )
+
+ expected_hashes_set = { hash.hex() for hash in hash_ids_to_hashes.values() }
+
+ self.assertEqual( set( d[ 'hashes' ] ), expected_hashes_set )
+
+ self.assertNotIn( 'file_ids', d )
+
[ ( args, kwargs ) ] = HG.test_controller.GetRead( 'file_query_ids' )
( file_search_context, ) = args
@@ -2629,6 +2692,7 @@ class TestClientAPI( unittest.TestCase ):
expected_identifier_result = { 'metadata' : metadata }
media_results = []
+ file_info_managers = []
urls = { "https://gelbooru.com/index.php?page=post&s=view&id=4841557", "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg" }
@@ -2653,6 +2717,8 @@ class TestClientAPI( unittest.TestCase ):
file_info_manager = ClientMediaManagers.FileInfoManager( file_id, hash, size = size, mime = mime, width = width, height = height, duration = duration, has_audio = has_audio )
+ file_info_managers.append( file_info_manager )
+
service_keys_to_statuses_to_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue_eyes', 'blonde_hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit' ] } }
service_keys_to_statuses_to_display_tags = { CC.DEFAULT_LOCAL_TAG_SERVICE_KEY : { HC.CONTENT_STATUS_CURRENT : [ 'blue eyes', 'blonde hair' ], HC.CONTENT_STATUS_PENDING : [ 'bodysuit', 'clothing' ] } }
@@ -2684,6 +2750,7 @@ class TestClientAPI( unittest.TestCase ):
metadata = []
detailed_known_urls_metadata = []
with_notes_metadata = []
+ only_return_basic_information_metadata = []
services_manager = HG.client_controller.services_manager
@@ -2704,7 +2771,12 @@ class TestClientAPI( unittest.TestCase ):
'duration' : file_info_manager.duration,
'has_audio' : file_info_manager.has_audio,
'num_frames' : file_info_manager.num_frames,
- 'num_words' : file_info_manager.num_words,
+ 'num_words' : file_info_manager.num_words
+ }
+
+ only_return_basic_information_metadata.append( dict( metadata_row ) )
+
+ metadata_row.update( {
'file_services' : {
'current' : {
random_file_service_hex_current.hex() : {
@@ -2723,7 +2795,7 @@ class TestClientAPI( unittest.TestCase ):
'is_local' : False,
'is_trashed' : False,
'known_urls' : list( sorted_urls )
- }
+ } )
tags_manager = media_result.GetTagsManager()
@@ -2806,11 +2878,14 @@ class TestClientAPI( unittest.TestCase ):
expected_metadata_result = { 'metadata' : metadata }
expected_detailed_known_urls_metadata_result = { 'metadata' : detailed_known_urls_metadata }
expected_notes_metadata_result = { 'metadata' : with_notes_metadata }
+ expected_only_return_basic_information_result = { 'metadata' : only_return_basic_information_metadata }
HG.test_controller.SetRead( 'hash_ids_to_hashes', file_ids_to_hashes )
HG.test_controller.SetRead( 'media_results', media_results )
HG.test_controller.SetRead( 'media_results_from_ids', media_results )
+ HG.test_controller.SetRead( 'file_info_managers', file_info_managers )
+ HG.test_controller.SetRead( 'file_info_managers_from_ids', file_info_managers )
api_permissions.SetLastSearchResults( [ 1, 2, 3, 4, 5, 6 ] )
@@ -2856,6 +2931,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( d, expected_identifier_result )
+ # basic metadata from file_ids
+
+ path = '/get_files/file_metadata?file_ids={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) )
+
+ connection.request( 'GET', path, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ text = str( data, 'utf-8' )
+
+ self.assertEqual( response.status, 200 )
+
+ d = json.loads( text )
+
+ self.assertEqual( d, expected_only_return_basic_information_result )
+
# metadata from file_ids
path = '/get_files/file_metadata?file_ids={}'.format( urllib.parse.quote( json.dumps( [ 1, 2, 3 ] ) ) )
@@ -2900,6 +2993,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( d, expected_identifier_result )
+ # basic metadata from hashes
+
+ path = '/get_files/file_metadata?hashes={}&only_return_basic_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) )
+
+ connection.request( 'GET', path, headers = headers )
+
+ response = connection.getresponse()
+
+ data = response.read()
+
+ text = str( data, 'utf-8' )
+
+ self.assertEqual( response.status, 200 )
+
+ d = json.loads( text )
+
+ self.assertEqual( d, expected_only_return_basic_information_result )
+
# metadata from hashes
path = '/get_files/file_metadata?hashes={}'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) )
diff --git a/mkdocs-gh-pages.yml b/mkdocs-gh-pages.yml
index 922e396e..52e52687 100644
--- a/mkdocs-gh-pages.yml
+++ b/mkdocs-gh-pages.yml
@@ -33,7 +33,6 @@ plugins:
'help/getting_started_downloading.md': 'getting_started_downloading.md'
'help/getting_started_files.md': 'getting_started_files.md'
'help/getting_started_installing.md': 'getting_started_installing.md'
- 'help/getting_started_more_files.md': 'getting_started_more_files.md'
'help/getting_started_ratings.md': 'getting_started_ratings.md'
'help/getting_started_subscriptions.md': 'getting_started_subscriptions.md'
'help/getting_started_tags.md': 'getting_started_tags.md'
@@ -47,4 +46,4 @@ plugins:
'help/running_from_source.md': 'running_from_source.md'
'help/server.md': 'server.md'
'help/support.md': 'support.md'
- 'help/wine.md': 'wine.md'
\ No newline at end of file
+ 'help/wine.md': 'wine.md'
diff --git a/mkdocs.yml b/mkdocs.yml
index 132d3575..c95645d7 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -19,7 +19,8 @@ nav:
- PTR.md
- petitionPractices.md
- Next Steps:
- - getting_started_more_files.md
+ - getting_started_searching.md
+ - getting_started_exporting.md
- adding_new_downloaders.md
- getting_started_subscriptions.md
- filtering duplicates: duplicates.md