diff --git a/docs/access_keys.md b/docs/access_keys.md
index f96b60557..8e40d92e9 100644
--- a/docs/access_keys.md
+++ b/docs/access_keys.md
@@ -29,9 +29,9 @@ Most users should have very few worries about privacy. The general rule is that
## a note on resources { id="ssd" }
!!! danger
- **If you are on an HDD, or your SSD does not have at least 64GB of free space, do not add the PTR!**
+ **If your database files are stored on an HDD, or your SSD does not have at least 96GB of free space, do not add the PTR!**
-The PTR has been operating since 2011 and is now huge, more than a billion mappings! Your client will be downloading and indexing them all, which is currently (2021-06) about 6GB of bandwidth and 50GB of hard drive space. It will take _hours_ of total processing time to catch up on all the years of submissions. Furthermore, because of mechanical drive latency, HDDs are too slow to process all the content in reasonable time. Syncing is only recommended if your [hydrus db is on an SSD](database_migration.md). Even then, it is healthier and allows the client to 'grow into' the PTR if the work is done in small pieces in the background, either during idle time or shutdown time, rather than trying to do it all at once. Just leave it to download and process on its own--it usually takes a couple of weeks to quietly catch up. You'll see tags appear on your files as it proceeds, first on older, then all the way up to new files just uploaded a couple days ago. Once you are synced, the daily processing work to stay synced is usually just a few minutes. If you leave your client on all the time in the background, you'll likely never notice it.
+The PTR has been operating since 2011 and is now huge, more than two billion mappings! Your client will be downloading and indexing them all, which is currently (2021-06) about 6GB of bandwidth and 50GB of hard drive space. It will take _hours_ of total processing time to catch up on all the years of submissions. Furthermore, because of mechanical drive latency, HDDs are too slow to process all the content in reasonable time. Syncing is only recommended if your [hydrus db is on an SSD](database_migration.md). It doesn't matter if you store your jpegs and webms and stuff on an external HDD; this is just your actual .db database files (normally in install_dir/db folder). Note also that it is healthier if the work is done in small pieces in the background, either during idle time or shutdown time, rather than trying to do it all at once. Just leave it to download and process on its own--it usually takes a couple of weeks to quietly catch up. If you happen to see it working, it will start as fast as 50,000 rows/s (with some bumps down to 1 rows/s as it commits data), and eventually it will slow, when fully synced, to 100-1,000 rows/s. You'll see tags appear on your files as processing continues, first on older, then all the way up to new files just uploaded a couple days ago. Once you are synced, the daily processing work to stay synced is usually just a few minutes. If you leave your client on all the time in the background, you'll likely never notice it.
## easy setup
diff --git a/docs/changelog.md b/docs/changelog.md
index ec7d76099..f563d6023 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -7,6 +7,48 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
+## [Version 573](https://github.com/hydrusnetwork/hydrus/releases/tag/v573)
+
+### new autocomplete tab, children
+
+* **this is an experiment. it is jank in form and workflow and may be buggy**
+* the search/edit tag autocomplete dropdowns now have a third tab, 'children', which shows the tag children of the current tag context, whether that is the current search tags or what you are editing
+* the idea is you type 'series:evangelion' but can't remember the character names; now you have a nice list of a bunch of stuff related to what was already entered
+* note you can select this tab real quick just by hitting 'left arrow' on an empty text input
+* this is a first draft, and I would like feedback and ideas, mostly around workflow improvement ideas. it seems to work ok if you have one or two tags with interesting children, but against a big list of stuff, it just becomes another multi-hundred list of spam blah that is difficult to navigate. maybe I could filter it to (and sort by?) the top n most count-heavy results?
+* I wonder if it could also show children on the same level, so if you have 'shinji', it'll also show 'rei' and 'asuka'. I would call this relationship 'siblings', but then we'd be in an even bigger semantic mess
+* also obviously please let me know if this fails anywhere. I think I have it hooked up correct, but some of the code around here is a bit old/messy so some scenario may not update properly
+* don't worry about background lag if you regularly manage lots of tags--it only actually fetches the list of children when you switch to the tab, so you're only spending CPU if you actively engage with it
+
+### misc
+
+* a user and I figured out a new twitter tweet downloader using the excellent fxtwitter mirror service. it doesn't do search, but dropping a tweet URL on the client should work again. should handle quoted media and works for multi/mixed-image/video posts, too. note it will nest-pursue quoted tweets, so if there's like fifty in the nested chain, it'll get them all--let me know if this is a big pain and I'll figure out a different solution. I learned that there is another twitter downloader made by a different user on the discord; I have made the update code check for this and not replace it with this if you have it already, and I expect I'll integrate what that can do into these defaults next week
+* the archive/delete and duplicate filters now yes/no confirm when you say to 'forget' at the end of a filtering run
+* the duplicate filter page now only allows you to set the search location to local file domains--so it'll only ever try to search and show pairs for files you actually have
+* fixed the system predicate parsing of `system:duration: has duration` and `system:duration: no duration` when entered by hand, and added a unit test to catch it in future
+* the manage siblings/parents dialogs now have a little shorter minimum height
+* updated some text around the PTR processing in the help--it is only the database proper, the .db files normally in `install_dir/db`, that needs to be on an SSD, and temporary processing slowdowns to 1 row/s are normal
+* touched up some of the 'installing' and 'running from source' help, particularly for some Linux vagaries
+
+### some build stuff
+
+* all the builds and the setup_venv scripts are moved from 'python-mpv' to 'mpv', the new name for this library, and the version is updated to 1.0.6, which supports libmpv version >=0.38.x. if you are a windows user and want to live on the edge, feel free to try out this very new libmpv2.dll here, which I have been testing and seems to work well: https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20240421-git-b364e4a.7z/download
+* updated the setup_venv scripts' Qt step to better talk about which Qt version to use for which Python version. it turns out Python 3.12 cannot run something I was recommending for >=3.11, so the whole thing is a lot clearer now
+
+### boring stuff
+
+* refactored some question/button dialog stuff
+* fixed up some file domain filtering code in the autocomplete filter and variable names to better specify what is being filtered where
+
+### local booru deconstruction
+
+* _reminder: I am removing the local booru, an ancient, mostly undocumented experiment._ if you used it, please check out https://github.com/floogulinc/hyshare for a replacement!
+* the local booru service no longer boots as a server
+* deleted the local booru share cache
+* the local booru review services panel no longer shows nor allows management of its shares
+* deleted the local booru unit tests
+* deleted the local booru help and ancient screenshots
+
## [Version 572](https://github.com/hydrusnetwork/hydrus/releases/tag/v572)
### misc
@@ -401,38 +443,3 @@ title: Changelog
* now they have had time to breathe, I optimised the recently split Github build scripts. the 'send to an ubuntu runner and then upload' step is now removed from all three, so they are natively uploaded in the first runner step. it works just a little nicer and faster now, although it did require learning how to truncate and export a variable to the Github Environment Variables file in Powershell, aiiieeeee
* also, Github is moving from Node 16 to Node 20 soon, and I have moved two of the four actions we rely on to their newer v20 versions. a third action should be ready to update next week, and another, a general download file function, I have replaced with curl (for macOS) and Powershell's magical Invoke-WebRequest adventure
-
-## [Version 562](https://github.com/hydrusnetwork/hydrus/releases/tag/v562)
-
-### misc
-
-* page tab drag and drops will now not start unless the click has lasted more than 100ms
-* same for thumbnail drag and drop--it perviously did a 20 pixel deadzone, but time checks detect accidental/spastic clicks better and stops false negatives when you start dragging on certain edges
-* added a 'BUGFIX: disable page tab drag and drop' setting to _options->gui pages_. while adding this, I may have accidentally fixed the issue I wanted to investigate (rare hangs on page DnD)
-* the manage tags dialog now shows the current count of tags for each page tab, and, if there are outstanding changes, shows an asterisk
-* the `migrate database` dialog is renamed `move media files`
-
-### fixes
-
-* fixed the basic copy/paste in the single 'edit datetime' panel, wich was often raising a dumb error. this thing also now exports millisecond data (issue #1520)
-* I am pretty sure I fixed the column-resizing problem in the very new PySide6 (Qt) 6.6.1, which it seems AUR users were recently updated to in an automatic OS update. all columns were setting to 100px width on initialisation. I think it is now safe to try out 6.6.1. I am still not sure why it was doing this, but some extra safeguards seem to have fixed it and also not broken things for <=6.6.0, so let me know what you run into! if you were affected by this, recall that you can right-click on any multi-column list header and say 'reset widths' to get something sensible back here
-* when exporting files, the max size is now clipped another 84 characters (64 + 20 more, which usually ends up about 150 characters max for the output filename), in order to give padding for longer sidecar suffixes and also avoid going right to the filesystem limit, which broadly isn't sensible
-* I think I fixed an issue where the mouse could stay hidden, perhaps, just on Wayland, after closing the media viewer with your keyboard (issue #1518)
-* fixed inc/dec ratings in the media viewer not updating their tooltips on new media correctly
-* if you hit 'open this location' on the export files window and the location does not exist, you now get a nice messagebox rather than a semi-silent error
-
-### analyze
-
-* background: some databases that process the PTR superfast or otherwise import a lot of data to a new file domain sometimes encounter massively massively slow tag update actions (typically tag-delete when the tags involved have siblings/parents), so I want to make the critical 'ANALYZE' call more timely
-* the 'analyze' database maintenance call will be soft-called far more regularly during normal repository processing, not just on first sync
-* sped up how some pre-analyze calculation is done
-* the size limit for automatic database analyze maintenance is raised from 100k rows to 10M
-* I hope to do more work here in future, probably making a review panel like we did for vacuum
-* if your repository processing sometimes hangs your whole damn client for 10-15 minutes, hit _database->db maintenance->analyze->full_! this job may take 30-60 minutes to finish
-
-### boring code cleanup
-
-* finished the HG->CG.client_controller refactor I started last week. this was a thousand lines changed from one braindead format to another, but it will be a useful step towards untangling the hell-nest import hierarchy
-* did a scattering of the clientinterface typing, getting a feel for where I want to take this
-* deleted the old in-client server-test's 'boot' variant; this is no longer used and was always super hacky to maintain
-* I removed an old basic error raising routine that would sometimes kick in when a hash definition is missing. this routine now always fills in the missing data with garbage and does its best to recover the invalid situation automatically, with decent logging, while still informing the user that things are well busted m8. it isn't the user's job to fix this, and there is no good fix anyway, so no point halting work and giving it to the user to figure out!
diff --git a/docs/getting_started_installing.md b/docs/getting_started_installing.md
index 5b005ba3e..83268a333 100644
--- a/docs/getting_started_installing.md
+++ b/docs/getting_started_installing.md
@@ -14,9 +14,6 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
## Installing
-!!! warning ""
- The hydrus releases are 64-bit only. If you are a python expert, there is the slimmest chance you'll be able to get it running from source on a 32-bit machine, but it would be easier just to find a newer computer to run it on.
-
=== "Windows"
* If you want the easy solution, download the .exe installer. Run it, hit ok several times.
@@ -40,9 +37,17 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
!!! warning "Wayland"
Unfortunately, hydrus has several bad bugs in Wayland. The mpv window will often not embed properly into the media viewer, menus and windows may position on the wrong screen, and the taskbar icon may not work at all. [Running from source](running_from_source.md) may improve the situation, but some of these issues seem to be intractable for now. X11 is much happier with hydrus.
-
+
+ !!! note "XCB Qt compatibility"
+
+ If you run into trouble running Qt6, usually with an XCB-related error like `qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.`, try installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+
+ * `sudo apt-get install libicu-dev`
+ * `sudo apt-get install libxcb-cursor-dev`
+
+
* Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. The build is made on Ubuntu, so if you run something else, compatibility is hit and miss.
- * If you have problems running the Ubuntu build, users with some python experience generally find running from source works well.
+ * If you have problems running the Ubuntu build, [running from source](running_from_source.md) is usually an improvement, and it is easy to set up these days.
* You might need to get 'libmpv1' to get mpv working and playing video/audio. This is the mpv _library_, not the necessarily the player. Check _help->about_ to see if it is available--if not, see if you can get it like so:
* `apt-get install libmpv1`
* Use _options->media_ to set your audio/video/animations to 'show using mpv' once you have it installed.
diff --git a/docs/images/local_booru_dialog.png b/docs/images/local_booru_dialog.png
deleted file mode 100644
index f2db86cad..000000000
Binary files a/docs/images/local_booru_dialog.png and /dev/null differ
diff --git a/docs/images/local_booru_html.png b/docs/images/local_booru_html.png
deleted file mode 100644
index c30f01102..000000000
Binary files a/docs/images/local_booru_html.png and /dev/null differ
diff --git a/docs/images/local_booru_services.png b/docs/images/local_booru_services.png
deleted file mode 100644
index 958c8a1b8..000000000
Binary files a/docs/images/local_booru_services.png and /dev/null differ
diff --git a/docs/images/screenshot_booru.png b/docs/images/screenshot_booru.png
deleted file mode 100644
index 1682c0f89..000000000
Binary files a/docs/images/screenshot_booru.png and /dev/null differ
diff --git a/docs/images/screenshot_booru_thumb.png b/docs/images/screenshot_booru_thumb.png
deleted file mode 100644
index 287cea2a9..000000000
Binary files a/docs/images/screenshot_booru_thumb.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index 36968e598..c6ccd703f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -54,5 +54,4 @@ If you would like to try hydrus, I _**strongly**_ recommend you check out the **
[![](images/screenshot_gunnerkrigg_collect_thumb.png)](images/screenshot_gunnerkrigg_collect.png "Files can be sorted and collected by their tags.")
[![](images/screenshot_fullscreen_blame_thumb.png)](images/screenshot_fullscreen_blame.png "The media viewer can work as a resizable window or completely fullscreen. It is clean and fast.")
[![](images/screenshot_video_thumb.png)](images/screenshot_video.png "Many file formats are supported.")
-[![](images/screenshot_booru_thumb.png)](images/screenshot_booru.png "You can run your own (simple!) booru")
[![](images/screenshot_advanced_autocomplete_thumb.png)](images/screenshot_advanced_autocomplete.png "The client can get complicated if you want it to. This screenshot shows a tag sibling, where one tag is immediately swapped with another, and a non-local search, where results that are known but not on the computer are shown.")
diff --git a/docs/local_booru.md b/docs/local_booru.md
deleted file mode 100644
index 62eff3772..000000000
--- a/docs/local_booru.md
+++ /dev/null
@@ -1,67 +0,0 @@
----
-title: Local Booru
----
-
-# local booru
-
-!!! warning
- This was a fun project, but it never advanced beyond a prototype. The future of this system is other people's nice applications plugging into the [Client API](client_api.md).
-
-The hydrus client has a simple booru to help you share your files with others over the internet.
-
-First of all, this is **hosted from your client**, which means other people will be connecting to your computer and fetching files you choose to share from your hard drive. If you close your client or shut your computer down, the local booru will no longer work.
-
-## how to do it { id="setting_up" }
-
-First of all, turn the local booru server on by going to _services->manage services_ and giving it a port:
-
-![](images/local_booru_services.png)
-
-It doesn't matter what you pick, but make it something fairly high. When you ok that dialog, the client should start the booru. You may get a firewall warning.
-
-Then right click some files you want to share and select _share->local booru_. This will throw up a small dialog, like so:
-
-![](images/local_booru_dialog.png)
-
-This lets you enter an optional _name_, which titles the share and helps you keep track of it, an optional _text_, which lets you say some words or html to the people you are sharing with, and an _expiry_, which lets you determine if and when the share will no longer work.
-
-You can also copy either the internal or external link to your clipboard. The internal link (usually starting something like `http://127.0.0.1:45866/`) works inside your network and is great just for testing, while the external link (starting `http://[your external ip address]:[external port]/`) will work for anyone around the world, **as long as your booru's port is being forwarded correctly**.
-
-If you use a dynamic-ip service like [No-IP](https://www.noip.com/), you can replace your external IP with your redirect hostname. You have to do it by hand right now, but I'll add a way to do it automatically in future.
-
-!!! danger
- Note that anyone with the external link will be able to see your share, so make sure you only share links with people you trust.
-
-## forwarding your port { id="port_forwarding" }
-
-Your home router acts as a barrier between the computers inside the network and the internet. Those inside can see out, but outsiders can only see what you tell the router to permit. Since you want to let people connect to your computer, you need to tell the router to forward all requests of a certain kind to your computer, and thus your client.
-
-If you have never done this before, it can be a headache, especially doing it manually. Luckily, a technology called UPnP makes it a ton easier, and this is how your Skype or Bittorrent clients do it automatically. Not all routers support it, but most do. You can have hydrus try to open a port this way back on _services->manage services_. Unless you know what you are doing and have a good reason to make them different, you might as well keep the internal and external ports the same.
-
-Once you have it set up, the client will try to make sure your router keeps that port open for your client. If it all works, you should see the new mapping appear in your _services->manage local upnp_ dialog, which lists all your router's current port mappings.
-
-If you want to test that the port forward is set up correctly, going to `http://[external ip]:[external port]/` should give a little html just saying hello. Your ISP might not allow you to talk to yourself, though, so ask a friend to try if you are having trouble.
-
-If you still do not understand what is going on here, [this](http://www.howtogeek.com/66214/how-to-forward-ports-on-your-router/) is a good article explaining everything.
-
-If you do not like UPnP or your router does not support it, you can set the port forward up manually, but I encourage you to keep the internal and external port the same, because absent a 'upnp port' option, the 'copy external share link' button will use the internal port.
-
-## so, what do you get? { id="example" }
-
-The html layout is very simple:
-
-* * *
-
-![](images/local_booru_html.png)
-
-* * *
-
-It uses a very similar stylesheet to these help pages. If you would like to change the style, have a look at the html and then edit install\_dir/static/local\_booru_style.css. The thumbnails will be the same size as in your client.
-
-## editing an existing share { id="editing_shares" }
-
-You can review all your shares on _services->review services_, under _local->booru_. You can copy the links again, change the title/text/expiration, and delete any shares you don't want any more.
-
-## future plans { id="future" }
-
-This was a fun project, but it never advanced beyond a prototype. The future of this system is other people's nice applications plugging into the [Client API](client_api.md).
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index 2f6976d7f..4bc8c0643 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,41 @@
+ -
+
+
+ new autocomplete tab, children
+ - **this is an experiment. it is jank in form and workflow and may be buggy**
+ - the search/edit tag autocomplete dropdowns now have a third tab, 'children', which shows the tag children of the current tag context, whether that is the current search tags or what you are editing
+ - the idea is you type 'series:evangelion' but can't remember the character names; now you have a nice list of a bunch of stuff related to what was already entered
+ - note you can select this tab real quick just by hitting 'left arrow' on an empty text input
+ - this is a first draft, and I would like feedback and ideas, mostly around workflow improvement ideas. it seems to work ok if you have one or two tags with interesting children, but against a big list of stuff, it just becomes another multi-hundred list of spam blah that is difficult to navigate. maybe I could filter it to (and sort by?) the top n most count-heavy results?
+ - I wonder if it could also show children on the same level, so if you have 'shinji', it'll also show 'rei' and 'asuka'. I would call this relationship 'siblings', but then we'd be in an even bigger semantic mess
+ - also obviously please let me know if this fails anywhere. I think I have it hooked up correct, but some of the code around here is a bit old/messy so some scenario may not update properly
+ - don't worry about background lag if you regularly manage lots of tags--it only actually fetches the list of children when you switch to the tab, so you're only spending CPU if you actively engage with it
+ misc
+ - a user and I figured out a new twitter tweet downloader using the excellent fxtwitter mirror service. it doesn't do search, but dropping a tweet URL on the client should work again. should handle quoted media and works for multi/mixed-image/video posts, too. note it will nest-pursue quoted tweets, so if there's like fifty in the nested chain, it'll get them all--let me know if this is a big pain and I'll figure out a different solution. I learned that there is another twitter downloader made by a different user on the discord; I have made the update code check for this and not replace it with this if you have it already, and I expect I'll integrate what that can do into these defaults next week
+ - the archive/delete and duplicate filters now yes/no confirm when you say to 'forget' at the end of a filtering run
+ - the duplicate filter page now only allows you to set the search location to local file domains--so it'll only ever try to search and show pairs for files you actually have
+ - fixed the system predicate parsing of `system:duration: has duration` and `system:duration: no duration` when entered by hand, and added a unit test to catch it in future
+ - the manage siblings/parents dialogs now have a little shorter minimum height
+ - updated some text around the PTR processing in the help--it is only the database proper, the .db files normally in `install_dir/db`, that needs to be on an SSD, and temporary processing slowdowns to 1 row/s are normal
+ - touched up some of the 'installing' and 'running from source' help, particularly for some Linux vagaries
+ some build stuff
+ - all the builds and the setup_venv scripts are moved from 'python-mpv' to 'mpv', the new name for this library, and the version is updated to 1.0.6, which supports libmpv version >=0.38.x. if you are a windows user and want to live on the edge, feel free to try out this very new libmpv2.dll here, which I have been testing and seems to work well: https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20240421-git-b364e4a.7z/download
+ - updated the setup_venv scripts' Qt step to better talk about which Qt version to use for which Python version. it turns out Python 3.12 cannot run something I was recommending for >=3.11, so the whole thing is a lot clearer now
+ boring stuff
+ - refactored some question/button dialog stuff
+ - fixed up some file domain filtering code in the autocomplete filter and variable names to better specify what is being filtered where
+ local booru deconstruction
+ - _reminder: I am removing the local booru, an ancient, mostly undocumented experiment._ if you used it, please check out https://github.com/floogulinc/hyshare for a replacement!
+ - the local booru service no longer boots as a server
+ - deleted the local booru share cache
+ - the local booru review services panel no longer shows nor allows management of its shares
+ - deleted the local booru unit tests
+ - deleted the local booru help and ancient screenshots
+
+
-
diff --git a/docs/running_from_source.md b/docs/running_from_source.md
index 778d8131e..5cae29e40 100644
--- a/docs/running_from_source.md
+++ b/docs/running_from_source.md
@@ -90,11 +90,11 @@ There are three special external libraries. You just have to get them and put th
1. mpv
1. If you are on Windows 8.1 or older, [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210228-git-d1be8bb.7z) is known safe.
- 2. If you are on Windows 10 or newer and want the simple answer, try [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20220501-git-9ffaa6b.7z).
- 3. Ideally, go for [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20230212-git-a40958c.7z), but you have to rename `libmpv-2.dll` to `mpv-2.dll`.
- 4. I have been testing [this newer version](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20230820-git-19384e0.7z) and [this very new version](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20231231-git-abc2a74.7z) and things seem to be fine too, at least on updated Windows. If you use the '(t)est' python-mpv, 1.0.5, you do not have to rename `libmpv-2.dll` to `mpv-2.dll`.
+ 2. If you are on Windows 10 or newer and want the very safe answer, try [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20220501-git-9ffaa6b.7z).
+ 3. Otherwise, go for [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20230820-git-19384e0.7z).
+ 4. I have been testing [this newer version](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20231231-git-abc2a74.7z) and [this very new version](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20240421-git-b364e4a.7z/download) and things seem to be fine too, at least on updated Windows.
- Then open that archive and place the 'mpv-1.dll' or 'mpv-2.dll' into `install_dir`.
+ Then open that archive and place the 'mpv-1.dll'/'mpv-2.dll'/'libmpv-2.dll' into `install_dir`.
??? info "mpv on older Windows"
I have word that that newer mpv, the API version 2.1 that you have to rename to mpv-2.dll, will work on Qt5 and Windows 7. If this applies to you, have a play around with different versions here. You'll need the newer mpv choice in the setup-venv script however, which, depending on your situation, may not be possible.
@@ -127,13 +127,6 @@ There are three special external libraries. You just have to get them and put th
You should already have ffmpeg. Just type `ffmpeg` into a new terminal, and it should give a basic version response. If you somehow don't have ffmpeg, check your package manager.
- ??? "Qt compatibility note"
-
- If you run into trouble running newer versions of Qt6, which you will be setting up later, some users have fixed it by installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
-
- * `sudo apt-get install libicu-dev`
- * `sudo apt-get install libxcb-cursor-dev`
-
=== "macOS"
@@ -204,6 +197,16 @@ Then run the 'setup_help' script to build the help. This isn't necessary, but it
=== "Linux"
+ !!! note "Qt compatibility"
+
+ If you run into trouble running newer versions of Qt6, some users have fixed it by installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+
+ * `sudo apt-get install libicu-dev`
+ * `sudo apt-get install libxcb-cursor-dev`
+
+ If you still have trouble with the default Qt6 version, try running setup_venv again and choose a different version. There are several to choose from, including (w)riting a custom version. Check the advanced requirements.txts files in `install_dir/static/requirements/advanced` for more info, and you can also work off this list: [PySide6](https://pypi.org/project/PySide6/#history)
+
+
Run 'hydrus_client.sh' to start the client. Don't forget to set `chmod +x hydrus_client.sh` if you need it.
=== "macOS"
@@ -320,16 +323,14 @@ If you want to set QT_API in a batch file, do this:
If you run <= Windows 8.1 or Ubuntu 18.04, you cannot run Qt6. Try PySide2 or PyQt5.
-??? "Qt compatibility notes"
+!!! note "Qt compatibility"
- If you run into trouble running newer versions of Qt6 on Linux, some users have fixed it by installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
+ If you run into trouble running newer versions of Qt6 on Linux, often with an XCB-related error such as `qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.`, try installing the packages `libicu-dev` and `libxcb-cursor-dev`. With `apt` that will be:
* `sudo apt-get install libicu-dev`
* `sudo apt-get install libxcb-cursor-dev`
- If you still have trouble with the default Qt6 version, or you rebuilt your venv and the newer version of Qt6 gives you problems, check out the setup_venv script language and the advanced requirements.txts files it relies on in `install_dir/static/requirements/advanced`. There should be several older version examples you can try out.
-
- To install a specific version of a library with pip, activate your venv and then type something like `pip install PySide6==6.3.1`.
+ If you still have trouble with the default Qt6 version, check the advanced requirements.txts in `install_dir/static/requirements/advanced`. There should be several older version examples you can explore, and you can also work off these lists: [PySide6](https://pypi.org/project/PySide6/#history) [PyQt6](https://pypi.org/project/PyQt6/#history) [PySide2](https://pypi.org/project/PySide2/#history) [Pyqt5](https://pypi.org/project/PyQt5/#history)
### mpv { id="mpv" }
diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py
index 6eee474fe..a2f17da67 100644
--- a/hydrus/client/ClientController.py
+++ b/hydrus/client/ClientController.py
@@ -1180,8 +1180,6 @@ def InitModel( self ):
#
- self.local_booru_manager = ClientCaches.LocalBooruCache( self )
-
self.file_viewing_stats_manager = ClientManagers.FileViewingStatsManager( self )
#
@@ -1672,7 +1670,7 @@ def ResetPageChangeTimer( self ):
def RestartClientServerServices( self ):
- services = [ self.services_manager.GetService( service_key ) for service_key in ( CC.LOCAL_BOORU_SERVICE_KEY, CC.CLIENT_API_SERVICE_KEY ) ]
+ services = [ self.services_manager.GetService( service_key ) for service_key in ( CC.CLIENT_API_SERVICE_KEY, ) ]
services = [ service for service in services if service.GetPort() is not None ]
@@ -1922,11 +1920,7 @@ def StartServices( *args, **kwargs ):
from hydrus.client.networking import ClientLocalServer
- if service_type == HC.LOCAL_BOORU:
-
- http_factory = ClientLocalServer.HydrusServiceBooru( service, allow_non_local_connections = allow_non_local_connections )
-
- elif service_type == HC.CLIENT_API_SERVICE:
+ if service_type == HC.CLIENT_API_SERVICE:
http_factory = ClientLocalServer.HydrusServiceClientAPI( service, allow_non_local_connections = allow_non_local_connections )
diff --git a/hydrus/client/ClientLocation.py b/hydrus/client/ClientLocation.py
index 5e2d13cae..122dc8551 100644
--- a/hydrus/client/ClientLocation.py
+++ b/hydrus/client/ClientLocation.py
@@ -36,13 +36,13 @@ def FilterOutRedundantMetaServices( list_of_service_keys: typing.List[ bytes ] )
return list_of_service_keys
-def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_local_file_domains_allowed: bool ):
+def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool ):
services_manager = CG.client_controller.services_manager
service_types_in_order = [ HC.LOCAL_FILE_DOMAIN ]
- if not only_local_file_domains_allowed:
+ if not only_importable_domains_allowed:
advanced_mode = CG.client_controller.new_options.GetBoolean( 'advanced_mode' )
@@ -62,15 +62,21 @@ def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_lo
service_types_in_order.append( HC.COMBINED_LOCAL_FILE )
- service_types_in_order.append( HC.COMBINED_DELETED_FILE )
-
-
- service_types_in_order.append( HC.FILE_REPOSITORY )
- service_types_in_order.append( HC.IPFS )
- if all_known_files_allowed:
+ if not only_local_file_domains_allowed:
+
+ if advanced_mode:
+
+ service_types_in_order.append( HC.COMBINED_DELETED_FILE )
+
+
+ service_types_in_order.append( HC.FILE_REPOSITORY )
+ service_types_in_order.append( HC.IPFS )
- service_types_in_order.append( HC.COMBINED_FILE )
+ if all_known_files_allowed:
+
+ service_types_in_order.append( HC.COMBINED_FILE )
+
@@ -81,7 +87,7 @@ def GetPossibleFileDomainServicesInOrder( all_known_files_allowed: bool, only_lo
def SortFileServiceKeysNicely( list_of_service_keys ):
- services_in_nice_order = GetPossibleFileDomainServicesInOrder( False, False )
+ services_in_nice_order = GetPossibleFileDomainServicesInOrder( False, False, False )
service_keys_in_nice_order = [ service.GetServiceKey() for service in services_in_nice_order ]
diff --git a/hydrus/client/caches/ClientCaches.py b/hydrus/client/caches/ClientCaches.py
index b014da37f..59ff9225e 100644
--- a/hydrus/client/caches/ClientCaches.py
+++ b/hydrus/client/caches/ClientCaches.py
@@ -23,159 +23,6 @@
from hydrus.client import ClientThreading
from hydrus.client.caches import ClientCachesBase
-class LocalBooruCache( object ):
-
- def __init__( self, controller ):
-
- self._controller = controller
-
- self._lock = threading.Lock()
-
- self._RefreshShares()
-
- self._controller.sub( self, 'RefreshShares', 'refresh_local_booru_shares' )
- self._controller.sub( self, 'RefreshShares', 'restart_client_server_service' )
-
-
- def _CheckDataUsage( self ):
-
- if not self._local_booru_service.BandwidthOK():
-
- raise HydrusExceptions.InsufficientCredentialsException( 'This booru has used all its monthly data. Please try again next month.' )
-
-
-
- def _CheckFileAuthorised( self, share_key, hash ):
-
- self._CheckShareAuthorised( share_key )
-
- info = self._GetInfo( share_key )
-
- if hash not in info[ 'hashes_set' ]:
-
- raise HydrusExceptions.NotFoundException( 'That file was not found in that share.' )
-
-
-
- def _CheckShareAuthorised( self, share_key ):
-
- self._CheckDataUsage()
-
- info = self._GetInfo( share_key )
-
- timeout = info[ 'timeout' ]
-
- if timeout is not None and HydrusTime.TimeHasPassed( timeout ):
-
- raise HydrusExceptions.NotFoundException( 'This share has expired.' )
-
-
-
- def _GetInfo( self, share_key ):
-
- try: info = self._keys_to_infos[ share_key ]
- except: raise HydrusExceptions.NotFoundException( 'Did not find that share on this booru.' )
-
- if info is None:
-
- info = self._controller.Read( 'local_booru_share', share_key )
-
- hashes = info[ 'hashes' ]
-
- info[ 'hashes_set' ] = set( hashes )
-
- media_results = self._controller.Read( 'media_results', hashes )
-
- info[ 'media_results' ] = media_results
-
- hashes_to_media_results = { media_result.GetHash() : media_result for media_result in media_results }
-
- info[ 'hashes_to_media_results' ] = hashes_to_media_results
-
- self._keys_to_infos[ share_key ] = info
-
-
- return info
-
-
- def _RefreshShares( self ):
-
- self._local_booru_service = self._controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )
-
- self._keys_to_infos = {}
-
- share_keys = self._controller.Read( 'local_booru_share_keys' )
-
- for share_key in share_keys:
-
- self._keys_to_infos[ share_key ] = None
-
-
-
- def CheckShareAuthorised( self, share_key ):
-
- with self._lock: self._CheckShareAuthorised( share_key )
-
-
- def CheckFileAuthorised( self, share_key, hash ):
-
- with self._lock: self._CheckFileAuthorised( share_key, hash )
-
-
- def GetGalleryInfo( self, share_key ):
-
- with self._lock:
-
- self._CheckShareAuthorised( share_key )
-
- info = self._GetInfo( share_key )
-
- name = info[ 'name' ]
- text = info[ 'text' ]
- timeout = info[ 'timeout' ]
- media_results = info[ 'media_results' ]
-
- return ( name, text, timeout, media_results )
-
-
-
- def GetMediaResult( self, share_key, hash ):
-
- with self._lock:
-
- info = self._GetInfo( share_key )
-
- media_result = info[ 'hashes_to_media_results' ][ hash ]
-
- return media_result
-
-
-
- def GetPageInfo( self, share_key, hash ):
-
- with self._lock:
-
- self._CheckFileAuthorised( share_key, hash )
-
- info = self._GetInfo( share_key )
-
- name = info[ 'name' ]
- text = info[ 'text' ]
- timeout = info[ 'timeout' ]
- media_result = info[ 'hashes_to_media_results' ][ hash ]
-
- return ( name, text, timeout, media_result )
-
-
-
- def RefreshShares( self, *args, **kwargs ):
-
- with self._lock:
-
- self._RefreshShares()
-
-
-
class ParsingCache( object ):
def __init__( self ):
diff --git a/hydrus/client/db/ClientDB.py b/hydrus/client/db/ClientDB.py
index 93960e754..e2e89b604 100644
--- a/hydrus/client/db/ClientDB.py
+++ b/hydrus/client/db/ClientDB.py
@@ -6818,6 +6818,7 @@ def _Read( self, action, *args, **kwargs ):
elif action == 'services': result = self.modules_services.GetServices( *args, **kwargs )
elif action == 'similar_files_maintenance_status': result = self.modules_similar_files.GetMaintenanceStatus( *args, **kwargs )
elif action == 'related_tags': result = self._GetRelatedTags( *args, **kwargs )
+ elif action == 'tag_descendants_lookup': result = self.modules_tag_display.GetDescendantsForTags( *args, **kwargs )
elif action == 'tag_display_application': result = self.modules_tag_display.GetApplication( *args, **kwargs )
elif action == 'tag_display_maintenance_status': result = self._CacheTagDisplayGetApplicationStatusNumbers( *args, **kwargs )
elif action == 'tag_parents': result = self.modules_tag_parents.GetTagParents( *args, **kwargs )
@@ -10203,6 +10204,57 @@ def ask_what_to_do_zip_docx_scan():
+ if version == 572:
+
+ try:
+
+ domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
+
+ domain_manager.Initialise()
+
+ parsers = domain_manager.GetParsers()
+
+ parser_names = { parser.GetName() for parser in parsers }
+
+ # checking for floog's downloader
+ if 'fxtwitter api status parser' not in parser_names and 'vxtwitter api status parser' not in parser_names:
+
+ domain_manager.DeleteURLClasses( [
+ 'twitter tweet (i/web/status)',
+ 'twitter tweet',
+ 'twitter syndication api tweet-result',
+ 'twitter syndication api timeline-profile'
+ ])
+
+ domain_manager.OverwriteDefaultParsers( [
+ 'fxtwitter tweet api parser'
+ ] )
+
+ domain_manager.OverwriteDefaultURLClasses( [
+ 'x post',
+ 'twitter tweet',
+ 'fxtwitter tweet api'
+ ] )
+
+ #
+
+ domain_manager.TryToLinkURLClassesAndParsers()
+
+ #
+
+ self.modules_serialisable.SetJSONDump( domain_manager )
+
+
+ except Exception as e:
+
+ HydrusData.PrintException( e )
+
+ message = 'Trying to update some downloaders failed! Please let hydrus dev know!'
+
+ self.pub_initial_message( message )
+
+
+
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
diff --git a/hydrus/client/db/ClientDBSerialisable.py b/hydrus/client/db/ClientDBSerialisable.py
index 53f047276..8ac59aa31 100644
--- a/hydrus/client/db/ClientDBSerialisable.py
+++ b/hydrus/client/db/ClientDBSerialisable.py
@@ -206,20 +206,9 @@ def DeleteYAMLDump( self, dump_type, dump_name = None ):
else:
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU: dump_name = dump_name.hex()
-
self._Execute( 'DELETE FROM yaml_dumps WHERE dump_type = ? AND dump_name = ?;', ( dump_type, dump_name ) )
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU:
-
- service_id = self.modules_services.GetServiceId( CC.LOCAL_BOORU_SERVICE_KEY )
-
- self._Execute( 'DELETE FROM service_info WHERE service_id = ? AND info_type = ?;', ( service_id, HC.SERVICE_INFO_NUM_SHARES ) )
-
- CG.client_controller.pub( 'refresh_local_booru_shares' )
-
-
def GetAllExpectedHashedJSONHashes( self ) -> typing.Collection[ bytes ]:
@@ -931,13 +920,4 @@ def SetYAMLDump( self, dump_type, dump_name, data ):
raise
- if dump_type == YAML_DUMP_ID_LOCAL_BOORU:
-
- service_id = self.modules_services.GetServiceId( CC.LOCAL_BOORU_SERVICE_KEY )
-
- self._Execute( 'DELETE FROM service_info WHERE service_id = ? AND info_type = ?;', ( service_id, HC.SERVICE_INFO_NUM_SHARES ) )
-
- CG.client_controller.pub( 'refresh_local_booru_shares' )
-
-
diff --git a/hydrus/client/db/ClientDBTagDisplay.py b/hydrus/client/db/ClientDBTagDisplay.py
index 856550016..c8faaaf6e 100644
--- a/hydrus/client/db/ClientDBTagDisplay.py
+++ b/hydrus/client/db/ClientDBTagDisplay.py
@@ -275,6 +275,56 @@ def GetChainsMembers( self, display_type, tag_service_id, tag_ids ) -> typing.Se
'''
+ def GetDescendantsForTags( self, service_key, tags ):
+
+ if service_key == CC.COMBINED_TAG_SERVICE_KEY:
+
+ tag_services = self.modules_services.GetServices( HC.REAL_TAG_SERVICES )
+
+ search_service_keys = [ tag_service.GetServiceKey() for tag_service in tag_services ]
+
+ else:
+
+ search_service_keys = [ service_key ]
+
+
+ tags_to_descendants = collections.defaultdict( set )
+
+ for service_key in search_service_keys:
+
+ tag_service_id = self.modules_services.GetServiceId( service_key )
+
+ existing_tags = { tag for tag in tags if self.modules_tags.TagExists( tag ) }
+
+ existing_tag_ids_to_tags = self.modules_tags.GetTagIdsToTags( tags = existing_tags )
+
+ existing_tag_ids = set( existing_tag_ids_to_tags.keys() )
+
+ tag_ids_to_ideal_tag_ids = self.modules_tag_siblings.GetTagIdsToIdealTagIds( ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, tag_service_id, existing_tag_ids )
+
+ ideal_tag_ids = set( tag_ids_to_ideal_tag_ids.values() )
+
+ ideal_tag_ids_to_descendant_tag_ids = self.modules_tag_parents.GetTagsToDescendants( ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, tag_service_id, ideal_tag_ids )
+
+ all_tag_ids = set()
+
+ all_tag_ids.update( itertools.chain.from_iterable( ideal_tag_ids_to_descendant_tag_ids.values() ) )
+
+ tag_ids_to_tags = self.modules_tags_local_cache.GetTagIdsToTags( tag_ids = all_tag_ids )
+
+ for ( tag_id, tag ) in existing_tag_ids_to_tags.items():
+
+ ideal_tag_id = tag_ids_to_ideal_tag_ids[ tag_id ]
+ descendant_tag_ids = ideal_tag_ids_to_descendant_tag_ids[ ideal_tag_id ]
+ descendants = { tag_ids_to_tags[ descendant_tag_id ] for descendant_tag_id in descendant_tag_ids }
+
+ tags_to_descendants[ tag ].update( descendants )
+
+
+
+ return tags_to_descendants
+
+
def GetImpliedBy( self, display_type, tag_service_id, tag_id ) -> typing.Set[ int ]:
ideal_tag_id = self.modules_tag_siblings.GetIdealTagId( display_type, tag_service_id, tag_id )
diff --git a/hydrus/client/gui/ClientGUI.py b/hydrus/client/gui/ClientGUI.py
index 58285151c..0dc25c4e2 100644
--- a/hydrus/client/gui/ClientGUI.py
+++ b/hydrus/client/gui/ClientGUI.py
@@ -991,7 +991,7 @@ def do_it():
text += '\n' * 2
text += 'Over the coming weeks, your client will download updates and then process them into your database in idle time, and the PTR\'s tags will increasingly appear across your files. If you decide to upload tags, it is just a couple of clicks (under services->manage services again) to generate your own account that has permission to do so.'
text += '\n' * 2
- text += 'Be aware that the PTR has been growing since 2011 and now has more than a billion mappings. As of 2021-06, it requires about 6GB of bandwidth and file storage, and your database itself will grow by 50GB! Processing also takes a lot of CPU and HDD work, and, due to the unavoidable mechanical latency of HDDs, will only work in reasonable time if your hydrus database is on an SSD.'
+ text += 'Be aware that the PTR has been growing since 2011 and now has more than two billion mappings. As of 2021-06, it requires about 6GB of bandwidth and file storage, and your database itself will grow by 50GB! Processing also takes a lot of CPU and HDD work, and, due to the unavoidable mechanical latency of HDDs, will only work if your hydrus database (the .db files, normally in install_dir/db) is on an SSD.'
text += '\n' * 2
text += '++++If you are on a mechanical HDD or will not be able to free up enough space on your SSD, cancel out now.++++'
diff --git a/hydrus/client/gui/ClientGUIDialogs.py b/hydrus/client/gui/ClientGUIDialogs.py
index 3e221e32c..b791208d0 100644
--- a/hydrus/client/gui/ClientGUIDialogs.py
+++ b/hydrus/client/gui/ClientGUIDialogs.py
@@ -254,169 +254,7 @@ def EventOK( self ):
-class DialogInputLocalBooruShare( Dialog ):
-
- def __init__( self, parent, share_key, name, text, timeout, hashes, new_share = False ):
-
- Dialog.__init__( self, parent, 'configure local booru share' )
-
- self._name = QW.QLineEdit( self )
-
- self._text = QW.QPlainTextEdit( self )
- self._text.setMinimumHeight( 100 )
-
- message = 'expires in'
-
- self._timeout_number = ClientGUICommon.NoneableSpinCtrl( self, message, none_phrase = 'no expiration', max = 1000000, multiplier = 1 )
-
- self._timeout_multiplier = ClientGUICommon.BetterChoice( self )
- self._timeout_multiplier.addItem( 'minutes', 60 )
- self._timeout_multiplier.addItem( 'hours', 60 * 60 )
- self._timeout_multiplier.addItem( 'days', 60 * 60 * 24 )
-
- self._copy_internal_share_link = QW.QPushButton( 'copy internal share link', self )
- self._copy_internal_share_link.clicked.connect( self.EventCopyInternalShareURL )
-
- self._copy_external_share_link = QW.QPushButton( 'copy external share link', self )
- self._copy_external_share_link.clicked.connect( self.EventCopyExternalShareURL )
-
- self._ok = QW.QPushButton( 'ok', self )
- self._ok.clicked.connect( self.accept )
- self._ok.setObjectName( 'HydrusAccept' )
-
- self._cancel = QW.QPushButton( 'cancel', self )
- self._cancel.clicked.connect( self.reject )
- self._cancel.setObjectName( 'HydrusCancel' )
-
- #
-
- self._share_key = share_key
- self._name.setText( name )
- self._text.setPlainText( text )
-
- if timeout is None:
-
- self._timeout_number.SetValue( None )
-
- self._timeout_multiplier.SetValue( 60 )
-
- else:
-
- time_left = HydrusTime.GetTimeDeltaUntilTime( timeout )
-
- if time_left < 60 * 60 * 12: time_value = 60
- elif time_left < 60 * 60 * 24 * 7: time_value = 60 * 60
- else: time_value = 60 * 60 * 24
-
- self._timeout_number.SetValue( time_left // time_value )
-
- self._timeout_multiplier.SetValue( time_value )
-
-
- self._hashes = hashes
-
- self._service = CG.client_controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )
-
- internal_port = self._service.GetPort()
-
- if internal_port is None:
-
- self._copy_internal_share_link.setEnabled( False )
- self._copy_external_share_link.setEnabled( False )
-
-
- #
-
- rows = []
-
- rows.append( ( 'share name: ', self._name ) )
- rows.append( ( 'share text: ', self._text ) )
-
- gridbox = ClientGUICommon.WrapInGrid( self, rows )
-
- timeout_box = QP.HBoxLayout()
- QP.AddToLayout( timeout_box, self._timeout_number, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( timeout_box, self._timeout_multiplier, CC.FLAGS_EXPAND_BOTH_WAYS )
-
- link_box = QP.HBoxLayout()
- QP.AddToLayout( link_box, self._copy_internal_share_link, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( link_box, self._copy_external_share_link, CC.FLAGS_CENTER_PERPENDICULAR )
-
- b_box = QP.HBoxLayout()
- QP.AddToLayout( b_box, self._ok, CC.FLAGS_CENTER_PERPENDICULAR )
- QP.AddToLayout( b_box, self._cancel, CC.FLAGS_CENTER_PERPENDICULAR )
-
- vbox = QP.VBoxLayout()
-
- intro = 'Sharing ' + HydrusData.ToHumanInt( len( self._hashes ) ) + ' files.'
- intro += '\n' + 'Title and text are optional.'
-
- if new_share: intro += '\n' + 'The link will not work until you ok this dialog.'
-
- QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,intro), CC.FLAGS_EXPAND_PERPENDICULAR )
- QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- QP.AddToLayout( vbox, timeout_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
- QP.AddToLayout( vbox, link_box, CC.FLAGS_ON_RIGHT )
- QP.AddToLayout( vbox, b_box, CC.FLAGS_ON_RIGHT )
-
- self.setLayout( vbox )
-
- size_hint = self.sizeHint()
-
- size_hint.setWidth( max( size_hint.width(), 350 ) )
-
- QP.SetInitialSize( self, size_hint )
-
- ClientGUIFunctions.SetFocusLater( self._ok )
-
-
- def EventCopyExternalShareURL( self ):
-
- internal_port = self._service.GetPort()
-
- if internal_port is None:
-
- ClientGUIDialogsMessage.ShowWarning( self, 'The local booru is not currently running!' )
-
-
- try:
-
- url = self._service.GetExternalShareURL( self._share_key )
-
- except Exception as e:
-
- HydrusData.ShowException( e )
-
- ClientGUIDialogsMessage.ShowCritical( self, 'Error', 'Unfortunately, could not generate an external URL: {}'.format(e) )
-
- return
-
-
- CG.client_controller.pub( 'clipboard', 'text', url )
-
-
- def EventCopyInternalShareURL( self ):
-
- self._service = CG.client_controller.services_manager.GetService( CC.LOCAL_BOORU_SERVICE_KEY )
-
- url = self._service.GetInternalShareURL( self._share_key )
-
- CG.client_controller.pub( 'clipboard', 'text', url )
-
-
- def GetInfo( self ):
-
- name = self._name.text()
-
- text = self._text.toPlainText()
-
- timeout = self._timeout_number.GetValue()
-
- if timeout is not None: timeout = timeout * self._timeout_multiplier.GetValue() + HydrusTime.GetNow()
-
- return ( self._share_key, name, text, timeout, self._hashes )
-
-
+
class DialogInputNamespaceRegex( Dialog ):
def __init__( self, parent, namespace = '', regex = '' ):
@@ -529,6 +367,8 @@ def __init__( self, parent, service_key, tag_display_type, tags, message = '' ):
self._tag_autocomplete = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterTags, default_location_context, service_key, show_paste_button = True )
+ self._tags.tagsChanged.connect( self._tag_autocomplete.SetContextTags )
+
self._tag_autocomplete.nullEntered.connect( self.OK )
self._ok = ClientGUICommon.BetterButton( self, 'OK', self.done, QW.QDialog.Accepted )
diff --git a/hydrus/client/gui/ClientGUIDialogsQuick.py b/hydrus/client/gui/ClientGUIDialogsQuick.py
index 1b1dc983f..bac855c63 100644
--- a/hydrus/client/gui/ClientGUIDialogsQuick.py
+++ b/hydrus/client/gui/ClientGUIDialogsQuick.py
@@ -46,49 +46,6 @@ def GetDeleteFilesJobs( win, media, default_reason, suggested_file_service_key =
-def GetFinishArchiveDeleteFilteringAnswer( win, kept_label, deletion_options ):
-
- with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, 'filtering done?' ) as dlg:
-
- panel = ClientGUIScrolledPanelsButtonQuestions.QuestionArchiveDeleteFinishFilteringPanel( dlg, kept_label, deletion_options )
-
- dlg.SetPanel( panel )
-
- result = dlg.exec()
- location_context = panel.GetLocationContext()
- was_cancelled = dlg.WasCancelled()
-
- return ( result, location_context, was_cancelled )
-
-
-
-def GetFinishFilteringAnswer( win, label ):
-
- with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, label ) as dlg:
-
- panel = ClientGUIScrolledPanelsButtonQuestions.QuestionFinishFilteringPanel( dlg, label )
-
- dlg.SetPanel( panel )
-
- result = ( dlg.exec(), dlg.WasCancelled() )
-
- return result
-
-
-def GetInterstitialFilteringAnswer( win, label ):
-
- with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, label ) as dlg:
-
- panel = ClientGUIScrolledPanelsButtonQuestions.QuestionCommitInterstitialFilteringPanel( dlg, label )
-
- dlg.SetPanel( panel )
-
- result = dlg.exec()
-
- return result
-
-
-
def run_auto_yes_no_gubbins( dlg: QW.QDialog, time_to_fire, original_title, action_description, end_state ):
def qt_set_title():
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
index 16922e688..a4d221b20 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsButtonQuestions.py
@@ -1,246 +1,11 @@
-import os
-import typing
-
-from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
-from hydrus.core import HydrusGlobals as HG
-
from hydrus.client import ClientConstants as CC
-from hydrus.client import ClientGlobals as CG
-from hydrus.client import ClientLocation
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
-class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
-
- def __init__( self, parent, label ):
-
- ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
-
- self._commit = ClientGUICommon.BetterButton( self, 'commit and continue', self.parentWidget().done, QW.QDialog.Accepted )
- self._commit.setObjectName( 'HydrusAccept' )
-
- self._back = ClientGUICommon.BetterButton( self, 'go back', self.parentWidget().done, QW.QDialog.Rejected )
-
- vbox = QP.VBoxLayout()
-
- 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 )
-
- 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 )
-
- ClientGUIFunctions.SetFocusLater( self._commit )
-
-
-class QuestionArchiveDeleteFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
-
- def __init__( self, parent, kept_label: typing.Optional[ str ], deletion_options ):
-
- ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
-
- self._location_context = ClientLocation.LocationContext() # empty
-
- vbox = QP.VBoxLayout()
-
- first_commit = None
-
- if len( deletion_options ) == 0:
-
- if kept_label is None:
-
- kept_label = 'ERROR: do not seem to have any actions at all!'
-
-
- label = '{}?'.format( kept_label )
-
- st = ClientGUICommon.BetterStaticText( self, label )
-
- st.setAlignment( QC.Qt.AlignCenter )
-
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- first_commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, ClientLocation.LocationContext() )
- first_commit.setObjectName( 'HydrusAccept' )
-
- QP.AddToLayout( vbox, first_commit, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- elif len( deletion_options ) == 1:
-
- ( location_context, delete_label ) = deletion_options[0]
-
- if kept_label is None:
-
- label = '{}?'.format( delete_label )
-
- else:
-
- label = '{} and {}?'.format( kept_label, delete_label )
-
-
- st = ClientGUICommon.BetterStaticText( self, label )
-
- st.setAlignment( QC.Qt.AlignCenter )
-
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- first_commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, location_context )
- first_commit.setObjectName( 'HydrusAccept' )
-
- QP.AddToLayout( vbox, first_commit, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- else:
-
- if kept_label is not None:
-
- label = f'{kept_label}\n-and-'
-
- st = ClientGUICommon.BetterStaticText( self, label )
-
- st.setAlignment( QC.Qt.AlignCenter )
-
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
-
-
- delay_delete_buttons = len( deletion_options ) > 0
- delayed_delete_buttons = []
-
- for ( location_context, delete_label ) in deletion_options:
-
- label = '{}?'.format( delete_label )
-
- st = ClientGUICommon.BetterStaticText( self, label )
-
- st.setAlignment( QC.Qt.AlignCenter )
-
- QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, location_context )
- commit.setObjectName( 'HydrusAccept' )
-
- QP.AddToLayout( vbox, commit, CC.FLAGS_EXPAND_PERPENDICULAR )
-
- if first_commit is None:
-
- first_commit = commit
-
-
- if delay_delete_buttons:
-
- delayed_delete_buttons.append( commit )
- commit.setEnabled( False )
-
-
-
- def do_it():
-
- for b in delayed_delete_buttons:
-
- b.setEnabled( True )
-
-
-
- if len( delayed_delete_buttons ) > 0:
-
- CG.client_controller.CallLaterQtSafe( self, 1.2, 'delayed button enable', do_it )
-
-
-
- self._forget = ClientGUICommon.BetterButton( self, 'forget', self.parentWidget().done, QW.QDialog.Rejected )
- self._forget.setObjectName( 'HydrusCancel' )
-
- self._back = ClientGUICommon.BetterButton( self, 'back to filtering', self.DoGoBack )
-
- QP.AddToLayout( vbox, self._forget, 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 )
-
- ClientGUIFunctions.SetFocusLater( first_commit )
-
-
- def DoGoBack( self ):
-
- self.parentWidget().SetCancelled( True )
- self.parentWidget().done( QW.QDialog.Rejected )
-
-
- def DoCommit( self, location_context ):
-
- self._location_context = location_context
- self.parentWidget().done( QW.QDialog.Accepted )
-
-
- def GetLocationContext( self ) -> ClientLocation.LocationContext:
-
- return self._location_context
-
-
-class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
-
- def __init__( self, parent, label ):
-
- ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
-
- self._commit = ClientGUICommon.BetterButton( self, 'commit', self.parentWidget().done, QW.QDialog.Accepted )
- self._commit.setObjectName( 'HydrusAccept' )
-
- self._forget = ClientGUICommon.BetterButton( self, 'forget', self.parentWidget().done, QW.QDialog.Rejected )
- self._forget.setObjectName( 'HydrusCancel' )
-
- def cancel_callback( parent ):
-
- parent.SetCancelled( True )
- parent.done( QW.QDialog.Rejected )
-
-
- self._back = ClientGUICommon.BetterButton( self, 'back to filtering', cancel_callback, parent )
-
- hbox = QP.HBoxLayout()
-
- QP.AddToLayout( hbox, self._commit, CC.FLAGS_EXPAND_BOTH_WAYS )
- QP.AddToLayout( hbox, self._forget, CC.FLAGS_EXPAND_BOTH_WAYS )
-
- vbox = QP.VBoxLayout()
-
- 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 )
-
- 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 )
-
- ClientGUIFunctions.SetFocusLater( self._commit )
-
-
class QuestionYesNoPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
def __init__( self, parent, message, yes_label = 'yes', no_label = 'no' ):
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsCommitFiltering.py b/hydrus/client/gui/ClientGUIScrolledPanelsCommitFiltering.py
new file mode 100644
index 000000000..8d8bd018f
--- /dev/null
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsCommitFiltering.py
@@ -0,0 +1,312 @@
+import typing
+
+from qtpy import QtCore as QC
+from qtpy import QtWidgets as QW
+
+from hydrus.client import ClientConstants as CC
+from hydrus.client import ClientGlobals as CG
+from hydrus.client import ClientLocation
+from hydrus.client.gui import ClientGUIFunctions
+from hydrus.client.gui import ClientGUIDialogsQuick
+from hydrus.client.gui import ClientGUIScrolledPanels
+from hydrus.client.gui import ClientGUITopLevelWindowsPanels
+from hydrus.client.gui import QtPorting as QP
+from hydrus.client.gui.widgets import ClientGUICommon
+
+def GetFinishArchiveDeleteFilteringAnswer( win, kept_label, deletion_options ):
+
+ with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, 'filtering done?' ) as dlg:
+
+ panel = QuestionArchiveDeleteFinishFilteringPanel( dlg, kept_label, deletion_options )
+
+ dlg.SetPanel( panel )
+
+ result = dlg.exec()
+ location_context = panel.GetLocationContext()
+ was_cancelled = dlg.WasCancelled()
+
+ return ( result, location_context, was_cancelled )
+
+
+
+def GetFinishFilteringAnswer( win, label ):
+
+ with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, label ) as dlg:
+
+ panel = QuestionFinishFilteringPanel( dlg, label )
+
+ dlg.SetPanel( panel )
+
+ result = ( dlg.exec(), dlg.WasCancelled() )
+
+ return result
+
+
+
+def GetInterstitialFilteringAnswer( win, label ):
+
+ with ClientGUITopLevelWindowsPanels.DialogCustomButtonQuestion( win, label ) as dlg:
+
+ panel = QuestionCommitInterstitialFilteringPanel( dlg, label )
+
+ dlg.SetPanel( panel )
+
+ result = dlg.exec()
+
+ return result
+
+
+
+class QuestionCommitInterstitialFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
+
+ def __init__( self, parent, label ):
+
+ ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
+
+ self._commit = ClientGUICommon.BetterButton( self, 'commit and continue', self.parentWidget().done, QW.QDialog.Accepted )
+ self._commit.setObjectName( 'HydrusAccept' )
+
+ self._back = ClientGUICommon.BetterButton( self, 'go back', self.parentWidget().done, QW.QDialog.Rejected )
+
+ vbox = QP.VBoxLayout()
+
+ 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 )
+
+ 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 )
+
+ ClientGUIFunctions.SetFocusLater( self._commit )
+
+
+
+class QuestionArchiveDeleteFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
+
+ def __init__( self, parent, kept_label: typing.Optional[ str ], deletion_options ):
+
+ ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
+
+ self._location_context = ClientLocation.LocationContext() # empty
+
+ vbox = QP.VBoxLayout()
+
+ first_commit = None
+
+ if len( deletion_options ) == 0:
+
+ if kept_label is None:
+
+ kept_label = 'ERROR: do not seem to have any actions at all!'
+
+
+ label = '{}?'.format( kept_label )
+
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ first_commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, ClientLocation.LocationContext() )
+ first_commit.setObjectName( 'HydrusAccept' )
+
+ QP.AddToLayout( vbox, first_commit, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ elif len( deletion_options ) == 1:
+
+ ( location_context, delete_label ) = deletion_options[0]
+
+ if kept_label is None:
+
+ label = '{}?'.format( delete_label )
+
+ else:
+
+ label = '{} and {}?'.format( kept_label, delete_label )
+
+
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ first_commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, location_context )
+ first_commit.setObjectName( 'HydrusAccept' )
+
+ QP.AddToLayout( vbox, first_commit, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ else:
+
+ if kept_label is not None:
+
+ label = f'{kept_label}\n-and-'
+
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+
+ delay_delete_buttons = len( deletion_options ) > 0
+ delayed_delete_buttons = []
+
+ for ( location_context, delete_label ) in deletion_options:
+
+ label = '{}?'.format( delete_label )
+
+ st = ClientGUICommon.BetterStaticText( self, label )
+
+ st.setAlignment( QC.Qt.AlignCenter )
+
+ QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ commit = ClientGUICommon.BetterButton( self, 'commit', self.DoCommit, location_context )
+ commit.setObjectName( 'HydrusAccept' )
+
+ QP.AddToLayout( vbox, commit, CC.FLAGS_EXPAND_PERPENDICULAR )
+
+ if first_commit is None:
+
+ first_commit = commit
+
+
+ if delay_delete_buttons:
+
+ delayed_delete_buttons.append( commit )
+ commit.setEnabled( False )
+
+
+
+ def do_it():
+
+ for b in delayed_delete_buttons:
+
+ b.setEnabled( True )
+
+
+
+ if len( delayed_delete_buttons ) > 0:
+
+ CG.client_controller.CallLaterQtSafe( self, 1.2, 'delayed button enable', do_it )
+
+
+
+ self._forget = ClientGUICommon.BetterButton( self, 'forget', self.DoForget )
+ self._forget.setObjectName( 'HydrusCancel' )
+
+ self._back = ClientGUICommon.BetterButton( self, 'back to filtering', self.DoGoBack )
+
+ QP.AddToLayout( vbox, self._forget, 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 )
+
+ ClientGUIFunctions.SetFocusLater( first_commit )
+
+
+ def DoForget( self ):
+
+ message = 'Quit filtering now and forget your work?'
+
+ result = ClientGUIDialogsQuick.GetYesNo( self, message )
+
+ if result == QW.QDialog.Accepted:
+
+ self.parentWidget().done( QW.QDialog.Rejected )
+
+
+
+ def DoGoBack( self ):
+
+ self.parentWidget().SetCancelled( True )
+ self.parentWidget().done( QW.QDialog.Rejected )
+
+
+ def DoCommit( self, location_context ):
+
+ self._location_context = location_context
+ self.parentWidget().done( QW.QDialog.Accepted )
+
+
+ def GetLocationContext( self ) -> ClientLocation.LocationContext:
+
+ return self._location_context
+
+
+
+class QuestionFinishFilteringPanel( ClientGUIScrolledPanels.ResizingScrolledPanel ):
+
+ def __init__( self, parent, label ):
+
+ ClientGUIScrolledPanels.ResizingScrolledPanel.__init__( self, parent )
+
+ self._commit = ClientGUICommon.BetterButton( self, 'commit', self.parentWidget().done, QW.QDialog.Accepted )
+ self._commit.setObjectName( 'HydrusAccept' )
+
+ self._forget = ClientGUICommon.BetterButton( self, 'forget', self.DoForget )
+ self._forget.setObjectName( 'HydrusCancel' )
+
+ def cancel_callback( parent ):
+
+ parent.SetCancelled( True )
+ parent.done( QW.QDialog.Rejected )
+
+
+ self._back = ClientGUICommon.BetterButton( self, 'back to filtering', cancel_callback, parent )
+
+ hbox = QP.HBoxLayout()
+
+ QP.AddToLayout( hbox, self._commit, CC.FLAGS_EXPAND_BOTH_WAYS )
+ QP.AddToLayout( hbox, self._forget, CC.FLAGS_EXPAND_BOTH_WAYS )
+
+ vbox = QP.VBoxLayout()
+
+ 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 )
+
+ 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 )
+
+ ClientGUIFunctions.SetFocusLater( self._commit )
+
+
+ def DoForget( self ):
+
+ message = 'Quit filtering now and forget your work?'
+
+ result = ClientGUIDialogsQuick.GetYesNo( self, message )
+
+ if result == QW.QDialog.Accepted:
+
+ self.parentWidget().done( QW.QDialog.Rejected )
+
+
+
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index d8f97e6d4..dc3e1e50c 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -4109,6 +4109,8 @@ def __init__( self, parent, new_options ):
self._favourites = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( favourites_panel, CC.COMBINED_TAG_SERVICE_KEY, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE )
self._favourites_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( favourites_panel, self._favourites.AddTags, default_location_context, CC.COMBINED_TAG_SERVICE_KEY, show_paste_button = True )
+ self._favourites.tagsChanged.connect( self._favourites_input.SetContextTags )
+
#
self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
@@ -4503,6 +4505,8 @@ def __init__( self, parent, new_options ):
self._suggested_favourites_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( suggested_tags_favourites_panel, self._suggested_favourites.AddTags, default_location_context, CC.COMBINED_TAG_SERVICE_KEY, show_paste_button = True )
+ self._suggested_favourites.tagsChanged.connect( self._suggested_favourites_input.SetContextTags )
+
#
suggested_tags_related_panel = QW.QWidget( suggest_tags_panel_notebook )
diff --git a/hydrus/client/gui/ClientGUITags.py b/hydrus/client/gui/ClientGUITags.py
index f8b3792b2..78dfa03c4 100644
--- a/hydrus/client/gui/ClientGUITags.py
+++ b/hydrus/client/gui/ClientGUITags.py
@@ -2670,6 +2670,8 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
self._add_tag_box.nullEntered.connect( self.OK )
+ self._tags_box.tagsChanged.connect( self._add_tag_box.SetContextTags )
+
self._tags_box.SetTagServiceKey( self._tag_service_key )
self._suggested_tags = ClientGUITagSuggestions.SuggestedTagsPanel( self, self._tag_service_key, self._tag_presentation_location, len( media ) == 1, self.AddTags )
@@ -3512,12 +3514,12 @@ def __init__( self, parent, service_key, tags = None ):
self._pursue_whole_chain.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
# leave up here since other things have updates based on them
- self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL )
- self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL )
+ self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, height_num_chars = 4 )
+ self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, height_num_chars = 4 )
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._tag_parents = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
+ self._tag_parents = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_PARENTS.ID, 6, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
self._listctrl_panel.SetListCtrl( self._tag_parents )
@@ -3557,6 +3559,9 @@ def __init__( self, parent, service_key, tags = None ):
self._parent_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterParents, default_location_context, service_key, show_paste_button = True )
self._parent_input.setEnabled( False )
+ self._children.tagsChanged.connect( self._child_input.SetContextTags )
+ self._parents.tagsChanged.connect( self._parent_input.SetContextTags )
+
#
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising' + HC.UNICODE_ELLIPSIS + '\n' + '.' )
@@ -4451,7 +4456,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
- s = '\n' * 2
+ s = '\n'
status_text = s.join( ( service_part, maintenance_part, changes_part ) )
@@ -4669,12 +4674,12 @@ def __init__( self, parent, service_key, tags = None ):
self._show_all = QW.QCheckBox( self )
# leave up here since other things have updates based on them
- self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL )
+ self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, height_num_chars = 4 )
self._new_sibling = ClientGUICommon.BetterStaticText( self )
self._listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
- self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 8, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
+ self._tag_siblings = ClientGUIListCtrl.BetterListCtrl( self._listctrl_panel, CGLC.COLUMN_LIST_TAG_SIBLINGS.ID, 6, self._ConvertPairToListCtrlTuples, delete_key_callback = self._DeleteSelectedRows, activation_callback = self._DeleteSelectedRows )
self._listctrl_panel.SetListCtrl( self._tag_siblings )
@@ -4713,6 +4718,8 @@ def __init__( self, parent, service_key, tags = None ):
self._new_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.SetNew, default_location_context, service_key )
self._new_input.setEnabled( False )
+ self._old_siblings.tagsChanged.connect( self._old_input.SetContextTags )
+
#
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising' + HC.UNICODE_ELLIPSIS )
@@ -5836,7 +5843,7 @@ def qt_code( original_statuses_to_pairs, current_statuses_to_pairs, service_keys
- s = '\n' * 2
+ s = '\n'
status_text = s.join( ( service_part, maintenance_part, changes_part ) )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index 0aa8b6e22..b32f4b576 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -26,6 +26,7 @@
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIRatings
+from hydrus.client.gui import ClientGUIScrolledPanelsCommitFiltering
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIScrolledPanelsManagement
from hydrus.client.gui import ClientGUIShortcuts
@@ -2973,7 +2974,7 @@ def pair_is_good( pair ):
label = 'commit {} and continue?'.format( ' and '.join( components ) )
- result = ClientGUIDialogsQuick.GetInterstitialFilteringAnswer( self, label )
+ result = ClientGUIScrolledPanelsCommitFiltering.GetInterstitialFilteringAnswer( self, label )
if result == QW.QDialog.Accepted:
@@ -3272,7 +3273,7 @@ def TryToDoPreClose( self ):
label = 'commit {}?'.format( ' and '.join( components ) )
- ( result, cancelled ) = ClientGUIDialogsQuick.GetFinishFilteringAnswer( self, label )
+ ( result, cancelled ) = ClientGUIScrolledPanelsCommitFiltering.GetFinishFilteringAnswer( self, label )
if cancelled:
@@ -3792,7 +3793,7 @@ def TryToDoPreClose( self ):
- ( result, deletee_location_context, cancelled ) = ClientGUIDialogsQuick.GetFinishArchiveDeleteFilteringAnswer( self, kept_label, deletion_options )
+ ( result, deletee_location_context, cancelled ) = ClientGUIScrolledPanelsCommitFiltering.GetFinishArchiveDeleteFilteringAnswer( self, kept_label, deletion_options )
if cancelled:
diff --git a/hydrus/client/gui/importing/ClientGUIImport.py b/hydrus/client/gui/importing/ClientGUIImport.py
index daadd804c..0c0ba5af8 100644
--- a/hydrus/client/gui/importing/ClientGUIImport.py
+++ b/hydrus/client/gui/importing/ClientGUIImport.py
@@ -455,6 +455,8 @@ def __init__( self, parent, service_key, filename_tagging_options, present_for_a
self._tag_autocomplete_all.movePageLeft.connect( self.movePageLeft )
self._tag_autocomplete_all.movePageRight.connect( self.movePageRight )
+ self._tags.tagsChanged.connect( self._tag_autocomplete_all.SetContextTags )
+
self._tags_paste_button = ClientGUICommon.BetterButton( self._tags_panel, 'paste tags', self._PasteTags )
#
@@ -472,6 +474,8 @@ def __init__( self, parent, service_key, filename_tagging_options, present_for_a
self._tag_autocomplete_selection.movePageLeft.connect( self.movePageLeft )
self._tag_autocomplete_selection.movePageRight.connect( self.movePageRight )
+ self._single_tags.tagsChanged.connect( self._tag_autocomplete_selection.SetContextTags )
+
self.SetSelectedPaths( [] )
if not self._present_for_accompanying_file_list:
diff --git a/hydrus/client/gui/lists/ClientGUIListBoxes.py b/hydrus/client/gui/lists/ClientGUIListBoxes.py
index 3d95df564..2cad749de 100644
--- a/hydrus/client/gui/lists/ClientGUIListBoxes.py
+++ b/hydrus/client/gui/lists/ClientGUIListBoxes.py
@@ -3742,6 +3742,8 @@ def SetTagSlices( self, tag_slices ):
class ListBoxTagsDisplayCapable( ListBoxTags ):
+ tagsChanged = QC.Signal( list )
+
def __init__( self, parent, service_key = None, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, **kwargs ):
if service_key is None:
@@ -3755,6 +3757,8 @@ def __init__( self, parent, service_key = None, tag_display_type = ClientTags.TA
ListBoxTags.__init__( self, parent, has_async_text_info = has_async_text_info, tag_display_type = tag_display_type, **kwargs )
+ self.listBoxChanged.connect( self._NotifyListBoxChanged )
+
def _ApplyAsyncInfoToTerm( self, term, info ) -> typing.Tuple[ bool, bool ]:
@@ -3831,6 +3835,14 @@ def work_callable( args ):
return ( pre_work_callable, work_callable )
+ def _NotifyListBoxChanged( self ):
+
+ # we only want the top tags here, not all parents and so on
+ tags = [ term.GetTag() for term in self._ordered_terms ]
+
+ self.tagsChanged.emit( list( self._GetTagsFromTerms( self._ordered_terms ) ) )
+
+
def _SelectFilesWithTags( self, and_or_or ):
if self._page_key is not None:
@@ -3858,20 +3870,30 @@ def SetTagServiceKey( self, service_key ):
+
class ListBoxTagsStrings( ListBoxTagsDisplayCapable ):
+ tagsChanged = QC.Signal( list )
+
def __init__( self, parent, service_key = None, sort_tags = True, **kwargs ):
self._sort_tags = sort_tags
ListBoxTagsDisplayCapable.__init__( self, parent, service_key = service_key, **kwargs )
+ self.listBoxChanged.connect( self._NotifyListBoxChanged )
+
def _GenerateTermFromTag( self, tag: str ) -> ClientGUIListBoxesData.ListBoxItemTextTag:
return ClientGUIListBoxesData.ListBoxItemTextTag( tag )
+ def _NotifyListBoxChanged( self ):
+
+ self.tagsChanged.emit( list( self.GetTags() ) )
+
+
def GetTags( self ):
return set( self._GetTagsFromTerms( self._ordered_terms ) )
diff --git a/hydrus/client/gui/pages/ClientGUIManagementPanels.py b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
index 4abd59e60..09aff2df6 100644
--- a/hydrus/client/gui/pages/ClientGUIManagementPanels.py
+++ b/hydrus/client/gui/pages/ClientGUIManagementPanels.py
@@ -544,8 +544,8 @@ def __init__( self, parent, page, controller, management_controller: ClientGUIMa
synchronised = True
- self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_1, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, synchronised = synchronised, force_system_everything = True )
- self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_2, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, synchronised = synchronised, force_system_everything = True )
+ self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_1, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
+ self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_2, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, only_allow_local_file_domains = True, synchronised = synchronised, force_system_everything = True )
self._dupe_search_type = ClientGUICommon.BetterChoice( self._filtering_panel )
diff --git a/hydrus/client/gui/search/ClientGUIACDropdown.py b/hydrus/client/gui/search/ClientGUIACDropdown.py
index 8af179c15..7dcf11f95 100644
--- a/hydrus/client/gui/search/ClientGUIACDropdown.py
+++ b/hydrus/client/gui/search/ClientGUIACDropdown.py
@@ -20,6 +20,7 @@
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
from hydrus.client import ClientThreading
+from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
@@ -36,6 +37,7 @@
from hydrus.client.gui.search import ClientGUISearch
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.metadata import ClientTags
+from hydrus.client.metadata import ClientTagSorting
from hydrus.client.search import ClientSearch
from hydrus.client.search import ClientSearchAutocomplete
from hydrus.client.search import ClientSearchParseSystemPredicates
@@ -1548,12 +1550,11 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
self._last_prefetch_job_status = None
+ self._current_context_tags = {}
self._tag_service_key = tag_service_key
AutoCompleteDropdown.__init__( self, parent )
- tag_service = CG.client_controller.services_manager.GetService( self._tag_service_key )
-
self._location_context_button = ClientGUILocation.LocationSearchContextButton( self._dropdown_window, location_context, is_paired_with_tag_domain = True )
self._location_context_button.setMinimumWidth( 20 )
@@ -1568,6 +1569,12 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
self._dropdown_notebook.addTab( self._favourites_list, 'favourites' )
+ self._children_list = ListBoxTagsPredicatesAC( self._dropdown_notebook, self.BroadcastChoices, self._float_mode, self._tag_service_key, tag_display_type = ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL, height_num_chars = 4 )
+ self._tags_to_children_cache = dict()
+ self._children_list_needs_updating = True
+
+ self._dropdown_notebook.addTab( self._children_list, 'children' )
+
#
self._location_context_button.locationChanged.connect( self._LocationContextJustChanged )
@@ -1576,6 +1583,8 @@ def __init__( self, parent, location_context: ClientLocation.LocationContext, ta
CG.client_controller.sub( self, 'RefreshFavouriteTags', 'notify_new_favourite_tags' )
CG.client_controller.sub( self, 'NotifyNewServices', 'notify_new_services' )
+ self._dropdown_notebook.currentChanged.connect( self._UpdateChildrenListIfNeeded )
+
def _BroadcastChoices( self, predicates, shift_down ):
@@ -1624,6 +1633,13 @@ def _LocationContextJustChanged( self, location_context: ClientLocation.Location
self._SetListDirty()
+ def _NotifyChildrenListNeedsUpdating( self ):
+
+ self._children_list_needs_updating = True
+
+ self._UpdateChildrenListIfNeeded()
+
+
def _SetLocationContext( self, location_context: ClientLocation.LocationContext ):
location_context = location_context.Duplicate()
@@ -1719,6 +1735,10 @@ def _TagContextJustChanged( self, tag_context: ClientSearch.TagContext ):
self._search_results_list.SetTagServiceKey( self._tag_service_key )
self._favourites_list.SetTagServiceKey( self._tag_service_key )
+ self._children_list.SetTagServiceKey( self._tag_service_key )
+
+ self._tags_to_children_cache = dict()
+ self._NotifyChildrenListNeedsUpdating()
self.tagServiceChanged.emit( self._tag_service_key )
@@ -1732,6 +1752,88 @@ def _TakeResponsibilityForEnter( self, shift_down ):
raise NotImplementedError()
+ def _UpdateChildrenListIfNeeded( self ):
+
+ if self._children_list_needs_updating and self._dropdown_notebook.currentWidget() == self._children_list:
+
+ tag_service_key = self._tag_service_key
+ context_tags = set( self._current_context_tags )
+ tags_to_children_cache = dict( self._tags_to_children_cache )
+
+ def work_callable():
+
+ uncached_context_tags = { tag for tag in context_tags if tag not in tags_to_children_cache }
+
+ if len( uncached_context_tags ) > 0:
+
+ new_tags_to_children = CG.client_controller.Read( 'tag_descendants_lookup', tag_service_key, uncached_context_tags )
+
+ else:
+
+ new_tags_to_children = dict()
+
+
+ child_tags = set()
+
+ for tag in context_tags:
+
+ if tag in tags_to_children_cache:
+
+ child_tags.update( tags_to_children_cache[ tag ] )
+
+ elif tag in new_tags_to_children:
+
+ child_tags.update( new_tags_to_children[ tag ] )
+
+
+
+ child_tags.difference_update( context_tags )
+
+ return ( tag_service_key, child_tags, new_tags_to_children )
+
+
+ def publish_callable( result ):
+
+ ( job_tag_service_key, child_tags, new_tags_to_children ) = result
+
+ if job_tag_service_key != self._tag_service_key:
+
+ self._children_list.SetPredicates( [] )
+
+ return
+
+
+ child_tags = list( child_tags )
+
+ self._tags_to_children_cache.update( new_tags_to_children )
+
+ tag_sort = ClientTagSorting.TagSort( sort_type = ClientTagSorting.SORT_BY_HUMAN_TAG, sort_order = CC.SORT_ASC )
+
+ ClientTagSorting.SortTags( tag_sort, child_tags )
+
+ predicates = [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = tag ) for tag in child_tags ]
+
+ self._children_list.SetPredicates( predicates, preserve_single_selection = True )
+
+ self._children_list_needs_updating = False
+
+
+ def errback_callable( etype, value, tb ):
+
+ self._children_list.SetPredicates( [] )
+
+ self._children_list_needs_updating = False
+
+ HydrusData.ShowText( 'Trying to load some child tags failed, please send this to hydev:' )
+ HydrusData.ShowExceptionTuple( etype, value, tb, do_wait = False )
+
+
+ job = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable, errback_callable = errback_callable )
+
+ job.start()
+
+
+
def GetLocationContext( self ) -> ClientLocation.LocationContext:
return self._location_context_button.GetValue()
@@ -1794,6 +1896,16 @@ def SetPrefetchResults( self, job_status: ClientThreading.JobStatus, predicates:
+ def SetContextTags( self, tags: typing.Collection[ str ] ):
+ """
+ The search context or the taglist we are editing just changed, so let's tell anything in here that wants to filter or do lookups based on that.
+ """
+
+ self._current_context_tags = set( tags )
+
+ self._NotifyChildrenListNeedsUpdating()
+
+
def SetTagServiceKey( self, tag_service_key ):
self._SetTagService( tag_service_key )
@@ -1804,7 +1916,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
searchChanged = QC.Signal( ClientSearch.FileSearchContext )
searchCancelled = QC.Signal()
- def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearch.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ):
+ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSearch.FileSearchContext, media_sort_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaSortControl ] = None, media_collect_widget: typing.Optional[ ClientGUIResultsSortCollect.MediaCollectControl ] = None, media_callable = None, synchronised = True, include_unusual_predicate_types = True, allow_all_known_files = True, only_allow_local_file_domains = False, force_system_everything = False, hide_favourites_edit_actions = False, fixed_results_list_height = None ):
self._page_key = page_key
@@ -1833,6 +1945,7 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
AutoCompleteDropdownTags.__init__( self, parent, location_context, tag_context.service_key )
+ self._location_context_button.SetOnlyLocalFileDomainsAllowed( only_allow_local_file_domains )
self._location_context_button.SetAllKnownFilesAllowed( allow_all_known_files, True )
#
@@ -1918,6 +2031,12 @@ def __init__( self, parent: QW.QWidget, page_key, file_search_context: ClientSea
self._include_pending_tags.valueChanged.connect( self._IncludePendingChanged )
self._search_pause_play.valueChanged.connect( self._SynchronisedChanged )
+ predicates = self._file_search_context.GetPredicates()
+
+ tags = [ predicate.GetValue() for predicate in predicates if predicate.GetType() == ClientSearch.PREDICATE_TYPE_TAG ]
+
+ self.SetContextTags( tags )
+
def _AdvancedORInput( self ):
@@ -2362,7 +2481,13 @@ def _SetupTopListBox( self ):
def _NotifyPredicatesBoxChanged( self ):
- self._file_search_context.SetPredicates( self._predicates_listbox.GetPredicates() )
+ predicates = self._predicates_listbox.GetPredicates()
+
+ self._file_search_context.SetPredicates( predicates )
+
+ tags = [ predicate.GetValue() for predicate in predicates if predicate.GetType() == ClientSearch.PREDICATE_TYPE_TAG ]
+
+ self.SetContextTags( tags )
self._SignalNewSearchState()
@@ -2845,14 +2970,11 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
nullEntered = QC.Signal()
tagsPasted = QC.Signal( list )
- def __init__( self, parent, chosen_tag_callable, location_context, tag_service_key, tag_service_key_changed_callable = None, show_paste_button = False ):
+ def __init__( self, parent, chosen_tag_callable, location_context, tag_service_key, show_paste_button = False ):
self._display_tag_service_key = tag_service_key
self._chosen_tag_callable = chosen_tag_callable
- self._tag_service_key_changed_callable = tag_service_key_changed_callable
-
- service = CG.client_controller.services_manager.GetService( tag_service_key )
tag_autocomplete_options = CG.client_controller.tag_display_manager.GetTagAutocompleteOptions( tag_service_key )
@@ -2897,16 +3019,6 @@ def _BroadcastChoices( self, predicates, shift_down ):
self._ClearInput()
- def _TagContextJustChanged( self, tag_context: ClientSearch.TagContext ):
-
- it_changed = AutoCompleteDropdownTags._TagContextJustChanged( self, tag_context )
-
- if it_changed and self._tag_service_key_changed_callable is not None:
-
- self._tag_service_key_changed_callable( self._tag_service_key )
-
-
-
def _GetCurrentBroadcastTextPredicate( self ) -> typing.Optional[ ClientSearch.Predicate ]:
parsed_autocomplete_text = self._GetParsedAutocompleteText()
diff --git a/hydrus/client/gui/search/ClientGUILocation.py b/hydrus/client/gui/search/ClientGUILocation.py
index 152c1a15f..7a8c41e03 100644
--- a/hydrus/client/gui/search/ClientGUILocation.py
+++ b/hydrus/client/gui/search/ClientGUILocation.py
@@ -17,17 +17,18 @@
class EditMultipleLocationContextPanel( ClientGUIScrolledPanels.EditPanel ):
- def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext, all_known_files_allowed: bool, only_local_file_domains_allowed: bool ):
+ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.LocationContext, all_known_files_allowed: bool, only_importable_domains_allowed: bool, only_local_file_domains_allowed: bool ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._original_location_context = location_context
self._all_known_files_allowed = all_known_files_allowed
+ self._only_importable_domains_allowed = only_importable_domains_allowed
self._only_local_file_domains_allowed = only_local_file_domains_allowed
self._location_list = ClientGUICommon.BetterCheckBoxList( self )
- services = ClientLocation.GetPossibleFileDomainServicesInOrder( all_known_files_allowed, only_local_file_domains_allowed )
+ services = ClientLocation.GetPossibleFileDomainServicesInOrder( all_known_files_allowed, only_importable_domains_allowed, only_local_file_domains_allowed )
for service in services:
@@ -41,7 +42,7 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio
advanced_mode = CG.client_controller.new_options.GetBoolean( 'advanced_mode' )
- if advanced_mode and not only_local_file_domains_allowed:
+ if advanced_mode and not ( only_local_file_domains_allowed or only_importable_domains_allowed ):
for service in services:
@@ -142,13 +143,14 @@ def __init__( self, parent: QW.QWidget, location_context: ClientLocation.Locatio
self._all_known_files_allowed = True
self._all_known_files_allowed_only_in_advanced_mode = False
self._only_importable_domains_allowed = False
+ self._only_local_file_domains_allowed = False
self.SetValue( location_context, force_label = True )
def _EditLocation( self ):
- services = ClientLocation.GetPossibleFileDomainServicesInOrder( self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed )
+ services = ClientLocation.GetPossibleFileDomainServicesInOrder( self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed )
menu = ClientGUIMenus.GenerateMenu( self )
@@ -222,7 +224,7 @@ def _EditMultipleLocationContext( self ):
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit multiple location' ) as dlg:
- panel = EditMultipleLocationContextPanel( dlg, self._location_context, self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed )
+ panel = EditMultipleLocationContextPanel( dlg, self._location_context, self._IsAllKnownFilesServiceTypeAllowed(), self._only_importable_domains_allowed, self._only_local_file_domains_allowed )
dlg.SetPanel( panel )
@@ -264,6 +266,11 @@ def SetOnlyImportableDomainsAllowed( self, only_importable_domains_allowed: bool
self._only_importable_domains_allowed = only_importable_domains_allowed
+ def SetOnlyLocalFileDomainsAllowed( self, only_local_file_domains_allowed: bool ):
+
+ self._only_local_file_domains_allowed = only_local_file_domains_allowed
+
+
def SetAllKnownFilesAllowed( self, all_known_files_allowed: bool, all_known_files_allowed_only_in_advanced_mode: bool ):
self._all_known_files_allowed = all_known_files_allowed
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index 06b2b07d8..064575085 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -1684,11 +1684,6 @@ def __init__( self, parent, service ):
subpanels.append( ( ReviewServiceIPFSSubPanel( self, service ), CC.FLAGS_EXPAND_BOTH_WAYS ) )
- if service_type == HC.LOCAL_BOORU:
-
- subpanels.append( ( ReviewServiceLocalBooruSubPanel( self, service ), CC.FLAGS_EXPAND_BOTH_WAYS ) )
-
-
if service_type == HC.CLIENT_API_SERVICE:
subpanels.append( ( ReviewServiceClientAPISubPanel( self, service ), CC.FLAGS_EXPAND_BOTH_WAYS ) )
@@ -3676,267 +3671,6 @@ def qt_code( ipfs_shares ):
-class ReviewServiceLocalBooruSubPanel( ClientGUICommon.StaticBox ):
-
- def __init__( self, parent, service ):
-
- ClientGUICommon.StaticBox.__init__( self, parent, 'local booru' )
-
- self._service = service
-
- self._share_key_info = {}
-
- self._my_updater = ClientGUIAsync.FastThreadToGUIUpdater( self, self._Refresh )
-
- self._service_status = ClientGUICommon.BetterStaticText( self )
- self._service_status.setWordWrap( True )
-
- booru_share_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
-
- self._booru_shares = ClientGUIListCtrl.BetterListCtrl( booru_share_panel, CGLC.COLUMN_LIST_LOCAL_BOORU_SHARES.ID, 10, self._ConvertDataToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
-
- booru_share_panel.SetListCtrl( self._booru_shares )
-
- booru_share_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
- booru_share_panel.AddButton( 'delete', self._Delete, enabled_only_on_selection = True )
- booru_share_panel.AddSeparator()
- booru_share_panel.AddButton( 'open in new page', self._OpenSearch, enabled_only_on_selection = True )
- booru_share_panel.AddButton( 'copy internal share url', self._CopyInternalShareURL, enabled_check_func = self._CanCopyURL )
- booru_share_panel.AddButton( 'copy external share url', self._CopyExternalShareURL, enabled_check_func = self._CanCopyURL )
-
- self._booru_shares.Sort()
-
- #
-
- self._Refresh()
-
- #
-
- self.Add( self._service_status, CC.FLAGS_EXPAND_PERPENDICULAR )
- self.Add( booru_share_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
-
- CG.client_controller.sub( self, 'ServiceUpdated', 'service_updated' )
-
-
- def _CanCopyURL( self ):
-
- has_selected = self._booru_shares.HasSelected()
- service_is_running = self._service.GetPort() is not None
-
- return has_selected and service_is_running
-
-
- def _ConvertDataToListCtrlTuples( self, share_key ):
-
- info = self._share_key_info[ share_key ]
-
- name = info[ 'name' ]
- text = info[ 'text' ]
- timeout = info[ 'timeout' ]
- hashes = info[ 'hashes' ]
-
- num_hashes = len( hashes )
-
- pretty_name = name
- pretty_text = text
- pretty_timeout = HydrusTime.TimestampToPrettyExpires( timeout )
- pretty_hashes = HydrusData.ToHumanInt( num_hashes )
-
- sort_timeout = ClientGUIListCtrl.SafeNoneInt( timeout )
-
- display_tuple = ( pretty_name, pretty_text, pretty_timeout, pretty_hashes )
- sort_tuple = ( name, text, sort_timeout, num_hashes )
-
- return ( display_tuple, sort_tuple )
-
-
- def _CopyExternalShareURL( self ):
-
- internal_port = self._service.GetPort()
-
- if internal_port is None:
-
- ClientGUIDialogsMessage.ShowWarning( self, 'The local booru is not currently running!' )
-
-
- urls = []
-
- for share_key in self._booru_shares.GetData( only_selected = True ):
-
- try:
-
- url = self._service.GetExternalShareURL( share_key )
-
- except Exception as e:
-
- HydrusData.ShowException( e )
-
- ClientGUIDialogsMessage.ShowCritical( self, 'Problem!', f'Unfortunately, could not generate an external URL: {e}' )
-
- return
-
-
- urls.append( url )
-
-
- text = '\n'.join( urls )
-
- CG.client_controller.pub( 'clipboard', 'text', text )
-
-
- def _CopyInternalShareURL( self ):
-
- internal_port = self._service.GetPort()
-
- if internal_port is None:
-
- ClientGUIDialogsMessage.ShowWarning( self, 'The local booru is not currently running!' )
-
-
- urls = []
-
- for share_key in self._booru_shares.GetData( only_selected = True ):
-
- url = self._service.GetInternalShareURL( share_key )
-
- urls.append( url )
-
-
- text = '\n'.join( urls )
-
- CG.client_controller.pub( 'clipboard', 'text', text )
-
-
- def _Delete( self ):
-
- result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
-
- if result == QW.QDialog.Accepted:
-
- for share_key in self._booru_shares.GetData( only_selected = True ):
-
- CG.client_controller.Write( 'delete_local_booru_share', share_key )
-
-
- self._booru_shares.DeleteSelected()
-
-
-
- def _Edit( self ):
-
- for share_key in self._booru_shares.GetData( only_selected = True ):
-
- info = self._share_key_info[ share_key ]
-
- name = info[ 'name' ]
- text = info[ 'text' ]
- timeout = info[ 'timeout' ]
- hashes = info[ 'hashes' ]
-
- with ClientGUIDialogs.DialogInputLocalBooruShare( self, share_key, name, text, timeout, hashes, new_share = False) as dlg:
-
- if dlg.exec() == QW.QDialog.Accepted:
-
- ( share_key, name, text, timeout, hashes ) = dlg.GetInfo()
-
- info = {}
-
- info[ 'name' ] = name
- info[ 'text' ] = text
- info[ 'timeout' ] = timeout
- info[ 'hashes' ] = hashes
-
- CG.client_controller.Write( 'local_booru_share', share_key, info )
-
- else:
-
- break
-
-
-
-
- self._Refresh()
-
-
- def _OpenSearch( self ):
-
- location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
-
- for share_key in self._booru_shares.GetData( only_selected = True ):
-
- info = self._share_key_info[ share_key ]
-
- name = info[ 'name' ]
- hashes = info[ 'hashes' ]
-
- CG.client_controller.pub( 'new_page_query', location_context, initial_hashes = hashes, page_name = 'booru share: ' + name )
-
-
-
- def _Refresh( self ):
-
- if not self or not QP.isValid( self ):
-
- return
-
-
- port = self._service.GetPort()
-
- if port is None:
-
- status = 'The local booru is not running.'
-
- else:
-
- status = 'The local booru should be running on port {}.'.format( port )
-
- upnp_port = self._service.GetUPnPPort()
-
- if upnp_port is not None:
-
- status += ' It should be open via UPnP on external port {}.'.format( upnp_port )
-
-
-
- status += ' NOTE: I am sunsetting this service in the coming weeks, everything will be removed.'
-
- self._service_status.setText( status )
-
- CG.client_controller.CallToThread( self.THREADFetchInfo, self._service )
-
-
- def ServiceUpdated( self, service ):
-
- if service.GetServiceKey() == self._service.GetServiceKey():
-
- self._service = service
-
- self._my_updater.Update()
-
-
-
- def THREADFetchInfo( self, service ):
-
- def qt_code( booru_shares ):
-
- if not self or not QP.isValid( self ):
-
- return
-
-
- self._share_key_info.update( booru_shares )
-
- self._booru_shares.SetData( list(booru_shares.keys()) )
-
- self._booru_shares.Sort()
-
-
- booru_shares = CG.client_controller.Read( 'local_booru_shares' )
-
- QP.CallAfter( qt_code, booru_shares )
-
-
-
class ReviewServiceRatingSubPanel( ClientGUICommon.StaticBox ):
def __init__( self, parent, service ):
diff --git a/hydrus/client/networking/ClientLocalServer.py b/hydrus/client/networking/ClientLocalServer.py
index f69a62c3d..7dd65980f 100644
--- a/hydrus/client/networking/ClientLocalServer.py
+++ b/hydrus/client/networking/ClientLocalServer.py
@@ -20,21 +20,7 @@ def __init__( self, service, allow_non_local_connections ):
HydrusServer.HydrusService.__init__( self, service )
-class HydrusServiceBooru( HydrusClientService ):
-
- def _InitRoot( self ):
-
- root = HydrusClientService._InitRoot( self )
-
- root.putChild( b'gallery', ClientLocalServerResources.HydrusResourceBooruGallery( self._service, self._client_requests_domain ) )
- root.putChild( b'page', ClientLocalServerResources.HydrusResourceBooruPage( self._service, self._client_requests_domain ) )
- root.putChild( b'file', ClientLocalServerResources.HydrusResourceBooruFile( self._service, self._client_requests_domain ) )
- root.putChild( b'thumbnail', ClientLocalServerResources.HydrusResourceBooruThumbnail( self._service, self._client_requests_domain ) )
- root.putChild( b'style.css', ClientLocalServerResources.local_booru_css )
-
- return root
-
-
+
class HydrusServiceClientAPI( HydrusClientService ):
def _InitRoot( self ):
diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py
index 340471e5b..fd3affb0a 100644
--- a/hydrus/client/networking/ClientLocalServerResources.py
+++ b/hydrus/client/networking/ClientLocalServerResources.py
@@ -61,9 +61,6 @@
from hydrus.client.search import ClientSearchParseSystemPredicates
from hydrus.client.gui import ClientGUIPopupMessages
-
-local_booru_css = FileResource( os.path.join( HC.STATIC_DIR, 'local_booru_style.css' ), defaultType = 'text/css' )
-
LOCAL_BOORU_INT_PARAMS = set()
LOCAL_BOORU_BYTE_PARAMS = { 'share_key', 'hash' }
LOCAL_BOORU_STRING_PARAMS = set()
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index c47ae3fcb..022deddc1 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,7 +105,7 @@
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 572
+SOFTWARE_VERSION = 573
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/external/SystemPredicateParser.py b/hydrus/external/SystemPredicateParser.py
index 75c52b1cd..7359b3643 100644
--- a/hydrus/external/SystemPredicateParser.py
+++ b/hydrus/external/SystemPredicateParser.py
@@ -570,6 +570,13 @@ def parse_value( string: str, spec ):
elif spec == Value.TIME_SEC_MSEC:
+
+ # 'has duration'
+ if string.startswith( 'has' ) or string.startswith( 'no' ):
+
+ return '', ( 0, 0 )
+
+
match = re.match( '((?P0|([1-9][0-9]*))\s*(seconds|second|secs|sec|s))?\s*((?P0|([1-9][0-9]*))\s*(milliseconds|millisecond|msecs|msec|ms))?', string )
if match and (match.group( 'sec' ) or match.group( 'msec' )):
seconds = int( match.group( 'sec' ) ) if match.group( 'sec' ) else 0
@@ -724,7 +731,9 @@ def parse_operator( string: str, spec ):
if spec is None:
return string, None
elif spec in ( Operators.RELATIONAL, Operators.RELATIONAL_EXACT, Operators.RELATIONAL_TIME ):
+
exact = spec == Operators.RELATIONAL_EXACT
+
ops = [ '=', '<', '>' ]
if spec == Operators.RELATIONAL_TIME:
@@ -736,8 +745,6 @@ def parse_operator( string: str, spec ):
op_string = string[ : re_result.start() ]
string_result = re_result.group()
- invert_ops = not string_looks_like_date( string_result )
-
looks_like_date = string_looks_like_date( string_result )
invert_ops = not looks_like_date
diff --git a/hydrus/test/TestClientTags.py b/hydrus/test/TestClientTags.py
index 71151ad4c..4a621e039 100644
--- a/hydrus/test/TestClientTags.py
+++ b/hydrus/test/TestClientTags.py
@@ -2046,6 +2046,8 @@ def test_system_predicate_parsing( self ):
( 'system:duration: has duration', "system:has_duration" ),
( 'system:duration: no duration', " system:no_duration" ),
( 'system:duration: no duration', "system:no duration" ),
+ ( 'system:duration: has duration', "system:duration: has duration" ),
+ ( 'system:duration: no duration', "system:duration: no duration" ),
( 'system:is the best quality file of its duplicate group', "system:is the best quality file of its group" ),
( 'system:is not the best quality file of its duplicate group', "system:isn't the best quality file of its duplicate group" ),
( 'system:is not the best quality file of its duplicate group', 'system:is not the best quality file of its duplicate group' ),
diff --git a/hydrus/test/TestController.py b/hydrus/test/TestController.py
index 85ef1f3ca..5e682de76 100644
--- a/hydrus/test/TestController.py
+++ b/hydrus/test/TestController.py
@@ -237,7 +237,6 @@ def show_text( text ): pass
services = []
- services.append( ClientServices.GenerateService( CC.LOCAL_BOORU_SERVICE_KEY, HC.LOCAL_BOORU, 'local booru' ) )
services.append( ClientServices.GenerateService( CC.CLIENT_API_SERVICE_KEY, HC.CLIENT_API_SERVICE, 'client api' ) )
services.append( ClientServices.GenerateService( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, HC.COMBINED_LOCAL_FILE, 'all local files' ) )
services.append( ClientServices.GenerateService( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY, HC.COMBINED_LOCAL_MEDIA, 'all my files' ) )
@@ -314,7 +313,6 @@ def show_text( text ): pass
self.bitmap_manager = ClientManagers.BitmapManager( self )
- self.local_booru_manager = ClientCaches.LocalBooruCache( self )
self.client_api_manager = ClientAPI.APIManager()
self._cookies = {}
diff --git a/hydrus/test/TestHydrusServer.py b/hydrus/test/TestHydrusServer.py
index 089f07236..c991ea861 100644
--- a/hydrus/test/TestHydrusServer.py
+++ b/hydrus/test/TestHydrusServer.py
@@ -58,8 +58,6 @@ def setUpClass( cls ):
cls._clientside_tag_service.SetCredentials( HydrusNetwork.Credentials( '127.0.0.1', HC.DEFAULT_SERVICE_PORT, cls._access_key ) )
cls._clientside_admin_service.SetCredentials( HydrusNetwork.Credentials( '127.0.0.1', HC.DEFAULT_SERVER_ADMIN_PORT, cls._access_key ) )
- cls._local_booru = ClientServices.GenerateService( CC.LOCAL_BOORU_SERVICE_KEY, HC.LOCAL_BOORU, 'local booru' )
-
services_manager = HG.test_controller.services_manager
services_manager._keys_to_services[ cls._clientside_file_service.GetServiceKey() ] = cls._clientside_file_service
@@ -95,8 +93,6 @@ def TWISTEDSetup():
reactor.listenSSL( HC.DEFAULT_SERVICE_PORT + 1, ServerServer.HydrusServiceRepositoryFile( cls._serverside_file_service ), context_factory )
reactor.listenSSL( HC.DEFAULT_SERVICE_PORT, ServerServer.HydrusServiceRepositoryTag( cls._serverside_tag_service ), context_factory )
- reactor.listenTCP( 45866, ClientLocalServer.HydrusServiceBooru( cls._local_booru, allow_non_local_connections = False ) )
-
reactor.callFromThread( TWISTEDSetup )
@@ -245,144 +241,6 @@ def _test_file_repo( self, service ):
except: pass
- def _test_local_booru( self, host, port ):
-
- #
-
- connection = http.client.HTTPConnection( host, port, timeout = 10 )
-
- #
-
- with open( os.path.join( HC.STATIC_DIR, 'local_booru_style.css' ), 'rb' ) as f:
-
- css = f.read()
-
-
- connection.request( 'GET', '/style.css' )
-
- response = connection.getresponse()
-
- data = response.read()
-
- self.assertEqual( data, css )
-
- #
-
- share_key = HydrusData.GenerateKey()
- hashes = [ HydrusData.GenerateKey() for i in range( 5 ) ]
-
- client_files_default = os.path.join( TestController.DB_DIR, 'client_files' )
-
- hash_encoded = hashes[0].hex()
-
- prefix = hash_encoded[:2]
-
- file_path = os.path.join( client_files_default, 'f' + prefix, hash_encoded + '.jpg' )
- thumbnail_path = os.path.join( client_files_default, 't' + prefix, hash_encoded + '.thumbnail' )
-
- with open( file_path, 'wb' ) as f:
-
- f.write( EXAMPLE_FILE )
-
-
- with open( thumbnail_path, 'wb' ) as f:
-
- f.write( EXAMPLE_THUMBNAIL )
-
-
- local_booru_manager = CG.client_controller.local_booru_manager
-
- #
-
- self._test_local_booru_requests( connection, share_key, hashes[0], 404 )
-
- #
-
- info = {
- 'name' : 'name',
- 'text' : 'text',
- 'timeout' : 0,
- 'hashes' : hashes
- }
-
- media_results = []
-
- for hash in hashes:
-
- file_info_manager = ClientMediaManagers.FileInfoManager( 1, hash, 500, HC.IMAGE_JPEG, 640, 480 )
-
- times_manager = ClientMediaManagers.TimesManager()
-
- notes_manager = ClientMediaManagers.NotesManager( {} )
-
- file_viewing_stats_manager = ClientMediaManagers.FileViewingStatsManager.STATICGenerateEmptyManager( times_manager )
-
- media_result = ClientMediaResult.MediaResult(
- file_info_manager,
- ClientMediaManagers.TagsManager( {}, {} ),
- times_manager,
- ClientMediaManagers.LocationsManager( set(), set(), set(), set(), times_manager ),
- ClientMediaManagers.RatingsManager( {} ),
- notes_manager,
- file_viewing_stats_manager
- )
-
- media_results.append( media_result )
-
-
- HG.test_controller.SetRead( 'local_booru_share_keys', [ share_key ] )
- HG.test_controller.SetRead( 'local_booru_share', info )
- HG.test_controller.SetRead( 'media_results', media_results )
-
- local_booru_manager.RefreshShares()
-
- #
-
- self._test_local_booru_requests( connection, share_key, hashes[0], 404 )
-
- #
-
- info[ 'timeout' ] = None
- HG.test_controller.SetRead( 'local_booru_share', info )
-
- local_booru_manager.RefreshShares()
-
- #
-
- self._test_local_booru_requests( connection, share_key, hashes[0], 200 )
-
- #
-
- HG.test_controller.SetRead( 'local_booru_share_keys', [] )
-
- local_booru_manager.RefreshShares()
-
- #
-
- self._test_local_booru_requests( connection, share_key, hashes[0], 404 )
-
-
- def _test_local_booru_requests( self, connection, share_key, hash, expected_result ):
-
- requests = []
-
- requests.append( '/gallery?share_key=' + share_key.hex() )
- requests.append( '/page?share_key=' + share_key.hex() + '&hash=' + hash.hex() )
- requests.append( '/file?share_key=' + share_key.hex() + '&hash=' + hash.hex() )
- requests.append( '/thumbnail?share_key=' + share_key.hex() + '&hash=' + hash.hex() )
-
- for request in requests:
-
- connection.request( 'GET', request )
-
- response = connection.getresponse()
-
- data = response.read()
-
- self.assertEqual( response.status, expected_result )
-
-
-
def _test_repo( self, service ):
service_key = service.GetServiceKey()
@@ -689,15 +547,6 @@ def test_server_admin( self ):
self._test_server_admin( self._clientside_admin_service )
- def test_local_booru( self ):
-
- host = '127.0.0.1'
- port = 45866
-
- self._test_basics( host, port, https = False )
- self._test_local_booru( host, port )
-
-
'''
class TestAMP( unittest.TestCase ):
diff --git a/mkdocs-gh-pages.yml b/mkdocs-gh-pages.yml
index bb948ec64..f165b1538 100644
--- a/mkdocs-gh-pages.yml
+++ b/mkdocs-gh-pages.yml
@@ -43,7 +43,6 @@ plugins:
'help/introduction.md': 'introduction.md'
'help/ipfs.md': 'ipfs.md'
'help/launch_arguments.md': 'launch_arguments.md'
- 'help/local_booru.md': 'local_booru.md'
'help/privacy.md': 'privacy.md'
'help/reducing_lag.md': 'reducing_lag.md'
'help/running_from_source.md': 'running_from_source.md'
@@ -61,4 +60,4 @@ theme:
- search.suggest
- content.code.annotate
- navigation.instant
- - content.action.edit
\ No newline at end of file
+ - content.action.edit
diff --git a/mkdocs.yml b/mkdocs.yml
index d3b11fecd..f9067ecb3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -36,7 +36,6 @@ nav:
- database_migration.md
- launch_arguments.md
- ipfs.md
- - local_booru.md
- server.md
- Alternate Installations:
- docker.md
@@ -161,4 +160,4 @@ markdown_extensions:
plugins:
- search
- - offline
\ No newline at end of file
+ - offline
diff --git a/requirements.txt b/requirements.txt
index 0fc4ad253..0f505b848 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,7 +23,7 @@ service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.8.1.78
-python-mpv==1.0.5
+mpv==1.0.6
requests==2.31.0
QtPy==2.4.1
diff --git a/setup_venv.bat b/setup_venv.bat
index 170e8c9a9..a3fcab90e 100644
--- a/setup_venv.bat
+++ b/setup_venv.bat
@@ -120,7 +120,7 @@ goto :parse_fail
:question_qt_advanced
ECHO:
-ECHO If you have multi-monitor menu position bugs with the normal Qt6, try "o" on Python ^<=3.10 or "m" on Python ^>=3.11.
+ECHO If you have multi-monitor menu position bugs with the normal Qt6, try "o" on Python 3.9 or "m" on Python 3.10.
SET /P qt="Do you want Qt6 (o)lder, Qt6 (m)iddle, Qt6 (t)est, or (w)rite your own? "
IF "%qt%" == "o" goto :question_mpv
@@ -132,8 +132,12 @@ goto :parse_fail
:question_qt_custom
ECHO:
-SET /P qt_custom_pyside6="Enter the exact PySide6 version you want, e.g. '6.6.0': "
-SET /P qt_custom_qtpy="Enter the exact qtpy version you want (probably '2.4.1'): "
+ECHO Enter the exact PySide6 version you want, e.g. '6.6.0':
+ECHO - For Python 3.10, your earliest available version is 6.2.0
+ECHO - For Python 3.11, your earliest available version is 6.4.0.1
+ECHO - For Python 3.12, your earliest available version is 6.6.0
+SET /P qt_custom_pyside6="Version: "
+SET /P qt_custom_qtpy="Enter the exact qtpy version you want (probably '2.4.1'; if older try '2.3.1'): "
goto :question_mpv
diff --git a/setup_venv.command b/setup_venv.command
index 664dd1755..b5d34c423 100755
--- a/setup_venv.command
+++ b/setup_venv.command
@@ -83,7 +83,7 @@ elif [ "$install_type" = "a" ]; then
if [ "$qt" = "a" ]; then
echo
- echo "If you are <=10.15 (Catalina) or otherwise have trouble with the normal Qt6, try \"o\" on Python <=3.10 or \"m\" on Python >=3.11."
+ echo "If you are <=10.15 (Catalina) or otherwise have trouble with the normal Qt6, try \"o\" on Python 3.9 or \"m\" on Python 3.10."
echo "Do you want Qt6 (o)lder, Qt6 (m)iddle, Qt6 (t)est, or (w)rite your own? "
read -r qt
if [ "$qt" = "o" ]; then
@@ -102,9 +102,13 @@ elif [ "$install_type" = "a" ]; then
if [ "$qt" = "w" ]; then
echo
- echo "Enter the exact PySide6 version you want, e.g. '6.6.0': "
+ echo "Enter the exact PySide6 version you want, e.g. '6.6.0':"
+ echo "- For Python 3.10, your earliest available version is 6.2.0"
+ echo "- For Python 3.11, your earliest available version is 6.4.0.1"
+ echo "- For Python 3.12, your earliest available version is 6.6.0"
+ echo "Version: "
read -r qt_custom_pyside6
- echo "Enter the exact qtpy version you want (probably '2.4.1'): "
+ echo "Enter the exact qtpy version you want (probably '2.4.1'; if older try '2.3.1'): "
read -r qt_custom_qtpy
fi
diff --git a/setup_venv.sh b/setup_venv.sh
index f5316bdd1..721921df9 100755
--- a/setup_venv.sh
+++ b/setup_venv.sh
@@ -83,7 +83,7 @@ elif [ "$install_type" = "a" ]; then
if [ "$qt" = "a" ]; then
echo
- echo "If you cannot boot with the normal Qt6, try \"o\" on Python ^<=3.10 or \"m\" on Python ^>=3.11."
+ echo "If you cannot boot with the normal Qt6, try \"o\" on Python 3.9 or \"m\" on Python 3.10."
echo "Do you want Qt6 (o)lder, Qt6 (m)iddle, Qt6 (t)est, or (w)rite your own? "
read -r qt
if [ "$qt" = "o" ]; then
@@ -102,9 +102,13 @@ elif [ "$install_type" = "a" ]; then
if [ "$qt" = "w" ]; then
echo
- echo "Enter the exact PySide6 version you want, e.g. '6.6.0': "
+ echo "Enter the exact PySide6 version you want, e.g. '6.6.0':"
+ echo "- For Python 3.10, your earliest available version is 6.2.0"
+ echo "- For Python 3.11, your earliest available version is 6.4.0.1"
+ echo "- For Python 3.12, your earliest available version is 6.6.0"
+ echo "Version: "
read -r qt_custom_pyside6
- echo "Enter the exact qtpy version you want (probably '2.4.1'): "
+ echo "Enter the exact qtpy version you want (probably '2.4.1'; if older try '2.3.1'): "
read -r qt_custom_qtpy
fi
diff --git a/static/build_files/linux/requirements.txt b/static/build_files/linux/requirements.txt
index d9622d753..ea75c04f8 100644
--- a/static/build_files/linux/requirements.txt
+++ b/static/build_files/linux/requirements.txt
@@ -23,7 +23,7 @@ service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.8.1.78
-python-mpv==1.0.5
+mpv==1.0.6
requests==2.31.0
QtPy==2.4.1
diff --git a/static/build_files/macos/requirements.txt b/static/build_files/macos/requirements.txt
index b3ecfb481..fb014ce0d 100644
--- a/static/build_files/macos/requirements.txt
+++ b/static/build_files/macos/requirements.txt
@@ -23,7 +23,7 @@ service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.8.1.78
-python-mpv==1.0.5
+mpv==1.0.6
requests==2.31.0
QtPy==2.4.1
diff --git a/static/build_files/windows/requirements.txt b/static/build_files/windows/requirements.txt
index e283a57ad..018425a64 100644
--- a/static/build_files/windows/requirements.txt
+++ b/static/build_files/windows/requirements.txt
@@ -23,7 +23,7 @@ service-identity>=18.1.0
Twisted>=20.3.0
opencv-python-headless==4.8.1.78
-python-mpv==1.0.5
+mpv==1.0.6
requests==2.31.0
QtPy==2.4.1
diff --git a/static/default/parsers/fxtwitter tweet api parser.png b/static/default/parsers/fxtwitter tweet api parser.png
new file mode 100644
index 000000000..ed12b6be2
Binary files /dev/null and b/static/default/parsers/fxtwitter tweet api parser.png differ
diff --git a/static/default/url_classes/fxtwitter tweet api.png b/static/default/url_classes/fxtwitter tweet api.png
new file mode 100644
index 000000000..508370784
Binary files /dev/null and b/static/default/url_classes/fxtwitter tweet api.png differ
diff --git a/static/default/url_classes/twitter syndication api timeline-profile.png b/static/default/url_classes/twitter syndication api timeline-profile.png
deleted file mode 100644
index de6a17ed4..000000000
Binary files a/static/default/url_classes/twitter syndication api timeline-profile.png and /dev/null differ
diff --git a/static/default/url_classes/twitter syndication api tweet-result.png b/static/default/url_classes/twitter syndication api tweet-result.png
deleted file mode 100644
index 08094398b..000000000
Binary files a/static/default/url_classes/twitter syndication api tweet-result.png and /dev/null differ
diff --git a/static/default/url_classes/twitter tweet (i_web_status).png b/static/default/url_classes/twitter tweet (i_web_status).png
deleted file mode 100644
index 05d0522e1..000000000
Binary files a/static/default/url_classes/twitter tweet (i_web_status).png and /dev/null differ
diff --git a/static/default/url_classes/twitter tweet.png b/static/default/url_classes/twitter tweet.png
index 37773efcb..8890e0535 100644
Binary files a/static/default/url_classes/twitter tweet.png and b/static/default/url_classes/twitter tweet.png differ
diff --git a/static/default/url_classes/x post.png b/static/default/url_classes/x post.png
new file mode 100644
index 000000000..5475b40c9
Binary files /dev/null and b/static/default/url_classes/x post.png differ
diff --git a/static/local_booru_style.css b/static/local_booru_style.css
deleted file mode 100644
index dbe229c3c..000000000
--- a/static/local_booru_style.css
+++ /dev/null
@@ -1,25 +0,0 @@
-a { color: #222; text-decoration: none; font-weight: bold; }
-a:hover { color: #555 }
-body { font-family: "Calibri", Arial, sans-serif; color: #555; line-height: 1.5; }
-h3 { color: #222; }
-ul li { list-style: none; margin: 1.0em 0em; }
-ul.bulletpoint li { list-style: disc; margin: 1.0em 0em; }
-
-.dealwithit { color: #ff0000; text-shadow:
-1px 1px 0 #ff7f00,
-2px 2px 0 #ffff00,
-3px 3px 0 #00ff00,
-4px 4px 0 #0000ff,
-5px 5px 0 #6600ff,
-6px 6px 0 #8b00ff; }
-.warning { color: #d00 }
-.lololol { text-decoration: line-through; }
-.right { text-align: right; }
-.screenshot { float: right; clear: both; margin: 10px; }
-
-.footer { text-align: center; font-size: 80% }
-.media { clear: both; }
-.thumbnail { margin: 1px; border: 1px rgb( 223, 227, 230 ) solid; display: inline-block; }
-.thumbnail_container { text-align: center; display: table-cell; vertical-align: middle; }
-.thumbnail_container img { display: block; margin-left: auto; margin-right: auto; }
-.timeout { font-size: 80%; float: right; margin: 2px }
\ No newline at end of file
diff --git a/static/requirements/advanced/requirements_mpv_new.txt b/static/requirements/advanced/requirements_mpv_new.txt
index 1ae80d037..7c73bbf6f 100644
--- a/static/requirements/advanced/requirements_mpv_new.txt
+++ b/static/requirements/advanced/requirements_mpv_new.txt
@@ -1 +1 @@
-python-mpv==1.0.5
+mpv==1.0.6
diff --git a/static/requirements/advanced/requirements_mpv_test.txt b/static/requirements/advanced/requirements_mpv_test.txt
index 1ae80d037..7c73bbf6f 100644
--- a/static/requirements/advanced/requirements_mpv_test.txt
+++ b/static/requirements/advanced/requirements_mpv_test.txt
@@ -1 +1 @@
-python-mpv==1.0.5
+mpv==1.0.6