Version 497

This commit is contained in:
Hydrus Network Developer 2022-08-24 16:06:25 -05:00
parent 67c3d129e7
commit dabdbe2861
43 changed files with 6028 additions and 5573 deletions

View File

@ -345,28 +345,13 @@ jobs:
pyinstaller server-win.spec
pyinstaller client-win.spec
dir -r
-
# yo pretty sure we'll need to install this manually once we are on windows server 2022
# https://github.com/actions/virtual-environments/issues/4856
name: InnoSetup
run: |
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
ISCC.exe InnoSetup.iss /DVersion=${{ github.ref_name }}
-
name: Compress Client
run: |
cd .\dist
7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract5.zip 'Hydrus Network'
cd ..
-
name: Upload a Build Artifact
uses: actions/upload-artifact@v2
with:
name: Windows-Install
path: dist\HydrusInstaller.exe
if-no-files-found: error
retention-days: 2
-
-
name: Upload a Build Artifact
uses: actions/upload-artifact@v2
with:
@ -447,12 +432,27 @@ jobs:
pyinstaller server-win.spec
pyinstaller client-win.spec
dir -r
-
# yo pretty sure we'll need to install this manually once we are on windows server 2022
# https://github.com/actions/virtual-environments/issues/4856
name: InnoSetup
run: |
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
ISCC.exe InnoSetup.iss /DVersion=${{ github.ref_name }}
-
name: Compress Client
run: |
cd .\dist
7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract6.zip 'Hydrus Network'
cd ..
-
name: Upload a Build Artifact
uses: actions/upload-artifact@v2
with:
name: Windows-Install
path: dist\HydrusInstaller.exe
if-no-files-found: error
retention-days: 2
-
name: Upload a Build Artifact
uses: actions/upload-artifact@v2
@ -485,7 +485,7 @@ jobs:
mkdir ubuntu windows
mv MacOS-DMG5/HydrusNetwork5.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt5.-.App.dmg
mv MacOS-DMG6/HydrusNetwork6.dmg Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.macOS.Qt6.-.App.dmg
mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe
mv Windows-Install/HydrusInstaller.exe Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Installer.exe
mv Windows-Extract5/Windows-Extract5.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip
mv Windows-Extract6/Windows-Extract6.zip Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip
mv Ubuntu-Extract5/Ubuntu-Extract5.tar.gz Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz
@ -496,7 +496,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Installer.exe
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Installer.exe
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt5.-.Extract.only.zip
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Windows.Qt6.-.Extract.only.zip
Hydrus.Network.${{ steps.meta.outputs.version_short }}.-.Linux.Qt5.-.Executable.tar.gz

View File

@ -3,6 +3,42 @@
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 497](https://github.com/hydrusnetwork/hydrus/releases/tag/v497)
### misc
* I bulked out the 'star' rating shape a bit more, since the new pentragram, while it looked better than my old 'by-eye' star, was a bit thin. if you prefer the pentagram, this is now selectable as a new shape type under manage services
* the Windows installer is now Qt6 exclusively. there are no special update instructions, it should all just work™
* the 'manage tag siblings/parents' dialogs now have explicit delete buttons, which should make mass-deletes a little easier to do. some of the background code is cleaned up too, and the 'add' button is moved up to the main button row
* you can now hide all sibling and/or parent text-suffix 'decorators' in the manage tags and autocomplete dropdown taglists, with four new checkboxes under _options->tags_. the right-click menus of these lists let you temporarily show/hide too, just like 'hide/show parent rows'
* when you change the namespace sort in the options, the existing collect-by dropdowns now update instantly (previously, existing pages needed a client restart to see any changes)
* I updated how the media viewer 'note' hover window lays out and does its 'how tall should I be?' estimate. it fits better, being exactly just tall enough in more cases, but it still seems to have trouble with multiple notes that include wrapping text
* added a link to the new flatpak release (easy Linux running-from-source setup) that a user made to the install help
* fixed an issue with the new 'default' file import options when you right-click a watcher/gallery download--the 'show files' menu now correctly adapts to you having a default file import options
* if you are set to elide page tab names, then all pages will tooltip their names on mouseover
* new clients now start with (ctrl+page up/down) as 'move page selection left/right'
### client api
* the Client API routine that fetches file statuses for a given URL no longer double-checks 'already in db' results against your actual file system. this check is more appropriate to an actual working import process, so it now defaults off in the Client API
* if you want to do this check because you are searching for missing files, you can turn it back on with the new 'doublecheck_file_system' parameter.
* the client api help has been updated to reference this
* the client api's Server header is now "client api/32 (497)". NOT "client api/17". it was stating the hydrus network version erroneously. it now states client api version and software version. if you are able to parse this header, it makes '/api_version' request superfluous
* the client api version is now 32
### multiline parsing
* the parser now supports limited multiline parsing. the main changes are hardcoded: the formulae beneath note content parsers and those that do subsidiary page parser splitting no longer remove newlines when they parse. all the parsing UI and the test panels and so on are now aware of this and set flags in all the right places, and parsed notes are now washed through the new trimming/cleaning method, and everything _seems_ to basically work. the main remaining problems is the complicated string processing UI has mixed single/multi-line testing support. some looks great, most gets coerced to single-line just for the previewed test results
* as an example, the default hentai foundry downloader now grabs the artist description as a multi-line note
* the parsing sub-system that extracts cohesive strings from complex html blocks now inserts newlines at 'p' and 'br' tags
* trying to parse clean multiline notes still caused several formatting issues this week, so I have updated the automatic note-washing routine to standardise hydrus notes in several new ways that I hope will not be too disruptive to manually written notes:
* the note washing routine now coerces all newline characters to 'backslash-n', regardless of platform
* the note washing routine now trims each line, so no leading or trailing whitespace anywhere. I am open to changing this in future, maybe for handwritten notes where you really want an indent somewhere, but parsing from complex nested html tags is making a heap of weird extra whitespace, for which this is a clean solution
* the note washing routine now trims newline gaps that are greater than two-newlines. you can split paragraphs by one empty line, but no more
* there may be other issues figuring out cleanly formatted strings from nested html tags--so give it a go and let me know what you think. maybe p and br blocks should always make two newlines, so we have separated paragraphs, maybe I need to parse more blocks, like h1 and friends. any specific example html blocks would also be helpful
### cleanup
* refactored ClientGUIParsing to its own 'parsing' module and split everything into four less tangled files
* cleaned up a bunch of taglist text presentation code, mostly simplicity and clarity in prep for future updates
* updated the checker options button to use a Qt signal instead of a callable
## [Version 496](https://github.com/hydrusnetwork/hydrus/releases/tag/v496)
### note import options
@ -307,43 +343,3 @@
* last week's update also gives a time estimate in its pre-popup, based on 60k files per minute
* removed some old database cache data that wasn't cleared in a previous update
* a variety of misc UI text fixes and cleanup
## [Version 486](https://github.com/hydrusnetwork/hydrus/releases/tag/v486)
* **This week's release is for advanced users only! I make a big change, and I want to make sure the update is fast and there are no unusual problems before rolling it out to all users.**
### all my files
* the client adds a new virtual file service this week, 'all my files', which is an umbrella covering all your local file domains. if you do not engage the multiple local file services system, you won't see it much, but if you do, you'll now have a convenient tool for saying 'all my stuff' without including trash and repository updates
* **it will take a minute or two to generate this new service on update. if you have a client with millions of files, it may take a while**
* 'all my files' now appears in the file domain selector button on your tag entry box if you have more than one local file domain. selecting this searches the union of all your local file domains with fast and precise count (as opposed to 'multiple locations' of the full union, which will have imprecise counts and be slower). it also does duplicate file work laser-fast (again, unlike 'multiple locations', which is often slow due to UNION complexity)
* 'all my files' also appears in review and manage services, very similarly to 'all local files'
* a heap of hacks I instituted when getting multiple local file services ready are now replaced with this clean 'yeah this file is valued and worth looking at' domain. for instance, downloader pages now view files in this way.
* mr bones and the file history chart also use 'all my files', and are significantly faster to calculate. the chart also excludes repo update files and trash now
* calls to delete or undelete on 'all my files' (this is mostly Client API and some 'default' situations) will be converted to a blanket 'force send to trash' and 'force undelete all deleted records'
* the 'undelete files?' dialog is now a button selection dialog. it also now has an 'all the above' option when more than one local service may apply, which tells the client to undelete to all services the files have been deleted from
* updated multiple local file services help to talk a little about the new domain
* rearranged the sort in a couple of places where the different local file services appear. they should now be: local file domains, all my files, trash, repo updates, all local files
* ADVANCED: the 'presentation import options' under 'file import options' now allows a full-fledged location context using the new multiple local file services system rather than the previous 'in your files(and trash too)' choice. it defaults to the new 'all my files' domain
### misc
* thanks to a user, the 'getting started with downloading' help has had a full pass. if you have had trouble with downloaders, particularly if you are unsure about what file import options are for, or what subscriptions are, please check it out!
* the 'media viewers' shortcut set gets three new zoom actions: 'switch between 100% and max', 'switch between canvas and max', and 'zoom to max' (issue #1141)
* if a media type is set to do 'exact zooms', it will now not exceed the otherwise specified max zoom
* the file sort widget will now preserve ascending/descending status on sort type changes (rather than resetting to default) if the asc/desc strings do not change. so, if you are on 'import time'/'oldest first', and switch to 'archive time', it will now stay on 'oldest' rather than resetting to 'newest'
* the manage tag siblings dialog now tries to automatically break loops for you, just like it will automatically break A->B, A->C conflicts. this works on manual entry or mass import
* the manage tag siblings dialog now shows the stated 'reason' for any pair change (e.g. "AUTO-PETITION TO BREAK LOOP") in the 'note' column
* the 'short' animation scanbar--when your mouse is away--now keeps a short disabled volume button beside it. I found it very annoying how the scan nub would jump a few pixels left/right as this popped up and down, so now it is the same width big and small
* right-clicking on files when in pages with 'multiple locations' file domains is now much much faster
* the filename tagging dialog now starts with the 'tags for all' focused, and the 'press up/down on empty input' shortcuts are now plugged in, so pressing up/down will change service
* I believe I may have completely eliminated the additional superlag that sometimes occurs when adding or deleting a service. it was a database maintenance routine getting carried away with other outstanding work
* move/add actions in the new multiple local file system now operate asynchronously and politely, spreading their work time out when the client is busy, and for large jobs they will also make a cancellable progress popup
* cleaned up how the autocomplete entry sends some of its signals to other parts of the program
* did some misc help and code edits/refactoring, including brushing up the Windows install section with more advanced options
* removed the 'hydrus zooms big bad' warning from the 'media' options page. hydrus zooms big good now!
### some database stuff
* tl;dr: database cleans up after itself better now
* some users have had trouble with database journal files (the 'wal' files in your db directory) on certain clients getting huge after lots of work, multiple GB, and causing the OS a headache if the journal is doing work through a computer sleep. these journals are 'supposed' to checkpoint and clean themselves up naturally, but I think a busy database chokes them. therefore, I have improved the hydrus maintenance this week: 1) the 'journal size limit' PRAGMA, which applies softly after every 30 seconds or so, is now 128MB down from 1GB. 2) databases in PERSIST (rare) mode will now specifically zero out their journal fifteen minutes. 3) databases in WAL mode (the default), in addition to regular PASSIVE checkpointing now every five minutes, will force an additional TRUNCATE checkpoint every fifteen. this should force a regular full flush and maybe help some other problems like gigantic memory bloat the same users sometimes saw. if you are a very advanced user and do active debug on the database while hydrus is using it, please note this new TRUNCATE command is aggressive and may block itself or you inconveniently. let me know how you get on!
* moved the recent 'be careful of usb drives' section in 'installing' help to 'help my db is broke.txt'. it is very likely this problem was related to the above WAL stuff, and it was not just usb drives, I rewrote it as generalised help for anyone who gets 'delayed write failed' errors at the OS level
* massively optimised several critical duplicate files filtering methods if the current location context has more than one file domain, and I think I cleared out the basic 'get duplicate info for this file' call of all slow calls in complex location contexts
* the repair routine that regenerates mapping caches if any tables are missing on boot is now more reliable and covers the entirety of the mappings cache system using the new modules system. it also now regenerates just for the tag services with missing tables, not the whole cache
* if multiple types of mapping cache tables are missing on boot, and multiple waves of regenerations covering different areas are planned, duplicate regenerations will now be skipped

View File

@ -120,7 +120,8 @@ Required Headers: n/a
Arguments: n/a
Response:
: Some simple JSON describing the current api version (and hydrus client version, if you are interested).
: Some simple JSON describing the current api version (and hydrus client version, if you are interested).
: Note that this is mostly obselete now, since the 'Server' header of every response (and a duplicated 'Hydrus-Server' one, if you have a complicated proxy situation that overwrites 'Server') are now in the form "client api/{client_api_version} ({software_version})", e.g. "client api/32 (497)".
```json title="Example response"
{
@ -670,6 +671,7 @@ Required Headers: n/a
Arguments:
:
* `url`: (the url you want to ask about)
* `doublecheck_file_system`: true or false (optional, defaults False)
Example request:
: for URL `http://safebooru.org/index.php?page=post&s=view&id=2753608`:
@ -691,17 +693,18 @@ Response:
]
}
```
The `url_file_statuses` is a list of zero-to-n JSON Objects, each representing a file match the client found in its database for the URL. Typically, it will be of length 0 (for as-yet-unvisited URLs or Gallery/Watchable URLs that are not attached to files) or 1, but sometimes multiple files are given the same URL (sometimes by mistaken misattribution, sometimes by design, such as pixiv manga pages). Handling n files per URL is a pain but an unavoidable issue you should account for.
`status` is the same as for `/add_files/add_file`:
* 0 - File not in database, ready for import (you will only see this very rarely--usually in this case you will just get no matches)
* 2 - File already in database
* 3 - File previously deleted
`hash` is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text you may recognise from hydrus's normal import workflow.
The `url_file_statuses` is a list of zero-to-n JSON Objects, each representing a file match the client found in its database for the URL. Typically, it will be of length 0 (for as-yet-unvisited URLs or Gallery/Watchable URLs that are not attached to files) or 1, but sometimes multiple files are given the same URL (sometimes by mistaken misattribution, sometimes by design, such as pixiv manga pages). Handling n files per URL is a pain but an unavoidable issue you should account for.
`status` is the same as for `/add_files/add_file`:
* 0 - File not in database, ready for import (you will only see this very rarely--usually in this case you will just get no matches)
* 2 - File already in database
* 3 - File previously deleted
`hash` is the file's SHA256 hash in hexadecimal, and 'note' is some occasional additional human-readable text you may recognise from hydrus's normal import workflow.
If you set `doublecheck_file_system` to `true`, then any result that is 'already in db' (2) will be double-checked against the actual file system. This check happens on any normal file import process, just to check for and fix missing files (if the file is missing, the status becomes 0--new), but the check can take more than a few milliseconds on an HDD or a network drive, so the default behaviour, assuming you mostly just want to spam for 'seen this before' file statuses, is to not do it.
### **GET `/add_urls/get_url_info`** { id="add_urls_get_url_info" }

View File

@ -49,7 +49,9 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
5. If it still doesn't work, see if you can do the same for libmpv.so and libcdio.so--or consider [running from source](running_from_source.md)
* You can also try [running the Windows version in wine](wine.md).
* **Third parties (not maintained by Hydrus Developer)**:
If you use Arch Linux, you can check out the AUR package a user maintains [here](https://aur.archlinux.org/packages/hydrus/).
* (These both run from source, so if you have trouble with the built release, they may work better for you!)
* [AUR package](https://aur.archlinux.org/packages/hydrus/)
* [flatpak](https://flathub.org/apps/details/io.github.hydrusnetwork.hydrus)
=== "From Source"

View File

@ -33,6 +33,42 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_497"><a href="#version_497">version 497</a></h3></li>
<ul>
<li>misc:</li>
<li>I bulked out the 'star' rating shape a bit more, since the new pentragram, while it looked better than my old 'by-eye' star, was a bit thin. if you prefer the pentagram, this is now selectable as a new shape type under manage services</li>
<li>the Windows installer is now Qt6 exclusively. there are no special update instructions, it should all just work™</li>
<li>the 'manage tag siblings/parents' dialogs now have explicit delete buttons, which should make mass-deletes a little easier to do. some of the background code is cleaned up too, and the 'add' button is moved up to the main button row</li>
<li>you can now hide all sibling and/or parent text-suffix 'decorators' in the manage tags and autocomplete dropdown taglists, with four new checkboxes under _options->tags_. the right-click menus of these lists let you temporarily show/hide too, just like 'hide/show parent rows'</li>
<li>when you change the namespace sort in the options, the existing collect-by dropdowns now update instantly (previously, existing pages needed a client restart to see any changes)</li>
<li>I updated how the media viewer 'note' hover window lays out and does its 'how tall should I be?' estimate. it fits better, being exactly just tall enough in more cases, but it still seems to have trouble with multiple notes that include wrapping text</li>
<li>added a link to the new flatpak release (easy Linux running-from-source setup) that a user made to the install help</li>
<li>fixed an issue with the new 'default' file import options when you right-click a watcher/gallery download--the 'show files' menu now correctly adapts to you having a default file import options</li>
<li>if you are set to elide page tab names, then all pages will tooltip their names on mouseover</li>
<li>new clients now start with (ctrl+page up/down) as 'move page selection left/right'</li>
<li>.</li>
<li>client api:</li>
<li>the Client API routine that fetches file statuses for a given URL no longer double-checks 'already in db' results against your actual file system. this check is more appropriate to an actual working import process, so it now defaults off in the Client API</li>
<li>if you want to do this check because you are searching for missing files, you can turn it back on with the new 'doublecheck_file_system' parameter.</li>
<li>the client api help has been updated to reference this</li>
<li>the client api's Server header is now "client api/32 (497)". NOT "client api/17". it was stating the hydrus network version erroneously. it now states client api version and software version. if you are able to parse this header, it makes '/api_version' request superfluous</li>
<li>the client api version is now 32</li>
<li>.</li>
<li>multiline parsing:</li>
<li>the parser now supports limited multiline parsing. the main changes are hardcoded: the formulae beneath note content parsers and those that do subsidiary page parser splitting no longer remove newlines when they parse. all the parsing UI and the test panels and so on are now aware of this and set flags in all the right places, and parsed notes are now washed through the new trimming/cleaning method, and everything _seems_ to basically work. the main remaining problems is the complicated string processing UI has mixed single/multi-line testing support. some looks great, most gets coerced to single-line just for the previewed test results</li>
<li>as an example, the default hentai foundry downloader now grabs the artist description as a multi-line note</li>
<li>the parsing sub-system that extracts cohesive strings from complex html blocks now inserts newlines at 'p' and 'br' tags</li>
<li>trying to parse clean multiline notes still caused several formatting issues this week, so I have updated the automatic note-washing routine to standardise hydrus notes in several new ways that I hope will not be too disruptive to manually written notes:</li>
<li>the note washing routine now coerces all newline characters to 'backslash-n', regardless of platform</li>
<li>the note washing routine now trims each line, so no leading or trailing whitespace anywhere. I am open to changing this in future, maybe for handwritten notes where you really want an indent somewhere, but parsing from complex nested html tags is making a heap of weird extra whitespace, for which this is a clean solution</li>
<li>the note washing routine now trims newline gaps that are greater than two-newlines. you can split paragraphs by one empty line, but no more</li>
<li>there may be other issues figuring out cleanly formatted strings from nested html tags--so give it a go and let me know what you think. maybe p and br blocks should always make two newlines, so we have separated paragraphs, maybe I need to parse more blocks, like h1 and friends. any specific example html blocks would also be helpful</li>
<li>.</li>
<li>cleanup:</li>
<li>refactored ClientGUIParsing to its own 'parsing' module and split everything into four less tangled files</li>
<li>cleaned up a bunch of taglist text presentation code, mostly simplicity and clarity in prep for future updates</li>
<li>updated the checker options button to use a Qt signal instead of a callable</li>
</ul>
<li><h3 id="version_496"><a href="#version_496">version 496</a></h3></li>
<ul>
<li>note import options:</li>

View File

@ -376,6 +376,9 @@ def GetDefaultShortcuts():
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_CHARACTER, ord( 'Z' ), ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_UNDO ) )
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_CHARACTER, ord( 'P' ), ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_OPEN_COMMAND_PALETTE ) )
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_PAGE_UP, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MOVE_PAGES_SELECTION_LEFT ) )
main_gui.SetCommand( ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_SPECIAL, ClientGUIShortcuts.SHORTCUT_KEY_SPECIAL_PAGE_DOWN, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ), CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_MOVE_PAGES_SELECTION_RIGHT ) )
shortcuts.append( main_gui )
media_viewer_browser = ClientGUIShortcuts.ShortcutSet( 'media_viewer_browser' )

View File

@ -244,6 +244,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_taglists' ] = True
self._dictionary[ 'booleans' ][ 'expand_parents_on_storage_autocomplete_taglists' ] = True
self._dictionary[ 'booleans' ][ 'show_parent_decorators_on_storage_taglists' ] = True
self._dictionary[ 'booleans' ][ 'show_parent_decorators_on_storage_autocomplete_taglists' ] = True
self._dictionary[ 'booleans' ][ 'show_sibling_decorators_on_storage_taglists' ] = True
self._dictionary[ 'booleans' ][ 'show_sibling_decorators_on_storage_autocomplete_taglists' ] = True
self._dictionary[ 'booleans' ][ 'show_session_size_warnings' ] = True
self._dictionary[ 'booleans' ][ 'delete_lock_for_archived_files' ] = False

View File

@ -84,7 +84,7 @@ def ConvertParseResultToPrettyString( result ):
note_name = additional_info
return 'note "{}": {}'.format( note_name, parsed_text )
return 'note "{}":{}{}'.format( note_name, os.linesep, parsed_text )
elif content_type == HC.CONTENT_TYPE_HASH:
@ -192,7 +192,7 @@ def ConvertParsableContentToPrettyString( parsable_content, include_veto = False
note_names = sorted( additional_infos )
s = 'notes: {}'.format( ', '.join( note_names ) )
s = 'notes:{}'.format( ', '.join( note_names ) )
pretty_strings.append( s )
@ -348,10 +348,26 @@ def GetHTMLTagString( tag: bs4.Tag ):
# on a version update, these suddenly went semi bonkers and wouldn't pull text unless the types of the subtag were explicitly set
# so we'll just do it ourselves
all_strings = []
try:
all_strings = [ str( c ) for c in tag.descendants if isinstance( c, ( bs4.NavigableString, bs4.CData ) ) ]
all_strings = [ s for s in all_strings if len( s ) > 0 ]
for sub_tag in tag.descendants:
if sub_tag.name in ( 'br', 'p' ):
all_strings.append( os.linesep )
continue
if not isinstance( sub_tag, ( bs4.NavigableString, bs4.CData ) ):
continue
all_strings.append( str( sub_tag ) )
except:
@ -666,7 +682,7 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
return os.linesep
def _ParseRawTexts( self, parsing_context, parsing_text ):
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
raise NotImplementedError()
@ -676,20 +692,23 @@ class ParseFormula( HydrusSerialisable.SerialisableBase ):
return self._string_processor
def Parse( self, parsing_context, parsing_text ):
def Parse( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
raw_texts = self._ParseRawTexts( parsing_context, parsing_text )
raw_texts = self._ParseRawTexts( parsing_context, parsing_text, collapse_newlines )
raw_texts = [ HydrusText.RemoveNewlines( raw_text ) for raw_text in raw_texts ]
if collapse_newlines:
raw_texts = [ HydrusText.RemoveNewlines( raw_text ) for raw_text in raw_texts ]
texts = self._string_processor.ProcessStrings( raw_texts )
return texts
def ParsePretty( self, parsing_context, parsing_text ):
def ParsePretty( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
texts = self.Parse( parsing_context, parsing_text )
texts = self.Parse( parsing_context, parsing_text, collapse_newlines )
pretty_texts = [ MakeParsedTextPretty( text ) for text in texts ]
@ -766,7 +785,7 @@ class ParseFormulaCompound( ParseFormula ):
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
def _ParseRawTexts( self, parsing_context, parsing_text ):
def _ParseRawTexts( self, parsing_context, parsing_text: str, collapse_newlines: bool ):
def get_stream_string( index, s ):
@ -788,7 +807,7 @@ class ParseFormulaCompound( ParseFormula ):
for formula in self._formulae:
stream = formula.Parse( parsing_context, parsing_text )
stream = formula.Parse( parsing_context, parsing_text, collapse_newlines )
if len( stream ) == 0: # no contents were found for one of the /1 replace components, so no valid strings can be made.
@ -912,7 +931,7 @@ class ParseFormulaContextVariable( ParseFormula ):
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
def _ParseRawTexts( self, parsing_context, parsing_text ):
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
raw_texts = []
@ -1124,7 +1143,7 @@ class ParseFormulaHTML( ParseFormula ):
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
def _ParseRawTexts( self, parsing_context, parsing_text ):
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
try:
@ -1758,7 +1777,7 @@ class ParseFormulaJSON( ParseFormula ):
self._string_processor = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_string_processor )
def _ParseRawTexts( self, parsing_context, parsing_text ):
def _ParseRawTexts( self, parsing_context, parsing_text, collapse_newlines: bool ):
try:
@ -2158,7 +2177,9 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
try:
parsed_texts = list( self._formula.Parse( parsing_context, parsing_text ) )
collapse_newlines = self._content_type != HC.CONTENT_TYPE_NOTES
parsed_texts = list( self._formula.Parse( parsing_context, parsing_text, collapse_newlines ) )
except HydrusExceptions.ParseException as e:
@ -2169,6 +2190,11 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
raise e
if self._content_type == HC.CONTENT_TYPE_NOTES:
parsed_texts = [ HydrusText.CleanNoteText( parsed_text ) for parsed_text in parsed_texts ]
if self._content_type == HC.CONTENT_TYPE_URLS:
if 'url' in parsing_context:
@ -2240,7 +2266,7 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
def ParsePretty( self, parsing_context, parsing_text ):
def ParsePretty( self, parsing_context, parsing_text: str ):
try:
@ -2553,7 +2579,9 @@ class PageParser( HydrusSerialisable.SerialisableBaseNamed ):
try:
posts = formula.Parse( parsing_context, converted_parsing_text )
collapse_newlines = False
posts = formula.Parse( parsing_context, converted_parsing_text, collapse_newlines )
except HydrusExceptions.ParseException:

View File

@ -1324,7 +1324,7 @@ class DB( HydrusDB.HydrusDB ):
from hydrus.client.metadata import ClientRatings
dictionary[ 'shape' ] = ClientRatings.STAR
dictionary[ 'shape' ] = ClientRatings.FAT_STAR
like_colours = {}
@ -10423,7 +10423,7 @@ class DB( HydrusDB.HydrusDB ):
from hydrus.client.metadata import ClientRatings
dictionary[ 'shape' ] = ClientRatings.STAR
dictionary[ 'shape' ] = ClientRatings.FAT_STAR
like_colours = {}
@ -11447,6 +11447,36 @@ class DB( HydrusDB.HydrusDB ):
if version == 496:
try:
domain_manager = self.modules_serialisable.GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
domain_manager.Initialise()
#
domain_manager.OverwriteDefaultParsers( ( 'hentai foundry file page parser', ) )
#
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, ) )

View File

@ -57,7 +57,6 @@ from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUILogin
from hydrus.client.gui import ClientGUIMediaControls
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIParsing
from hydrus.client.gui import ClientGUIPopupMessages
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
@ -86,6 +85,8 @@ from hydrus.client.gui.networking import ClientGUINetwork
from hydrus.client.gui.pages import ClientGUIManagement
from hydrus.client.gui.pages import ClientGUIPages
from hydrus.client.gui.pages import ClientGUISession
from hydrus.client.gui.parsing import ClientGUIParsing
from hydrus.client.gui.parsing import ClientGUIParsingLegacy
from hydrus.client.gui.services import ClientGUIClientsideServices
from hydrus.client.gui.services import ClientGUIServersideServices
from hydrus.client.gui.widgets import ClientGUICommon
@ -4327,7 +4328,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
with ClientGUITopLevelWindowsPanels.DialogManage( self, title ) as dlg:
panel = ClientGUIParsing.ManageParsingScriptsPanel( dlg )
panel = ClientGUIParsingLegacy.ManageParsingScriptsPanel( dlg )
dlg.SetPanel( panel )

View File

@ -17,7 +17,6 @@ from hydrus.client import ClientPaths
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIParsing
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
@ -26,6 +25,7 @@ from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.networking import ClientGUINetworkJobControl
from hydrus.client.gui.parsing import ClientGUIParsing
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.importing import ClientImporting

File diff suppressed because it is too large Load Diff

View File

@ -24,20 +24,7 @@ default_numerical_colours[ ClientRatings.DISLIKE ] = ( ( 0, 0, 0 ), ( 255, 255,
default_numerical_colours[ ClientRatings.NULL ] = ( ( 0, 0, 0 ), ( 191, 191, 191 ) )
default_numerical_colours[ ClientRatings.MIXED ] = ( ( 0, 0, 0 ), ( 95, 95, 95 ) )
STAR_COORDS = []
STAR_COORDS.append( QC.QPointF( 6, 0 ) ) # top
STAR_COORDS.append( QC.QPointF( 9, 4 ) )
STAR_COORDS.append( QC.QPointF( 12, 4 ) ) # right
STAR_COORDS.append( QC.QPointF( 9, 8 ) )
STAR_COORDS.append( QC.QPointF( 10, 12 ) ) # bottom right
STAR_COORDS.append( QC.QPointF( 6, 10 ) )
STAR_COORDS.append( QC.QPointF( 2, 12 ) ) # bottom left
STAR_COORDS.append( QC.QPointF( 3, 8 ) )
STAR_COORDS.append( QC.QPointF( 0, 4 ) ) # left
STAR_COORDS.append( QC.QPointF( 3, 4 ) )
STAR_COORDS = [
PENTAGRAM_STAR_COORDS = [
QC.QPointF( 6, 0 ), # top
QC.QPointF( 7.5, 4.5 ),
QC.QPointF( 12, 4.5 ), # right
@ -50,6 +37,19 @@ STAR_COORDS = [
QC.QPointF( 4.5, 4.5 )
]
FAT_STAR_COORDS = [
QC.QPointF( 6, 0 ), # top
QC.QPointF( 7.8, 4.1 ),
QC.QPointF( 12, 4.6 ), # right
QC.QPointF( 8.9, 7.6 ),
QC.QPointF( 9.8, 12 ), # bottom right
QC.QPointF( 6, 9.8 ),
QC.QPointF( 2.2, 12 ), # bottom left
QC.QPointF( 3.1, 7.6 ),
QC.QPointF( 0, 4.5 ), # left
QC.QPointF( 4.2, 4.1 )
]
def DrawLike( painter, x, y, service_key, rating_state ):
painter.setRenderHint( QG.QPainter.Antialiasing, True )
@ -69,13 +69,15 @@ def DrawLike( painter, x, y, service_key, rating_state ):
painter.drawRect( x+2, y+2, 12, 12 )
elif shape == ClientRatings.STAR:
elif shape in ( ClientRatings.FAT_STAR, ClientRatings.PENTAGRAM_STAR ):
offset = QC.QPoint( x + 1, y + 1 )
painter.translate( offset )
painter.drawPolygon( QG.QPolygonF( STAR_COORDS ) )
coords = FAT_STAR_COORDS if shape == ClientRatings.FAT_STAR else PENTAGRAM_STAR_COORDS
painter.drawPolygon( QG.QPolygonF( coords ) )
painter.translate( -offset )
@ -104,13 +106,15 @@ def DrawNumerical( painter, x, y, service_key, rating_state, rating ):
painter.drawRect( x + 2 + x_delta, y + 2, 12, 12 )
elif shape == ClientRatings.STAR:
elif shape in ( ClientRatings.FAT_STAR, ClientRatings.PENTAGRAM_STAR ):
offset = QC.QPoint( x + 1 + x_delta, y + 1 )
painter.translate( offset )
painter.drawPolygon( QG.QPolygonF( STAR_COORDS ) )
coords = FAT_STAR_COORDS if shape == ClientRatings.FAT_STAR else PENTAGRAM_STAR_COORDS
painter.drawPolygon( QG.QPolygonF( coords ) )
painter.translate( -offset )
@ -162,7 +166,7 @@ def GetStars( service_key, rating_state, rating ):
except HydrusExceptions.DataMissing:
return ( ClientRatings.STAR, 0 )
return ( ClientRatings.FAT_STAR, 0 )
shape = service.GetShape()

View File

@ -3406,6 +3406,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._expand_parents_on_storage_taglists = QW.QCheckBox( general_panel )
self._expand_parents_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
self._show_parent_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
self._show_parent_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
self._show_sibling_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
self._show_sibling_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
self._ac_select_first_with_count = QW.QCheckBox( general_panel )
#
@ -3442,13 +3448,23 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._default_tag_service_search_page.SetValue( self._new_options.GetKey( 'default_tag_service_search_page' ) )
self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
self._expand_parents_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' )
self._expand_parents_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
self._expand_parents_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
self._show_parent_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' ) )
self._show_parent_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents either hang below tags or summarise in a suffix.' )
self._show_parent_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
self._show_parent_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
self._show_sibling_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' ) )
self._show_sibling_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and siblings summarise in a suffix.' )
self._show_sibling_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
self._ac_select_first_with_count.setChecked( self._new_options.GetBoolean( 'ac_select_first_with_count' ) )
#
@ -3464,8 +3480,12 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Default tag service in manage tag dialogs: ', self._default_tag_service_tab ) )
rows.append( ( 'Remember last used default tag service in manage tag dialogs: ', self._save_default_tag_service_tab_on_change ) )
rows.append( ( 'Default tag service in search pages: ', self._default_tag_service_search_page ) )
rows.append( ( 'Show parent info by default on edit/write taglists: ', self._show_parent_decorators_on_storage_taglists ) )
rows.append( ( 'Show parent info by default on edit/write autocomplete taglists: ', self._show_parent_decorators_on_storage_autocomplete_taglists ) )
rows.append( ( 'Show parents expanded by default on edit/write taglists: ', self._expand_parents_on_storage_taglists ) )
rows.append( ( 'Show parents expanded by default on edit/write autocomplete taglists: ', self._expand_parents_on_storage_autocomplete_taglists ) )
rows.append( ( 'Show sibling info by default on edit/write taglists: ', self._show_sibling_decorators_on_storage_taglists ) )
rows.append( ( 'Show sibling info by default on edit/write autocomplete taglists: ', self._show_sibling_decorators_on_storage_autocomplete_taglists ) )
rows.append( ( 'By default, select the first tag result with actual count in write-autocomplete: ', self._ac_select_first_with_count ) )
gridbox = ClientGUICommon.WrapInGrid( general_panel, rows )
@ -3507,8 +3527,13 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetKey( 'default_tag_service_search_page', self._default_tag_service_search_page.GetValue() )
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_taglists', self._show_parent_decorators_on_storage_taglists.isChecked() )
self._new_options.SetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists', self._show_parent_decorators_on_storage_autocomplete_taglists.isChecked() )
self._new_options.SetBoolean( 'expand_parents_on_storage_taglists', self._expand_parents_on_storage_taglists.isChecked() )
self._new_options.SetBoolean( 'expand_parents_on_storage_autocomplete_taglists', self._expand_parents_on_storage_autocomplete_taglists.isChecked() )
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_taglists', self._show_sibling_decorators_on_storage_taglists.isChecked() )
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists', self._show_sibling_decorators_on_storage_autocomplete_taglists.isChecked() )
self._new_options.SetBoolean( 'ac_select_first_with_count', self._ac_select_first_with_count.isChecked() )
#

View File

@ -208,7 +208,7 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
self._this_is_a_random_sample_sub = QW.QCheckBox( self._file_limits_panel )
self._this_is_a_random_sample_sub.setToolTip( 'If you check this, you will not get warnings if the normal file limit is hit. Useful if you have a randomly sorted gallery, or you just want a recurring small sample of files.' )
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options, update_callable = self._CheckerOptionsUpdated )
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._file_limits_panel, checker_options )
self._file_presentation_panel = ClientGUICommon.StaticBox( self, 'file publication' )
@ -336,6 +336,8 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
self.widget().setLayout( vbox )
self._checker_options.valueChanged.connect( self._CheckerOptionsUpdated )
self._UpdateDelayText()

View File

@ -15,10 +15,10 @@ from hydrus.client import ClientParsing
from hydrus.client import ClientSearch
from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIParsing
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListBoxesData
from hydrus.client.gui.parsing import ClientGUIParsingLegacy
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.metadata import ClientTags
from hydrus.client.metadata import ClientTagSorting
@ -463,7 +463,7 @@ class FileLookupScriptTagsPanel( QW.QWidget ):
self._fetch_button.setEnabled( False )
self._script_management = ClientGUIParsing.ScriptManagementControl( self )
self._script_management = ClientGUIParsingLegacy.ScriptManagementControl( self )
self._tags = ListBoxTagsSuggestionsFavourites( self, self._service_key, activate_callable, sort_tags = True )

View File

@ -2932,14 +2932,21 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._show_all = QW.QCheckBox( self )
# leave up here since other things have updates based on them
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
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._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
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._listctrl_panel.SetListCtrl( self._tag_parents )
self._tag_parents.Sort()
self._listctrl_panel.AddButton( 'add', self._AddButton, enabled_check_func = self._CanAddFromCurrentInput )
self._listctrl_panel.AddButton( 'delete', self._DeleteSelectedRows, enabled_only_on_selection = True )
menu_items = []
menu_items.append( ( 'normal', 'from clipboard', 'Load parents from text in your clipboard.', HydrusData.Call( self._ImportFromClipboard, False ) ) )
@ -2958,9 +2965,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._listctrl_panel.setEnabled( False )
self._children = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
self._parents = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._children, ( 12, 6 ) )
self._children.setMinimumHeight( preview_height )
@ -2974,10 +2978,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._parent_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.EnterParents, default_location_context, service_key, show_paste_button = True )
self._parent_input.setEnabled( False )
self._add = QW.QPushButton( 'add', self )
self._add.clicked.connect( self.EventAddButton )
self._add.setEnabled( False )
#
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' + os.linesep + '.' )
@ -3014,7 +3014,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._add, CC.FLAGS_ON_RIGHT )
QP.AddToLayout( vbox, tags_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, input_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@ -3022,8 +3021,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
#
self._tag_parents.itemSelectionChanged.connect( self._SetButtonStatus )
self._children.listBoxChanged.connect( self._UpdateListCtrlData )
self._parents.listBoxChanged.connect( self._UpdateListCtrlData )
self._show_all.clicked.connect( self._UpdateListCtrlData )
@ -3031,6 +3028,23 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
def _AddButton( self ):
children = self._children.GetTags()
parents = self._parents.GetTags()
pairs = list( itertools.product( children, parents ) )
self._AddPairs( pairs )
self._children.SetTags( [] )
self._parents.SetTags( [] )
self._UpdateListCtrlData()
self._listctrl_panel.UpdateButtons()
def _AddPairs( self, pairs, add_only = False ):
pairs = list( pairs )
@ -3323,6 +3337,16 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
return True
def _CanAddFromCurrentInput( self ):
if len( self._children.GetTags() ) == 0 or len( self._parents.GetTags() ) == 0:
return False
return True
def _ConvertPairToListCtrlTuples( self, pair ):
( child, parent ) = pair
@ -3350,6 +3374,18 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
return ( display_tuple, sort_tuple )
def _DeleteSelectedRows( self ):
parents_to_children = collections.defaultdict( set )
pairs = self._tag_parents.GetData( only_selected = True )
if len( pairs ) > 0:
self._AddPairs( pairs )
def _DeserialiseImportString( self, import_string ):
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
@ -3467,30 +3503,6 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
def _ListCtrlActivated( self ):
parents_to_children = collections.defaultdict( set )
pairs = self._tag_parents.GetData( only_selected = True )
if len( pairs ) > 0:
self._AddPairs( pairs )
def _SetButtonStatus( self ):
if len( self._children.GetTags() ) == 0 or len( self._parents.GetTags() ) == 0:
self._add.setEnabled( False )
else:
self._add.setEnabled( True )
def _UpdateListCtrlData( self ):
children = self._children.GetTags()
@ -3553,7 +3565,7 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
self._SetButtonStatus()
self._listctrl_panel.UpdateButtons()
@ -3567,27 +3579,10 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
self._SetButtonStatus()
self._listctrl_panel.UpdateButtons()
def EventAddButton( self ):
children = self._children.GetTags()
parents = self._parents.GetTags()
pairs = list( itertools.product( children, parents ) )
self._AddPairs( pairs )
self._children.SetTags( [] )
self._parents.SetTags( [] )
self._UpdateListCtrlData()
self._SetButtonStatus()
def GetContentUpdates( self ):
# we make it manually here because of the mass pending tags done (but not undone on a rescind) on a pending pair!
@ -3935,12 +3930,19 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
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, ClientTags.TAG_DISPLAY_ACTUAL )
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._ListCtrlActivated, activation_callback = self._ListCtrlActivated )
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._listctrl_panel.SetListCtrl( self._tag_siblings )
self._listctrl_panel.AddButton( 'add', self._AddButton, enabled_check_func = self._CanAddFromCurrentInput )
self._listctrl_panel.AddButton( 'delete', self._DeleteSelectedRows, enabled_only_on_selection = True )
self._tag_siblings.Sort()
menu_items = []
@ -3961,9 +3963,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self._listctrl_panel.setEnabled( False )
self._old_siblings = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( self, self._service_key, ClientTags.TAG_DISPLAY_ACTUAL )
self._new_sibling = ClientGUICommon.BetterStaticText( self )
( gumpf, preview_height ) = ClientGUIFunctions.ConvertTextToPixels( self._old_siblings, ( 12, 6 ) )
self._old_siblings.setMinimumHeight( preview_height )
@ -3976,10 +3975,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self._new_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( self, self.SetNew, default_location_context, service_key )
self._new_input.setEnabled( False )
self._add = QW.QPushButton( 'add', self )
self._add.clicked.connect( self.EventAddButton )
self._add.setEnabled( False )
#
self._status_st = ClientGUICommon.BetterStaticText( self, 'initialising\u2026' )
@ -4016,7 +4011,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
QP.AddToLayout( vbox, self._count_st, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, ClientGUICommon.WrapInText(self._show_all,self,'show all pairs'), CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._add, CC.FLAGS_ON_RIGHT )
QP.AddToLayout( vbox, text_box, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, input_box, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@ -4024,14 +4018,35 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
#
self._tag_siblings.itemSelectionChanged.connect( self._SetButtonStatus )
self._show_all.clicked.connect( self._UpdateListCtrlData )
self._old_siblings.listBoxChanged.connect( self._UpdateListCtrlData )
HG.client_controller.CallToThread( self.THREADInitialise, tags, self._service_key )
def _AddButton( self ):
if self._current_new is not None and len( self._old_siblings.GetTags() ) > 0:
olds = self._old_siblings.GetTags()
pairs = [ ( old, self._current_new ) for old in olds ]
self._AutoPetitionConflicts( pairs )
self._AutoPetitionLoops( pairs )
self._AddPairs( pairs )
self._old_siblings.SetTags( set() )
self.SetNew( set() )
self._UpdateListCtrlData()
self._listctrl_panel.UpdateButtons()
def _AddPairs( self, pairs, add_only = False, remove_only = False, default_reason = None ):
pairs = list( pairs )
@ -4376,6 +4391,16 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
return True
def _CanAddFromCurrentInput( self ):
if self._current_new is None or len( self._old_siblings.GetTags() ) == 0:
return False
return True
def _ConvertPairToListCtrlTuples( self, pair ):
( old, new ) = pair
@ -4424,6 +4449,18 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
return ( display_tuple, sort_tuple )
def _DeleteSelectedRows( self ):
pairs = self._tag_siblings.GetData( only_selected = True )
if len( pairs ) > 0:
self._AddPairs( pairs )
self._UpdateListCtrlData()
def _DeserialiseImportString( self, import_string ):
tags = HydrusText.DeserialiseNewlinedTexts( import_string )
@ -4549,30 +4586,6 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
def _ListCtrlActivated( self ):
pairs = self._tag_siblings.GetData( only_selected = True )
if len( pairs ) > 0:
self._AddPairs( pairs )
self._UpdateListCtrlData()
def _SetButtonStatus( self ):
if self._current_new is None or len( self._old_siblings.GetTags() ) == 0:
self._add.setEnabled( False )
else:
self._add.setEnabled( True )
def _UpdateListCtrlData( self ):
olds = self._old_siblings.GetTags()
@ -4640,30 +4653,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
self._SetButtonStatus()
def EventAddButton( self ):
if self._current_new is not None and len( self._old_siblings.GetTags() ) > 0:
olds = self._old_siblings.GetTags()
pairs = [ ( old, self._current_new ) for old in olds ]
self._AutoPetitionConflicts( pairs )
self._AutoPetitionLoops( pairs )
self._AddPairs( pairs )
self._old_siblings.SetTags( set() )
self.SetNew( set() )
self._UpdateListCtrlData()
self._SetButtonStatus()
self._listctrl_panel.UpdateButtons()
def GetContentUpdates( self ):
@ -4741,7 +4731,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
self._UpdateListCtrlData()
self._SetButtonStatus()
self._listctrl_panel.UpdateButtons()
def SetTagBoxFocus( self ):

View File

@ -2376,228 +2376,3 @@ class WidgetEventFilter ( QC.QObject ):
def EVT_SIZE( self, callback ):
self._AddCallback( 'EVT_SIZE', callback )
# wew lad
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
class CheckBoxDelegate(QW.QStyledItemDelegate):
def __init__(self, parent=None):
super( CheckBoxDelegate, self ).__init__(parent)
def createEditor( self, parent, op, idx ):
self.editor = QW.QCheckBox( parent )
class CollectComboCtrl( QW.QComboBox ):
itemChanged = QC.Signal()
def __init__( self, parent, media_collect ):
QW.QComboBox.__init__( self, parent )
self.view().pressed.connect( self._HandleItemPressed )
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
self.setItemDelegate( CheckBoxDelegate() )
self.setModel( QG.QStandardItemModel( self ) )
text_and_data_tuples = set()
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
namespaces = media_sort.GetNamespaces()
try:
text_and_data_tuples.update( namespaces )
except:
HydrusData.DebugPrint( 'Bad namespaces: {}'.format( namespaces ) )
HydrusData.ShowText( 'Hey, your namespace-based sorts are likely damaged. Details have been written to the log, please let hydev know!' )
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
for ratings_service in ratings_services:
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
for ( text, data ) in text_and_data_tuples:
self.Append( text, data )
# Trick to display custom text
self._cached_text = ''
if media_collect.DoesACollect():
CallAfter( self.SetCollectByValue, media_collect )
def paintEvent( self, e ):
painter = QW.QStylePainter( self )
painter.setPen( self.palette().color( QG.QPalette.Text ) )
opt = QW.QStyleOptionComboBox()
self.initStyleOption( opt )
opt.currentText = self._cached_text
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
def GetValues( self ):
namespaces = []
rating_service_keys = []
for index in self.GetCheckedIndices():
(collect_type, collect_data) = self.itemData( index, QC.Qt.UserRole )
if collect_type == 'namespace':
namespaces.append( collect_data )
elif collect_type == 'rating':
rating_service_keys.append( collect_data )
collect_strings = self.GetCheckedStrings()
if len( collect_strings ) > 0:
description = 'collect by ' + '-'.join( collect_strings )
else:
description = 'no collections'
return ( namespaces, rating_service_keys, description )
def hidePopup(self):
if not self.view().underMouse():
QW.QComboBox.hidePopup( self )
def SetValue( self, text ):
self._cached_text = text
self.setCurrentText( text )
def SetCollectByValue( self, media_collect ):
try:
indices_to_check = []
for index in range( self.count() ):
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
if p1 or p2:
indices_to_check.append( index )
self.SetCheckedIndices( indices_to_check )
self.itemChanged.emit()
except Exception as e:
HydrusData.ShowText( 'Failed to set a collect-by value!' )
HydrusData.ShowException( e )
def SetCheckedIndices( self, indices_to_check ):
for idx in range( self.count() ):
item = self.model().item( idx )
if idx in indices_to_check:
item.setCheckState( QC.Qt.Checked )
else:
item.setCheckState( QC.Qt.Unchecked )
def GetCheckedIndices( self ):
indices = []
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked: indices.append( idx )
return indices
def GetCheckedStrings( self ):
strings = [ ]
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked: strings.append( item.text() )
return strings
def Append( self, str, data ):
self.addItem( str, userData = data )
item = self.model().item( self.count() - 1, 0 )
item.setCheckState( QC.Qt.Unchecked )
def _HandleItemPressed( self, index ):
item = self.model().itemFromIndex( index )
if item.checkState() == QC.Qt.Checked:
item.setCheckState( QC.Qt.Unchecked )
else:
item.setCheckState( QC.Qt.Checked )
self.SetValue( self._cached_text )
self.itemChanged.emit()

View File

@ -40,8 +40,6 @@ class RatingLikeCanvas( ClientGUIRatings.RatingLike ):
self._canvas_key = canvas_key
self._current_media = None
service = HG.client_controller.services_manager.GetService( service_key )
HG.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
HG.client_controller.sub( self, 'SetDisplayMedia', 'canvas_new_display_media' )
@ -1346,7 +1344,7 @@ class NotePanel( QW.QWidget ):
vbox = QP.VBoxLayout( margin = 0 )
QP.AddToLayout( vbox, self._note_name, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._note_text, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._note_text, CC.FLAGS_EXPAND_PERPENDICULAR )
self._note_text.setVisible( self._note_visible )
@ -1382,16 +1380,38 @@ class NotePanel( QW.QWidget ):
spacing = self.layout().spacing()
margin = self.layout().contentsMargins().top()
height = self._note_name.heightForWidth( width ) + margin * 2
total_height = 0
expected_widget_height = self._note_name.heightForWidth( width )
if self._note_name.width() >= width and self._note_name.height() > expected_widget_height:
# there's some mysterious padding that I can't explain, probably a layout flag legacy issue, so we override here if the width seems correct
expected_widget_height = self._note_name.height()
total_height += expected_widget_height
if self._note_text.isVisibleTo( self ):
height += spacing
total_height += spacing
height += self._note_text.heightForWidth( width ) + margin * 2
expected_widget_height = self._note_text.heightForWidth( width )
if self._note_text.width() >= width and self._note_text.height() > expected_widget_height:
# there's some mysterious padding that I can't explain, probably a layout flag legacy issue, so we override here if the width seems correct
expected_widget_height = self._note_text.height()
total_height += expected_widget_height
return height
total_height += margin * 2
return total_height
def IsNoteVisible( self ) -> bool:
@ -1469,7 +1489,12 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
spacing = self.layout().spacing()
margin = self.layout().contentsMargins().top()
best_guess_at_height_for_width = sum( ( spacing + ( margin * 2 ) + note_panel.heightForWidth( my_ideal_width ) for note_panel in self._names_to_note_panels.values() ) ) - spacing
my_axis_frame_width = self.frameWidth() * 2
my_axis_margin = margin * 2
note_panel_width = my_ideal_width - ( my_axis_frame_width + my_axis_margin )
best_guess_at_height_for_width = sum( ( spacing + ( margin * 2 ) + note_panel.heightForWidth( note_panel_width ) for note_panel in self._names_to_note_panels.values() ) ) - spacing
best_guess_at_height_for_width += self.frameWidth() * 2
@ -1514,7 +1539,7 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
note_panel = NotePanel( self, name, note, note_visible )
QP.AddToLayout( self._vbox, note_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( self._vbox, note_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
self._names_to_note_panels[ name ] = note_panel

View File

@ -39,12 +39,13 @@ from hydrus.client.metadata import ClientTags
class CheckerOptionsButton( ClientGUICommon.BetterButton ):
def __init__( self, parent, checker_options, update_callable = None ):
valueChanged = QC.Signal( ClientImportOptions.CheckerOptions )
def __init__( self, parent, checker_options: ClientImportOptions.CheckerOptions ):
ClientGUICommon.BetterButton.__init__( self, parent, 'checker options', self._EditOptions )
self._checker_options = checker_options
self._update_callable = update_callable
self._SetToolTip()
@ -77,10 +78,7 @@ class CheckerOptionsButton( ClientGUICommon.BetterButton ):
self._SetToolTip()
if self._update_callable is not None:
self._update_callable( self._checker_options )
self.valueChanged.emit( self._checker_options )
def GetValue( self ):
@ -1547,7 +1545,7 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ):
checker_options = ClientImportOptions.CheckerOptions()
self._checker_options_button = CheckerOptionsButton( checker_panel, checker_options, update_callable = self._SetCheckerOptions )
self._checker_options_button = CheckerOptionsButton( checker_panel, checker_options )
self._checker_download_control = ClientGUINetworkJobControl.NetworkJobControl( checker_panel )
@ -1615,6 +1613,8 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ):
self._import_options_button.noteImportOptionsChanged.connect( self._SetNoteImportOptions )
self._import_options_button.tagImportOptionsChanged.connect( self._SetTagImportOptions )
self._checker_options_button.valueChanged.connect( self._SetCheckerOptions )
self._UpdateControlsForNewWatcher()
HG.client_controller.gui.RegisterUIUpdateWindow( self )

View File

@ -927,7 +927,7 @@ class ListBox( QW.QScrollArea ):
TEXT_X_PADDING = 3
def __init__( self, parent: QW.QWidget, child_rows_allowed: bool, terms_may_have_child_rows: bool, height_num_chars = 10, has_async_text_info = False ):
def __init__( self, parent: QW.QWidget, terms_may_have_sibling_or_parent_info: bool, height_num_chars = 10, has_async_text_info = False ):
QW.QScrollArea.__init__( self, parent )
self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
@ -956,8 +956,11 @@ class ListBox( QW.QScrollArea ):
self._num_rows_per_page = 0
self._child_rows_allowed = child_rows_allowed
self._terms_may_have_child_rows = terms_may_have_child_rows
self._show_sibling_decorators = True
self._show_parent_decorators = True
self._extra_parent_rows_allowed = True
self._terms_may_have_sibling_or_parent_info = terms_may_have_sibling_or_parent_info
#
@ -1068,7 +1071,9 @@ class ListBox( QW.QScrollArea ):
self._StartAsyncTextInfoLookup( term )
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
self._total_positional_rows += term.GetRowCount( show_parent_rows )
if len( previously_selected_terms ) > 0:
@ -1650,7 +1655,9 @@ class ListBox( QW.QScrollArea ):
self._terms_to_positional_indices[ term ] = self._total_positional_rows
self._positional_indices_to_terms[ self._total_positional_rows ] = term
self._total_positional_rows += term.GetRowCount( self._child_rows_allowed )
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
self._total_positional_rows += term.GetRowCount( show_parent_rows )
@ -2006,11 +2013,11 @@ class ListBox( QW.QScrollArea ):
def SetChildRowsAllowed( self, value: bool ):
def SetExtraParentRowsAllowed( self, value: bool ):
if self._terms_may_have_child_rows and self._child_rows_allowed != value:
if self._terms_may_have_sibling_or_parent_info and self._extra_parent_rows_allowed != value:
self._child_rows_allowed = value
self._extra_parent_rows_allowed = value
self._RegenTermsToIndices()
@ -2025,6 +2032,34 @@ class ListBox( QW.QScrollArea ):
self._minimum_height_num_chars = minimum_height_num_chars
def SetParentDecoratorsAllowed( self, value: bool ):
if self._terms_may_have_sibling_or_parent_info and self._show_parent_decorators != value:
self._show_parent_decorators = value
# i.e. if we just hid/showed parent sub-rows
if self._extra_parent_rows_allowed:
self._RegenTermsToIndices()
self._SetVirtualSize()
self.widget().update()
def SetSiblingDecoratorsAllowed( self, value: bool ):
if self._terms_may_have_sibling_or_parent_info and self._show_sibling_decorators != value:
self._show_sibling_decorators = value
self.widget().update()
def sizeHint( self ):
size_hint = QW.QScrollArea.sizeHint( self )
@ -2055,15 +2090,20 @@ class ListBoxTags( ListBox ):
self._tag_display_type = tag_display_type
child_rows_allowed = HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_taglists' )
terms_may_have_child_rows = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
terms_may_have_sibling_or_parent_info = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
ListBox.__init__( self, parent, child_rows_allowed, terms_may_have_child_rows, *args, **kwargs )
ListBox.__init__( self, parent, terms_may_have_sibling_or_parent_info, *args, **kwargs )
if terms_may_have_sibling_or_parent_info:
self._show_parent_decorators = HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' )
self._show_sibling_decorators = HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' )
self._extra_parent_rows_allowed = HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_taglists' )
self._render_for_user = not self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
self._sibling_decoration_allowed = self._tag_display_type == ClientTags.TAG_DISPLAY_STORAGE
self._page_key = None # placeholder. if a subclass sets this, it changes menu behaviour to allow 'select this tag' menu pubsubs
self._UpdateBackgroundColour()
@ -2135,7 +2175,9 @@ class ListBoxTags( ListBox ):
namespace_colours = self._GetNamespaceColours()
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._sibling_decoration_allowed, self._child_rows_allowed )
show_parent_rows = self._show_parent_decorators and self._extra_parent_rows_allowed
rows_of_texts_and_namespaces = term.GetRowsOfPresentationTextsWithNamespaces( self._render_for_user, self._show_sibling_decorators, self._show_parent_decorators, show_parent_rows )
rows_of_texts_and_colours = []
@ -2391,32 +2433,39 @@ class ListBoxTags( ListBox ):
menu = QW.QMenu()
if self._terms_may_have_child_rows:
if self._terms_may_have_sibling_or_parent_info:
add_it = True
if self._child_rows_allowed:
if self._show_parent_decorators:
if len( self._ordered_terms ) == self._total_positional_rows:
if self._extra_parent_rows_allowed:
# no parents to hide!
if len( self._ordered_terms ) != self._total_positional_rows:
ClientGUIMenus.AppendMenuItem( menu, 'collapse parent rows', 'Show/hide parents.', self.SetExtraParentRowsAllowed, not self._extra_parent_rows_allowed )
add_it = False
else:
ClientGUIMenus.AppendMenuItem( menu, 'expand parent rows', 'Show/hide parents.', self.SetExtraParentRowsAllowed, not self._extra_parent_rows_allowed )
message = 'hide parent rows'
ClientGUIMenus.AppendMenuItem( menu, 'hide parent decorators', 'Show/hide parent info.', self.SetParentDecoratorsAllowed, not self._show_parent_decorators )
else:
message = 'show parent rows'
ClientGUIMenus.AppendMenuItem( menu, 'show parent decorators', 'Show/hide parent info.', self.SetParentDecoratorsAllowed, not self._show_parent_decorators )
if add_it:
if self._show_sibling_decorators:
ClientGUIMenus.AppendMenuItem( menu, message, 'Show/hide parents.', self.SetChildRowsAllowed, not self._child_rows_allowed )
ClientGUIMenus.AppendMenuItem( menu, 'hide sibling decorators', 'Show/hide sibling info.', self.SetSiblingDecoratorsAllowed, not self._show_sibling_decorators )
ClientGUIMenus.AppendSeparator( menu )
else:
ClientGUIMenus.AppendMenuItem( menu, 'show sibling decorators', 'Show/hide sibling info.', self.SetSiblingDecoratorsAllowed, not self._show_sibling_decorators )
ClientGUIMenus.AppendSeparator( menu )
copy_menu = QW.QMenu( menu )
@ -3612,7 +3661,7 @@ class ListBoxTagsMedia( ListBoxTagsDisplayCapable ):
item_to_tag_key_wrapper = lambda term: term.GetTag()
item_to_sibling_key_wrapper = item_to_tag_key_wrapper
if self._sibling_decoration_allowed:
if self._show_sibling_decorators:
item_to_sibling_key_wrapper = lambda term: term.GetBestTag()

View File

@ -56,12 +56,12 @@ class ListBoxItem( object ):
raise NotImplementedError()
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
raise NotImplementedError()
def GetRowCount( self, child_rows_allowed: bool ):
def GetRowCount( self, show_parent_rows: bool ):
return 1
@ -95,7 +95,7 @@ class ListBoxItemTagSlice( ListBoxItem ):
return []
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.Tuple[ str, str ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.Tuple[ str, str ] ]:
presentation_text = self.GetCopyableText()
@ -167,7 +167,7 @@ class ListBoxItemNamespaceColour( ListBoxItem ):
return []
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
return [ [ ( self.GetCopyableText(), self._namespace ) ] ]
@ -245,9 +245,9 @@ class ListBoxItemTextTag( ListBoxItem ):
return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = self._tag ) ]
def GetRowCount( self, child_rows_allowed: bool ):
def GetRowCount( self, show_parent_rows: bool ):
if self._parent_tags is None or not child_rows_allowed:
if self._parent_tags is None or not show_parent_rows:
return 1
@ -257,7 +257,7 @@ class ListBoxItemTextTag( ListBoxItem ):
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
# this should be with counts or whatever, but we need to think about this more lad
@ -276,11 +276,11 @@ class ListBoxItemTextTag( ListBoxItem ):
if self._parent_tags is not None:
if child_rows_allowed:
if show_parent_rows:
self._AppendParentsTextWithNamespaces( rows_of_texts_with_namespaces, render_for_user )
elif sibling_decoration_allowed:
elif parent_decoration_allowed:
self._AppendParentSuffixTagTextWithNamespace( first_row_of_texts_with_namespaces )
@ -338,7 +338,7 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
if with_counts:
return ''.join( ( text for ( text, namespace ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, False )[0] ) )
return ''.join( ( text for ( text, namespace ) in self.GetRowsOfPresentationTextsWithNamespaces( False, False, False, False )[0] ) )
else:
@ -353,7 +353,7 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
return [ ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_TAG, value = self._tag ) ]
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
# this should be with counts or whatever, but we need to think about this more lad
@ -412,11 +412,11 @@ class ListBoxItemTextTagWithCounts( ListBoxItemTextTag ):
if self._parent_tags is not None:
if child_rows_allowed:
if show_parent_rows:
self._AppendParentsTextWithNamespaces( rows_of_texts_with_namespaces, render_for_user )
elif sibling_decoration_allowed:
elif parent_decoration_allowed:
self._AppendParentSuffixTagTextWithNamespace( first_row_of_texts_with_namespaces )
@ -473,9 +473,9 @@ class ListBoxItemPredicate( ListBoxItem ):
def GetRowCount( self, child_rows_allowed: bool ):
def GetRowCount( self, show_parent_rows: bool ):
if child_rows_allowed:
if show_parent_rows:
return 1 + len( self._predicate.GetParentPredicates() )
@ -485,7 +485,7 @@ class ListBoxItemPredicate( ListBoxItem ):
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, child_rows_allowed: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
def GetRowsOfPresentationTextsWithNamespaces( self, render_for_user: bool, sibling_decoration_allowed: bool, parent_decoration_allowed: bool, show_parent_rows: bool ) -> typing.List[ typing.List[ typing.Tuple[ str, str ] ] ]:
rows_of_texts_and_namespaces = []
@ -508,14 +508,14 @@ class ListBoxItemPredicate( ListBoxItem ):
if len( parent_preds ) > 0:
if child_rows_allowed:
if show_parent_rows:
for parent_pred in self._predicate.GetParentPredicates():
rows_of_texts_and_namespaces.append( parent_pred.GetTextsAndNamespaces( render_for_user ) )
elif sibling_decoration_allowed:
elif parent_decoration_allowed:
parents_text = ' ({} parents)'.format( HydrusData.ToHumanInt( len( parent_preds ) ) )

View File

@ -27,7 +27,6 @@ from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUIParsing
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIFileSeedCache
from hydrus.client.gui import ClientGUIGallerySeedLog
@ -45,6 +44,7 @@ from hydrus.client.gui.networking import ClientGUIHydrusNetwork
from hydrus.client.gui.networking import ClientGUINetworkJobControl
from hydrus.client.gui.pages import ClientGUIResults
from hydrus.client.gui.pages import ClientGUIResultsSortCollect
from hydrus.client.gui.parsing import ClientGUIParsingFormulae
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIControls
@ -975,6 +975,8 @@ class ManagementPanel( QW.QScrollArea ):
self._media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self, management_controller = self._management_controller, silent = silent_collect )
self._media_collect.ListenForNewOptions()
if not self.SHOW_COLLECT:
self._media_collect.hide()
@ -2206,7 +2208,9 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
( importer, ) = selected_importers
single_selected_presentation_import_options = importer.GetFileImportOptions().GetPresentationImportOptions()
fio = importer.GetFileImportOptions()
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
AddPresentationSubmenu( menu, 'downloader', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
@ -2757,7 +2761,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
self._watcher_url_input.setPlaceholderText( 'watcher url' )
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._watchers_panel, checker_options, self._OptionsUpdated )
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._watchers_panel, checker_options )
show_downloader_options = True
allow_default_selection = True
@ -2811,6 +2815,8 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
self._import_options_button.noteImportOptionsChanged.connect( self._OptionsUpdated )
self._import_options_button.tagImportOptionsChanged.connect( self._OptionsUpdated )
self._checker_options.valueChanged.connect( self._OptionsUpdated )
def _AddURLs( self, urls, filterable_tags = None, additional_service_keys_to_tags = None ):
@ -3076,7 +3082,9 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
( watcher, ) = selected_watchers
single_selected_presentation_import_options = watcher.GetFileImportOptions().GetPresentationImportOptions()
fio = watcher.GetFileImportOptions()
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
AddPresentationSubmenu( menu, 'watcher', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
@ -3780,7 +3788,7 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
formula = simple_downloader_formula.GetFormula()
control = ClientGUIParsing.EditFormulaPanel( panel, formula, lambda: ClientParsing.ParsingTestData( {}, ( '', ) ) )
control = ClientGUIParsingFormulae.EditFormulaPanel( panel, formula, lambda: ClientParsing.ParsingTestData( {}, ( '', ) ) )
panel.SetControl( control )

View File

@ -1559,7 +1559,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
page_name = HydrusText.ElideText( full_page_name, max_page_name_chars )
do_tooltip = len( page_name ) != len( full_page_name )
do_tooltip = len( page_name ) != len( full_page_name ) or HG.client_controller.new_options.GetBoolean( 'elide_page_tab_names' )
num_string = ''

View File

@ -4933,6 +4933,7 @@ class Thumbnail( Selectable ):
#
# EDIT 2: I think it may only look weird when the thumb banner has opacity. Maybe I need to learn about CompositionModes
#
# EDIT 3: Appalently Qt 6.4.0 may fix the basic 100% UI scale QImage init bug!
painter = QG.QPainter( qt_image )
@ -4973,6 +4974,7 @@ class Thumbnail( Selectable ):
f = QG.QFont( HG.client_controller.gui.font() )
# this line magically fixes the bad text, as above
f.setStyleStrategy( QG.QFont.PreferAntialias )
painter.setFont( f )

View File

@ -5,6 +5,7 @@ from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
@ -21,6 +22,258 @@ from hydrus.client.gui.widgets import ClientGUIMenuButton
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientTags
# wew lad
# https://stackoverflow.com/questions/46456238/checkbox-not-visible-inside-combobox
class CheckBoxDelegate( QW.QStyledItemDelegate ):
def __init__( self, parent = None ):
super( CheckBoxDelegate, self ).__init__( parent )
def createEditor( self, parent, op, idx ):
self.editor = QW.QCheckBox( parent )
class CollectComboCtrl( QW.QComboBox ):
itemChanged = QC.Signal()
def __init__( self, parent, media_collect ):
QW.QComboBox.__init__( self, parent )
self.view().pressed.connect( self._HandleItemPressed )
# this was previously 'if Fusion style only', but as it works for normal styles too, it is more helpful to have it always on
self.setItemDelegate( CheckBoxDelegate() )
self.setModel( QG.QStandardItemModel( self ) )
self._InitialiseChoices()
# Trick to display custom text
self._cached_text = ''
if media_collect.DoesACollect():
QP.CallAfter( self.SetCollectByValue, media_collect )
def _InitialiseChoices( self ):
text_and_data_tuples = set()
for media_sort in HG.client_controller.new_options.GetDefaultNamespaceSorts():
namespaces = media_sort.GetNamespaces()
try:
text_and_data_tuples.update( namespaces )
except:
HydrusData.DebugPrint( 'Bad namespaces: {}'.format( namespaces ) )
HydrusData.ShowText( 'Hey, your namespace-based sorts are likely damaged. Details have been written to the log, please let hydev know!' )
text_and_data_tuples = sorted( ( ( namespace, ( 'namespace', namespace ) ) for namespace in text_and_data_tuples ) )
ratings_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
for ratings_service in ratings_services:
text_and_data_tuples.append( ( ratings_service.GetName(), ('rating', ratings_service.GetServiceKey() ) ) )
for ( text, data ) in text_and_data_tuples:
self.Append( text, data )
def paintEvent( self, e ):
painter = QW.QStylePainter( self )
painter.setPen( self.palette().color( QG.QPalette.Text ) )
opt = QW.QStyleOptionComboBox()
self.initStyleOption( opt )
opt.currentText = self._cached_text
painter.drawComplexControl( QW.QStyle.CC_ComboBox, opt )
painter.drawControl( QW.QStyle.CE_ComboBoxLabel, opt )
def GetValues( self ):
namespaces = []
rating_service_keys = []
for index in self.GetCheckedIndices():
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
if collect_type == 'namespace':
namespaces.append( collect_data )
elif collect_type == 'rating':
rating_service_keys.append( collect_data )
collect_strings = self.GetCheckedStrings()
if len( collect_strings ) > 0:
description = 'collect by ' + '-'.join( collect_strings )
else:
description = 'no collections'
return ( namespaces, rating_service_keys, description )
def hidePopup(self):
if not self.view().underMouse():
QW.QComboBox.hidePopup( self )
def SetValue( self, text ):
self._cached_text = text
self.setCurrentText( text )
def SetCollectByValue( self, media_collect ):
try:
indices_to_check = []
for index in range( self.count() ):
( collect_type, collect_data ) = self.itemData( index, QC.Qt.UserRole )
p1 = collect_type == 'namespace' and collect_data in media_collect.namespaces
p2 = collect_type == 'rating' and collect_data in media_collect.rating_service_keys
if p1 or p2:
indices_to_check.append( index )
self.SetCheckedIndices( indices_to_check )
self.itemChanged.emit()
except Exception as e:
HydrusData.ShowText( 'Failed to set a collect-by value!' )
HydrusData.ShowException( e )
def SetCheckedIndices( self, indices_to_check ):
for idx in range( self.count() ):
item = self.model().item( idx )
if idx in indices_to_check:
item.setCheckState( QC.Qt.Checked )
else:
item.setCheckState( QC.Qt.Unchecked )
def GetCheckedIndices( self ):
indices = []
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked:
indices.append( idx )
return indices
def GetCheckedStrings( self ):
strings = [ ]
for idx in range( self.count() ):
item = self.model().item( idx )
if item.checkState() == QC.Qt.Checked:
strings.append( item.text() )
return strings
def Append( self, str, data ):
self.addItem( str, userData = data )
item = self.model().item( self.count() - 1, 0 )
item.setCheckState( QC.Qt.Unchecked )
def ReinitialiseChoices( self ):
self.clear()
self._InitialiseChoices()
def _HandleItemPressed( self, index ):
item = self.model().itemFromIndex( index )
if item.checkState() == QC.Qt.Checked:
item.setCheckState( QC.Qt.Unchecked )
else:
item.setCheckState( QC.Qt.Checked )
self.SetValue( self._cached_text )
self.itemChanged.emit()
class MediaCollectControl( QW.QWidget ):
def __init__( self, parent, management_controller = None, silent = False ):
@ -42,7 +295,7 @@ class MediaCollectControl( QW.QWidget ):
self._silent = silent
self._collect_comboctrl = QP.CollectComboCtrl( self, self._media_collect )
self._collect_comboctrl = CollectComboCtrl( self, self._media_collect )
choice_tuples = [
( 'collect unmatched', True ),
@ -146,12 +399,26 @@ class MediaCollectControl( QW.QWidget ):
return self._media_collect
def ListenForNewOptions( self ):
HG.client_controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
def NotifyAdvancedMode( self ):
self._UpdateButtonsVisible()
def SetCollect( self, media_collect: ClientMedia.MediaCollect ):
def NotifyNewOptions( self ):
media_collect = self._media_collect.Duplicate()
self._collect_comboctrl.ReinitialiseChoices()
self.SetCollect( media_collect, do_broadcast = False )
def SetCollect( self, media_collect: ClientMedia.MediaCollect, do_broadcast = True ):
self._media_collect = media_collect
@ -166,7 +433,10 @@ class MediaCollectControl( QW.QWidget ):
self._collect_comboctrl.blockSignals( False )
self._collect_unmatched.blockSignals( False )
self._BroadcastCollect()
if do_broadcast:
self._BroadcastCollect()
def SetCollectFromPage( self, page_key, media_collect ):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,744 @@
import json
import os
import sys
import traceback
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 HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusFileHandling
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTemp
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientParsing
from hydrus.client import ClientStrings
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIStringControls
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.networking import ClientNetworkingJobs
class TestPanel( QW.QWidget ):
def __init__( self, parent, object_callable, test_data: typing.Optional[ ClientParsing.ParsingTestData ] = None ):
QW.QWidget.__init__( self, parent )
if test_data is None:
test_data = ClientParsing.ParsingTestData( {}, ( '', ) )
self._collapse_newlines = True
self._object_callable = object_callable
self._example_parsing_context = ClientGUIStringControls.StringToStringDictButton( self, 'edit example parsing context' )
self._data_preview_notebook = QW.QTabWidget( self )
raw_data_panel = QW.QWidget( self._data_preview_notebook )
self._example_data_raw_description = ClientGUICommon.BetterStaticText( raw_data_panel )
self._copy_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().copy, self._Copy )
self._copy_button.setToolTip( 'Copy the current example data to the clipboard.' )
self._fetch_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().link, self._FetchFromURL )
self._fetch_button.setToolTip( 'Fetch data from a URL.' )
self._paste_button = ClientGUICommon.BetterBitmapButton( raw_data_panel, CC.global_pixmaps().paste, self._Paste )
self._paste_button.setToolTip( 'Paste the current clipboard data into here.' )
self._example_data_raw_preview = QW.QPlainTextEdit( raw_data_panel )
self._example_data_raw_preview.setReadOnly( True )
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._example_data_raw_preview, ( 60, 9 ) )
self._example_data_raw_preview.setMinimumWidth( width )
self._example_data_raw_preview.setMinimumHeight( height )
self._test_parse = ClientGUICommon.BetterButton( self, 'test parse', self.TestParse )
self._results = QW.QPlainTextEdit( self )
( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._results, ( 80, 12 ) )
self._results.setMinimumWidth( width )
self._results.setMinimumHeight( height )
#
self._example_parsing_context.SetValue( test_data.parsing_context )
self._example_data_raw = ''
self._results.setPlainText( 'Successfully parsed results will be printed here.' )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._example_data_raw_description, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._copy_button, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._fetch_button, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._example_data_raw_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
raw_data_panel.setLayout( vbox )
self._data_preview_notebook.addTab( raw_data_panel, 'raw data' )
self._data_preview_notebook.setCurrentWidget( raw_data_panel )
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._example_parsing_context, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._data_preview_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( vbox, self._test_parse, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._results, CC.FLAGS_EXPAND_BOTH_WAYS )
self.setLayout( vbox )
if len( test_data.texts ) > 0:
QP.CallAfter( self._SetExampleData, test_data.texts[0] )
def _Copy( self ):
HG.client_controller.pub( 'clipboard', 'text', self._example_data_raw )
def _FetchFromURL( self ):
def qt_code( example_data, example_bytes ):
if not self or not QP.isValid( self ):
return
example_parsing_context = self._example_parsing_context.GetValue()
example_parsing_context[ 'url' ] = url
example_parsing_context[ 'post_index' ] = '0'
self._example_parsing_context.SetValue( example_parsing_context )
self._SetExampleData( example_data, example_bytes = example_bytes )
def do_it( url ):
network_job = ClientNetworkingJobs.NetworkJob( 'GET', url )
network_job.OverrideBandwidth()
HG.client_controller.network_engine.AddJob( network_job )
example_bytes = None
try:
network_job.WaitUntilDone()
example_data = network_job.GetContentText()
example_bytes = network_job.GetContentBytes()
except HydrusExceptions.CancelledException:
example_data = 'fetch cancelled'
except Exception as e:
example_data = 'fetch failed:' + os.linesep * 2 + str( e )
HydrusData.ShowException( e )
QP.CallAfter( qt_code, example_data, example_bytes )
message = 'Enter URL to fetch data for.'
with ClientGUIDialogs.DialogTextEntry( self, message, placeholder = 'enter url', allow_blank = False) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
url = dlg.GetValue()
HG.client_controller.CallToThread( do_it, url )
def _Paste( self ):
try:
raw_text = HG.client_controller.GetClipboardText()
try:
raw_bytes = raw_text.decode( 'utf-8' )
except:
raw_bytes = None
except HydrusExceptions.DataMissing as e:
QW.QMessageBox.critical( self, 'Error', str(e) )
return
self._SetExampleData( raw_text, example_bytes = raw_bytes )
def _SetExampleData( self, example_data, example_bytes = None ):
self._example_data_raw = example_data
test_parse_ok = True
looked_like_json = False
MAX_CHARS_IN_PREVIEW = 1024 * 64
if len( example_data ) > 0:
good_type_found = True
if HydrusText.LooksLikeJSON( example_data ):
# prioritise this, so if the JSON contains some HTML, it'll overwrite here. decent compromise
looked_like_json = True
parse_phrase = 'looks like JSON'
elif HydrusText.LooksLikeHTML( example_data ):
# can't just throw this at bs4 to see if it 'works', as it'll just wrap any unparsable string in some bare <html><body><p> tags
parse_phrase = 'looks like HTML'
else:
good_type_found = False
if example_bytes is not None:
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath()
try:
with open( temp_path, 'wb' ) as f:
f.write( example_bytes )
mime = HydrusFileHandling.GetMime( temp_path )
except:
mime = HC.APPLICATION_UNKNOWN
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
else:
mime = HC.APPLICATION_UNKNOWN
if good_type_found:
description = HydrusData.ToHumanBytes( len( example_data ) ) + ' total, ' + parse_phrase
example_data_to_show = example_data
if looked_like_json:
try:
j = HG.client_controller.parsing_cache.GetJSON( example_data )
example_data_to_show = json.dumps( j, indent = 4 )
except:
pass
if len( example_data_to_show ) > MAX_CHARS_IN_PREVIEW:
preview = 'PREVIEW:' + os.linesep + str( example_data_to_show[:MAX_CHARS_IN_PREVIEW] )
else:
preview = example_data_to_show
else:
if mime in HC.ALLOWED_MIMES:
description = 'that looked like a {}!'.format( HC.mime_string_lookup[ mime ] )
preview = 'no preview'
test_parse_ok = False
else:
description = 'that did not look like HTML or JSON, but will try to show it anyway'
if len( example_data ) > MAX_CHARS_IN_PREVIEW:
preview = 'PREVIEW:' + os.linesep + repr( example_data[:MAX_CHARS_IN_PREVIEW] )
else:
preview = repr( example_data )
else:
description = 'no example data set yet'
preview = ''
test_parse_ok = False
self._test_parse.setEnabled( test_parse_ok )
self._example_data_raw_description.setText( description )
self._example_data_raw_preview.setPlainText( preview )
def GetExampleParsingContext( self ):
return self._example_parsing_context.GetValue()
def GetTestData( self ):
example_parsing_context = self._example_parsing_context.GetValue()
return ClientParsing.ParsingTestData( example_parsing_context, ( self._example_data_raw, ) )
def GetTestDataForChild( self ):
return self.GetTestData()
def SetCollapseNewlines( self, value: bool ):
self._collapse_newlines = value
def SetExampleData( self, example_data, example_bytes = None ):
self._SetExampleData( example_data, example_bytes = example_bytes )
def SetExampleParsingContext( self, example_parsing_context ):
self._example_parsing_context.SetValue( example_parsing_context )
def TestParse( self ):
obj = self._object_callable()
test_data = self.GetTestData()
test_text = ''
# change this to be for every text, do a diff panel, whatever
if len( test_data.texts ) > 0:
test_text = test_data.texts[0]
try:
if 'post_index' in test_data.parsing_context:
del test_data.parsing_context[ 'post_index' ]
if isinstance( obj, ClientParsing.ParseFormula ):
results_text = obj.ParsePretty( test_data.parsing_context, test_data.texts[0], self._collapse_newlines )
else:
results_text = obj.ParsePretty( test_data.parsing_context, test_data.texts[0] )
self._results.setPlainText( results_text )
except Exception as e:
etype = type( e )
( etype, value, tb ) = sys.exc_info()
trace = ''.join( traceback.format_exception( etype, value, tb ) )
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
self._results.setPlainText( message )
class TestPanelFormula( TestPanel ):
def GetTestDataForStringProcessor( self ):
example_parsing_context = self._example_parsing_context.GetValue()
formula = self._object_callable()
try:
formula.SetStringProcessor( ClientStrings.StringProcessor() )
texts = formula.Parse( example_parsing_context, self._example_data_raw, self._collapse_newlines )
except:
texts = [ '' ]
return ClientParsing.ParsingTestData( example_parsing_context, texts )
class TestPanelPageParser( TestPanel ):
def __init__( self, parent, object_callable, pre_parsing_converter_callable, test_data = None ):
self._pre_parsing_converter_callable = pre_parsing_converter_callable
TestPanel.__init__( self, parent, object_callable, test_data = test_data )
post_conversion_panel = QW.QWidget( self._data_preview_notebook )
self._example_data_post_conversion_description = ClientGUICommon.BetterStaticText( post_conversion_panel )
self._copy_button_post_conversion = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().copy, self._CopyPostConversion )
self._copy_button_post_conversion.setToolTip( 'Copy the current post conversion data to the clipboard.' )
self._refresh_post_conversion_button = ClientGUICommon.BetterBitmapButton( post_conversion_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
self._example_data_post_conversion_preview = QW.QPlainTextEdit( post_conversion_panel )
self._example_data_post_conversion_preview.setReadOnly( True )
#
self._example_data_post_conversion = ''
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._example_data_post_conversion_description, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._copy_button_post_conversion, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._refresh_post_conversion_button, CC.FLAGS_CENTER_PERPENDICULAR )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._example_data_post_conversion_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
post_conversion_panel.setLayout( vbox )
#
self._data_preview_notebook.addTab( post_conversion_panel, 'post pre-parsing conversion' )
def _CopyPostConversion( self ):
HG.client_controller.pub( 'clipboard', 'text', self._example_data_post_conversion )
def _RefreshDataPreviews( self ):
self._SetExampleData( self._example_data_raw )
def _SetExampleData( self, example_data, example_bytes = None ):
TestPanel._SetExampleData( self, example_data, example_bytes = example_bytes )
pre_parsing_converter = self._pre_parsing_converter_callable()
if pre_parsing_converter.MakesChanges():
try:
post_conversion_example_data = ClientParsing.MakeParsedTextPretty( pre_parsing_converter.Convert( self._example_data_raw ) )
if len( post_conversion_example_data ) > 1024:
preview = 'PREVIEW:' + os.linesep + str( post_conversion_example_data[:1024] )
else:
preview = post_conversion_example_data
parse_phrase = 'uncertain data type'
# can't just throw this at bs4 to see if it 'works', as it'll just wrap any unparsable string in some bare <html><body><p> tags
if HydrusText.LooksLikeHTML( post_conversion_example_data ):
parse_phrase = 'looks like HTML'
# put this second, so if the JSON contains some HTML, it'll overwrite here. decent compromise
if HydrusText.LooksLikeJSON( example_data ):
parse_phrase = 'looks like JSON'
description = HydrusData.ToHumanBytes( len( post_conversion_example_data ) ) + ' total, ' + parse_phrase
except Exception as e:
post_conversion_example_data = self._example_data_raw
etype = type( e )
( etype, value, tb ) = sys.exc_info()
trace = ''.join( traceback.format_exception( etype, value, tb ) )
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
preview = message
description = 'Could not convert.'
else:
post_conversion_example_data = self._example_data_raw
preview = 'No changes made.'
description = self._example_data_raw_description.text()
self._example_data_post_conversion_description.setText( description )
self._example_data_post_conversion = post_conversion_example_data
self._example_data_post_conversion_preview.setPlainText( preview )
def GetTestDataForChild( self ):
example_parsing_context = self._example_parsing_context.GetValue()
return ClientParsing.ParsingTestData( example_parsing_context, ( self._example_data_post_conversion, ) )
class TestPanelPageParserSubsidiary( TestPanelPageParser ):
def __init__( self, parent, object_callable, pre_parsing_converter_callable, formula_callable, test_data = None ):
TestPanelPageParser.__init__( self, parent, object_callable, pre_parsing_converter_callable, test_data = test_data )
self._formula_callable = formula_callable
post_separation_panel = QW.QWidget( self._data_preview_notebook )
self._example_data_post_separation_description = ClientGUICommon.BetterStaticText( post_separation_panel )
self._copy_button_post_separation = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().copy, self._CopyPostSeparation )
self._copy_button_post_separation.setToolTip( 'Copy the current post separation data to the clipboard.' )
self._refresh_post_separation_button = ClientGUICommon.BetterBitmapButton( post_separation_panel, CC.global_pixmaps().refresh, self._RefreshDataPreviews )
self._example_data_post_separation_preview = QW.QPlainTextEdit( post_separation_panel )
self._example_data_post_separation_preview.setReadOnly( True )
#
self._example_data_post_separation = []
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._example_data_post_separation_description, CC.FLAGS_EXPAND_BOTH_WAYS )
QP.AddToLayout( hbox, self._copy_button_post_separation, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._refresh_post_separation_button, CC.FLAGS_CENTER_PERPENDICULAR )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._example_data_post_separation_preview, CC.FLAGS_EXPAND_BOTH_WAYS )
post_separation_panel.setLayout( vbox )
#
self._data_preview_notebook.addTab( post_separation_panel, 'post separation' )
def _CopyPostSeparation( self ):
joiner = os.linesep * 2
HG.client_controller.pub( 'clipboard', 'text', joiner.join( self._example_data_post_separation ) )
def _SetExampleData( self, example_data, example_bytes = None ):
TestPanelPageParser._SetExampleData( self, example_data, example_bytes = example_bytes )
formula = self._formula_callable()
if formula is None:
separation_example_data = []
description = 'No formula set!'
preview = ''
else:
try:
example_parsing_context = self._example_parsing_context.GetValue()
separation_example_data = formula.Parse( example_parsing_context, self._example_data_post_conversion, self._collapse_newlines )
joiner = os.linesep * 2
preview = joiner.join( separation_example_data )
if len( preview ) > 1024:
preview = 'PREVIEW:' + os.linesep + str( preview[:1024] )
description = HydrusData.ToHumanInt( len( separation_example_data ) ) + ' subsidiary posts parsed'
except Exception as e:
separation_example_data = []
etype = type( e )
( etype, value, tb ) = sys.exc_info()
trace = ''.join( traceback.format_exception( etype, value, tb ) )
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
preview = message
description = 'Could not convert.'
self._example_data_post_separation_description.setText( description )
self._example_data_post_separation = separation_example_data
self._example_data_post_separation_preview.setPlainText( preview )
def GetTestDataForChild( self ):
example_parsing_context = self._example_parsing_context.GetValue()
return ClientParsing.ParsingTestData( example_parsing_context, list( self._example_data_post_separation ) )
def TestParse( self ):
formula = self._formula_callable()
page_parser = self._object_callable()
try:
test_data = self.GetTestData()
test_data.parsing_context[ 'post_index' ] = 0
if formula is None:
posts = test_data.texts
else:
posts = []
collapse_newlines = False
for test_text in test_data.texts:
posts.extend( formula.Parse( test_data.parsing_context, test_text, collapse_newlines ) )
pretty_texts = []
for post in posts:
pretty_text = page_parser.ParsePretty( test_data.parsing_context, post )
pretty_texts.append( pretty_text )
separator = os.linesep * 2
end_pretty_text = separator.join( pretty_texts )
self._results.setPlainText( end_pretty_text )
except Exception as e:
etype = type( e )
( etype, value, tb ) = sys.exc_info()
trace = ''.join( traceback.format_exception( etype, value, tb ) )
message = 'Exception:' + os.linesep + str( etype.__name__ ) + ': ' + str( e ) + os.linesep + trace
self._results.setPlainText( message )

View File

@ -0,0 +1 @@

View File

@ -2618,7 +2618,9 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
favs_list = ListBoxTagsStringsAC( self._dropdown_notebook, self.BroadcastChoices, self._display_tag_service_key, self._float_mode, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE, height_num_chars = height_num_chars )
favs_list.SetChildRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
favs_list.SetExtraParentRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
favs_list.SetParentDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
favs_list.SetSiblingDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
return favs_list
@ -2629,7 +2631,9 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
preds_list = ListBoxTagsPredicatesAC( self._dropdown_notebook, self.BroadcastChoices, self._display_tag_service_key, self._float_mode, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE, height_num_chars = height_num_chars )
preds_list.SetChildRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
preds_list.SetExtraParentRowsAllowed( HG.client_controller.new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
preds_list.SetParentDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
preds_list.SetSiblingDecoratorsAllowed( HG.client_controller.new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
return preds_list

View File

@ -1245,7 +1245,8 @@ class EditServiceRatingsSubPanel( ClientGUICommon.StaticBox ):
self._shape.addItem( 'circle', ClientRatings.CIRCLE )
self._shape.addItem( 'square', ClientRatings.SQUARE )
self._shape.addItem( 'star', ClientRatings.STAR )
self._shape.addItem( 'fat star', ClientRatings.FAT_STAR )
self._shape.addItem( 'pentagram star', ClientRatings.PENTAGRAM_STAR )
self._colour_ctrls = {}
@ -1287,10 +1288,22 @@ class EditServiceRatingsSubPanel( ClientGUICommon.StaticBox ):
QP.AddToLayout( hbox, border_ctrl, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, fill_ctrl, CC.FLAGS_CENTER_PERPENDICULAR )
if colour_type == ClientRatings.LIKE: colour_text = 'liked'
elif colour_type == ClientRatings.DISLIKE: colour_text = 'disliked'
elif colour_type == ClientRatings.NULL: colour_text = 'not rated'
elif colour_type == ClientRatings.MIXED: colour_text = 'a mixture of ratings'
if colour_type == ClientRatings.LIKE:
colour_text = 'liked'
elif colour_type == ClientRatings.DISLIKE:
colour_text = 'disliked'
elif colour_type == ClientRatings.NULL:
colour_text = 'not rated'
else:
colour_text = 'a mixture of ratings'
rows.append( ( 'border/fill for ' + colour_text + ': ', hbox ) )

View File

@ -312,10 +312,10 @@ class SimpleDownloaderImport( HydrusSerialisable.SerialisableBase ):
parsing_context[ 'url' ] = url
parsing_formula = simple_downloader_formula.GetFormula()
collapse_newlines = True
file_seeds = []
for parsed_text in parsing_formula.Parse( parsing_context, parsing_text ):
for parsed_text in parsing_formula.Parse( parsing_context, parsing_text, collapse_newlines ):
try:

View File

@ -9,7 +9,8 @@ MIXED = 4
CIRCLE = 0
SQUARE = 1
STAR = 2
FAT_STAR = 2
PENTAGRAM_STAR = 3
def GetLikeStateFromMedia( media, service_key ):
@ -118,7 +119,7 @@ def GetShape( service_key ):
except HydrusExceptions.DataMissing:
shape = STAR
shape = FAT_STAR
return shape

View File

@ -57,7 +57,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'file_service_key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name', 'reason' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'only_return_basic_information', 'create_new_file_ids', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@ -1911,6 +1911,8 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientA
url = request.parsed_request_args.GetValue( 'url', str )
do_file_system_check = request.parsed_request_args.GetValue( 'doublecheck_file_system', bool, default_value = False )
if url == '':
raise HydrusExceptions.BadRequestException( 'Given URL was empty!' )
@ -1931,7 +1933,10 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientA
for file_import_status in url_statuses:
file_import_status = ClientImportFiles.CheckFileImportStatus( file_import_status )
if do_file_system_check:
file_import_status = ClientImportFiles.CheckFileImportStatus( file_import_status )
d = {}

View File

@ -1996,7 +1996,14 @@ def CheckHydrusVersion( service_type, response ):
raise HydrusExceptions.WrongServiceTypeException( 'Target was not a ' + service_string + '!' )
( service_string_gumpf, network_version ) = server_header.split( '/' )
# might be "hydrus tag repository/17" or "hydrus tag repository/17 (498)" kind of thing
( service_string_gumpf, network_version ) = server_header.split( '/', 1 )
if ' ' in network_version:
( network_version, software_version_gumpf ) = network_version.split( ' ', 1 )
network_version = int( network_version )

View File

@ -765,7 +765,9 @@ class NetworkLoginManager( HydrusSerialisable.SerialisableBase ):
formula = ClientParsing.ParseFormulaHTML( tag_rules = [ ClientParsing.ParseRuleHTML( rule_type = ClientParsing.HTML_RULE_TYPE_DESCENDING, tag_name = 'meta', tag_attributes = { 'id' : 'tumblr_form_key' } ) ], content_to_fetch = ClientParsing.HTML_CONTENT_ATTRIBUTE, attribute_to_fetch = "content" )
results = formula.Parse( {}, html )
collapse_newlines = True
results = formula.Parse( {}, html, collapse_newlines )
if len( results ) != 1:

View File

@ -80,8 +80,8 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 496
CLIENT_API_VERSION = 31
SOFTWARE_VERSION = 497
CLIENT_API_VERSION = 32
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -21,10 +21,34 @@ re_leading_space_or_garbage = re.compile( r'^(\s|-|system:)+' )
re_leading_single_colon = re.compile( '^:(?!:)' )
re_leading_byte_order_mark = re.compile( '^\ufeff' ) # unicode .txt files prepend with this, wew
HYDRUS_NOTE_NEWLINE = '\n'
def CleanNoteText( t: str ):
# trim leading and trailing whitespace
t = t.strip()
# wash all newlines to be os.linesep
lines = t.splitlines()
# now trim each line
lines = [ line.strip() for line in lines ]
t = HYDRUS_NOTE_NEWLINE.join( lines )
# now replace big gaps with reasonable ones
double_newline = HYDRUS_NOTE_NEWLINE * 2
triple_newline = HYDRUS_NOTE_NEWLINE * 3
while triple_newline in t:
t = t.replace( triple_newline, double_newline )
return t

View File

@ -23,7 +23,14 @@ class HydrusService( Site ):
service_type = self._service.GetServiceType()
self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION )
if service_type == HC.CLIENT_API_SERVICE:
self._server_version_string = '{}/{} ({})'.format( HC.service_string_lookup[ service_type ], str( HC.CLIENT_API_VERSION ), str( HC.SOFTWARE_VERSION ) )
else:
self._server_version_string = '{}/{}'.format( HC.service_string_lookup[ service_type ], str( HC.NETWORK_VERSION ) )
root = self._InitRoot()
@ -63,4 +70,3 @@ class HydrusService( Site ):
request.setHeader( 'Hydrus-Server', self._server_version_string )
return Site.getResourceFor( self, request )

View File

@ -432,10 +432,6 @@ class HydrusResource( Resource ):
self._service_key = self._service.GetServiceKey()
self._domain = domain
service_type = self._service.GetServiceType()
self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION )
def _callbackCheckAccountRestrictions( self, request: HydrusServerRequest.HydrusRequest ):
@ -733,6 +729,11 @@ class HydrusResource( Resource ):
if client == 'hydrus':
if ' ' in network_version:
( network_version, software_version_gumpf ) = network_version.split( ' ', 1 )
request.is_hydrus_user_agent = True
network_version = int( network_version )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB