Version 509
|
@ -22,15 +22,16 @@ jobs:
|
|||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
-
|
||||
name: Install mkdocs-material
|
||||
run: python3 -m pip install mkdocs-material
|
||||
-
|
||||
name: Build docs to /help
|
||||
run: mkdocs build -d help
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
python3 -m pip install mkdocs-material
|
||||
mkdocs build -d help
|
||||
-
|
||||
name: Install PyOxidizer
|
||||
run: python3 -m pip install pyoxidizer
|
||||
run: python3 -m pip install pyoxidizer==0.22.0
|
||||
-
|
||||
name: Build Hydrus
|
||||
run: |
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEY4+qDBYJKwYBBAHaRw8BAQdASU9rid8CK3cAWZL7eWnJp9Q/A0yfUQ9iid8B
|
||||
kOIHRay0MUh5ZHJ1cyBOZXR3b3JrIERldmVsb3BlciA8aHlkcnVzLmFkbWluQGdt
|
||||
YWlsLmNvbT6ImQQTFgoAQRYhBCVN2xmDKUePYg2hr3YknwUyEhM8BQJjj6oMAhsD
|
||||
BQkUsQQUBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEHYknwUyEhM8hJ8B
|
||||
AOqxFvGKQmra0hDHrWYqiG+GTmKj/DGJI9nPlpkl9f7WAP9FQ+gKhdfI3Szd95va
|
||||
C2H1iVaLCJiQpFecsxZBVredA7g4BGOPqgwSCisGAQQBl1UBBQEBB0Dk6fxwmYMm
|
||||
o0LHiBDmU7+1igXdm7IxHp6KaM+B5LZ8HQMBCAeIfgQYFgoAJhYhBCVN2xmDKUeP
|
||||
Yg2hr3YknwUyEhM8BQJjj6oMAhsMBQkUsQQUAAoJEHYknwUyEhM86+wA/3R1Ye2q
|
||||
2rCA64BlJiwlvmJSK9wpajKtxGMhty7mUYfjAQDfnyEsQg8jEc5XF0YLrq+OdnP/
|
||||
6/JN2h5YwyrpLCHICQ==
|
||||
=TMs7
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
@ -7,6 +7,50 @@ title: Changelog
|
|||
!!! note
|
||||
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
|
||||
|
||||
## [Version 509](https://github.com/hydrusnetwork/hydrus/releases/tag/v509)
|
||||
|
||||
### misc
|
||||
|
||||
* added an option 'mouse wheel can "scroll" through menu buttons' to _options->gui_. this turns off the behaviour where a mouse wheel event over, for instance, the file sort asc/desc button, will change the button's value rather than scrolling the underlying panel. if you found this annoying, you can finally turn it off!
|
||||
* fixed an annoying 'save service' bug that some users saw last week with the introduction of serverside Tag Filters. some users had an old datatype in their service data storage--a legacy issue--but the system now coerces all datatypes and direct sub-objects to a saveable format on load or update
|
||||
* the tag washing system now collapses more types of whitespace character to `space`. mostly this means tab is now converted to space, but some unicode stuff goes too
|
||||
* the hangul filler character `\u3164` is no longer permitted as a namespace or subtag. it can be in longer tags, but isn't allowed on its own (where it appears to be a blank space). (hydev saw one in the wild, probably from some cheeky post title)
|
||||
* let me know if you run across a newly invalid tag already in your system and the UI goes bananas--ideally hydrus should now catch this and either fix itself or report with a polite note, but let's see. if things go crazy, run _database->check and repair->fix invaliid tags_
|
||||
* improved some image transparency detection and slicing logic. it is more accurate and saves more memory now. also, the system that saves thumbnails will more reliably use jpegs when it doesn't need png's transparency
|
||||
* fixed some PSD thumbs showing a fully transparent transparency layer
|
||||
* fixed a bug where you could enter capital letters into the namespace colour list in 'tag presentation' options panel
|
||||
* the default twitter downloaders are all renamed to remove the confusing and technical 'syndication' label
|
||||
* 'speedcopy' is now an optional supported library. a couple users have suggested this to make network copies on Windows and Linux much faster. I'd like some advanced users who run from source to try adding it to their venvs, and we'll see how it works out IRL in different situations (you can see if it is loaded under _help->about_)
|
||||
* if you run from source, the 'advanced' setup route now offers a (t)est Qt install, which sets PySide6 6.4.1 (up from 6.3.21). feel free to try it out--it works well for me, but I want to test it more before trying to roll it to the releases
|
||||
* in a side thing, thanks to the user who walked me through setting up signed commits to github with my own PGP key. you can see my new key in the contacts help page, id 76249F053212133C, and I am now committing with it. I'm not very familiar with the sheer mechanics of this tech, so bear with me, but I'm pretty sure I can sign or encrypt something if ever needed
|
||||
|
||||
### macOS build fix
|
||||
|
||||
* since v505, many macOS users were unable to boot the built app. it has taken multiple rounds of back and forth with users, but we figured it out. (looks like pyoxidizer updating from 0.22.0 to 0.23.0 simply broke qtpy/Qt bindings, so we force a rollback this week)
|
||||
* also, the macOS app moves from PySide6 to PyQt6 this week. they are basically the same, but PyQt6 packages into a 258MB dmg, less than half the 548MB PySide6 one!
|
||||
* let me know if the macOS app gives any more trouble. otherwise, to the people who helped out here, thank you very much for the help!
|
||||
|
||||
### mostly boring tag filter panel
|
||||
|
||||
* removed the 'add' buttons; added 'delete' buttons to the simple whitelist and blacklist panels; added 'block everything' to simple blacklist panel
|
||||
* the panel now talks about the special sibling and namespace rules when you edit an explicit blacklist-mode-only filter (the tag import options blacklist works this way)
|
||||
* the 'you didn't need to add that exception' text and 'filter is too complicated for this panel' texts now show/hide rather than waste empty space
|
||||
* some of the simple-advanced interactions are better, but there's still some logical bork here. mostly stuff like when you hit the 'unnamespace' checkbox in the whitelist panel, it gets needlessly added to the 'except' column in the advanced, rather than just removed from the advanced 'exclude'. I'll fix this up in the near future
|
||||
* the two namespace checkbox lists are now sized more appropriately
|
||||
* the white/blacklist panels disable more simply and reliably
|
||||
|
||||
### boring cleanup
|
||||
|
||||
* the confusing 'view this file's duplicates' menu label, which was an artifact of an old submenu label, is removed. if the duplicate menu wants to present the 'view' commands for two locations, it'll title with the respective location, otherwise the commands speak for themselves, no label
|
||||
* some old 'check(er) timings' nomenclature is renamed to 'checker options' across the board
|
||||
* the hydrus serialisable dictionary now washes any nested lists or dicts to hydrus serialised equivalents, which should stop situations like the save service bug in future
|
||||
* the hydrus serialisable list can now handle a mix of hydrus serialisables and python primitives. it also washes its lists or dicts to serialisable equivalents
|
||||
* improved the data-stability of some image channel slicing
|
||||
* fixed some PIL fallback thumbnail generation, and improved its 'has transparency' png/jpeg decision-making
|
||||
* fixed the main thumbnail loader being confused at times about which thumbnail mime to load with. the check I have added is ultra-fast on data we are loading anyway, so we shouldn't notice a difference, but if you get slow thumb loads, let me know
|
||||
* fixed the media container embed buttons using the file mime rather than the thumb mime when loading thumbnails (again causing transparency issues)
|
||||
* fixed more generally bad mime handling in the thumbnail generation routine that could have caused more unusual transparency handling for clip, psd, or flash files
|
||||
|
||||
## [Version 508](https://github.com/hydrusnetwork/hydrus/releases/tag/v508)
|
||||
|
||||
### misc
|
||||
|
@ -445,40 +489,3 @@ title: Changelog
|
|||
* refactored the file service pathing db code (this does directory structures and multihashes for ipfs) to a new module
|
||||
* refactored some tag display, tag filtering, and tag autocomplete calls down to appropriate db modules
|
||||
* refactored and extended some tag sibling database methods and names to clarify whether they were working with ids or strings
|
||||
|
||||
## [Version 499](https://github.com/hydrusnetwork/hydrus/releases/tag/v499)
|
||||
|
||||
### mpv
|
||||
* updated the mpv version for Windows. this is more complicated than it sounds and has been fraught with difficulty at times, so I do not try it often, but the situation seems to be much better now. today we are updating about twelve months. I may be imagining it, but things seem a bit smoother. a variety of weird file support should be better--an old transparent apng that I know crashed older mpv no longer causes a crash--and there's some acceleration now for very new CPU chipsets. I've also insisted on precise seeking (rather than keyframe seeking, which some users may have defaulted to). mpv-1.dll is now mpv-2.dll
|
||||
* I don't have an easy Linux testbed any more, so I would be interested in a Linux 'running from source' user trying out a similar update and letting me know how it goes. try getting the latest libmpv1 and then update python-mpv to 1.0.1 on pip. your 'mpv api version' in _help->about_ should now be 2.0. this new python-mpv seems to have several compatibility improvements, which is what has plagued us before here
|
||||
* mpv on macOS is still a frustrating question mark, but if this works on Linux, it may open another door. who knows, maybe the new version doesn't crash instantly on load
|
||||
|
||||
### search change for potential duplicates
|
||||
* this is subtle and complicated, so if you are a casual user of duplicates, don't worry about it. duplicates page = better now
|
||||
* for those who are more invested in dupes, I have altered the main potential duplicate search query. when the filter prepares some potential dupes to compare, or you load up some random thumbs in the page, or simply when the duplicates processing page presents counts, this all now only tests kings. previously, it could compare any member of a duplicate group to any other, and it would nominate kings as group representatives, but this lead to some odd situations where if you said 'must be pixel dupes', you could get two low quality pixel dupes offering their better king(s) up for actual comparison, giving you a comparison that was not a pixel dupe. same for the general searching of potentials, where if you search for 'bad quality', any bad quality file you set as a dupe but didn't delete could get matched (including in 'both match' mode), and offer a 'nicer' king as tribute that didn't have the tag. now, it only searches kings. kings match searches, and it is those kings that must match pixel dupe rules. this also means that kings will always be available on the current file domain, and no fallback king-nomination-from-filtered-members routine is needed any more
|
||||
* the knock-on effect here is minimal, but in general all database work in the duplicate filter should be a little faster, and some of your numbers may be a few counts smaller, typically after discounting weird edge case split-up duplicate groups that aren't real/common enough to really worry about. if you use a waterfall of multiple local file services to process your files, you might see significantly smaller counts due to kings not always being in the same file domain as their bad members, so you may want to try 'all my files' or just see how it goes--might be far less confusing, now you are only given unambiguous kings. anyway, in general, I think no big differences here for most users except better precision in searching!
|
||||
* but let me know how you get on IRL!
|
||||
|
||||
### misc
|
||||
* thank's to a user's hard work, the default twitter downloader gets some upgrades this week: you can now download from twitter lists, a twitter user's likes, and twitter collections (which are curated lists of tweets). the downloaders still get a lot of 'ignored' results for text-only tweets, and you still have to be logged in to get nsfw, but this adds some neat tools to the toolbox
|
||||
* thanks to a user, the Client API now reports brief caching information and should boost Hydrus Companion performance (issue #605)
|
||||
* the simple shortcut list in the edit shortcut action dialog now no longer shows any duplicates (such as 'close media viewer' in the dupes window)
|
||||
* added a new default reason for tag petitions, 'clearing mass-pasted junk'. 'not applicable' is now 'not applicable/incorrect'
|
||||
* in the petition processing page, the content boxes now specifically say ADD or DELETE to reinforce what you are doing and to differentiate the two boxes when you have a pixel petition
|
||||
* in the petition processing page, the content boxes now grow and shrink in height, up to a max of 20 rows, depending on how much stuff is in them. I _think_ I have pixel perfect heights here, so let me know if yours are wrong!
|
||||
* the 'service info' rows in review services are now presented in nicer order
|
||||
* updated the header/title formatting across the help documentation. when you search for a page title, it should now show up in results (e.g. you type 'running from source', you get that nicely at the top, not a confusing sub-header of that article). the section links are also all now capitalised
|
||||
* misc refactoring
|
||||
|
||||
### bunch of fixes
|
||||
* fixed a weird and possible crash-inducing scrolling bug in the tag list some users had in Qt6
|
||||
* fixed a typo error in file lookup scripts from when I added multi-line support to the parsing system (issue #1221)
|
||||
* fixed some bad labels in 'speed and memory' that talked about 'MB' when the widget allowed setting different units. also, I updated the 'video buffer' option on that page to a full 'bytes value' widget too (issue #1223)
|
||||
* the 'bytes value' widget, where you can set '100 MB' and similar, now gives the 'unit' dropdown a little more minimum width. it was getting a little thin on some styles and not showing the full text in the dropdown menu (issue #1222)
|
||||
* fixed a bug in similar-shape-search-tree-rebalancing maintenance in the rare case that the queue of branches in need of regeneration become out of sync with the main tree (issue #1219)
|
||||
* fixed a bug in archive/delete filter where clicks that were making actions would start borked drag-and-drop panning states if you dragged before releasing the click. it would cause warped media movement if you then clicked on hover window greyspace
|
||||
* fixed the 'this was a cloudflare problem' scanner for the new 1.2.64 version of cloudscraper
|
||||
* updated the popupmanager's positioning update code to use a nicer event filter and gave its position calculation code a quick pass. it might fix some popup toaster position bugs, not sure
|
||||
* fixed a weird menu creation bug involving a QStandardItem appearing in the menu actions
|
||||
* fixed a similar weird QStandardItem bug in the media viewer canvas code
|
||||
* fixed an error that could appear on force-emptied pages that receive sort signals
|
||||
|
|
|
@ -8,6 +8,8 @@ I welcome all your bug reports, questions, ideas, and comments. It is always int
|
|||
|
||||
You can contact me by email, twitter, discord, or the release threads on 8chan or Endchan--I do not mind which. Please know that I have difficulty with social media, and while I try to reply to all messages, it sometimes takes me a while to catch up.
|
||||
|
||||
If you need it, [here's](assets/hydev_key/Hydrus%20Network%20Developer_76249F053212133C_public.asc) my public GPG key.
|
||||
|
||||
The [Github Issue Tracker](https://github.com/hydrusnetwork/hydrus/issues) was turned off for some time, as it did not fit my workflow and I could not keep up, but it is now running again, managed by a team of volunteer users. Please feel free to submit feature requests there if you are comfortable with Github. I am not socially active on Github, please do not ping me there.
|
||||
|
||||
I am on the discord on Saturday afternoon, USA time, if you would like to talk live, and briefly on Wednesday after I put the release out. If that is not a good time for you, please leave me a DM and I will get to you when I can. There are also plenty of other hydrus users who idle who can help with support questions.
|
||||
|
|
|
@ -34,6 +34,45 @@
|
|||
<div class="content">
|
||||
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
|
||||
<ul>
|
||||
<li>
|
||||
<h2 id="version_509"><a href="#version_509">version 509</a></h2>
|
||||
<ul>
|
||||
<li><h3>misc</h3></li>
|
||||
<li>added an option 'mouse wheel can "scroll" through menu buttons' to _options->gui_. this turns off the behaviour where a mouse wheel event over, for instance, the file sort asc/desc button, will change the button's value rather than scrolling the underlying panel. if you found this annoying, you can finally turn it off!</li>
|
||||
<li>fixed an annoying 'save service' bug that some users saw last week with the introduction of serverside Tag Filters. some users had an old datatype in their service data storage--a legacy issue--but the system now coerces all datatypes and direct sub-objects to a saveable format on load or update</li>
|
||||
<li>the tag washing system now collapses more types of whitespace character to `space`. mostly this means tab is now converted to space, but some unicode stuff goes too</li>
|
||||
<li>the hangul filler character `\u3164` is no longer permitted as a namespace or subtag. it can be in longer tags, but isn't allowed on its own (where it appears to be a blank space). (hydev saw one in the wild, probably from some cheeky post title)</li>
|
||||
<li>let me know if you run across a newly invalid tag already in your system and the UI goes bananas--ideally hydrus should now catch this and either fix itself or report with a polite note, but let's see. if things go crazy, run _database->check and repair->fix invaliid tags_</li>
|
||||
<li>improved some image transparency detection and slicing logic. it is more accurate and saves more memory now. also, the system that saves thumbnails will more reliably use jpegs when it doesn't need png's transparency</li>
|
||||
<li>fixed some PSD thumbs showing a fully transparent transparency layer</li>
|
||||
<li>fixed a bug where you could enter capital letters into the namespace colour list in 'tag presentation' options panel</li>
|
||||
<li>the default twitter downloaders are all renamed to remove the confusing and technical 'syndication' label</li>
|
||||
<li>'speedcopy' is now an optional supported library. a couple users have suggested this to make network copies on Windows and Linux much faster. I'd like some advanced users who run from source to try adding it to their venvs, and we'll see how it works out IRL in different situations (you can see if it is loaded under _help->about_)</li>
|
||||
<li>if you run from source, the 'advanced' setup route now offers a (t)est Qt install, which sets PySide6 6.4.1 (up from 6.3.21). feel free to try it out--it works well for me, but I want to test it more before trying to roll it to the releases</li>
|
||||
<li>in a side thing, thanks to the user who walked me through setting up signed commits to github with my own PGP key. you can see my new key in the contacts help page, id 76249F053212133C, and I am now committing with it. I'm not very familiar with the sheer mechanics of this tech, so bear with me, but I'm pretty sure I can sign or encrypt something if ever needed</li>
|
||||
<li><h3>macOS build fix</h3></li>
|
||||
<li>since v505, many macOS users were unable to boot the built app. it has taken multiple rounds of back and forth with users, but we figured it out. (looks like pyoxidizer updating from 0.22.0 to 0.23.0 simply broke qtpy/Qt bindings, so we force a rollback this week)</li>
|
||||
<li>also, the macOS app moves from PySide6 to PyQt6 this week. they are basically the same, but PyQt6 packages into a 258MB dmg, less than half the 548MB PySide6 one!</li>
|
||||
<li>let me know if the macOS app gives any more trouble. otherwise, to the people who helped out here, thank you very much for the help!</li>
|
||||
<li><h3>mostly boring tag filter panel</h3></li>
|
||||
<li>removed the 'add' buttons; added 'delete' buttons to the simple whitelist and blacklist panels; added 'block everything' to simple blacklist panel</li>
|
||||
<li>the panel now talks about the special sibling and namespace rules when you edit an explicit blacklist-mode-only filter (the tag import options blacklist works this way)</li>
|
||||
<li>the 'you didn't need to add that exception' text and 'filter is too complicated for this panel' texts now show/hide rather than waste empty space</li>
|
||||
<li>some of the simple-advanced interactions are better, but there's still some logical bork here. mostly stuff like when you hit the 'unnamespace' checkbox in the whitelist panel, it gets needlessly added to the 'except' column in the advanced, rather than just removed from the advanced 'exclude'. I'll fix this up in the near future</li>
|
||||
<li>the two namespace checkbox lists are now sized more appropriately</li>
|
||||
<li>the white/blacklist panels disable more simply and reliably</li>
|
||||
<li><h3>boring cleanup</h3></li>
|
||||
<li>the confusing 'view this file's duplicates' menu label, which was an artifact of an old submenu label, is removed. if the duplicate menu wants to present the 'view' commands for two locations, it'll title with the respective location, otherwise the commands speak for themselves, no label</li>
|
||||
<li>some old 'check(er) timings' nomenclature is renamed to 'checker options' across the board</li>
|
||||
<li>the hydrus serialisable dictionary now washes any nested lists or dicts to hydrus serialised equivalents, which should stop situations like the save service bug in future</li>
|
||||
<li>the hydrus serialisable list can now handle a mix of hydrus serialisables and python primitives. it also washes its lists or dicts to serialisable equivalents</li>
|
||||
<li>improved the data-stability of some image channel slicing</li>
|
||||
<li>fixed some PIL fallback thumbnail generation, and improved its 'has transparency' png/jpeg decision-making</li>
|
||||
<li>fixed the main thumbnail loader being confused at times about which thumbnail mime to load with. the check I have added is ultra-fast on data we are loading anyway, so we shouldn't notice a difference, but if you get slow thumb loads, let me know</li>
|
||||
<li>fixed the media container embed buttons using the file mime rather than the thumb mime when loading thumbnails (again causing transparency issues)</li>
|
||||
<li>fixed more generally bad mime handling in the thumbnail generation routine that could have caused more unusual transparency handling for clip, psd, or flash files</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2 id="version_508"><a href="#version_508">version 508</a></h2>
|
||||
<ul>
|
||||
|
|
|
@ -6,6 +6,7 @@ import time
|
|||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusExceptions
|
||||
from hydrus.core import HydrusFileHandling
|
||||
from hydrus.core import HydrusImageHandling
|
||||
from hydrus.core import HydrusThreading
|
||||
from hydrus.core import HydrusData
|
||||
|
@ -652,16 +653,6 @@ class ThumbnailCache( object ):
|
|||
hash = display_media.GetHash()
|
||||
mime = display_media.GetMime()
|
||||
|
||||
thumbnail_mime = HC.IMAGE_JPEG
|
||||
|
||||
# we don't actually know this, it comes down to detailed stuff, but since this is png vs jpeg it isn't a huge deal down in the guts of image loading
|
||||
# only really matters with transparency, so anything that can be transparent we'll prime with a png thing
|
||||
# ain't like I am encoding EXIF rotation in my jpeg thumbs
|
||||
if mime in ( HC.IMAGE_APNG, HC.IMAGE_PNG, HC.IMAGE_GIF, HC.IMAGE_ICON, HC.IMAGE_WEBP ):
|
||||
|
||||
thumbnail_mime = HC.IMAGE_PNG
|
||||
|
||||
|
||||
locations_manager = display_media.GetLocationsManager()
|
||||
|
||||
try:
|
||||
|
@ -682,6 +673,8 @@ class ThumbnailCache( object ):
|
|||
|
||||
try:
|
||||
|
||||
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( path )
|
||||
|
||||
numpy_image = ClientImageHandling.GenerateNumPyImage( path, thumbnail_mime )
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
@ -268,6 +268,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
self._dictionary[ 'booleans' ][ 'start_note_editing_at_end' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'menu_choice_buttons_can_mouse_scroll' ] = True
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'focus_preview_on_ctrl_click' ] = False
|
||||
self._dictionary[ 'booleans' ][ 'focus_preview_on_ctrl_click_only_static' ] = False
|
||||
self._dictionary[ 'booleans' ][ 'focus_preview_on_shift_click' ] = False
|
||||
|
|
|
@ -708,6 +708,7 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
if collapse_newlines:
|
||||
|
||||
# maybe should use HydrusText.DeserialiseNewlinedTexts, but that might change/break some existing parsers with the strip() trim
|
||||
raw_texts = [ HydrusText.RemoveNewlines( raw_text ) for raw_text in raw_texts ]
|
||||
|
||||
|
||||
|
|
|
@ -967,7 +967,7 @@ class HydrusBitmap( object ):
|
|||
|
||||
if isinstance( data, memoryview ) and not data.c_contiguous:
|
||||
|
||||
data = data.copy()
|
||||
data = data.tobytes() # this _should_ work and is an emergency relief
|
||||
|
||||
|
||||
if self._compressed:
|
||||
|
|
|
@ -152,7 +152,7 @@ def ConvertSubtagToSearchable( subtag ):
|
|||
|
||||
subtag = subtag.translate( IGNORED_TAG_SEARCH_CHARACTERS_UNICODE_TRANSLATE )
|
||||
|
||||
subtag = HydrusText.re_multiple_spaces.sub( ' ', subtag )
|
||||
subtag = HydrusText.re_one_or_more_whitespace.sub( ' ', subtag )
|
||||
|
||||
subtag = subtag.strip()
|
||||
|
||||
|
|
|
@ -304,7 +304,7 @@ def LoadFromNumPyImage( numpy_image: numpy.array ):
|
|||
|
||||
if depth != 1:
|
||||
|
||||
numpy_image = numpy_image[:,:,0] # let's fetch one channel. if the png is a perfect RGB conversion of the original (or, let's say, a Firefox bmp export), this actually works
|
||||
numpy_image = numpy_image[:,:,0].copy() # let's fetch one channel. if the png is a perfect RGB conversion of the original (or, let's say, a Firefox bmp export), this actually works
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -193,7 +193,10 @@ class Service( object ):
|
|||
self._LoadFromDictionary( dictionary )
|
||||
|
||||
|
||||
def __hash__( self ): return self._service_key.__hash__()
|
||||
def __hash__( self ):
|
||||
|
||||
return self._service_key.__hash__()
|
||||
|
||||
|
||||
def _CheckFunctional( self ):
|
||||
|
||||
|
@ -1018,7 +1021,7 @@ class ServiceRestricted( ServiceRemote ):
|
|||
dictionary[ 'account' ] = HydrusNetwork.Account.GenerateSerialisableTupleFromAccount( self._account )
|
||||
dictionary[ 'next_account_sync' ] = self._next_account_sync
|
||||
dictionary[ 'network_sync_paused' ] = self._network_sync_paused
|
||||
dictionary[ 'service_options' ] = self._service_options
|
||||
dictionary[ 'service_options' ] = HydrusSerialisable.SerialisableDictionary( self._service_options )
|
||||
|
||||
return dictionary
|
||||
|
||||
|
@ -1049,12 +1052,7 @@ class ServiceRestricted( ServiceRemote ):
|
|||
dictionary[ 'service_options' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
|
||||
self._service_options = dictionary[ 'service_options' ]
|
||||
|
||||
|
||||
def _SetNewServiceOptions( self, service_options ):
|
||||
|
||||
self._service_options.update( service_options )
|
||||
self._service_options = HydrusSerialisable.SerialisableDictionary( dictionary[ 'service_options' ] )
|
||||
|
||||
|
||||
def _SetNewTagFilter( self, tag_filter: HydrusTags.TagFilter ):
|
||||
|
@ -1062,6 +1060,11 @@ class ServiceRestricted( ServiceRemote ):
|
|||
self._service_options[ 'tag_filter' ] = tag_filter
|
||||
|
||||
|
||||
def _UpdateServiceOptions( self, service_options ):
|
||||
|
||||
self._service_options.update( service_options )
|
||||
|
||||
|
||||
def CanSyncAccount( self, including_external_communication = True ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -1415,7 +1418,7 @@ class ServiceRestricted( ServiceRemote ):
|
|||
|
||||
service_options = options_response[ 'service_options' ]
|
||||
|
||||
self._SetNewServiceOptions( service_options )
|
||||
self._UpdateServiceOptions( service_options )
|
||||
|
||||
|
||||
except HydrusExceptions.SerialisationException:
|
||||
|
@ -1653,18 +1656,6 @@ class ServiceRepository( ServiceRestricted ):
|
|||
|
||||
|
||||
|
||||
def _SetNewServiceOptions( self, service_options ):
|
||||
|
||||
if 'update_period' in service_options and 'update_period' in self._service_options and service_options[ 'update_period' ] != self._service_options[ 'update_period' ]:
|
||||
|
||||
update_period = service_options[ 'update_period' ]
|
||||
|
||||
self._metadata.CalculateNewNextUpdateDue( update_period )
|
||||
|
||||
|
||||
ServiceRestricted._SetNewServiceOptions( self, service_options )
|
||||
|
||||
|
||||
def _SyncDownloadMetadata( self ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -2282,6 +2273,18 @@ class ServiceRepository( ServiceRestricted ):
|
|||
|
||||
|
||||
|
||||
def _UpdateServiceOptions( self, service_options ):
|
||||
|
||||
if 'update_period' in service_options and 'update_period' in self._service_options and service_options[ 'update_period' ] != self._service_options[ 'update_period' ]:
|
||||
|
||||
update_period = service_options[ 'update_period' ]
|
||||
|
||||
self._metadata.CalculateNewNextUpdateDue( update_period )
|
||||
|
||||
|
||||
ServiceRestricted._UpdateServiceOptions( self, service_options )
|
||||
|
||||
|
||||
def CanDoIdleShutdownWork( self ):
|
||||
|
||||
with self._lock:
|
||||
|
|
|
@ -10713,6 +10713,40 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 508:
|
||||
|
||||
try:
|
||||
|
||||
domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
|
||||
|
||||
domain_manager.Initialise()
|
||||
|
||||
#
|
||||
|
||||
domain_manager.RenameGUG( 'twitter syndication collection lookup', 'twitter collection lookup' )
|
||||
domain_manager.RenameGUG( 'twitter syndication likes lookup', 'twitter likes lookup' )
|
||||
domain_manager.RenameGUG( 'twitter syndication list lookup', 'twitter list lookup' )
|
||||
domain_manager.RenameGUG( 'twitter syndication profile lookup', 'twitter profile lookup' )
|
||||
domain_manager.RenameGUG( 'twitter syndication profile lookup (with replies)', 'twitter profile lookup (with replies)' )
|
||||
|
||||
#
|
||||
|
||||
domain_manager.TryToLinkURLClassesAndParsers()
|
||||
|
||||
#
|
||||
|
||||
self.modules_serialisable.SetJSONDump( domain_manager )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
message = 'Trying to update some downloader objects 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, ) )
|
||||
|
|
|
@ -26,6 +26,7 @@ from hydrus.core import HydrusConstants as HC
|
|||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusEncryption
|
||||
from hydrus.core import HydrusExceptions
|
||||
from hydrus.core import HydrusFileHandling
|
||||
from hydrus.core import HydrusImageHandling
|
||||
from hydrus.core import HydrusPaths
|
||||
from hydrus.core import HydrusGlobals as HG
|
||||
|
@ -755,6 +756,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
|
|||
library_versions.append( ( 'lxml present: ', str( ClientParsing.LXML_IS_OK ) ) )
|
||||
library_versions.append( ( 'lz4 present: ', str( HydrusCompression.LZ4_OK ) ) )
|
||||
library_versions.append( ( 'pyopenssl present:', str( HydrusEncryption.OPENSSL_OK ) ) )
|
||||
library_versions.append( ( 'speedcopy present:', str( HydrusFileHandling.SPEEDCOPY_OK ) ) )
|
||||
library_versions.append( ( 'install dir', HC.BASE_DIR ) )
|
||||
library_versions.append( ( 'db dir', HG.client_controller.db_dir ) )
|
||||
library_versions.append( ( 'temp dir', HydrusTemp.GetCurrentTempDir() ) )
|
||||
|
|
|
@ -80,14 +80,12 @@ def AddDuplicatesMenu( win: QW.QWidget, menu: QW.QMenu, location_context: Client
|
|||
|
||||
ClientGUIMenus.AppendSeparator( duplicates_menu )
|
||||
|
||||
label = 'view this file\'s relations'
|
||||
|
||||
if job_location_context is combined_local_location_context:
|
||||
if len( view_duplicate_relations_jobs ) > 1:
|
||||
|
||||
label = '{} ({})'.format( label, HG.client_controller.services_manager.GetName( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
|
||||
label = '-for {}-'.format( job_location_context.ToString( HG.client_controller.services_manager.GetName ) )
|
||||
|
||||
ClientGUIMenus.AppendMenuLabel( duplicates_menu, label, label )
|
||||
|
||||
|
||||
ClientGUIMenus.AppendMenuLabel( duplicates_menu, label, label )
|
||||
|
||||
if HC.DUPLICATE_MEMBER in file_duplicate_types_to_counts:
|
||||
|
||||
|
@ -111,7 +109,7 @@ def AddDuplicatesMenu( win: QW.QWidget, menu: QW.QMenu, location_context: Client
|
|||
|
||||
if count > 0:
|
||||
|
||||
label = HydrusData.ToHumanInt( count ) + ' ' + HC.duplicate_type_string_lookup[ duplicate_type ]
|
||||
label = 'view {} {}'.format( HydrusData.ToHumanInt( count ), HC.duplicate_type_string_lookup[ duplicate_type ] )
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( duplicates_menu, label, 'Show these duplicates in a new page.', ClientGUIMedia.ShowDuplicatesInNewPage, job_location_context, focused_hash, duplicate_type )
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from hydrus.core import HydrusGlobals as HG
|
|||
from hydrus.core import HydrusImageHandling
|
||||
from hydrus.core import HydrusPaths
|
||||
from hydrus.core import HydrusSerialisable
|
||||
from hydrus.core import HydrusTags
|
||||
from hydrus.core import HydrusText
|
||||
|
||||
from hydrus.client import ClientApplicationCommand as CAC
|
||||
|
@ -1226,6 +1227,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
tt = 'In many places across the program (typically import status lists), the client will state a timestamp as "5 days ago". If you would prefer a standard ISO string, like "2018-03-01 12:40:23", check this.'
|
||||
self._always_show_iso_time.setToolTip( tt )
|
||||
|
||||
self._menu_choice_buttons_can_mouse_scroll = QW.QCheckBox( self._misc_panel )
|
||||
tt = 'Many buttons that produce menus when clicked are also "scrollable", so if you wheel your mouse over them, the selection will scroll through the underlying menu. If this is annoying for you, turn it off here!'
|
||||
self._menu_choice_buttons_can_mouse_scroll.setToolTip( tt )
|
||||
|
||||
self._human_bytes_sig_figs = ClientGUICommon.BetterSpinBox( self._misc_panel, min = 1, max = 6 )
|
||||
self._human_bytes_sig_figs.setToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.')
|
||||
|
||||
|
@ -1265,6 +1270,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._always_show_iso_time.setChecked( self._new_options.GetBoolean( 'always_show_iso_time' ) )
|
||||
|
||||
self._menu_choice_buttons_can_mouse_scroll.setChecked( self._new_options.GetBoolean( 'menu_choice_buttons_can_mouse_scroll' ) )
|
||||
|
||||
self._human_bytes_sig_figs.setValue( self._new_options.GetInteger( 'human_bytes_sig_figs' ) )
|
||||
|
||||
self._discord_dnd_fix.setChecked( self._new_options.GetBoolean( 'discord_dnd_fix' ) )
|
||||
|
@ -1303,6 +1310,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
rows = []
|
||||
|
||||
rows.append( ( 'Prefer ISO time ("2018-03-01 12:40:23") to "5 days ago": ', self._always_show_iso_time ) )
|
||||
rows.append( ( 'Mouse wheel can "scroll" through menu buttons: ', self._menu_choice_buttons_can_mouse_scroll ) )
|
||||
rows.append( ( 'Copy temp files for drag-and-drop (works for <=25, <200MB file DnDs--fixes Discord!): ', self._discord_dnd_fix ) )
|
||||
rows.append( ( 'Drag-and-drop export filename pattern: ', self._discord_dnd_filename_pattern ) )
|
||||
rows.append( ( '', self._export_pattern_button ) )
|
||||
|
@ -1383,6 +1391,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
HC.options[ 'confirm_client_exit' ] = self._confirm_client_exit.isChecked()
|
||||
|
||||
self._new_options.SetBoolean( 'always_show_iso_time', self._always_show_iso_time.isChecked() )
|
||||
self._new_options.SetBoolean( 'menu_choice_buttons_can_mouse_scroll', self._menu_choice_buttons_can_mouse_scroll.isChecked() )
|
||||
|
||||
self._new_options.SetInteger( 'human_bytes_sig_figs', self._human_bytes_sig_figs.value() )
|
||||
|
||||
|
@ -3667,11 +3676,18 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
namespace = dlg.GetValue()
|
||||
|
||||
namespace = namespace.lower().strip()
|
||||
|
||||
if namespace.endswith( ':' ):
|
||||
|
||||
namespace = namespace[:-1]
|
||||
|
||||
|
||||
if namespace != 'system':
|
||||
|
||||
namespace = HydrusTags.StripTextOfGumpf( namespace )
|
||||
|
||||
|
||||
if namespace in ( '', ':' ):
|
||||
|
||||
QW.QMessageBox.warning( self, 'Not allowed', 'Sorry, that namespace means unnamespaced/default namespaced, which are already listed.' )
|
||||
|
|
|
@ -1366,7 +1366,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._subscriptions_panel.NewButtonRow()
|
||||
|
||||
self._subscriptions_panel.AddButton( 'select subscriptions', self.SelectSubscriptions )
|
||||
self._subscriptions_panel.AddButton( 'overwrite checker timings', self.SetCheckerOptions, enabled_only_on_selection = True )
|
||||
self._subscriptions_panel.AddButton( 'overwrite checker options', self.SetCheckerOptions, enabled_only_on_selection = True )
|
||||
self._subscriptions_panel.AddButton( 'overwrite file import options', self.SetFileImportOptions, enabled_only_on_selection = True )
|
||||
self._subscriptions_panel.AddButton( 'overwrite tag import options', self.SetTagImportOptions, enabled_only_on_selection = True )
|
||||
self._subscriptions_panel.AddButton( 'overwrite note import options', self.SetNoteImportOptions, enabled_only_on_selection = True )
|
||||
|
@ -2817,7 +2817,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
checker_options = subscriptions[0].GetCheckerOptions()
|
||||
|
||||
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit check timings' ) as dlg:
|
||||
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit checker options' ) as dlg:
|
||||
|
||||
panel = ClientGUITime.EditCheckerOptions( dlg, checker_options )
|
||||
|
||||
|
|
|
@ -697,6 +697,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
#
|
||||
|
||||
self._redundant_st = ClientGUICommon.BetterStaticText( self, '', ellipsize_end = True )
|
||||
self._redundant_st.setVisible( False )
|
||||
|
||||
self._current_filter_st = ClientGUICommon.BetterStaticText( self, 'currently keeping: ', ellipsize_end = True )
|
||||
|
||||
|
@ -751,6 +752,17 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
test_text_vbox = QP.VBoxLayout()
|
||||
|
||||
if self._only_show_blacklist:
|
||||
|
||||
message = 'This is a fixed blacklist. It will apply rules against all test tag siblings and apply unnamespaced rules to namespaced test tags.'
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self, message )
|
||||
|
||||
st.setWordWrap( True )
|
||||
|
||||
QP.AddToLayout( test_text_vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
|
||||
QP.AddToLayout( test_text_vbox, self._test_result_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
@ -799,13 +811,6 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _AdvancedAddBlacklistButton( self ):
|
||||
|
||||
tag_slice = self._advanced_blacklist_input.GetValue()
|
||||
|
||||
self._AdvancedAddBlacklist( tag_slice )
|
||||
|
||||
|
||||
def _AdvancedAddBlacklistMultiple( self, tag_slices ):
|
||||
|
||||
for tag_slice in tag_slices:
|
||||
|
@ -839,13 +844,6 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _AdvancedAddWhitelistButton( self ):
|
||||
|
||||
tag_slice = self._advanced_whitelist_input.GetValue()
|
||||
|
||||
self._AdvancedAddWhitelist( tag_slice )
|
||||
|
||||
|
||||
def _AdvancedAddWhitelistMultiple( self, tag_slices ):
|
||||
|
||||
for tag_slice in tag_slices:
|
||||
|
@ -865,7 +863,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _AdvancedDeleteBlacklist( self ):
|
||||
def _AdvancedDeleteBlacklistButton( self ):
|
||||
|
||||
selected_tag_slices = self._advanced_blacklist.GetSelectedTagSlices()
|
||||
|
||||
|
@ -882,7 +880,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _AdvancedDeleteWhitelist( self ):
|
||||
def _AdvancedDeleteWhitelistButton( self ):
|
||||
|
||||
selected_tag_slices = self._advanced_whitelist.GetSelectedTagSlices()
|
||||
|
||||
|
@ -1100,68 +1098,62 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
blacklist_panel = ClientGUICommon.StaticBox( advanced_panel, 'exclude these' )
|
||||
self._advanced_blacklist_panel = ClientGUICommon.StaticBox( advanced_panel, 'exclude these' )
|
||||
|
||||
self._advanced_blacklist = ClientGUIListBoxes.ListBoxTagsFilter( blacklist_panel, read_only = self._read_only )
|
||||
self._advanced_blacklist = ClientGUIListBoxes.ListBoxTagsFilter( self._advanced_blacklist_panel, read_only = self._read_only )
|
||||
|
||||
self._advanced_blacklist_input = ClientGUIControls.TextAndPasteCtrl( blacklist_panel, self._AdvancedAddBlacklistMultiple, allow_empty_input = True )
|
||||
self._advanced_blacklist_input = ClientGUIControls.TextAndPasteCtrl( self._advanced_blacklist_panel, self._AdvancedAddBlacklistMultiple, allow_empty_input = True )
|
||||
|
||||
add_blacklist_button = ClientGUICommon.BetterButton( blacklist_panel, 'add', self._AdvancedAddBlacklistButton )
|
||||
delete_blacklist_button = ClientGUICommon.BetterButton( blacklist_panel, 'delete', self._AdvancedDeleteBlacklist )
|
||||
blacklist_everything_button = ClientGUICommon.BetterButton( blacklist_panel, 'block everything', self._AdvancedBlacklistEverything )
|
||||
delete_blacklist_button = ClientGUICommon.BetterButton( self._advanced_blacklist_panel, 'delete', self._AdvancedDeleteBlacklistButton )
|
||||
blacklist_everything_button = ClientGUICommon.BetterButton( self._advanced_blacklist_panel, 'block everything', self._AdvancedBlacklistEverything )
|
||||
|
||||
#
|
||||
|
||||
whitelist_panel = ClientGUICommon.StaticBox( advanced_panel, 'except for these' )
|
||||
self._advanced_whitelist_panel = ClientGUICommon.StaticBox( advanced_panel, 'except for these' )
|
||||
|
||||
self._advanced_whitelist = ClientGUIListBoxes.ListBoxTagsFilter( whitelist_panel, read_only = self._read_only )
|
||||
self._advanced_whitelist = ClientGUIListBoxes.ListBoxTagsFilter( self._advanced_whitelist_panel, read_only = self._read_only )
|
||||
|
||||
self._advanced_whitelist_input = ClientGUIControls.TextAndPasteCtrl( whitelist_panel, self._AdvancedAddWhitelistMultiple, allow_empty_input = True )
|
||||
self._advanced_whitelist_input = ClientGUIControls.TextAndPasteCtrl( self._advanced_whitelist_panel, self._AdvancedAddWhitelistMultiple, allow_empty_input = True )
|
||||
|
||||
self._advanced_add_whitelist_button = ClientGUICommon.BetterButton( whitelist_panel, 'add', self._AdvancedAddWhitelistButton )
|
||||
delete_whitelist_button = ClientGUICommon.BetterButton( whitelist_panel, 'delete', self._AdvancedDeleteWhitelist )
|
||||
delete_whitelist_button = ClientGUICommon.BetterButton( self._advanced_whitelist_panel, 'delete', self._AdvancedDeleteWhitelistButton )
|
||||
|
||||
#
|
||||
|
||||
if self._read_only:
|
||||
|
||||
self._advanced_blacklist_input.hide()
|
||||
add_blacklist_button.hide()
|
||||
delete_blacklist_button.hide()
|
||||
blacklist_everything_button.hide()
|
||||
|
||||
self._advanced_whitelist_input.hide()
|
||||
self._advanced_add_whitelist_button.hide()
|
||||
delete_whitelist_button.hide()
|
||||
|
||||
|
||||
button_hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( button_hbox, self._advanced_blacklist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( button_hbox, add_blacklist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( button_hbox, delete_blacklist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( button_hbox, blacklist_everything_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
blacklist_panel.Add( self._advanced_blacklist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
blacklist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._advanced_blacklist_panel.Add( self._advanced_blacklist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
self._advanced_blacklist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
#
|
||||
|
||||
button_hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( button_hbox, self._advanced_whitelist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( button_hbox, self._advanced_add_whitelist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( button_hbox, delete_whitelist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
whitelist_panel.Add( self._advanced_whitelist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
whitelist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
self._advanced_whitelist_panel.Add( self._advanced_whitelist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
self._advanced_whitelist_panel.Add( button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
#
|
||||
|
||||
hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( hbox, blacklist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, whitelist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._advanced_blacklist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( hbox, self._advanced_whitelist_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
advanced_panel.setLayout( hbox )
|
||||
|
||||
|
@ -1175,11 +1167,16 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
#
|
||||
|
||||
self._simple_blacklist_error_st = ClientGUICommon.BetterStaticText( blacklist_panel )
|
||||
self._simple_blacklist_error_st.setVisible( False )
|
||||
|
||||
self._simple_blacklist_global_checkboxes = ClientGUICommon.BetterCheckBoxList( blacklist_panel )
|
||||
|
||||
self._simple_blacklist_global_checkboxes.Append( 'unnamespaced tags', '' )
|
||||
self._simple_blacklist_global_checkboxes.Append( 'namespaced tags', ':' )
|
||||
|
||||
( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._simple_blacklist_global_checkboxes, ( 20, 3 ) )
|
||||
|
||||
self._simple_blacklist_global_checkboxes.setFixedHeight( h )
|
||||
|
||||
self._simple_blacklist_namespace_checkboxes = ClientGUICommon.BetterCheckBoxList( blacklist_panel )
|
||||
|
||||
|
@ -1197,6 +1194,9 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._simple_blacklist_input = ClientGUIControls.TextAndPasteCtrl( blacklist_panel, self._SimpleAddBlacklistMultiple, allow_empty_input = True )
|
||||
|
||||
delete_blacklist_button = ClientGUICommon.BetterButton( blacklist_panel, 'remove', self._SimpleDeleteBlacklistButton )
|
||||
blacklist_everything_button = ClientGUICommon.BetterButton( blacklist_panel, 'block everything', self._AdvancedBlacklistEverything )
|
||||
|
||||
#
|
||||
|
||||
if self._read_only:
|
||||
|
@ -1206,17 +1206,25 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._simple_blacklist_input.hide()
|
||||
|
||||
delete_blacklist_button.hide()
|
||||
blacklist_everything_button.hide()
|
||||
|
||||
|
||||
left_vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( left_vbox, self._simple_blacklist_global_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
left_vbox.addStretch( 1 )
|
||||
QP.AddToLayout( left_vbox, self._simple_blacklist_namespace_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( left_vbox, self._simple_blacklist_namespace_checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
button_hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( button_hbox, self._simple_blacklist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( button_hbox, delete_blacklist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
QP.AddToLayout( button_hbox, blacklist_everything_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
right_vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( right_vbox, self._simple_blacklist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( right_vbox, self._simple_blacklist_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( right_vbox, button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
main_hbox = QP.HBoxLayout()
|
||||
|
||||
|
@ -1242,12 +1250,17 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
#
|
||||
|
||||
self._simple_whitelist_error_st = ClientGUICommon.BetterStaticText( whitelist_panel )
|
||||
self._simple_whitelist_error_st.setVisible( False )
|
||||
|
||||
self._simple_whitelist_global_checkboxes = ClientGUICommon.BetterCheckBoxList( whitelist_panel )
|
||||
|
||||
self._simple_whitelist_global_checkboxes.Append( 'unnamespaced tags', '' )
|
||||
self._simple_whitelist_global_checkboxes.Append( 'namespaced tags', ':' )
|
||||
|
||||
( w, h ) = ClientGUIFunctions.ConvertTextToPixels( self._simple_whitelist_global_checkboxes, ( 20, 3 ) )
|
||||
|
||||
self._simple_whitelist_global_checkboxes.setFixedHeight( h )
|
||||
|
||||
self._simple_whitelist_namespace_checkboxes = ClientGUICommon.BetterCheckBoxList( whitelist_panel )
|
||||
|
||||
for namespace in self._namespaces:
|
||||
|
@ -1264,6 +1277,8 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
self._simple_whitelist_input = ClientGUIControls.TextAndPasteCtrl( whitelist_panel, self._SimpleAddWhitelistMultiple, allow_empty_input = True )
|
||||
|
||||
delete_whitelist_button = ClientGUICommon.BetterButton( whitelist_panel, 'remove', self._SimpleDeleteWhitelistButton )
|
||||
|
||||
#
|
||||
|
||||
if self._read_only:
|
||||
|
@ -1272,19 +1287,23 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._simple_whitelist_namespace_checkboxes.setEnabled( False )
|
||||
|
||||
self._simple_whitelist_input.hide()
|
||||
delete_whitelist_button.hide()
|
||||
|
||||
|
||||
|
||||
left_vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( left_vbox, self._simple_whitelist_global_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
left_vbox.addStretch( 1 )
|
||||
QP.AddToLayout( left_vbox, self._simple_whitelist_namespace_checkboxes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( left_vbox, self._simple_whitelist_namespace_checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
button_hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( button_hbox, self._simple_whitelist_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( button_hbox, delete_whitelist_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
||||
right_vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( right_vbox, self._simple_whitelist, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( right_vbox, self._simple_whitelist_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( right_vbox, button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
main_hbox = QP.HBoxLayout()
|
||||
|
||||
|
@ -1386,17 +1405,16 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
def _ShowRedundantError( self, text ):
|
||||
|
||||
self._redundant_st.setVisible( True )
|
||||
|
||||
self._redundant_st.setText( text )
|
||||
|
||||
HG.client_controller.CallLaterQtSafe( self._redundant_st, 2, 'clear redundant error', self._redundant_st.setText, '' )
|
||||
HG.client_controller.CallLaterQtSafe( self._redundant_st, 2, 'clear redundant error', self._redundant_st.setVisible, False )
|
||||
|
||||
|
||||
def _SimpleAddBlacklistMultiple( self, tag_slices ):
|
||||
|
||||
for tag_slice in tag_slices:
|
||||
|
||||
self._AdvancedAddBlacklist( tag_slice )
|
||||
|
||||
self._AdvancedAddBlacklistMultiple( tag_slices )
|
||||
|
||||
|
||||
def _SimpleAddWhitelistMultiple( self, tag_slices ):
|
||||
|
@ -1427,6 +1445,44 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
pass
|
||||
|
||||
|
||||
def _SimpleDeleteBlacklistButton( self ):
|
||||
|
||||
selected_tag_slices = self._simple_blacklist.GetSelectedTagSlices()
|
||||
|
||||
if len( selected_tag_slices ) > 0:
|
||||
|
||||
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
|
||||
|
||||
if result == QW.QDialog.Accepted:
|
||||
|
||||
self._simple_blacklist.RemoveTagSlices( selected_tag_slices )
|
||||
|
||||
self._simple_blacklist.tagsRemoved.emit( selected_tag_slices )
|
||||
|
||||
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _SimpleDeleteWhitelistButton( self ):
|
||||
|
||||
selected_tag_slices = self._simple_whitelist.GetSelectedTagSlices()
|
||||
|
||||
if len( selected_tag_slices ) > 0:
|
||||
|
||||
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
|
||||
|
||||
if result == QW.QDialog.Accepted:
|
||||
|
||||
self._simple_whitelist.RemoveTagSlices( selected_tag_slices )
|
||||
|
||||
self._simple_whitelist.tagsRemoved.emit( selected_tag_slices )
|
||||
|
||||
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _SimpleWhitelistRemoved( self, tag_slices ):
|
||||
|
||||
tag_slices = set( tag_slices )
|
||||
|
@ -1457,16 +1513,13 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
( whitelist_possible, blacklist_possible ) = self._GetWhiteBlacklistsPossible()
|
||||
|
||||
whitelist_tag_slices = self._advanced_whitelist.GetTagSlices()
|
||||
blacklist_tag_slices = self._advanced_blacklist.GetTagSlices()
|
||||
|
||||
self._whitelist_panel.setEnabled( whitelist_possible )
|
||||
|
||||
self._simple_whitelist_error_st.setVisible( not whitelist_possible )
|
||||
|
||||
if whitelist_possible:
|
||||
|
||||
self._simple_whitelist_error_st.clear()
|
||||
|
||||
self._simple_whitelist.setEnabled( True )
|
||||
self._simple_whitelist_global_checkboxes.setEnabled( True )
|
||||
self._simple_whitelist_input.setEnabled( True )
|
||||
|
||||
whitelist_tag_slices = set( whitelist_tag_slices )
|
||||
|
||||
if not self._CurrentlyBlocked( '' ):
|
||||
|
@ -1503,12 +1556,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
else:
|
||||
|
||||
self._simple_whitelist_error_st.setText( 'The filter is currently more complicated than a simple whitelist, so cannot be shown here.' )
|
||||
|
||||
self._simple_whitelist.setEnabled( False )
|
||||
self._simple_whitelist_global_checkboxes.setEnabled( False )
|
||||
self._simple_whitelist_namespace_checkboxes.setEnabled( False )
|
||||
self._simple_whitelist_input.setEnabled( False )
|
||||
self._simple_whitelist_error_st.setText( 'The filter is currently more complicated than a simple whitelist, so it cannot be shown here.' )
|
||||
|
||||
self._simple_whitelist.SetTagSlices( '' )
|
||||
|
||||
|
@ -1525,17 +1573,14 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
whitelist_tag_slices = self._advanced_whitelist.GetTagSlices()
|
||||
blacklist_tag_slices = self._advanced_blacklist.GetTagSlices()
|
||||
|
||||
self._blacklist_panel.setEnabled( blacklist_possible )
|
||||
|
||||
self._simple_blacklist_error_st.setVisible( not blacklist_possible )
|
||||
|
||||
if blacklist_possible:
|
||||
|
||||
self._simple_blacklist_error_st.clear()
|
||||
|
||||
self._simple_blacklist.setEnabled( True )
|
||||
self._simple_blacklist_global_checkboxes.setEnabled( True )
|
||||
self._simple_blacklist_input.setEnabled( True )
|
||||
|
||||
if self._CurrentlyBlocked( ':' ):
|
||||
|
||||
self._simple_blacklist_namespace_checkboxes.setEnabled( False )
|
||||
|
@ -1563,12 +1608,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
else:
|
||||
|
||||
self._simple_blacklist_error_st.setText( 'The filter is currently more complicated than a simple blacklist, so cannot be shown here.' )
|
||||
|
||||
self._simple_blacklist.setEnabled( False )
|
||||
self._simple_blacklist_global_checkboxes.setEnabled( False )
|
||||
self._simple_blacklist_namespace_checkboxes.setEnabled( False )
|
||||
self._simple_blacklist_input.setEnabled( False )
|
||||
self._simple_blacklist_error_st.setText( 'The filter is currently more complicated than a simple blacklist, so it cannot be shown here.' )
|
||||
|
||||
self._simple_blacklist.SetTagSlices( '' )
|
||||
|
||||
|
@ -1588,16 +1628,7 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
whitelist_tag_slices = self._advanced_whitelist.GetTagSlices()
|
||||
blacklist_tag_slices = self._advanced_blacklist.GetTagSlices()
|
||||
|
||||
if len( blacklist_tag_slices ) == 0:
|
||||
|
||||
self._advanced_whitelist_input.setEnabled( False )
|
||||
self._advanced_add_whitelist_button.setEnabled( False )
|
||||
|
||||
else:
|
||||
|
||||
self._advanced_whitelist_input.setEnabled( True )
|
||||
self._advanced_add_whitelist_button.setEnabled( True )
|
||||
|
||||
self._advanced_whitelist_input.setEnabled( len( blacklist_tag_slices ) > 0 )
|
||||
|
||||
#
|
||||
|
||||
|
@ -1658,9 +1689,9 @@ class EditTagFilterPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
tags_to_siblings = HG.client_controller.Read( 'tag_siblings_lookup', CC.COMBINED_TAG_SERVICE_KEY, test_tags )
|
||||
|
||||
for ( test_tag, siblings ) in tags_to_siblings.items():
|
||||
for test_tag_and_siblings in tags_to_siblings.values():
|
||||
|
||||
results.append( False not in ( tag_filter.TagOK( sibling_tag, apply_unnamespaced_rules_to_namespaced_tags = True ) for sibling_tag in siblings ) )
|
||||
results.append( False not in ( tag_filter.TagOK( t, apply_unnamespaced_rules_to_namespaced_tags = True ) for t in test_tag_and_siblings ) )
|
||||
|
||||
|
||||
return results
|
||||
|
|
|
@ -61,15 +61,44 @@ if 'QT_API' in os.environ:
|
|||
|
||||
else:
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
|
||||
if HC.RUNNING_FROM_MACOS_APP:
|
||||
|
||||
os.environ[ 'FORCE_QT_API' ] = '1'
|
||||
|
||||
|
||||
QT_API_INITIAL_VALUE = None
|
||||
|
||||
try:
|
||||
if 'QT_API' not in os.environ:
|
||||
|
||||
import PySide6 # Qt6
|
||||
try:
|
||||
|
||||
import PySide6 # Qt6
|
||||
|
||||
os.environ[ 'QT_API' ] = 'pyside6'
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
os.environ[ 'QT_API' ] = 'pyside6'
|
||||
|
||||
if 'QT_API' not in os.environ:
|
||||
|
||||
except ImportError as e:
|
||||
try:
|
||||
|
||||
import PyQt6 # Qt6
|
||||
|
||||
os.environ[ 'QT_API' ] = 'pyqt6'
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
if 'QT_API' not in os.environ:
|
||||
|
||||
try:
|
||||
|
||||
|
@ -83,6 +112,20 @@ else:
|
|||
|
||||
|
||||
|
||||
if 'QT_API' not in os.environ:
|
||||
|
||||
try:
|
||||
|
||||
import PyQt5 # Qt5
|
||||
|
||||
os.environ[ 'QT_API' ] = 'pyqt5'
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
def get_qt_api_str_status():
|
||||
|
||||
|
@ -106,7 +149,9 @@ def get_qt_api_str_status():
|
|||
current_qt = 'Currently QT_API is not set.'
|
||||
|
||||
|
||||
return '{} {}'.format( initial_qt, current_qt )
|
||||
forced_qt = 'FORCE_QT_API is ON.' if 'FORCE_QT_API' in os.environ else 'FORCE_QT_API is not set.'
|
||||
|
||||
return '{} {} {}'.format( initial_qt, current_qt, forced_qt )
|
||||
|
||||
except Exception as e:
|
||||
|
||||
|
@ -173,7 +218,7 @@ except ModuleNotFoundError as e:
|
|||
|
||||
message += '\n' * 2
|
||||
|
||||
message += 'Here is info on your available Qt Libraries:\n{}'.format( get_qt_library_str_status() )
|
||||
message += 'Here is info on your available Qt Libraries:\n\n{}'.format( get_qt_library_str_status() )
|
||||
|
||||
message += '\n' * 2
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from qtpy import QtGui as QG
|
|||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusFileHandling
|
||||
from hydrus.core import HydrusGlobals as HG
|
||||
from hydrus.core import HydrusImageHandling
|
||||
from hydrus.core import HydrusPaths
|
||||
|
@ -2664,11 +2665,11 @@ class EmbedButton( QW.QWidget ):
|
|||
|
||||
if needs_thumb:
|
||||
|
||||
mime = self._media.GetMime()
|
||||
|
||||
thumbnail_path = HG.client_controller.client_files_manager.GetThumbnailPath( self._media )
|
||||
|
||||
self._thumbnail_qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetQtPixmap()
|
||||
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
|
||||
|
||||
self._thumbnail_qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, thumbnail_mime ).GetQtPixmap()
|
||||
|
||||
self.update()
|
||||
|
||||
|
@ -2692,11 +2693,11 @@ class OpenExternallyPanel( QW.QWidget ):
|
|||
|
||||
if self._media.GetLocationsManager().IsLocal() and self._media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
|
||||
|
||||
mime = self._media.GetMime()
|
||||
|
||||
thumbnail_path = HG.client_controller.client_files_manager.GetThumbnailPath( self._media )
|
||||
|
||||
qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, mime ).GetQtPixmap()
|
||||
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
|
||||
|
||||
qt_pixmap = ClientRendering.GenerateHydrusBitmap( thumbnail_path, thumbnail_mime ).GetQtPixmap()
|
||||
|
||||
thumbnail_window = ClientGUICommon.BufferedWindowIcon( self, qt_pixmap )
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import typing
|
|||
from qtpy import QtCore as QC
|
||||
from qtpy import QtWidgets as QW
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusGlobals as HG
|
||||
from hydrus.core import HydrusExceptions
|
||||
|
||||
from hydrus.client import ClientConstants as CC
|
||||
|
@ -229,7 +229,9 @@ class MenuChoiceButton( MenuMixin, ClientGUICommon.BetterButton ):
|
|||
|
||||
def wheelEvent( self, event ):
|
||||
|
||||
if self._value_index is not None:
|
||||
can_do_it = HG.client_controller.new_options.GetBoolean( 'menu_choice_buttons_can_mouse_scroll' )
|
||||
|
||||
if self._value_index is not None and can_do_it:
|
||||
|
||||
if event.angleDelta().y() > 0:
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 20
|
||||
SOFTWARE_VERSION = 508
|
||||
SOFTWARE_VERSION = 509
|
||||
CLIENT_API_VERSION = 37
|
||||
|
||||
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
|
|
@ -17,6 +17,25 @@ from hydrus.core import HydrusText
|
|||
from hydrus.core import HydrusVideoHandling
|
||||
from hydrus.core.networking import HydrusNetwork
|
||||
|
||||
try:
|
||||
|
||||
import speedcopy
|
||||
|
||||
speedcopy.patch_copyfile()
|
||||
|
||||
SPEEDCOPY_OK = True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
if not isinstance( e, ImportError ):
|
||||
|
||||
HydrusData.Print( 'Failed to initialise speedcopy:' )
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
|
||||
SPEEDCOPY_OK = False
|
||||
|
||||
|
||||
# Mime
|
||||
|
||||
headers_and_mime_thumbnails = [
|
||||
|
@ -80,19 +99,19 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
|
|||
|
||||
elif mime == HC.APPLICATION_PSD:
|
||||
|
||||
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath( suffix = '.png' )
|
||||
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath( suffix = '.jpeg' )
|
||||
|
||||
try:
|
||||
|
||||
HydrusVideoHandling.RenderImageToPNGPath( path, temp_path )
|
||||
HydrusVideoHandling.RenderImageToImagePath( path, temp_path )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_JPEG, clip_rect = clip_rect )
|
||||
|
||||
except:
|
||||
|
||||
thumb_path = os.path.join( HC.STATIC_DIR, 'psd.png' )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
|
||||
|
||||
finally:
|
||||
|
||||
|
@ -107,13 +126,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
|
|||
|
||||
HydrusClipHandling.ExtractDBPNGToPath( path, temp_path )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
|
||||
|
||||
except:
|
||||
|
||||
thumb_path = os.path.join( HC.STATIC_DIR, 'clip.png' )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
|
||||
|
||||
finally:
|
||||
|
||||
|
@ -128,13 +147,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
|
|||
|
||||
HydrusFlashHandling.RenderPageToFile( path, temp_path, 1 )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
|
||||
|
||||
except:
|
||||
|
||||
thumb_path = os.path.join( HC.STATIC_DIR, 'flash.png' )
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, mime, clip_rect = clip_rect )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
|
||||
|
||||
finally:
|
||||
|
||||
|
@ -160,7 +179,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
|
|||
|
||||
numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution ) # just in case ffmpeg doesn't deliver right
|
||||
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesNumPy( numpy_image, mime )
|
||||
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesNumPy( numpy_image )
|
||||
|
||||
renderer.Stop()
|
||||
|
||||
|
|
|
@ -345,12 +345,7 @@ def GenerateNumPyImage( path, mime, force_pil = False ) -> numpy.array:
|
|||
|
||||
|
||||
|
||||
if NumPyImageHasOpaqueAlphaChannel( numpy_image ):
|
||||
|
||||
convert = cv2.COLOR_RGBA2RGB
|
||||
|
||||
numpy_image = cv2.cvtColor( numpy_image, convert )
|
||||
|
||||
numpy_image = StripOutAnyOpaqueAlphaChannel( numpy_image )
|
||||
|
||||
return numpy_image
|
||||
|
||||
|
@ -441,7 +436,7 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
|
|||
|
||||
try:
|
||||
|
||||
thumbnail_bytes = GenerateThumbnailBytesNumPy( thumbnail_numpy_image, mime )
|
||||
thumbnail_bytes = GenerateThumbnailBytesNumPy( thumbnail_numpy_image )
|
||||
|
||||
return thumbnail_bytes
|
||||
|
||||
|
@ -460,24 +455,19 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
|
|||
|
||||
thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.ANTIALIAS )
|
||||
|
||||
thumbnail_bytes = GenerateThumbnailBytesPIL( pil_image, mime )
|
||||
thumbnail_bytes = GenerateThumbnailBytesPIL( thumbnail_pil_image )
|
||||
|
||||
return thumbnail_bytes
|
||||
|
||||
def GenerateThumbnailBytesNumPy( numpy_image, mime ) -> bytes:
|
||||
def GenerateThumbnailBytesNumPy( numpy_image ) -> bytes:
|
||||
|
||||
( im_height, im_width, depth ) = numpy_image.shape
|
||||
|
||||
numpy_image = StripOutAnyOpaqueAlphaChannel( numpy_image )
|
||||
|
||||
if depth == 4:
|
||||
|
||||
if NumPyImageHasOpaqueAlphaChannel( numpy_image ):
|
||||
|
||||
convert = cv2.COLOR_RGBA2BGR
|
||||
|
||||
else:
|
||||
|
||||
convert = cv2.COLOR_RGBA2BGRA
|
||||
|
||||
convert = cv2.COLOR_RGBA2BGRA
|
||||
|
||||
else:
|
||||
|
||||
|
@ -514,11 +504,11 @@ def GenerateThumbnailBytesNumPy( numpy_image, mime ) -> bytes:
|
|||
raise HydrusExceptions.CantRenderWithCVException( 'Thumb failed to encode!' )
|
||||
|
||||
|
||||
def GenerateThumbnailBytesPIL( pil_image: PILImage.Image, mime ) -> bytes:
|
||||
def GenerateThumbnailBytesPIL( pil_image: PILImage.Image ) -> bytes:
|
||||
|
||||
f = io.BytesIO()
|
||||
|
||||
if mime == HC.IMAGE_PNG or pil_image.mode == 'RGBA':
|
||||
if PILImageHasAlpha( pil_image ):
|
||||
|
||||
pil_image.save( f, 'PNG' )
|
||||
|
||||
|
@ -1122,7 +1112,7 @@ def NumPyImageHasOpaqueAlphaChannel( numpy_image: numpy.array ) -> bool:
|
|||
# if the alpha channel is all opaque, there is no use storing that info in our pixel hash
|
||||
# opaque means 255
|
||||
|
||||
alpha_channel = numpy_image[:,:,3]
|
||||
alpha_channel = numpy_image[:,:,3].copy()
|
||||
|
||||
if ( alpha_channel == numpy.full( ( shape[0], shape[1] ), 255, dtype = 'uint8' ) ).all():
|
||||
|
||||
|
@ -1234,3 +1224,19 @@ def RotateEXIFPILImage( pil_image: PILImage.Image )-> PILImage.Image:
|
|||
|
||||
return pil_image
|
||||
|
||||
|
||||
def StripOutAnyOpaqueAlphaChannel( numpy_image: numpy.array ) -> numpy.array:
|
||||
|
||||
if NumPyImageHasOpaqueAlphaChannel( numpy_image ):
|
||||
|
||||
numpy_image = numpy_image[:,:,:3].copy()
|
||||
|
||||
# old way, which doesn't actually remove the channel lmao lmao lmao
|
||||
'''
|
||||
convert = cv2.COLOR_RGBA2RGB
|
||||
|
||||
numpy_image = cv2.cvtColor( numpy_image, convert )
|
||||
'''
|
||||
|
||||
return numpy_image
|
||||
|
||||
|
|
|
@ -348,6 +348,17 @@ class SerialisableDictionary( SerialisableBase, dict ):
|
|||
|
||||
for ( key, value ) in self.items():
|
||||
|
||||
# after being caught out on a recursive legacy thing here, we now coerce. I'm 99.7% confident it can't hurt
|
||||
# careful not to do it through for SerialisableBase--don't want to coerce a SerialisableBytesDict to a SerialisableDict
|
||||
if isinstance( value, list ) and not isinstance( value, SerialisableBase ):
|
||||
|
||||
value = SerialisableList( value )
|
||||
|
||||
elif isinstance( value, dict ) and not isinstance( value, SerialisableBase ):
|
||||
|
||||
value = SerialisableDictionary( value )
|
||||
|
||||
|
||||
if isinstance( key, SerialisableBase ):
|
||||
|
||||
serialisable_key = key.GetSerialisableTuple()
|
||||
|
@ -551,7 +562,7 @@ class SerialisableList( SerialisableBase, list ):
|
|||
|
||||
SERIALISABLE_TYPE = SERIALISABLE_TYPE_LIST
|
||||
SERIALISABLE_NAME = 'Serialisable List'
|
||||
SERIALISABLE_VERSION = 1
|
||||
SERIALISABLE_VERSION = 2
|
||||
|
||||
def __init__( self, *args, **kwargs ):
|
||||
|
||||
|
@ -561,34 +572,90 @@ class SerialisableList( SerialisableBase, list ):
|
|||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return [ obj.GetSerialisableTuple() for obj in self ]
|
||||
serialisable_list_result = []
|
||||
|
||||
for obj in self:
|
||||
|
||||
# after being caught out on a recursive legacy thing here, we now coerce. I'm 99.7% confident it can't hurt
|
||||
# careful not to do it through for SerialisableBase--don't want to coerce a SerialisableBytesDict to a SerialisableDict
|
||||
if isinstance( obj, list ) and not isinstance( obj, SerialisableBase ):
|
||||
|
||||
obj = SerialisableList( obj )
|
||||
|
||||
elif isinstance( obj, dict ) and not isinstance( obj, SerialisableBase ):
|
||||
|
||||
obj = SerialisableDictionary( obj )
|
||||
|
||||
|
||||
if isinstance( obj, SerialisableBase ):
|
||||
|
||||
is_serialised = True
|
||||
|
||||
obj_data = obj.GetSerialisableTuple()
|
||||
|
||||
else:
|
||||
|
||||
is_serialised = False
|
||||
|
||||
obj_data = obj
|
||||
|
||||
|
||||
serialisable_list_result.append( ( is_serialised, obj_data ) )
|
||||
|
||||
|
||||
return serialisable_list_result
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
have_shown_load_error = False
|
||||
|
||||
for obj_tuple in serialisable_info:
|
||||
for ( is_serialised, obj_data ) in serialisable_info:
|
||||
|
||||
try:
|
||||
if is_serialised:
|
||||
|
||||
obj = CreateFromSerialisableTuple( obj_tuple )
|
||||
obj_tuple = obj_data
|
||||
|
||||
except HydrusExceptions.SerialisationException as e:
|
||||
|
||||
if not have_shown_load_error:
|
||||
try:
|
||||
|
||||
HydrusData.ShowText( 'An object in a list could not load. It has been discarded from the list. More may also have failed to load, but to stop error spam, they will go silently. Your client may be running on code versions behind its database. Depending on the severity of this error, you may need to rollback to a previous backup. If you have no backup, you may want to kill your hydrus process now to stop the cleansed list being saved back to the db.' )
|
||||
HydrusData.ShowException( e )
|
||||
obj = CreateFromSerialisableTuple( obj_tuple )
|
||||
|
||||
have_shown_load_error = True
|
||||
except HydrusExceptions.SerialisationException as e:
|
||||
|
||||
if not have_shown_load_error:
|
||||
|
||||
HydrusData.ShowText( 'An object in a list could not load. It has been discarded from the list. More may also have failed to load, but to stop error spam, they will go silently. Your client may be running on code versions behind its database. Depending on the severity of this error, you may need to rollback to a previous backup. If you have no backup, you may want to kill your hydrus process now to stop the cleansed list being saved back to the db.' )
|
||||
HydrusData.ShowException( e )
|
||||
|
||||
have_shown_load_error = True
|
||||
|
||||
|
||||
continue
|
||||
|
||||
|
||||
continue
|
||||
else:
|
||||
|
||||
obj = obj_data
|
||||
|
||||
|
||||
self.append( obj )
|
||||
|
||||
|
||||
|
||||
|
||||
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
|
||||
|
||||
if version == 1:
|
||||
|
||||
serialised_objects = old_serialisable_info
|
||||
|
||||
is_serialised = True
|
||||
|
||||
new_serialisable_info = [ ( is_serialised, serialised_object ) for serialised_object in serialised_objects ]
|
||||
|
||||
return ( 2, new_serialisable_info )
|
||||
|
||||
|
||||
|
||||
|
||||
SERIALISABLE_TYPES_TO_OBJECT_TYPES[ SERIALISABLE_TYPE_LIST ] = SerialisableList
|
||||
|
|
|
@ -307,23 +307,30 @@ def SplitTag( tag ):
|
|||
return ( '', tag )
|
||||
|
||||
|
||||
|
||||
NULL_CHARACTER = '\x00'
|
||||
HANGUL_FILLER_CHARACTER = '\u3164'
|
||||
|
||||
def StripTextOfGumpf( t ):
|
||||
|
||||
t = HydrusText.re_newlines.sub( '', t )
|
||||
|
||||
t = HydrusText.re_multiple_spaces.sub( ' ', t )
|
||||
t = HydrusText.re_one_or_more_whitespace.sub( ' ', t )
|
||||
|
||||
t = t.strip()
|
||||
|
||||
t = HydrusText.re_leading_space_or_garbage.sub( '', t )
|
||||
t = HydrusText.re_leading_garbage.sub( '', t )
|
||||
|
||||
t = t.strip()
|
||||
|
||||
if NULL_CHARACTER in t:
|
||||
|
||||
t = t.replace( NULL_CHARACTER, '' )
|
||||
|
||||
|
||||
if t == HANGUL_FILLER_CHARACTER:
|
||||
|
||||
t = ''
|
||||
|
||||
|
||||
return t
|
||||
|
||||
def TagOK( t ):
|
||||
|
|
|
@ -14,10 +14,9 @@ import re
|
|||
|
||||
from hydrus.core import HydrusExceptions
|
||||
|
||||
re_newlines = re.compile( '[\r\n]+' )
|
||||
re_multiple_spaces = re.compile( r'\s+' )
|
||||
re_one_or_more_whitespace = re.compile( r'\s+' ) # this does \t and friends too
|
||||
# want to keep the 'leading space' part here, despite tag.strip() elsewhere, in case of some crazy '- test' tag
|
||||
re_leading_space_or_garbage = re.compile( r'^(\s|-|system:)+' )
|
||||
re_leading_garbage = re.compile( r'^(-|system:)+' )
|
||||
re_leading_single_colon = re.compile( '^:(?!:)' )
|
||||
re_leading_byte_order_mark = re.compile( '^\ufeff' ) # unicode .txt files prepend with this, wew
|
||||
|
||||
|
@ -254,9 +253,9 @@ def NonFailingUnicodeDecode( data, encoding ):
|
|||
|
||||
return ( text, encoding )
|
||||
|
||||
def RemoveNewlines( text ):
|
||||
def RemoveNewlines( text: str ) -> str:
|
||||
|
||||
text = re.sub( r'[\r\n]', '', text )
|
||||
text = ''.join( text.splitlines() )
|
||||
|
||||
return text
|
||||
|
||||
|
|
|
@ -591,10 +591,22 @@ def HasVideoStream( path ) -> bool:
|
|||
|
||||
return ParseFFMPEGHasVideo( lines )
|
||||
|
||||
def RenderImageToPNGPath( path, temp_png_path ):
|
||||
def RenderImageToImagePath( path, temp_image_path ):
|
||||
|
||||
# this should auto-convert if you give it .png or .jpg
|
||||
# I noticed that ffmpeg got confused about alpha channels with PSDs, gave fully opaque alpha over an otherwise perfectly acceptable catgirl image, so we'll do jpeg for PSD from now on
|
||||
|
||||
# -y to overwrite the temp path
|
||||
cmd = [ FFMPEG_PATH, '-y', "-i", path, temp_png_path ]
|
||||
|
||||
if temp_image_path.endswith( '.jpeg' ):
|
||||
|
||||
# '-q:v 1' does high quality
|
||||
cmd = [ FFMPEG_PATH, '-y', "-i", path, "-q:v", "1", temp_image_path ]
|
||||
|
||||
else:
|
||||
|
||||
cmd = [ FFMPEG_PATH, '-y', "-i", path, temp_image_path ]
|
||||
|
||||
|
||||
sbp_kwargs = HydrusData.GetSubprocessKWArgs()
|
||||
|
||||
|
|
|
@ -2682,7 +2682,7 @@ class ServerServiceRestricted( ServerService ):
|
|||
dictionary[ 'service_options' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
|
||||
self._service_options = dictionary[ 'service_options' ]
|
||||
self._service_options = HydrusSerialisable.SerialisableDictionary( dictionary[ 'service_options' ] )
|
||||
|
||||
if 'server_message' not in self._service_options:
|
||||
|
||||
|
|
|
@ -45,10 +45,11 @@ goto :parse_fail
|
|||
|
||||
ECHO:
|
||||
ECHO If you are on Windows ^<=8.1, choose 5.
|
||||
SET /P qt=Do you want Qt(5) or Qt(6)?
|
||||
SET /P qt=Do you want Qt(5), Qt(6), or (t)est?
|
||||
|
||||
IF "%qt%" == "5" goto :question_mpv
|
||||
IF "%qt%" == "6" goto :question_mpv
|
||||
IF "%qt%" == "t" goto :question_mpv
|
||||
goto :parse_fail
|
||||
|
||||
:question_mpv
|
||||
|
@ -103,6 +104,7 @@ IF "%install_type%" == "s" (
|
|||
|
||||
IF "%qt%" == "5" python -m pip install -r static\requirements\advanced\requirements_qt5.txt
|
||||
IF "%qt%" == "6" python -m pip install -r static\requirements\advanced\requirements_qt6.txt
|
||||
IF "%qt%" == "t" python -m pip install -r static\requirements\advanced\requirements_qt6_test.txt
|
||||
|
||||
IF "%mpv%" == "o" python -m pip install -r static\requirements\advanced\requirements_old_mpv.txt
|
||||
IF "%mpv%" == "n" python -m pip install -r static\requirements\advanced\requirements_new_mpv.txt
|
||||
|
|
|
@ -35,12 +35,14 @@ if [ $install_type = "s" ]; then
|
|||
elif [ $install_type = "a" ]; then
|
||||
echo
|
||||
echo "If you are <= 10.13 (High Sierra), choose 5."
|
||||
echo "Do you want Qt(5) or Qt(6)? "
|
||||
echo "Do you want Qt(5), Qt(6), or (t)est? "
|
||||
read qt
|
||||
if [ $qt = "5" ]; then
|
||||
:
|
||||
elif [ $qt = "6" ]; then
|
||||
:
|
||||
elif [ $qt = "t" ]; then
|
||||
:
|
||||
else
|
||||
echo "Sorry, did not understand that input!"
|
||||
popd
|
||||
|
@ -104,6 +106,8 @@ elif [ $install_type = "a" ]; then
|
|||
python -m pip install -r static/requirements/advanced/requirements_qt5.txt
|
||||
elif [ $qt = "6" ]; then
|
||||
python -m pip install -r static/requirements/advanced/requirements_qt6.txt
|
||||
elif [ $qt = "t" ]; then
|
||||
python -m pip install -r static/requirements/advanced/requirements_qt6_test.txt
|
||||
fi
|
||||
|
||||
if [ $mpv = "o" ]; then
|
||||
|
|
|
@ -35,12 +35,14 @@ if [ $install_type = "s" ]; then
|
|||
elif [ $install_type = "a" ]; then
|
||||
echo
|
||||
echo "If you are <=Ubuntu 18.04 or equivalent, choose 5."
|
||||
echo "Do you want Qt(5) or Qt(6)? "
|
||||
echo "Do you want Qt(5), Qt(6), or (t)est? "
|
||||
read qt
|
||||
if [ $qt = "5" ]; then
|
||||
:
|
||||
elif [ $qt = "6" ]; then
|
||||
:
|
||||
elif [ $qt = "t" ]; then
|
||||
:
|
||||
else
|
||||
echo "Sorry, did not understand that input!"
|
||||
exit 1
|
||||
|
@ -103,6 +105,8 @@ elif [ $install_type = "a" ]; then
|
|||
python -m pip install -r static/requirements/advanced/requirements_qt5.txt
|
||||
elif [ $qt = "6" ]; then
|
||||
python -m pip install -r static/requirements/advanced/requirements_qt6.txt
|
||||
elif [ $qt = "t" ]; then
|
||||
python -m pip install -r static/requirements/advanced/requirements_qt6_test.txt
|
||||
fi
|
||||
|
||||
if [ $mpv = "o" ]; then
|
||||
|
|
|
@ -26,4 +26,5 @@ QtPy==2.2.1
|
|||
requests==2.28.1
|
||||
setuptools==65.4.1
|
||||
|
||||
PySide6==6.3.2
|
||||
zope==5.5.0
|
||||
PyQt6==6.3.1
|
||||
|
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,2 @@
|
|||
QtPy==2.3.0
|
||||
PySide6==6.4.1
|