Merge branch 'develop'

This commit is contained in:
Hydrus Network Developer 2024-04-24 15:40:44 -05:00
commit c63273e363
No known key found for this signature in database
GPG Key ID: 76249F053212133C
47 changed files with 1759 additions and 720 deletions

View File

@ -7,6 +7,43 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 572](https://github.com/hydrusnetwork/hydrus/releases/tag/v572)
### misc
* added a new checkbox to _options->files and trash_ to say 'include skipped files when you remove files after archive/delete'
* thanks to a user, we now have an 'e621' stylsheet in _options->style_. this is the first default stylesheet that uses assets (some checkbox etc.. svgs), which means some users--I think just those who run from source--will need to be careful that their CWD is the hydrus install dir when they boot, or this won't load properly! if you try it and get errors in your log as it tries to load the svgs, let me know!
### share menu
* like the 'open' menu a couple weeks ago, the 'share' menu off of thumbnails or the media viewer is rewritten to nicer code. no major differences, but it has a clearer, universal layout, provides more options for 'the currently focused file' vs 'all selected files', is more careful about only providing commands it can deliver on (e.g. no file copy for remote files), and now everything it does is mappable in the shortcut system under the 'media' shortcut set
* you can now copy a file's thumbnail as a bitmap from this menu!
* the canvas now supports 'export files'. the 'export files' window just pops on top of it with the one file
* 'copy file id' is no longer hidden by advanced mode--go nuts!
* the share menu no longer has 'share on local booru'. the local booru service was an interesting experiment, but I could never find time to properly dev it and there are better answers with the Client API or simple third-party image hosting services that you can drag and drop to. thus, I am finally sunsetting it. I'll strip away its features over the coming weeks until it is completely removed
### shortcut updates
* the 'copy file hash' shortcut actions, which used to be four separate things, have been collapsed to one action that has a 'hash type' dropdown (and a 'target' dropdown to select either all selected files or just the currently focused file, which will default to 'all selected' on update, which was the previous behaviour). you can also now set 'pixel_hash' or 'blurhash' as the hash type
* the 'copy file bitmap' shortcuts have similarly been collapsed down to one action with a dropdown, also with the new 'copy thumbnail' command
* the 'copy files', 'copy file paths', and 'copy file id' shortcuts now have a dropdown for whether you want all selected files or just the currently focused file. updated commands will default to 'all selected', which was the previous behaviour
* added a 'copy ipfs multihash' shortcut action, which has this new 'focused vs all selected' parameter and the ipfs service to copy from as its options
### boring code cleanup
* wrote a new command for copying arbitrary file hashes, with a new 'file command target'
* simplified the media hash copying code
* wrote a new command for copying arbitrary bitmap types
* combined the bitmap copying code into one shared function call and simplified the surrounding code
* combined the file and path copying code into shared functions, simplified the code, and added tech for focused vs all selected targeting
* and the same thing for copying ipfs multihashes
* wrote a routine to copy a file's thumbnail in the normal clipboard copying pubsub
* with the recent rounds of simplication, the core thumbnail menu call is now but a mere 600 lines of spaghetti code
* misc renaming of some enums here so they are more in agreement ('xxx files' instead of 'xxx file', etc...)
* renamed the various simple commands I have replaced in the past few weeks as 'legacy', so we don't accidentally refer to them again in real code
* the unit test for 'dateparser decode' is no longer run if dateparser is not in the environment
* fixed the file metadata parsing unit tests to account for newer ffmpeg, which sees a -10ms different duration on one of the test files, and made the various tests +/-20% lenient to handle this stuff if it comes up again in future
## [Version 571](https://github.com/hydrusnetwork/hydrus/releases/tag/v571)
### clean install

View File

@ -34,6 +34,38 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_572"><a href="#version_572">version 572</a></h2>
<ul>
<li><h3>misc</h3></li>
<li>added a new checkbox to _options-&gt;files and trash_ to say 'include skipped files when you remove files after archive/delete'</li>
<li>thanks to a user, we now have an 'e621' stylsheet in _options-&gt;style_. this is the first default stylesheet that uses assets (some checkbox etc.. svgs), which means some users--I think just those who run from source--will need to be careful that their CWD is the hydrus install dir when they boot, or this won't load properly! if you try it and get errors in your log as it tries to load the svgs, let me know!</li>
<li><h3>share menu</h3></li>
<li>like the 'open' menu a couple weeks ago, the 'share' menu off of thumbnails or the media viewer is rewritten to nicer code. no major differences, but it has a clearer, universal layout, provides more options for 'the currently focused file' vs 'all selected files', is more careful about only providing commands it can deliver on (e.g. no file copy for remote files), and now everything it does is mappable in the shortcut system under the 'media' shortcut set</li>
<li>you can now copy a file's thumbnail as a bitmap from this menu!</li>
<li>the canvas now supports 'export files'. the 'export files' window just pops on top of it with the one file</li>
<li>'copy file id' is no longer hidden by advanced mode--go nuts!</li>
<li>the share menu no longer has 'share on local booru'. the local booru service was an interesting experiment, but I could never find time to properly dev it and there are better answers with the Client API or simple third-party image hosting services that you can drag and drop to. thus, I am finally sunsetting it. I'll strip away its features over the coming weeks until it is completely removed</li>
<li><h3>shortcut updates</h3></li>
<li>the 'copy file hash' shortcut actions, which used to be four separate things, have been collapsed to one action that has a 'hash type' dropdown (and a 'target' dropdown to select either all selected files or just the currently focused file, which will default to 'all selected' on update, which was the previous behaviour). you can also now set 'pixel_hash' or 'blurhash' as the hash type</li>
<li>the 'copy file bitmap' shortcuts have similarly been collapsed down to one action with a dropdown, also with the new 'copy thumbnail' command</li>
<li>the 'copy files', 'copy file paths', and 'copy file id' shortcuts now have a dropdown for whether you want all selected files or just the currently focused file. updated commands will default to 'all selected', which was the previous behaviour</li>
<li>added a 'copy ipfs multihash' shortcut action, which has this new 'focused vs all selected' parameter and the ipfs service to copy from as its options</li>
<li><h3>boring code cleanup</h3></li>
<li>wrote a new command for copying arbitrary file hashes, with a new 'file command target'</li>
<li>simplified the media hash copying code</li>
<li>wrote a new command for copying arbitrary bitmap types</li>
<li>combined the bitmap copying code into one shared function call and simplified the surrounding code</li>
<li>combined the file and path copying code into shared functions, simplified the code, and added tech for focused vs all selected targeting</li>
<li>and the same thing for copying ipfs multihashes</li>
<li>wrote a routine to copy a file's thumbnail in the normal clipboard copying pubsub</li>
<li>with the recent rounds of simplication, the core thumbnail menu call is now but a mere 600 lines of spaghetti code</li>
<li>misc renaming of some enums here so they are more in agreement ('xxx files' instead of 'xxx file', etc...)</li>
<li>renamed the various simple commands I have replaced in the past few weeks as 'legacy', so we don't accidentally refer to them again in real code</li>
<li>the unit test for 'dateparser decode' is no longer run if dateparser is not in the environment</li>
<li>fixed the file metadata parsing unit tests to account for newer ffmpeg, which sees a -10ms different duration on one of the test files, and made the various tests +/-20% lenient to handle this stuff if it comes up again in future</li>
</ul>
</li>
<li>
<h2 id="version_571"><a href="#version_571">version 571</a></h2>
<ul>

View File

@ -17,14 +17,14 @@ SIMPLE_ARCHIVE_FILE = 4
SIMPLE_CHECK_ALL_IMPORT_FOLDERS = 5
SIMPLE_CLOSE_MEDIA_VIEWER = 6
SIMPLE_CLOSE_PAGE = 7
SIMPLE_COPY_BMP = 8
SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE = 9
SIMPLE_COPY_FILE = 10
SIMPLE_COPY_MD5_HASH = 11
SIMPLE_COPY_PATH = 12
SIMPLE_COPY_SHA1_HASH = 13
SIMPLE_COPY_SHA256_HASH = 14
SIMPLE_COPY_SHA512_HASH = 15
LEGACY_SIMPLE_COPY_BMP = 8
LEGACY_SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE = 9
SIMPLE_COPY_FILES = 10
LEGACY_SIMPLE_COPY_MD5_HASH = 11
SIMPLE_COPY_FILE_PATHS = 12
LEGACY_SIMPLE_COPY_SHA1_HASH = 13
LEGACY_SIMPLE_COPY_SHA256_HASH = 14
LEGACY_SIMPLE_COPY_SHA512_HASH = 15
SIMPLE_DELETE_FILE = 16
SIMPLE_DUPLICATE_FILTER_ALTERNATES = 17
SIMPLE_DUPLICATE_FILTER_BACK = 18
@ -46,10 +46,10 @@ SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT = 33
SIMPLE_FLIP_DARKMODE = 34
SIMPLE_FLIP_DEBUG_FORCE_IDLE_MODE_DO_NOT_SET_THIS = 35
SIMPLE_FOCUS_MEDIA_VIEWER = 36
SIMPLE_GET_SIMILAR_TO_EXACT = 37
SIMPLE_GET_SIMILAR_TO_SIMILAR = 38
SIMPLE_GET_SIMILAR_TO_SPECULATIVE = 39
SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR = 40
LEGACY_SIMPLE_GET_SIMILAR_TO_EXACT = 37
LEGACY_SIMPLE_GET_SIMILAR_TO_SIMILAR = 38
LEGACY_SIMPLE_GET_SIMILAR_TO_SPECULATIVE = 39
LEGACY_SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR = 40
SIMPLE_GLOBAL_AUDIO_MUTE = 41
SIMPLE_GLOBAL_AUDIO_MUTE_FLIP = 42
SIMPLE_GLOBAL_AUDIO_UNMUTE = 43
@ -156,7 +156,7 @@ SIMPLE_ZOOM_DEFAULT = 143
SIMPLE_SHOW_DUPLICATES = 144
SIMPLE_MANAGE_FILE_TIMESTAMPS = 145
SIMPLE_OPEN_FILE_IN_FILE_EXPLORER = 146
SIMPLE_COPY_LITTLE_BMP = 147
LEGACY_SIMPLE_COPY_LITTLE_BMP = 147
SIMPLE_MOVE_THUMBNAIL_FOCUS = 148
SIMPLE_SELECT_FILES = 149
SIMPLE_REARRANGE_THUMBNAILS = 150
@ -165,6 +165,10 @@ SIMPLE_COPY_URLS = 152
SIMPLE_OPEN_FILE_IN_WEB_BROWSER = 153
SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE = 154
SIMPLE_OPEN_SIMILAR_LOOKING_FILES = 155
SIMPLE_COPY_FILE_HASHES = 156
SIMPLE_COPY_FILE_BITMAP = 157
SIMPLE_COPY_FILE_ID = 158
SIMPLE_COPY_FILE_SERVICE_FILENAMES = 159
REARRANGE_THUMBNAILS_TYPE_FIXED = 0
REARRANGE_THUMBNAILS_TYPE_COMMAND = 1
@ -199,6 +203,26 @@ selection_status_enum_to_str_lookup = {
SELECTION_STATUS_SHIFT : 'shift-select'
}
FILE_COMMAND_TARGET_FOCUSED_FILE = 0
FILE_COMMAND_TARGET_SELECTED_FILES = 1
file_command_target_enum_to_str_lookup = {
FILE_COMMAND_TARGET_FOCUSED_FILE : 'focused file',
FILE_COMMAND_TARGET_SELECTED_FILES : 'selected files'
}
BITMAP_TYPE_FULL = 0
BITMAP_TYPE_SOURCE_LOOKUPS = 1
BITMAP_TYPE_FULL_OR_FILE = 2
BITMAP_TYPE_THUMBNAIL = 3
bitmap_type_enum_to_str_lookup = {
BITMAP_TYPE_FULL : 'full bitmap of image',
BITMAP_TYPE_SOURCE_LOOKUPS : '1024x1024 scaled for quick source lookups',
BITMAP_TYPE_FULL_OR_FILE : 'full bitmap; otherwise copy file',
BITMAP_TYPE_THUMBNAIL : 'thumbnail bitmap',
}
simple_enum_to_str_lookup = {
SIMPLE_ARCHIVE_DELETE_FILTER_BACK : 'archive/delete filter: back',
SIMPLE_ARCHIVE_DELETE_FILTER_DELETE : 'archive/delete filter: delete',
@ -208,15 +232,19 @@ simple_enum_to_str_lookup = {
SIMPLE_CHECK_ALL_IMPORT_FOLDERS : 'check all import folders now',
SIMPLE_CLOSE_MEDIA_VIEWER : 'close media viewer',
SIMPLE_CLOSE_PAGE : 'close page',
SIMPLE_COPY_BMP : 'copy bmp of image',
SIMPLE_COPY_LITTLE_BMP : 'copy small bmp of image for quick source lookups',
SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE : 'copy bmp of image; otherwise copy file',
SIMPLE_COPY_FILE : 'copy file',
SIMPLE_COPY_MD5_HASH : 'copy md5 hash',
SIMPLE_COPY_PATH : 'copy file paths',
SIMPLE_COPY_SHA1_HASH : 'copy sha1 hash',
SIMPLE_COPY_SHA256_HASH : 'copy sha256 hash',
SIMPLE_COPY_SHA512_HASH : 'copy sha512 hash',
LEGACY_SIMPLE_COPY_BMP : 'copy bmp of image',
LEGACY_SIMPLE_COPY_LITTLE_BMP : 'copy small bmp of image for quick source lookups',
LEGACY_SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE : 'copy bmp of image; otherwise copy file',
SIMPLE_COPY_FILES : 'copy file',
SIMPLE_COPY_FILE_PATHS : 'copy file paths',
LEGACY_SIMPLE_COPY_MD5_HASH : 'copy md5 hash',
LEGACY_SIMPLE_COPY_SHA1_HASH : 'copy sha1 hash',
LEGACY_SIMPLE_COPY_SHA256_HASH : 'copy sha256 hash',
LEGACY_SIMPLE_COPY_SHA512_HASH : 'copy sha512 hash',
SIMPLE_COPY_FILE_SERVICE_FILENAMES : 'copy ipfs multihash',
SIMPLE_COPY_FILE_HASHES : 'copy file hashes',
SIMPLE_COPY_FILE_ID : 'copy file id',
SIMPLE_COPY_FILE_BITMAP : 'copy file bitmap',
SIMPLE_DELETE_FILE : 'delete file',
SIMPLE_DUPLICATE_FILTER_ALTERNATES : 'duplicate filter: set as alternates',
SIMPLE_DUPLICATE_FILTER_BACK : 'duplicate filter: back',
@ -238,10 +266,10 @@ simple_enum_to_str_lookup = {
SIMPLE_FLIP_DARKMODE : 'flip darkmode (will be replaced by style/qss soon)',
SIMPLE_FLIP_DEBUG_FORCE_IDLE_MODE_DO_NOT_SET_THIS : 'force debug idle mode (do not use this!)',
SIMPLE_FOCUS_MEDIA_VIEWER : 'keyboard focus: to the media viewer',
SIMPLE_GET_SIMILAR_TO_EXACT : 'show similar files: 0 (exact)',
SIMPLE_GET_SIMILAR_TO_SIMILAR : 'show similar files: 4 (similar)',
SIMPLE_GET_SIMILAR_TO_SPECULATIVE : 'show similar files: 8 (speculative)',
SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR : 'show similar files: 2 (very similar)',
LEGACY_SIMPLE_GET_SIMILAR_TO_EXACT : 'show similar files: 0 (exact)',
LEGACY_SIMPLE_GET_SIMILAR_TO_SIMILAR : 'show similar files: 4 (similar)',
LEGACY_SIMPLE_GET_SIMILAR_TO_SPECULATIVE : 'show similar files: 8 (speculative)',
LEGACY_SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR : 'show similar files: 2 (very similar)',
SIMPLE_GLOBAL_AUDIO_MUTE : 'mute global audio',
SIMPLE_GLOBAL_AUDIO_MUTE_FLIP : 'mute/unmute global audio',
SIMPLE_GLOBAL_AUDIO_UNMUTE : 'unmute global audio',
@ -367,14 +395,14 @@ legacy_simple_str_to_enum_lookup = {
'check_all_import_folders' : SIMPLE_CHECK_ALL_IMPORT_FOLDERS,
'close_media_viewer' : SIMPLE_CLOSE_MEDIA_VIEWER,
'close_page' : SIMPLE_CLOSE_PAGE,
'copy_bmp' : SIMPLE_COPY_BMP,
'copy_bmp_or_file_if_not_bmpable' : SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE,
'copy_file' : SIMPLE_COPY_FILE,
'copy_md5_hash' : SIMPLE_COPY_MD5_HASH,
'copy_path' : SIMPLE_COPY_PATH,
'copy_sha1_hash' : SIMPLE_COPY_SHA1_HASH,
'copy_sha256_hash' : SIMPLE_COPY_SHA256_HASH,
'copy_sha512_hash' : SIMPLE_COPY_SHA512_HASH,
'copy_bmp' : LEGACY_SIMPLE_COPY_BMP,
'copy_bmp_or_file_if_not_bmpable' : LEGACY_SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE,
'copy_file' : SIMPLE_COPY_FILES,
'copy_md5_hash' : LEGACY_SIMPLE_COPY_MD5_HASH,
'copy_path' : SIMPLE_COPY_FILE_PATHS,
'copy_sha1_hash' : LEGACY_SIMPLE_COPY_SHA1_HASH,
'copy_sha256_hash' : LEGACY_SIMPLE_COPY_SHA256_HASH,
'copy_sha512_hash' : LEGACY_SIMPLE_COPY_SHA512_HASH,
'delete_file' : SIMPLE_DELETE_FILE,
'duplicate_filter_alternates' : SIMPLE_DUPLICATE_FILTER_ALTERNATES,
'duplicate_filter_back' : SIMPLE_DUPLICATE_FILTER_BACK,
@ -397,10 +425,10 @@ legacy_simple_str_to_enum_lookup = {
'flip_darkmode' : SIMPLE_FLIP_DARKMODE,
'flip_debug_force_idle_mode_do_not_set_this' : SIMPLE_FLIP_DEBUG_FORCE_IDLE_MODE_DO_NOT_SET_THIS,
'focus_media_viewer' : SIMPLE_FOCUS_MEDIA_VIEWER,
'get_similar_to_exact' : SIMPLE_GET_SIMILAR_TO_EXACT,
'get_similar_to_similar' : SIMPLE_GET_SIMILAR_TO_SIMILAR,
'get_similar_to_speculative' : SIMPLE_GET_SIMILAR_TO_SPECULATIVE,
'get_similar_to_very_similar' : SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR,
'get_similar_to_exact' : LEGACY_SIMPLE_GET_SIMILAR_TO_EXACT,
'get_similar_to_similar' : LEGACY_SIMPLE_GET_SIMILAR_TO_SIMILAR,
'get_similar_to_speculative' : LEGACY_SIMPLE_GET_SIMILAR_TO_SPECULATIVE,
'get_similar_to_very_similar' : LEGACY_SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR,
'global_audio_mute' : SIMPLE_GLOBAL_AUDIO_MUTE,
'global_audio_mute_flip' : SIMPLE_GLOBAL_AUDIO_MUTE_FLIP,
'global_audio_unmute' : SIMPLE_GLOBAL_AUDIO_UNMUTE,
@ -472,7 +500,7 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND
SERIALISABLE_NAME = 'Application Command'
SERIALISABLE_VERSION = 6
SERIALISABLE_VERSION = 7
def __init__( self, command_type = None, data = None ):
@ -501,6 +529,19 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
def __hash__( self ):
if self._command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
( simple_action, simple_data ) = self._data
# we are out here
if simple_action == SIMPLE_COPY_FILE_SERVICE_FILENAMES:
comparison_simple_data = tuple( sorted( simple_data.items() ) )
return ( self._command_type, ( simple_action, comparison_simple_data ) ).__hash__()
return ( self._command_type, self._data ).__hash__()
@ -515,6 +556,13 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
# I don't _think_ this was an overcomplicated mistake, but it is a little ugly atm
# it'll be better when all working on the same system. perhaps command_type too
# and maybe ditch the object's self._data variable too, which is crushed as anything. maybe just have a serialisable dict mate
#
# 2024-04 update: I'd also like a way to store arbitrary numbers of bytes objects! if we can store service_key(s!) or whatever without extra effort, in a SerialisableBytesDict,
# we can have all sorts of service or 'load favourite search' stuff without the headache
# So yeah I think ditch the _data thing, move more towards a SerialisableDict as our normal data-holding guy, and then we refer to that as we do jobs
# yeah if I instead move to a kwargs init for this whole object, then I can store whatever as long as it is serialisable. ditch the tuples, move to words
#
# also update __hash__ to be non-borked in this situation
if self._command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
@ -673,23 +721,23 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
simple_action = data_dict[ 'simple_action' ]
if simple_action in ( SIMPLE_GET_SIMILAR_TO_EXACT, SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR, SIMPLE_GET_SIMILAR_TO_SIMILAR, SIMPLE_GET_SIMILAR_TO_SPECULATIVE ):
if simple_action in ( LEGACY_SIMPLE_GET_SIMILAR_TO_EXACT, LEGACY_SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR, LEGACY_SIMPLE_GET_SIMILAR_TO_SIMILAR, LEGACY_SIMPLE_GET_SIMILAR_TO_SPECULATIVE ):
hamming_distance = 0
if simple_action == SIMPLE_GET_SIMILAR_TO_EXACT:
if simple_action == LEGACY_SIMPLE_GET_SIMILAR_TO_EXACT:
hamming_distance = 0
elif simple_action == SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR:
elif simple_action == LEGACY_SIMPLE_GET_SIMILAR_TO_VERY_SIMILAR:
hamming_distance = 2
elif simple_action == SIMPLE_GET_SIMILAR_TO_SIMILAR:
elif simple_action == LEGACY_SIMPLE_GET_SIMILAR_TO_SIMILAR:
hamming_distance = 4
elif simple_action == SIMPLE_GET_SIMILAR_TO_SPECULATIVE:
elif simple_action == LEGACY_SIMPLE_GET_SIMILAR_TO_SPECULATIVE:
hamming_distance = 8
@ -706,6 +754,75 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
return ( 6, new_serialisable_info )
if version == 6:
( command_type, serialisable_data ) = old_serialisable_info
if command_type == APPLICATION_COMMAND_TYPE_SIMPLE:
data_dict = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_data )
simple_action = data_dict[ 'simple_action' ]
if simple_action in ( LEGACY_SIMPLE_COPY_SHA256_HASH, LEGACY_SIMPLE_COPY_MD5_HASH, LEGACY_SIMPLE_COPY_SHA1_HASH, LEGACY_SIMPLE_COPY_SHA512_HASH ):
file_command_target = FILE_COMMAND_TARGET_SELECTED_FILES
hash_type = 'sha256'
if simple_action == LEGACY_SIMPLE_COPY_SHA256_HASH:
hash_type = 'sha256'
elif simple_action == LEGACY_SIMPLE_COPY_MD5_HASH:
hash_type = 'md5'
elif simple_action == LEGACY_SIMPLE_COPY_SHA1_HASH:
hash_type = 'sha1'
elif simple_action == LEGACY_SIMPLE_COPY_SHA512_HASH:
hash_type = 'sha512'
data_dict[ 'simple_action' ] = SIMPLE_COPY_FILE_HASHES
data_dict[ 'simple_data' ] = ( file_command_target, hash_type )
elif simple_action in ( LEGACY_SIMPLE_COPY_BMP, LEGACY_SIMPLE_COPY_LITTLE_BMP, LEGACY_SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE ):
bitmap_type = BITMAP_TYPE_FULL
if simple_action == LEGACY_SIMPLE_COPY_BMP:
bitmap_type = BITMAP_TYPE_FULL
elif simple_action == LEGACY_SIMPLE_COPY_LITTLE_BMP:
bitmap_type = BITMAP_TYPE_SOURCE_LOOKUPS
elif simple_action == LEGACY_SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE:
bitmap_type = BITMAP_TYPE_FULL_OR_FILE
data_dict[ 'simple_action' ] = SIMPLE_COPY_FILE_BITMAP
data_dict[ 'simple_data' ] = bitmap_type
elif simple_action in ( SIMPLE_COPY_FILES, SIMPLE_COPY_FILE_PATHS ):
data_dict[ 'simple_data' ] = FILE_COMMAND_TARGET_SELECTED_FILES
serialisable_data = data_dict.GetSerialisableTuple()
new_serialisable_info = ( command_type, serialisable_data )
return ( 7, new_serialisable_info )
def GetCommandType( self ):
@ -819,6 +936,50 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
s = f'{s} ({hamming_distance})'
elif action == SIMPLE_COPY_FILE_HASHES:
( file_command_target, hash_type ) = self.GetSimpleData()
s = f'{s} ({hash_type}, {file_command_target_enum_to_str_lookup[ file_command_target ]})'
elif action == SIMPLE_COPY_FILE_BITMAP:
bitmap_type = self.GetSimpleData()
s = f'{s} ({bitmap_type_enum_to_str_lookup[ bitmap_type ]})'
elif action in ( SIMPLE_COPY_FILES, SIMPLE_COPY_FILE_PATHS, SIMPLE_COPY_FILE_ID ):
file_command_target = self.GetSimpleData()
s = f'{s} ({file_command_target_enum_to_str_lookup[ file_command_target ]})'
elif action == SIMPLE_COPY_FILE_SERVICE_FILENAMES:
hacky_ipfs_dict = self.GetSimpleData()
try:
file_command_target_string = file_command_target_enum_to_str_lookup[ hacky_ipfs_dict[ 'file_command_target' ] ]
except:
file_command_target_string = 'unknown'
try:
ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ]
name = CG.client_controller.services_manager.GetName( ipfs_service_key )
except:
name = 'unknown service'
s = f'{s} ({name}, {file_command_target_string})'
elif action == SIMPLE_MOVE_THUMBNAIL_FOCUS:
( move_direction, selection_status ) = self.GetSimpleData()

View File

@ -2437,6 +2437,21 @@ class Controller( ClientControllerInterface.ClientControllerInterface, HydrusCon
self.CallToThreadLongRunning( THREADWait )
elif data_type == 'thumbnail_bmp':
media = data
if media.GetMime() not in HC.MIMES_WITH_THUMBNAILS:
return
thumbnail = self.GetCache( 'thumbnail' ).GetThumbnail( media )
qt_image = thumbnail.GetQtImage().copy()
QW.QApplication.clipboard().setImage( qt_image )
def UnclosePageKeys( self, page_keys ):

View File

@ -313,7 +313,7 @@ def GetDefaultShortcuts():
media.SetCommand(
ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_KEYBOARD_CHARACTER, ord( 'C' ), ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [ ClientGUIShortcuts.SHORTCUT_MODIFIER_CTRL ] ),
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE )
CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILES )
)
shortcuts.append( media )

View File

@ -126,6 +126,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'advanced_mode' ] = False
self._dictionary[ 'booleans' ][ 'remove_filtered_files_even_when_skipped' ] = False
self._dictionary[ 'booleans' ][ 'filter_inbox_and_archive_predicates' ] = False
self._dictionary[ 'booleans' ][ 'discord_dnd_fix' ] = False

View File

@ -848,7 +848,7 @@ class ThumbnailCache( object ):
def GetThumbnail( self, media ):
def GetThumbnail( self, media ) -> ClientRendering.HydrusBitmap:
display_media = media.GetDisplayMedia()

View File

@ -1036,6 +1036,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._only_show_delete_from_all_local_domains_when_filtering.setToolTip( ClientGUIFunctions.WrapToolTip( tt ) )
self._remove_filtered_files = QW.QCheckBox( self )
self._remove_filtered_files.setToolTip( 'This will remove all archived/deleted files from the source thumbnail page when you commit your archive/delete filter run.' )
self._remove_filtered_files_even_when_skipped = QW.QCheckBox( self )
self._remove_trashed_files = QW.QCheckBox( self )
self._remove_local_domain_moved_files = QW.QCheckBox( self )
@ -1089,6 +1092,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._only_show_delete_from_all_local_domains_when_filtering.setChecked( self._new_options.GetBoolean( 'only_show_delete_from_all_local_domains_when_filtering' ) )
self._remove_filtered_files.setChecked( HC.options[ 'remove_filtered_files' ] )
self._remove_filtered_files_even_when_skipped.setChecked( self._new_options.GetBoolean( 'remove_filtered_files_even_when_skipped' ) )
self._remove_trashed_files.setChecked( HC.options[ 'remove_trashed_files' ] )
self._remove_local_domain_moved_files.setChecked( self._new_options.GetBoolean( 'remove_local_domain_moved_files' ) )
self._trash_max_age.SetValue( HC.options[ 'trash_max_age' ] )
@ -1125,7 +1129,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'When physically deleting files or folders, send them to the OS\'s recycle bin: ', self._delete_to_recycle_bin ) )
rows.append( ( 'When maintenance physically deletes files, wait this many ms between each delete: ', self._ms_to_wait_between_physical_file_deletes ) )
rows.append( ( 'When finishing filtering, always delete from all possible domains: ', self._only_show_delete_from_all_local_domains_when_filtering ) )
rows.append( ( 'Remove files from view when they are filtered: ', self._remove_filtered_files ) )
rows.append( ( 'Remove files from view when they are archive/delete filtered: ', self._remove_filtered_files ) )
rows.append( ( '--even skipped files: ', self._remove_filtered_files_even_when_skipped ) )
rows.append( ( 'Remove files from view when they are sent to the trash: ', self._remove_trashed_files ) )
rows.append( ( 'Remove files from view when they are moved to another local file domain: ', self._remove_local_domain_moved_files ) )
rows.append( ( 'Number of hours a file can be in the trash before being deleted: ', self._trash_max_age ) )
@ -1167,6 +1172,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self.setLayout( vbox )
self._remove_filtered_files.clicked.connect( self._UpdateRemoveFiltered )
self._UpdateRemoveFiltered()
def _AddAFDR( self ):
@ -1201,6 +1210,11 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._advanced_file_deletion_reasons.setEnabled( advanced_enabled )
def _UpdateRemoveFiltered( self ):
self._remove_filtered_files_even_when_skipped.setEnabled( self._remove_filtered_files.isChecked() )
def UpdateOptions( self ):
HC.options[ 'export_path' ] = HydrusPaths.ConvertAbsPathToPortablePath( self._export_location.GetPath() )
@ -1211,6 +1225,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'confirm_trash' ] = self._confirm_trash.isChecked()
HC.options[ 'confirm_archive' ] = self._confirm_archive.isChecked()
HC.options[ 'remove_filtered_files' ] = self._remove_filtered_files.isChecked()
self._new_options.SetBoolean( 'remove_filtered_files_even_when_skipped', self._remove_filtered_files_even_when_skipped.isChecked() )
HC.options[ 'remove_trashed_files' ] = self._remove_trashed_files.isChecked()
self._new_options.SetBoolean( 'remove_local_domain_moved_files', self._remove_local_domain_moved_files.isChecked() )
HC.options[ 'trash_max_age' ] = self._trash_max_age.GetValue()
@ -3850,6 +3865,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
text = 'The current styles are what your Qt has available, the stylesheets are what .css and .qss files are currently in install_dir/static/qss.'
text += '\n' * 2
text += 'Note that there are several colours not handled by this yet. Check out the "colours" page of this options to change them.'
text += '\n' * 2
text += 'Also, if you run from source and you select e621 or another stylesheet that includes external (svg) assets, you must make sure that your CWD is the hydrus install folder when you boot.'
st = ClientGUICommon.BetterStaticText( self, label = text )

View File

@ -292,15 +292,12 @@ SHORTCUTS_MEDIA_ACTIONS = [
CAC.SIMPLE_OPEN_SELECTION_IN_NEW_DUPLICATES_FILTER_PAGE,
CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES,
CAC.SIMPLE_LAUNCH_THE_ARCHIVE_DELETE_FILTER,
CAC.SIMPLE_COPY_BMP,
CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE,
CAC.SIMPLE_COPY_LITTLE_BMP,
CAC.SIMPLE_COPY_FILE,
CAC.SIMPLE_COPY_PATH,
CAC.SIMPLE_COPY_SHA256_HASH,
CAC.SIMPLE_COPY_MD5_HASH,
CAC.SIMPLE_COPY_SHA1_HASH,
CAC.SIMPLE_COPY_SHA512_HASH,
CAC.SIMPLE_COPY_FILE_BITMAP,
CAC.SIMPLE_COPY_FILES,
CAC.SIMPLE_COPY_FILE_PATHS,
CAC.SIMPLE_COPY_FILE_HASHES,
CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES,
CAC.SIMPLE_COPY_FILE_ID,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_ALTERNATE_COLLECTIONS,
CAC.SIMPLE_DUPLICATE_MEDIA_SET_CUSTOM,

View File

@ -87,6 +87,7 @@ def InitialiseDefaults():
ORIGINAL_STYLESHEET = QW.QApplication.instance().styleSheet()
CURRENT_STYLESHEET = ORIGINAL_STYLESHEET
def SetStyleFromName( name: str ):
if QtInit.WE_ARE_QT5:
@ -118,6 +119,7 @@ def SetStyleFromName( name: str ):
def SetStyleSheet( stylesheet, prepend_hydrus = True ):
stylesheet_to_use = stylesheet
@ -138,6 +140,7 @@ def SetStyleSheet( stylesheet, prepend_hydrus = True ):
CURRENT_STYLESHEET = stylesheet_to_use
def SetStylesheetFromPath( filename ):
path = os.path.join( STYLESHEET_DIR, filename )

View File

@ -10,7 +10,6 @@ from hydrus.core import HydrusExceptions
from hydrus.core import HydrusLists
from hydrus.core import HydrusTags
from hydrus.core import HydrusTime
from hydrus.core.files.images import HydrusImageHandling
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@ -377,57 +376,6 @@ class Canvas( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
def _CopyBMPToClipboard( self, resolution = None ):
copied = False
if self._current_media is not None:
if self._current_media.IsStaticImage():
CG.client_controller.pub( 'clipboard', 'bmp', ( self._current_media, resolution ) )
copied = True
return copied
def _CopyHashToClipboard( self, hash_type ):
if self._current_media is None:
return
ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
def _CopyFileToClipboard( self ):
if self._current_media is not None:
client_files_manager = CG.client_controller.client_files_manager
paths = [ client_files_manager.GetFilePath( self._current_media.GetHash(), self._current_media.GetMime() ) ]
CG.client_controller.pub( 'clipboard', 'paths', paths )
def _CopyPathToClipboard( self ):
if self._current_media is not None:
client_files_manager = CG.client_controller.client_files_manager
path = client_files_manager.GetFilePath( self._current_media.GetHash(), self._current_media.GetMime() )
CG.client_controller.pub( 'clipboard', 'text', path )
def _Delete( self, media = None, default_reason = None, file_service_key = None, just_get_content_update_packages = False ) -> typing.Union[ bool, typing.Collection[ ClientContentUpdates.ContentUpdatePackage ] ]:
if media is None:
@ -838,62 +786,61 @@ class Canvas( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
self._Archive()
elif action in ( CAC.SIMPLE_COPY_BMP, CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE, CAC.SIMPLE_COPY_LITTLE_BMP ):
elif action == CAC.SIMPLE_COPY_FILE_BITMAP:
if self._current_media is None:
return
copied = False
bitmap_type = command.GetSimpleData()
if self._current_media.IsStaticImage():
ClientGUIMediaSimpleActions.CopyMediaBitmap( self._current_media, bitmap_type )
elif action == CAC.SIMPLE_COPY_FILES:
if self._current_media is not None:
( width, height ) = self._current_media.GetResolution()
ClientGUIMediaSimpleActions.CopyFilesToClipboard( [ self._current_media ] )
if width is not None and height is not None:
elif action == CAC.SIMPLE_COPY_FILE_ID:
if self._current_media is not None:
ClientGUIMediaSimpleActions.CopyFileIdsToClipboard( [ self._current_media ] )
elif action == CAC.SIMPLE_COPY_FILE_PATHS:
if self._current_media is not None:
ClientGUIMediaSimpleActions.CopyFilePathsToClipboard( [ self._current_media ] )
elif action == CAC.SIMPLE_COPY_FILE_HASHES:
( file_command_target, hash_type ) = command.GetSimpleData()
if file_command_target in ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, CAC.FILE_COMMAND_TARGET_SELECTED_FILES ):
if self._current_media is not None:
if action == CAC.SIMPLE_COPY_LITTLE_BMP and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( self._current_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
copied = self._CopyBMPToClipboard( resolution = target_resolution )
else:
copied = self._CopyBMPToClipboard()
ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
if action == CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE and not copied:
elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES:
hacky_ipfs_dict = command.GetSimpleData()
ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ]
if self._current_media is not None:
self._CopyFileToClipboard()
ClientGUIMediaSimpleActions.CopyServiceFilenamesToClipboard( ipfs_service_key, [ self._current_media ] )
elif action == CAC.SIMPLE_COPY_FILE:
self._CopyFileToClipboard()
elif action == CAC.SIMPLE_COPY_PATH:
self._CopyPathToClipboard()
elif action == CAC.SIMPLE_COPY_SHA256_HASH:
self._CopyHashToClipboard( 'sha256' )
elif action == CAC.SIMPLE_COPY_MD5_HASH:
self._CopyHashToClipboard( 'md5' )
elif action == CAC.SIMPLE_COPY_SHA1_HASH:
self._CopyHashToClipboard( 'sha1' )
elif action == CAC.SIMPLE_COPY_SHA512_HASH:
self._CopyHashToClipboard( 'sha512' )
elif action == CAC.SIMPLE_COPY_URLS:
if self._current_media is not None:
@ -971,6 +918,17 @@ class Canvas( CAC.ApplicationCommandProcessorMixin, QW.QWidget ):
ClientGUIMediaSimpleActions.ShowSimilarFilesInNewPage( [ self._current_media ], self._location_context, hamming_distance )
elif action in ( CAC.SIMPLE_EXPORT_FILES, CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT ):
do_export_and_then_quit = action == CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT
if self._current_media is not None:
medias = [ self._current_media ]
ClientGUIMediaModalActions.ExportFiles( self, medias, do_export_and_then_quit = do_export_and_then_quit )
elif action == CAC.SIMPLE_PAN_UP:
self._media_container.DoManualPan( 0, -1 )
@ -1531,59 +1489,7 @@ class CanvasPanel( Canvas ):
ClientGUIMediaMenus.AddOpenMenu( self, menu, self._current_media, [ self._current_media ] )
share_menu = ClientGUIMenus.GenerateMenu( menu )
copy_menu = ClientGUIMenus.GenerateMenu( share_menu )
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
copy_hash_menu = ClientGUIMenus.GenerateMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash.', self._CopyHashToClipboard, 'sha512' )
file_info_manager = self._current_media.GetFileInfoManager()
if file_info_manager.blurhash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'blurhash ({file_info_manager.blurhash})', 'Copy this file\'s blurhash.', self._CopyHashToClipboard, 'blurhash' )
if file_info_manager.pixel_hash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'pixel ({file_info_manager.pixel_hash.hex()})', 'Copy this file\'s pixel hash.', self._CopyHashToClipboard, 'pixel_hash' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
if advanced_mode:
hash_id_str = str( self._current_media.GetHashId() )
ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', CG.client_controller.pub, 'clipboard', 'text', hash_id_str )
if self._current_media.IsStaticImage():
ClientGUIMenus.AppendMenuItem( copy_menu, 'bitmap', 'Copy this file to your clipboard as a bitmap.', self._CopyBMPToClipboard )
( width, height ) = self._current_media.GetResolution()
if width is not None and height is not None and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( self._current_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
ClientGUIMenus.AppendMenuItem( copy_menu, 'source lookup bitmap ({}x{})'.format( target_resolution[0], target_resolution[1] ), 'Copy a smaller bitmap of this file, for quicker lookup on source-finding websites.', self._CopyBMPToClipboard, target_resolution )
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy this file\'s path to your clipboard.', self._CopyPathToClipboard )
ClientGUIMenus.AppendMenu( share_menu, copy_menu, 'copy' )
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
ClientGUIMediaMenus.AddShareMenu( self, menu, self._current_media, [ self._current_media ] )
CGC.core().PopupMenu( self, menu )
@ -3671,10 +3577,11 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.LocationContext, kept: typing.Collection[ ClientMedia.MediaSingleton ], deleted: typing.Collection[ ClientMedia.MediaSingleton ] ):
def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.LocationContext, kept: typing.Collection[ ClientMedia.MediaSingleton ], deleted: typing.Collection[ ClientMedia.MediaSingleton ], skipped: typing.Collection[ ClientMedia.MediaSingleton ] ):
kept = list( kept )
deleted = list( deleted )
skipped = list( skipped )
kept_hashes = [ m.GetHash() for m in kept ]
deleted_hashes = [ m.GetHash() for m in deleted ]
@ -3686,6 +3593,13 @@ def CommitArchiveDelete( page_key: bytes, location_context: ClientLocation.Locat
all_hashes.update( kept_hashes )
all_hashes.update( deleted_hashes )
if CG.client_controller.new_options.GetBoolean( 'remove_filtered_files_even_when_skipped' ):
skipped_hashes = [ m.GetHash() for m in skipped ]
all_hashes.update( skipped_hashes )
CG.client_controller.pub( 'remove_media', page_key, all_hashes )
@ -3757,6 +3671,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
self._kept = set()
self._deleted = set()
self._skipped = set()
CG.client_controller.sub( self, 'Delete', 'canvas_delete' )
CG.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
@ -3781,6 +3696,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
self._skipped.discard( self._current_media )
@ -3790,6 +3706,8 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
deleted = ClientMediaFileFilter.FilterAndReportDeleteLockFailures( self._deleted )
skipped = list( self._skipped )
if len( kept ) > 0 or len( deleted ) > 0:
if len( kept ) > 0:
@ -3878,15 +3796,9 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
if cancelled:
if self._current_media in self._kept:
self._kept.remove( self._current_media )
if self._current_media in self._deleted:
self._deleted.remove( self._current_media )
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
self._skipped.discard( self._current_media )
return False
@ -3894,10 +3806,11 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
self._kept = set()
self._deleted = set()
self._skipped = set()
self._current_media = self._GetFirst() # so the pubsub on close is better
CG.client_controller.CallToThread( CommitArchiveDelete, self._page_key, deletee_location_context, kept, deleted )
CG.client_controller.CallToThread( CommitArchiveDelete, self._page_key, deletee_location_context, kept, deleted, skipped )
@ -3955,6 +3868,8 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def _Skip( self ):
self._skipped.add( self._current_media )
if self._current_media == self._GetLast():
self._TryToCloseWindow()
@ -4688,59 +4603,7 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMediaMenus.AddOpenMenu( self, menu, self._current_media, [ self._current_media ] )
share_menu = ClientGUIMenus.GenerateMenu( menu )
copy_menu = ClientGUIMenus.GenerateMenu( share_menu )
ClientGUIMenus.AppendMenuItem( copy_menu, 'file', 'Copy this file to your clipboard.', self._CopyFileToClipboard )
copy_hash_menu = ClientGUIMenus.GenerateMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( self._current_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash to your clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash to your clipboard.', self._CopyHashToClipboard, 'sha512' )
file_info_manager = self._current_media.GetFileInfoManager()
if file_info_manager.blurhash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'blurhash ({file_info_manager.blurhash})', 'Copy this file\'s blurhash.', self._CopyHashToClipboard, 'blurhash' )
if file_info_manager.pixel_hash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'pixel ({file_info_manager.pixel_hash.hex()})', 'Copy this file\'s pixel hash.', self._CopyHashToClipboard, 'pixel_hash' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
if advanced_mode:
hash_id_str = str( self._current_media.GetHashId() )
ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', CG.client_controller.pub, 'clipboard', 'text', hash_id_str )
if self._current_media.IsStaticImage():
ClientGUIMenus.AppendMenuItem( copy_menu, 'bitmap', 'Copy this file to your clipboard as a bitmap.', self._CopyBMPToClipboard )
( width, height ) = self._current_media.GetResolution()
if width is not None and height is not None and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( self._current_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
ClientGUIMenus.AppendMenuItem( copy_menu, 'source lookup bitmap ({}x{})'.format( target_resolution[0], target_resolution[1] ), 'Copy a smaller bitmap of this file, for quicker lookup on source-finding websites.', self._CopyBMPToClipboard, target_resolution )
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy this file\'s path to your clipboard.', self._CopyPathToClipboard )
ClientGUIMenus.AppendMenu( share_menu, copy_menu, 'copy' )
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
ClientGUIMediaMenus.AddShareMenu( self, menu, self._current_media, [ self._current_media ] )
CGC.core().PopupMenu( self, menu )

View File

@ -1,3 +1,4 @@
import collections
import os
import random
import typing
@ -7,6 +8,8 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusData
from hydrus.core import HydrusSerialisable
from hydrus.core.files.images import HydrusImageHandling
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@ -780,3 +783,195 @@ def AddServiceKeysToMenu( menu, service_keys, submenu_name, description, bare_ca
ClientGUIMenus.AppendMenuOrItem( menu, submenu_name, menu_tuples )
def AddShareMenu( win: QW.QWidget, menu: QW.QMenu, focused_media: typing.Optional[ ClientMedia.Media ], selected_media: typing.Collection[ ClientMedia.Media ] ):
if focused_media is not None:
focused_media = focused_media.GetDisplayMedia()
ipfs_service_keys = set( CG.client_controller.services_manager.GetServiceKeys( ( HC.IPFS, ) ) )
selected_media = ClientMedia.FlattenMedia( selected_media )
focused_is_local = focused_media is not None and focused_media.GetLocationsManager().IsLocal()
selection_is_useful = len( selected_media ) > 0 and not ( len( selected_media ) == 1 and focused_media in selected_media )
local_selection = [ m for m in selected_media if m.GetLocationsManager().IsLocal() ]
local_selection_is_useful = len( local_selection ) > 0 and not ( len( local_selection ) == 1 and focused_media in local_selection )
if not focused_is_local and len( local_selection ) == 0:
# nothing to share!
return
share_menu = ClientGUIMenus.GenerateMenu( menu )
ClientGUIMenus.AppendMenuItem( share_menu, 'export files', 'Export the selected files to an external folder.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_EXPORT_FILES ) )
ClientGUIMenus.AppendSeparator( share_menu )
if local_selection_is_useful:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy files', 'Copy these files to your clipboard.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILES, simple_data = CAC.FILE_COMMAND_TARGET_SELECTED_FILES ) )
if local_selection_is_useful:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy paths', 'Copy these files\' paths to your clipboard, just as raw text.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_PATHS, simple_data = CAC.FILE_COMMAND_TARGET_SELECTED_FILES ) )
if selection_is_useful:
ipfs_service_keys_to_num_filenames = collections.Counter()
for media in selected_media:
ipfs_service_keys_to_num_filenames.update( ipfs_service_keys.intersection( media.GetLocationsManager().GetCurrent() ) )
ipfs_service_keys_in_order = sorted( ipfs_service_keys_to_num_filenames.keys(), key = CG.client_controller.services_manager.GetName )
for ipfs_service_key in ipfs_service_keys_in_order:
name = CG.client_controller.services_manager.GetName( ipfs_service_key )
hacky_ipfs_dict = HydrusSerialisable.SerialisableDictionary()
hacky_ipfs_dict[ 'file_command_target' ] = CAC.FILE_COMMAND_TARGET_SELECTED_FILES
hacky_ipfs_dict[ 'ipfs_service_key' ] = ipfs_service_key
application_command = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES, simple_data = hacky_ipfs_dict )
ClientGUIMenus.AppendMenuItem( share_menu, f'copy {name} multihashes ({HydrusData.ToHumanInt(ipfs_service_keys_to_num_filenames[ipfs_service_key])} hashes)', 'Copy the selected files\' multihashes to the clipboard.', win.ProcessApplicationCommand, application_command )
if selection_is_useful:
copy_hash_menu = ClientGUIMenus.GenerateMenu( share_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256', 'Copy these files\' SHA256 hashes to your clipboard.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'sha256' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy these files\' MD5 hashes to your clipboard. Your client may not know all of these.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'md5' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy these files\' SHA1 hashes to your clipboard. Your client may not know all of these.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'sha1' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy these files\' SHA512 hashes to your clipboard. Your client may not know all of these.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'sha512' ) ) )
blurhashes = [ media.GetFileInfoManager().blurhash for media in selected_media ]
blurhashes = [ b for b in blurhashes if b is not None ]
if len( blurhashes ) > 0:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'blurhash ({HydrusData.ToHumanInt(len(blurhashes))} hashes)', 'Copy these files\' blurhashes.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'blurhash' ) ) )
pixel_hashes = [ media.GetFileInfoManager().pixel_hash for media in selected_media ]
pixel_hashes = [ p for p in pixel_hashes if p is not None ]
if len( pixel_hashes ):
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'pixel hashes ({HydrusData.ToHumanInt(len(pixel_hashes))} hashes)', 'Copy these files\' pixel hashes.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, 'pixel_hash' ) ) )
ClientGUIMenus.AppendMenu( share_menu, copy_hash_menu, 'copy hashes' )
if selection_is_useful:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy file ids', 'Copy these files\' internal file/hash_ids.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_ID, simple_data = CAC.FILE_COMMAND_TARGET_SELECTED_FILES ) )
if focused_media is not None and selection_is_useful:
ClientGUIMenus.AppendSeparator( share_menu )
if focused_is_local:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy file', 'Copy this file to your clipboard.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILES, simple_data = CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) )
if focused_is_local:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy path', 'Copy this file\'s path to your clipboard, just as raw text.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_PATHS, simple_data = CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) )
if focused_media is not None:
for ipfs_service_key in ipfs_service_keys.intersection( focused_media.GetLocationsManager().GetCurrent() ):
name = CG.client_controller.services_manager.GetName( ipfs_service_key )
multihash = focused_media.GetLocationsManager().GetServiceFilename( ipfs_service_key )
hacky_ipfs_dict = HydrusSerialisable.SerialisableDictionary()
hacky_ipfs_dict[ 'file_command_target' ] = CAC.FILE_COMMAND_TARGET_FOCUSED_FILE
hacky_ipfs_dict[ 'ipfs_service_key' ] = ipfs_service_key
application_command = CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES, simple_data = hacky_ipfs_dict )
ClientGUIMenus.AppendMenuItem( share_menu, f'copy {name} multihash ({multihash})', 'Copy the selected file\'s multihash to the clipboard.', win.ProcessApplicationCommand, application_command )
if focused_media is not None:
copy_hash_menu = ClientGUIMenus.GenerateMenu( share_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( focused_media.GetHash().hex() ), 'Copy this file\'s SHA256 hash to your clipboard.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'sha256' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy this file\'s MD5 hash to your clipboard. Your client may not know this.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'md5' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy this file\'s SHA1 hash to your clipboard. Your client may not know this.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'sha1' ) ) )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy this file\'s SHA512 hash to your clipboard. Your client may not know this.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'sha512' ) ) )
file_info_manager = focused_media.GetMediaResult().GetFileInfoManager()
if file_info_manager.blurhash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'blurhash ({file_info_manager.blurhash})', 'Copy this file\'s blurhash.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'blurhash' ) ) )
if file_info_manager.pixel_hash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'pixel hash ({file_info_manager.pixel_hash.hex()})', 'Copy this file\'s pixel hash.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_HASHES, simple_data = ( CAC.FILE_COMMAND_TARGET_FOCUSED_FILE, 'pixel_hash' ) ) )
ClientGUIMenus.AppendMenu( share_menu, copy_hash_menu, 'copy hash' )
if focused_media is not None:
hash_id_str = HydrusData.ToHumanInt( focused_media.GetHashId() )
ClientGUIMenus.AppendMenuItem( share_menu, 'copy file id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_ID, simple_data = CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) )
if focused_is_local:
if focused_media.IsStaticImage():
ClientGUIMenus.AppendMenuItem( share_menu, 'copy bitmap', 'Copy this file\'s bitmap.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_BITMAP, simple_data = CAC.BITMAP_TYPE_FULL ) )
( width, height ) = focused_media.GetResolution()
if width is not None and height is not None and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( focused_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
ClientGUIMenus.AppendMenuItem( share_menu, 'copy source lookup bitmap ({}x{})'.format( target_resolution[0], target_resolution[1] ), 'Copy a smaller bitmap of this file, for quicker lookup on source-finding websites.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_BITMAP, simple_data = CAC.BITMAP_TYPE_SOURCE_LOOKUPS ) )
if focused_media.GetMime() in HC.MIMES_WITH_THUMBNAILS:
ClientGUIMenus.AppendMenuItem( share_menu, 'copy thumbnail bitmap', 'Copy this file\'s thumbnail\'s bitmap.', win.ProcessApplicationCommand, CAC.ApplicationCommand.STATICCreateSimpleCommand( CAC.SIMPLE_COPY_FILE_BITMAP, simple_data = CAC.BITMAP_TYPE_THUMBNAIL ) )
#
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )

View File

@ -27,6 +27,7 @@ from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIScrolledPanelsReview
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui.exporting import ClientGUIExport
from hydrus.client.gui.media import ClientGUIMediaSimpleActions
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
@ -155,6 +156,11 @@ def ClearDeleteRecord( win, media ):
def CopyHashesToClipboard( win: QW.QWidget, hash_type: str, medias: typing.Sequence[ ClientMedia.Media ] ):
if len( medias ) == 0:
return
hex_it = True
desired_hashes = []
@ -461,6 +467,22 @@ def EditFileTimestamps( win: QW.QWidget, ordered_medias: typing.List[ ClientMedi
def ExportFiles( win: QW.QWidget, medias: typing.Collection[ ClientMedia.Media ], do_export_and_then_quit = False ):
flat_media = ClientMedia.FlattenMedia( medias )
flat_media = [ m for m in flat_media if m.GetLocationsManager().IsLocal() ]
if len( flat_media ) > 0:
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( win, 'export files' )
panel = ClientGUIExport.ReviewExportFilesPanel( frame, flat_media, do_export_and_then_quit = do_export_and_then_quit )
frame.SetPanel( panel )
def GetContentUpdatesForAppliedContentApplicationCommandRatingsSetFlip( service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], rating: typing.Optional[ float ] ):
hashes = set()

View File

@ -1,12 +1,13 @@
import collections
import itertools
import os
import typing
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusPaths
from hydrus.core import HydrusData
from hydrus.core.files.images import HydrusImageHandling
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientGlobals as CG
from hydrus.client import ClientLocation
@ -15,6 +16,116 @@ from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.search import ClientSearch
def GetLocalMediaPaths( medias: typing.Collection[ ClientMedia.Media ] ):
medias = ClientMedia.FlattenMedia( medias )
client_files_manager = CG.client_controller.client_files_manager
paths = []
for media in medias:
if not media.GetLocationsManager().IsLocal():
continue
hash = media.GetHash()
mime = media.GetMime()
path = client_files_manager.GetFilePath( hash, mime, check_file_exists = False )
paths.append( path )
return paths
def CopyFilesToClipboard( medias: typing.Collection[ ClientMedia.Media ] ):
paths = GetLocalMediaPaths( medias )
if len( paths ) > 0:
CG.client_controller.pub( 'clipboard', 'paths', paths )
def CopyFileIdsToClipboard( medias: typing.Collection[ ClientMedia.Media ] ):
flat_media = ClientMedia.FlattenMedia( medias )
ids = [ media.GetMediaResult().GetHashId() for media in flat_media ]
if len( ids ) > 0:
text = '\n'.join( ( str( id ) for id in ids ) )
CG.client_controller.pub( 'clipboard', 'text', text )
def CopyFilePathsToClipboard( medias: typing.Collection[ ClientMedia.Media ] ):
paths = GetLocalMediaPaths( medias )
if len( paths ) > 0:
text = '\n'.join( paths )
CG.client_controller.pub( 'clipboard', 'text', text )
def CopyMediaBitmap( media: ClientMedia.MediaSingleton, bitmap_type: int ):
if bitmap_type == CAC.BITMAP_TYPE_THUMBNAIL:
if media.GetMime() not in HC.MIMES_WITH_THUMBNAILS:
return
CG.client_controller.pub( 'clipboard', 'thumbnail_bmp', media )
else:
if not media.GetLocationsManager().IsLocal():
return
copied = False
if media.IsStaticImage():
( width, height ) = media.GetResolution()
if width is not None and height is not None:
if bitmap_type == CAC.BITMAP_TYPE_SOURCE_LOOKUPS and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
CG.client_controller.pub( 'clipboard', 'bmp', ( media, target_resolution ) )
else:
CG.client_controller.pub( 'clipboard', 'bmp', ( media, None ) )
copied = True
if bitmap_type == CAC.BITMAP_TYPE_FULL_OR_FILE and not copied:
CopyFilesToClipboard( [ media ] )
def CopyMediaURLs( medias ):
urls = set()
@ -58,6 +169,46 @@ def CopyMediaURLClassURLs( medias, url_class ):
CG.client_controller.pub( 'clipboard', 'text', urls_string )
def CopyServiceFilenamesToClipboard( service_key: bytes, medias: typing.Collection[ ClientMedia.Media ] ):
flat_media = ClientMedia.FlattenMedia( medias )
flat_media = [ m for m in flat_media if service_key in m.GetLocationsManager().GetCurrent() ]
if len( flat_media ) == 0:
HydrusData.ShowText( 'Could not find any files with the requested service!' )
return
prefix = ''
service = CG.client_controller.services_manager.GetService( service_key )
if service.GetServiceType() == HC.IPFS:
prefix = service.GetMultihashPrefix()
filenames_or_none = [ media.GetLocationsManager().GetServiceFilename( service_key ) for media in flat_media ]
filenames = [ f for f in filenames_or_none if f is not None ]
lines = [ prefix + filename for filename in filenames ]
if len( lines ) > 0:
text = '\n'.join( lines )
CG.client_controller.pub( 'clipboard', 'text', text )
else:
HydrusData.ShowText( 'Could not find any service filenames for that selection!' )
def GetLocalFileActionServiceKeys( media: typing.Collection[ ClientMedia.MediaSingleton ] ):
local_media_file_service_keys = set( CG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )

View File

@ -272,168 +272,6 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
ClientGUIMediaModalActions.ClearDeleteRecord( self, media )
def _CopyBMPToClipboard( self, resolution = None ):
copied = False
if self._focused_media is not None:
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
if media.IsStaticImage():
CG.client_controller.pub( 'clipboard', 'bmp', ( media, resolution ) )
copied = True
return copied
def _CopyFilesToClipboard( self ):
client_files_manager = CG.client_controller.client_files_manager
media = self._GetSelectedFlatMedia( discriminant = CC.DISCRIMINANT_LOCAL )
paths = []
for m in media:
hash = m.GetHash()
mime = m.GetMime()
path = client_files_manager.GetFilePath( hash, mime, check_file_exists = False )
paths.append( path )
if len( paths ) > 0:
CG.client_controller.pub( 'clipboard', 'paths', paths )
def _CopyHashToClipboard( self, hash_type ):
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, [ media ] )
def _CopyHashesToClipboard( self, hash_type ):
medias = self._GetSelectedMediaOrdered()
ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, medias )
def _CopyPathToClipboard( self ):
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
client_files_manager = CG.client_controller.client_files_manager
path = client_files_manager.GetFilePath( media.GetHash(), media.GetMime() )
CG.client_controller.pub( 'clipboard', 'text', path )
def _CopyPathsToClipboard( self ):
media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, selected_media = set( self._selected_media ) )
client_files_manager = CG.client_controller.client_files_manager
paths = []
for media_result in media_results:
paths.append( client_files_manager.GetFilePath( media_result.GetHash(), media_result.GetMime(), check_file_exists = False ) )
if len( paths ) > 0:
text = '\n'.join( paths )
CG.client_controller.pub( 'clipboard', 'text', text )
def _CopyServiceFilenameToClipboard( self, service_key ):
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
hash = media.GetHash()
filename = media.GetLocationsManager().GetServiceFilename( service_key )
if filename is None:
return
service = CG.client_controller.services_manager.GetService( service_key )
if service.GetServiceType() == HC.IPFS:
multihash_prefix = service.GetMultihashPrefix()
filename = multihash_prefix + filename
CG.client_controller.pub( 'clipboard', 'text', filename )
def _CopyServiceFilenamesToClipboard( self, service_key ):
prefix = ''
service = CG.client_controller.services_manager.GetService( service_key )
if service.GetServiceType() == HC.IPFS:
prefix = service.GetMultihashPrefix()
flat_media = self._GetSelectedFlatMedia( is_in_file_service_key = service_key )
if len( flat_media ) > 0:
filenames_or_none = [ media.GetLocationsManager().GetServiceFilename( service_key ) for media in flat_media ]
filenames = [ prefix + filename for filename in filenames_or_none if filename is not None ]
if len( filenames ) > 0:
copy_string = '\n'.join( filenames )
CG.client_controller.pub( 'clipboard', 'text', copy_string )
else:
HydrusData.ShowText( 'Could not find any service filenames for that selection!' )
else:
HydrusData.ShowText( 'Could not find any files with the requested service!' )
def _Delete( self, file_service_key = None, only_those_in_file_service_key = None ):
if file_service_key is None:
@ -533,35 +371,6 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
self._media_added_in_current_shift_select = set()
def _ExportFiles( self, do_export_and_then_quit = False ):
if len( self._selected_media ) > 0:
flat_media = []
for media in self._sorted_media:
if media in self._selected_media:
if media.IsCollection():
flat_media.extend( media.GetFlatMedia() )
else:
flat_media.append( media )
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'export files' )
panel = ClientGUIExport.ReviewExportFilesPanel( frame, flat_media, do_export_and_then_quit = do_export_and_then_quit )
frame.SetPanel( panel )
def _GetFocusSingleton( self ) -> ClientMedia.MediaSingleton:
if self._focused_media is not None:
@ -577,6 +386,30 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
raise HydrusExceptions.DataMissing( 'No media singleton!' )
def _GetMediasForFileCommandTarget( self, file_command_target: int ) -> typing.Collection[ ClientMedia.MediaSingleton ]:
if file_command_target == CAC.FILE_COMMAND_TARGET_FOCUSED_FILE:
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
return [ media.GetDisplayMedia() ]
elif file_command_target == CAC.FILE_COMMAND_TARGET_SELECTED_FILES:
if len( self._selected_media ) > 0:
medias = self._GetSelectedMediaOrdered()
return ClientMedia.FlattenMedia( medias )
return []
def _GetNumSelected( self ):
return sum( [ media.GetNumFiles() for media in self._selected_media ] )
@ -1967,38 +1800,6 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
pass
def _ShareOnLocalBooru( self ):
if len( self._selected_media ) > 0:
share_key = HydrusData.GenerateKey()
name = ''
text = ''
timeout = HydrusTime.GetNow() + 60 * 60 * 24
hashes = self._GetSelectedHashes()
with ClientGUIDialogs.DialogInputLocalBooruShare( self, share_key, name, text, timeout, hashes, new_share = True ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
( share_key, name, text, timeout, hashes ) = dlg.GetInfo()
info = {}
info[ 'name' ] = name
info[ 'text' ] = text
info[ 'timeout' ] = timeout
info[ 'hashes' ] = hashes
CG.client_controller.Write( 'local_booru_share', share_key, info )
self.setFocus( QC.Qt.OtherFocusReason )
def _ShowSelectionInNewPage( self ):
hashes = self._GetSelectedHashes( ordered = True )
@ -2136,62 +1937,76 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
action = command.GetSimpleAction()
if action in ( CAC.SIMPLE_COPY_BMP, CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE, CAC.SIMPLE_COPY_LITTLE_BMP ):
if action == CAC.SIMPLE_COPY_FILE_BITMAP:
if self._focused_media is None:
if not self._HasFocusSingleton():
return
copied = False
focus_singleton = self._GetFocusSingleton()
if self._focused_media.IsStaticImage():
bitmap_type = command.GetSimpleData()
ClientGUIMediaSimpleActions.CopyMediaBitmap( focus_singleton, bitmap_type )
elif action == CAC.SIMPLE_COPY_FILES:
file_command_target = command.GetSimpleData()
medias = self._GetMediasForFileCommandTarget( file_command_target )
if len( medias ) > 0:
( width, height ) = self._focused_media.GetResolution()
if width is not None and height is not None:
if action == CAC.SIMPLE_COPY_LITTLE_BMP and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( self._focused_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
copied = self._CopyBMPToClipboard( resolution = target_resolution )
else:
copied = self._CopyBMPToClipboard()
ClientGUIMediaSimpleActions.CopyFilesToClipboard( medias )
if action == CAC.SIMPLE_COPY_BMP_OR_FILE_IF_NOT_BMPABLE and not copied:
elif action == CAC.SIMPLE_COPY_FILE_PATHS:
file_command_target = command.GetSimpleData()
medias = self._GetMediasForFileCommandTarget( file_command_target )
if len( medias ) > 0:
self._CopyFilesToClipboard()
ClientGUIMediaSimpleActions.CopyFilePathsToClipboard( medias )
elif action == CAC.SIMPLE_COPY_FILE:
elif action == CAC.SIMPLE_COPY_FILE_HASHES:
self._CopyFilesToClipboard()
( file_command_target, hash_type ) = command.GetSimpleData()
elif action == CAC.SIMPLE_COPY_PATH:
medias = self._GetMediasForFileCommandTarget( file_command_target )
self._CopyPathsToClipboard()
if len( medias ) > 0:
ClientGUIMediaModalActions.CopyHashesToClipboard( self, hash_type, medias )
elif action == CAC.SIMPLE_COPY_SHA256_HASH:
elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES:
self._CopyHashesToClipboard( 'sha256' )
hacky_ipfs_dict = command.GetSimpleData()
elif action == CAC.SIMPLE_COPY_MD5_HASH:
file_command_target = hacky_ipfs_dict[ 'file_command_target' ]
ipfs_service_key = hacky_ipfs_dict[ 'ipfs_service_key' ]
self._CopyHashesToClipboard( 'md5' )
medias = self._GetMediasForFileCommandTarget( file_command_target )
elif action == CAC.SIMPLE_COPY_SHA1_HASH:
if len( medias ) > 0:
ClientGUIMediaSimpleActions.CopyServiceFilenamesToClipboard( ipfs_service_key, medias )
self._CopyHashesToClipboard( 'sha1' )
elif action == CAC.SIMPLE_COPY_FILE_ID:
elif action == CAC.SIMPLE_COPY_SHA512_HASH:
file_command_target = command.GetSimpleData()
self._CopyHashesToClipboard( 'sha512' )
medias = self._GetMediasForFileCommandTarget( file_command_target )
if len( medias ) > 0:
ClientGUIMediaSimpleActions.CopyFileIdsToClipboard( medias )
elif action == CAC.SIMPLE_COPY_URLS:
@ -2431,13 +2246,18 @@ class MediaPanel( CAC.ApplicationCommandProcessorMixin, ClientMedia.ListeningMed
self._SetDuplicates( HC.DUPLICATE_SAME_QUALITY )
elif action == CAC.SIMPLE_EXPORT_FILES:
elif action in ( CAC.SIMPLE_EXPORT_FILES, CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT ):
self._ExportFiles()
do_export_and_then_quit = action == CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT
elif action == CAC.SIMPLE_EXPORT_FILES_QUICK_AUTO_EXPORT:
self._ExportFiles( do_export_and_then_quit = True )
if len( self._selected_media ) > 0:
medias = self._GetSelectedMediaOrdered()
flat_media = ClientMedia.FlattenMedia( medias )
ClientGUIMediaModalActions.ExportFiles( self, flat_media, do_export_and_then_quit = do_export_and_then_quit )
elif action == CAC.SIMPLE_MANAGE_FILE_RATINGS:
@ -3815,18 +3635,11 @@ class MediaPanelThumbnails( MediaPanel ):
def ShowMenu( self, do_not_show_just_return = False ):
new_options = CG.client_controller.new_options
advanced_mode = new_options.GetBoolean( 'advanced_mode' )
services_manager = CG.client_controller.services_manager
flat_selected_medias = ClientMedia.FlattenMedia( self._selected_media )
all_locations_managers = [ media.GetLocationsManager() for media in ClientMedia.FlattenMedia( self._sorted_media ) ]
selected_locations_managers = [ media.GetLocationsManager() for media in flat_selected_medias ]
selection_has_local = True in ( locations_manager.IsLocal() for locations_manager in selected_locations_managers )
selection_has_local_file_domain = True in ( locations_manager.IsLocal() and not locations_manager.IsTrashed() for locations_manager in selected_locations_managers )
selection_has_trash = True in ( locations_manager.IsTrashed() for locations_manager in selected_locations_managers )
selection_has_inbox = True in ( media.HasInbox() for media in self._selected_media )
@ -3838,8 +3651,6 @@ class MediaPanelThumbnails( MediaPanel ):
some_downloading = True in ( locations_manager.IsDownloading() for locations_manager in selected_locations_managers )
focused_is_local = False
has_local = True in ( locations_manager.IsLocal() for locations_manager in all_locations_managers )
has_remote = True in ( locations_manager.IsRemote() for locations_manager in all_locations_managers )
@ -3850,9 +3661,6 @@ class MediaPanelThumbnails( MediaPanel ):
multiple_selected = num_selected > 1
media_has_inbox = num_inbox > 0
media_has_archive = num_archive > 0
menu = ClientGUIMenus.GenerateMenu( self.window() )
if self._HasFocusSingleton():
@ -3875,14 +3683,8 @@ class MediaPanelThumbnails( MediaPanel ):
local_ratings_services = [ service for service in services if service.GetServiceType() in HC.RATINGS_SERVICES ]
local_booru_service = [ service for service in services if service.GetServiceType() == HC.LOCAL_BOORU ][0]
local_booru_is_running = local_booru_service.GetPort() is not None
i_can_post_ratings = len( local_ratings_services ) > 0
focused_is_local = CC.COMBINED_LOCAL_FILE_SERVICE_KEY in self._focused_media.GetLocationsManager().GetCurrent()
local_media_file_service_keys = { service.GetServiceKey() for service in services if service.GetServiceType() == HC.LOCAL_FILE_DOMAIN }
file_repository_service_keys = { repository.GetServiceKey() for repository in file_repositories }
@ -3892,8 +3694,6 @@ class MediaPanelThumbnails( MediaPanel ):
user_manage_permission_file_service_keys = { repository.GetServiceKey() for repository in file_repositories if repository.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_MODERATE ) }
ipfs_service_keys = { service.GetServiceKey() for service in ipfs_services }
focused_is_ipfs = not self._focused_media.GetLocationsManager().GetCurrent().isdisjoint( ipfs_service_keys )
if multiple_selected:
download_phrase = 'download all possible selected'
@ -3916,8 +3716,6 @@ class MediaPanelThumbnails( MediaPanel ):
delete_physically_phrase = 'delete selected physically now'
undelete_phrase = 'undelete selected'
clear_deletion_phrase = 'clear deletion record for selected'
export_phrase = 'files'
copy_phrase = 'files'
else:
@ -3941,8 +3739,6 @@ class MediaPanelThumbnails( MediaPanel ):
delete_physically_phrase = 'delete physically now'
undelete_phrase = 'undelete'
clear_deletion_phrase = 'clear deletion record'
export_phrase = 'file'
copy_phrase = 'file'
# info about the files
@ -4438,141 +4234,9 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMediaMenus.AddKnownURLsViewCopyMenu( self, menu, self._focused_media, selected_media = self._selected_media )
#
ClientGUIMediaMenus.AddOpenMenu( self, menu, self._focused_media, self._selected_media )
# share
share_menu = ClientGUIMenus.GenerateMenu( menu )
#
copy_menu = ClientGUIMenus.GenerateMenu( share_menu )
if selection_has_local:
ClientGUIMenus.AppendMenuItem( copy_menu, copy_phrase, 'Copy the selected files to the clipboard.', self._CopyFilesToClipboard )
copy_hash_menu = ClientGUIMenus.GenerateMenu( copy_menu )
if self._HasFocusSingleton():
focus_singleton = self._GetFocusSingleton()
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 ({})'.format( focus_singleton.GetHash().hex() ), 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected file\'s MD5 hash to the clipboard.', self._CopyHashToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected file\'s SHA1 hash to the clipboard.', self._CopyHashToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected file\'s SHA512 hash to the clipboard.', self._CopyHashToClipboard, 'sha512' )
file_info_manager = focus_singleton.GetFileInfoManager()
if file_info_manager.blurhash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'blurhash ({file_info_manager.blurhash})', 'Copy this file\'s blurhash.', self._CopyHashToClipboard, 'blurhash' )
if file_info_manager.pixel_hash is not None:
ClientGUIMenus.AppendMenuItem( copy_hash_menu, f'pixel ({file_info_manager.pixel_hash.hex()})', 'Copy this file\'s pixel hash.', self._CopyHashToClipboard, 'pixel_hash' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hash' )
if multiple_selected:
copy_hash_menu = ClientGUIMenus.GenerateMenu( copy_menu )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha256 (hydrus default)', 'Copy the selected files\' SHA256 hashes to the clipboard.', self._CopyHashesToClipboard, 'sha256' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'md5', 'Copy the selected files\' MD5 hashes to the clipboard.', self._CopyHashesToClipboard, 'md5' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha1', 'Copy the selected files\' SHA1 hashes to the clipboard.', self._CopyHashesToClipboard, 'sha1' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'sha512', 'Copy the selected files\' SHA512 hashes to the clipboard.', self._CopyHashesToClipboard, 'sha512' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'blurhash', 'Copy the selected files\' blurhashes to the clipboard.', self._CopyHashesToClipboard, 'blurhash' )
ClientGUIMenus.AppendMenuItem( copy_hash_menu, 'pixel', 'Copy the selected files\' pixel hashes to the clipboard.', self._CopyHashesToClipboard, 'pixel_hash' )
ClientGUIMenus.AppendMenu( copy_menu, copy_hash_menu, 'hashes' )
else:
ClientGUIMenus.AppendMenuItem( copy_menu, 'sha256 hash', 'Copy the selected file\'s SHA256 hash to the clipboard.', self._CopyHashToClipboard, 'sha256' )
if multiple_selected:
ClientGUIMenus.AppendMenuItem( copy_menu, 'sha256 hashes', 'Copy the selected files\' SHA256 hash to the clipboard.', self._CopyHashesToClipboard, 'sha256' )
if advanced_mode:
hash_id_str = str( focus_singleton.GetHashId() )
ClientGUIMenus.AppendMenuItem( copy_menu, 'file_id ({})'.format( hash_id_str ), 'Copy this file\'s internal file/hash_id.', CG.client_controller.pub, 'clipboard', 'text', hash_id_str )
for ipfs_service_key in self._focused_media.GetLocationsManager().GetCurrent().intersection( ipfs_service_keys ):
name = service_keys_to_names[ ipfs_service_key ]
ClientGUIMenus.AppendMenuItem( copy_menu, name + ' multihash', 'Copy the selected file\'s multihash to the clipboard.', self._CopyServiceFilenameToClipboard, ipfs_service_key )
if multiple_selected:
for ipfs_service_key in disparate_current_ipfs_service_keys.union( common_current_ipfs_service_keys ):
name = service_keys_to_names[ ipfs_service_key ]
ClientGUIMenus.AppendMenuItem( copy_menu, name + ' multihashes', 'Copy the selected files\' multihashes to the clipboard.', self._CopyServiceFilenamesToClipboard, ipfs_service_key )
if focused_is_local:
if self._focused_media.IsStaticImage():
ClientGUIMenus.AppendMenuItem( copy_menu, 'bitmap', 'Copy this file to your clipboard as a bitmap.', self._CopyBMPToClipboard )
( width, height ) = self._focused_media.GetResolution()
if width is not None and height is not None and ( width > 1024 or height > 1024 ):
target_resolution = HydrusImageHandling.GetThumbnailResolution( self._focused_media.GetResolution(), ( 1024, 1024 ), HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, 100 )
ClientGUIMenus.AppendMenuItem( copy_menu, 'source lookup bitmap ({}x{})'.format( target_resolution[0], target_resolution[1] ), 'Copy a smaller bitmap of this file, for quicker lookup on source-finding websites.', self._CopyBMPToClipboard, target_resolution )
ClientGUIMenus.AppendMenuItem( copy_menu, 'path', 'Copy the selected file\'s path to the clipboard.', self._CopyPathToClipboard )
if multiple_selected and selection_has_local:
ClientGUIMenus.AppendMenuItem( copy_menu, 'paths', 'Copy the selected files\' paths to the clipboard.', self._CopyPathsToClipboard )
ClientGUIMenus.AppendMenu( share_menu, copy_menu, 'copy' )
#
export_menu = ClientGUIMenus.GenerateMenu( share_menu )
ClientGUIMenus.AppendMenuItem( export_menu, export_phrase, 'Export the selected files to an external folder.', self._ExportFiles )
ClientGUIMenus.AppendMenu( share_menu, export_menu, 'export' )
#
if local_booru_is_running:
ClientGUIMenus.AppendMenuItem( share_menu, 'on local booru', 'Share the selected files on your client\'s local booru.', self._ShareOnLocalBooru )
#
ClientGUIMenus.AppendMenu( menu, share_menu, 'share' )
ClientGUIMediaMenus.AddShareMenu( self, menu, self._focused_media, self._selected_media )
if not do_not_show_just_return:
@ -4584,6 +4248,7 @@ class MediaPanelThumbnails( MediaPanel ):
return menu
def Sort( self, media_sort = None ):

View File

@ -3689,6 +3689,7 @@ class ReviewServiceLocalBooruSubPanel( ClientGUICommon.StaticBox ):
self._my_updater = ClientGUIAsync.FastThreadToGUIUpdater( self, self._Refresh )
self._service_status = ClientGUICommon.BetterStaticText( self )
self._service_status.setWordWrap( True )
booru_share_panel = ClientGUIListCtrl.BetterListCtrlPanel( self )
@ -3897,6 +3898,8 @@ class ReviewServiceLocalBooruSubPanel( ClientGUICommon.StaticBox ):
status += ' NOTE: I am sunsetting this service in the coming weeks, everything will be removed.'
self._service_status.setText( status )
CG.client_controller.CallToThread( self.THREADFetchInfo, self._service )

View File

@ -5,6 +5,7 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
@ -431,6 +432,12 @@ class SimpleSubPanel( QW.QWidget ):
self._duplicate_type = ClientGUICommon.BetterRadioBox( self._duplicates_type_panel, choices = choices )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._duplicate_type, CC.FLAGS_EXPAND_BOTH_WAYS )
self._duplicates_type_panel.setLayout( vbox )
#
self._thumbnail_rearrange_panel = QW.QWidget( self )
@ -448,8 +455,6 @@ class SimpleSubPanel( QW.QWidget ):
self._thumbnail_rearrange_type.addItem( CAC.move_enum_to_str_lookup[ rearrange_type ], rearrange_type )
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._thumbnail_rearrange_type, CC.FLAGS_EXPAND_BOTH_WAYS )
@ -458,14 +463,6 @@ class SimpleSubPanel( QW.QWidget ):
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._duplicate_type, CC.FLAGS_EXPAND_BOTH_WAYS )
self._duplicates_type_panel.setLayout( vbox )
#
self._seek_panel = QW.QWidget( self )
choices = [
@ -478,15 +475,11 @@ class SimpleSubPanel( QW.QWidget ):
self._seek_duration_s = ClientGUICommon.BetterSpinBox( self._seek_panel, max=3599, width = 60 )
self._seek_duration_ms = ClientGUICommon.BetterSpinBox( self._seek_panel, max=999, width = 60 )
#
self._seek_duration_s.setValue( 5 )
self._seek_duration_ms.setValue( 0 )
self._seek_duration_s.value() * 1000 + self._seek_duration_ms.value()
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._seek_direction, CC.FLAGS_EXPAND_BOTH_WAYS )
@ -514,8 +507,6 @@ class SimpleSubPanel( QW.QWidget ):
self._move_direction.addItem( CAC.move_enum_to_str_lookup[ m ], m )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._selection_status, CC.FLAGS_CENTER )
@ -541,14 +532,6 @@ class SimpleSubPanel( QW.QWidget ):
self._file_filter.addItem( file_filter.ToString(), file_filter )
#
self._hamming_distance_panel = QW.QWidget( self )
self._hamming_distance = ClientGUICommon.BetterSpinBox( self._hamming_distance_panel, min = 0, max = 64 )
#
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, self._file_filter, CC.FLAGS_CENTER )
@ -557,6 +540,10 @@ class SimpleSubPanel( QW.QWidget ):
#
self._hamming_distance_panel = QW.QWidget( self )
self._hamming_distance = ClientGUICommon.BetterSpinBox( self._hamming_distance_panel, min = 0, max = 64 )
rows = []
rows.append( ( 'Search distance:', self._hamming_distance ) )
@ -567,6 +554,102 @@ class SimpleSubPanel( QW.QWidget ):
#
self._file_command_target_panel = QW.QWidget( self )
choices = [ ( CAC.file_command_target_enum_to_str_lookup[ file_command_target ], file_command_target ) for file_command_target in ( CAC.FILE_COMMAND_TARGET_SELECTED_FILES, CAC.FILE_COMMAND_TARGET_FOCUSED_FILE ) ]
self._file_command_target = ClientGUICommon.BetterRadioBox( self._file_command_target_panel, choices = choices )
self._file_command_target.SetValue( CAC.FILE_COMMAND_TARGET_SELECTED_FILES )
self._file_command_target.setToolTip( 'This is only important in the thumbnail view, where the "focused file" means the one currently in the preview view, usually the one you last clicked on. In the media viewer, actions are always applied to the current file.' )
rows = []
rows.append( ( 'Files to apply to:', self._file_command_target ) )
gridbox = ClientGUICommon.WrapInGrid( self._file_command_target_panel, rows )
self._file_command_target_panel.setLayout( gridbox )
#
self._bitmap_type_panel = QW.QWidget( self )
self._bitmap_type = ClientGUICommon.BetterChoice( self._bitmap_type_panel )
for bitmap_type in (
CAC.BITMAP_TYPE_FULL,
CAC.BITMAP_TYPE_SOURCE_LOOKUPS,
CAC.BITMAP_TYPE_THUMBNAIL,
CAC.BITMAP_TYPE_FULL_OR_FILE
):
self._bitmap_type.addItem( CAC.bitmap_type_enum_to_str_lookup[ bitmap_type ], bitmap_type )
self._bitmap_type.SetValue( CAC.BITMAP_TYPE_FULL )
rows = []
rows.append( ( 'Bitmap to copy:', self._bitmap_type ) )
gridbox = ClientGUICommon.WrapInGrid( self._bitmap_type_panel, rows )
self._bitmap_type_panel.setLayout( gridbox )
#
self._hash_type_panel = QW.QWidget( self )
self._hash_type = ClientGUICommon.BetterChoice( self._hash_type_panel )
for hash_type in (
'sha256',
'md5',
'sha1',
'sha512',
'blurhash',
'pixel_hash'
):
self._hash_type.addItem( hash_type, hash_type )
self._hash_type.SetValue( 'sha256' )
rows = []
rows.append( ( 'Hash type to copy:', self._hash_type ) )
gridbox = ClientGUICommon.WrapInGrid( self._hash_type_panel, rows )
self._hash_type_panel.setLayout( gridbox )
#
self._ipfs_service_panel = QW.QWidget( self )
self._ipfs_service_key = ClientGUICommon.BetterChoice( self._ipfs_service_panel )
for service in CG.client_controller.services_manager.GetServices( ( HC.IPFS, ) ):
name = service.GetName()
service_key = service.GetServiceKey()
self._ipfs_service_key.addItem( name, service_key )
rows = []
rows.append( ( 'Service to copy:', self._ipfs_service_key ) )
gridbox = ClientGUICommon.WrapInGrid( self._ipfs_service_panel, rows )
self._ipfs_service_panel.setLayout( gridbox )
#
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._simple_actions, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
@ -576,6 +659,10 @@ class SimpleSubPanel( QW.QWidget ):
QP.AddToLayout( vbox, self._file_filter_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._hamming_distance_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._thumbnail_rearrange_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._file_command_target_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._bitmap_type_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._hash_type_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
QP.AddToLayout( vbox, self._ipfs_service_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.setLayout( vbox )
@ -588,12 +675,24 @@ class SimpleSubPanel( QW.QWidget ):
action = self._simple_actions.GetValue()
file_command_target_actions = {
CAC.SIMPLE_COPY_FILES,
CAC.SIMPLE_COPY_FILE_PATHS,
CAC.SIMPLE_COPY_FILE_ID,
CAC.SIMPLE_COPY_FILE_HASHES,
CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES
}
self._thumbnail_rearrange_panel.setVisible( action == CAC.SIMPLE_REARRANGE_THUMBNAILS )
self._duplicates_type_panel.setVisible( action == CAC.SIMPLE_SHOW_DUPLICATES )
self._seek_panel.setVisible( action == CAC.SIMPLE_MEDIA_SEEK_DELTA )
self._thumbnail_move_panel.setVisible( action == CAC.SIMPLE_MOVE_THUMBNAIL_FOCUS )
self._file_filter_panel.setVisible( action == CAC.SIMPLE_SELECT_FILES )
self._hamming_distance_panel.setVisible( action == CAC.SIMPLE_OPEN_SIMILAR_LOOKING_FILES )
self._hash_type_panel.setVisible( action == CAC.SIMPLE_COPY_FILE_HASHES )
self._ipfs_service_panel.setVisible( action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES )
self._file_command_target_panel.setVisible( action in file_command_target_actions )
self._bitmap_type_panel.setVisible( action == CAC.SIMPLE_COPY_FILE_BITMAP )
def GetValue( self ):
@ -646,6 +745,37 @@ class SimpleSubPanel( QW.QWidget ):
simple_data = ( CAC.REARRANGE_THUMBNAILS_TYPE_COMMAND, rearrange_type )
elif action in ( CAC.SIMPLE_COPY_FILES, CAC.SIMPLE_COPY_FILE_PATHS, CAC.SIMPLE_COPY_FILE_ID ):
file_command_target = self._file_command_target.GetValue()
simple_data = file_command_target
elif action == CAC.SIMPLE_COPY_FILE_BITMAP:
bitmap_type = self._bitmap_type.GetValue()
simple_data = bitmap_type
elif action == CAC.SIMPLE_COPY_FILE_HASHES:
file_command_target = self._file_command_target.GetValue()
hash_type = self._hash_type.GetValue()
simple_data = ( file_command_target, hash_type )
elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES:
file_command_target = self._file_command_target.GetValue()
ipfs_service_key = self._ipfs_service_key.GetValue()
hacky_ipfs_dict = HydrusSerialisable.SerialisableDictionary()
hacky_ipfs_dict[ 'file_command_target' ] = file_command_target
hacky_ipfs_dict[ 'ipfs_service_key' ] = ipfs_service_key
simple_data = hacky_ipfs_dict
else:
simple_data = None
@ -705,10 +835,37 @@ class SimpleSubPanel( QW.QWidget ):
self._thumbnail_rearrange_type.SetValue( rearrange_data )
elif action in ( CAC.SIMPLE_COPY_FILES, CAC.SIMPLE_COPY_FILE_PATHS, CAC.SIMPLE_COPY_FILE_ID ):
file_command_target = command.GetSimpleData()
self._file_command_target.SetValue( file_command_target )
elif action == CAC.SIMPLE_COPY_FILE_BITMAP:
bitmap_type = command.GetSimpleData()
self._bitmap_type.SetValue( bitmap_type )
elif action == CAC.SIMPLE_COPY_FILE_HASHES:
( file_command_target, hash_type ) = command.GetSimpleData()
self._file_command_target.SetValue( file_command_target )
self._hash_type.SetValue( hash_type )
elif action == CAC.SIMPLE_COPY_FILE_SERVICE_FILENAMES:
hacky_ipfs_dict = command.GetSimpleData()
self._file_command_target.SetValue( hacky_ipfs_dict[ 'file_command_target' ] )
self._ipfs_service_key.SetValue( hacky_ipfs_dict[ 'ipfs_service_key' ] )
self._UpdateControls()
class TagSubPanel( QW.QWidget ):
def __init__( self, parent: QW.QWidget ):

View File

@ -307,7 +307,7 @@ class Media( object ):
return self.__hash__() != other.__hash__()
def GetDisplayMedia( self ) -> 'Media':
def GetDisplayMedia( self ) -> 'MediaSingleton':
raise NotImplementedError()

View File

@ -105,7 +105,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 571
SOFTWARE_VERSION = 572
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -550,6 +550,7 @@ class SerialisableDictionary( SerialisableBase, dict ):
SERIALISABLE_TYPES_TO_OBJECT_TYPES[ SERIALISABLE_TYPE_DICTIONARY ] = SerialisableDictionary
# yo now that SerialisableDict can handle bytes anywhere, is this guy obsolete?
class SerialisableBytesDictionary( SerialisableBase, dict ):
SERIALISABLE_TYPE = SERIALISABLE_TYPE_BYTES_DICT

View File

@ -1276,7 +1276,7 @@ class TestClientDB( unittest.TestCase ):
test_files.append( ( 'muh_swf.swf', 'edfef9905fdecde38e0752a5b6ab7b6df887c3968d4246adc9cffc997e168cdf', 456774, HC.APPLICATION_FLASH, 400, 400, { 33 }, { 1 }, True, None ) )
test_files.append( ( 'muh_mp4.mp4', '2fa293907144a046d043d74e9570b1c792cbfd77ee3f5c93b2b1a1cb3e4c7383', 570534, HC.VIDEO_MP4, 480, 480, { 6266, 6290 }, { 151 }, True, None ) )
test_files.append( ( 'muh_mpeg.mpeg', 'aebb10aaf3b27a5878fd2732ea28aaef7bbecef7449eaa759421c4ba4efff494', 772096, HC.VIDEO_MPEG, 657, 480, { 3500 }, { 105 }, False, None ) ) # not actually 720, as this has mickey-mouse SAR, it turns out
test_files.append( ( 'muh_mpeg.mpeg', 'aebb10aaf3b27a5878fd2732ea28aaef7bbecef7449eaa759421c4ba4efff494', 772096, HC.VIDEO_MPEG, 657, 480, { 3490, 3500 }, { 105 }, False, None ) ) # not actually 720, as this has mickey-mouse SAR, it turns out
test_files.append( ( 'muh_webm.webm', '55b6ce9d067326bf4b2fbe66b8f51f366bc6e5f776ba691b0351364383c43fcb', 84069, HC.VIDEO_WEBM, 640, 360, { 4010 }, { 120 }, True, None ) )
test_files.append( ( 'muh_jpg.jpg', '5d884d84813beeebd59a35e474fa3e4742d0f2b6679faa7609b245ddbbd05444', 42296, HC.IMAGE_JPEG, 392, 498, { None }, { None }, False, None ) )
test_files.append( ( 'muh_png.png', 'cdc67d3b377e6e1397ffa55edc5b50f6bdf4482c7a6102c6f27fa351429d6f49', 31452, HC.IMAGE_PNG, 191, 196, { None }, { None }, False, None ) )
@ -1286,7 +1286,7 @@ class TestClientDB( unittest.TestCase ):
file_import_options = FileImportOptions.FileImportOptions()
file_import_options.SetIsDefault( True )
for ( filename, hex_hash, size, mime, width, height, durations, num_frames, has_audio, num_words ) in test_files:
for ( filename, hex_hash, size, mime, width, height, durations, possible_num_frames, has_audio, num_words ) in test_files:
HG.test_controller.SetRead( 'hash_status', ClientImportFiles.FileImportStatus.STATICGetUnknownStatus() )
@ -1326,8 +1326,29 @@ class TestClientDB( unittest.TestCase ):
self.assertEqual( mr_mime, mime )
self.assertEqual( mr_width, width )
self.assertEqual( mr_height, height )
self.assertIn( mr_duration, durations )
self.assertIn( mr_num_frames, num_frames )
if mr_duration is None:
self.assertIn( mr_duration, durations )
else:
duration_tests = { duration * 0.8 <= mr_duration <= duration * 1.2 for duration in durations }
self.assertIn( True, duration_tests )
if mr_num_frames is None:
self.assertIn( mr_num_frames, possible_num_frames )
else:
num_frames_tests = { num_frames * 0.8 <= mr_num_frames <= num_frames * 1.2 for num_frames in possible_num_frames }
self.assertIn( True, num_frames_tests )
self.assertEqual( mr_has_audio, has_audio )
self.assertEqual( mr_num_words, num_words )

View File

@ -8,6 +8,7 @@ from hydrus.core import HydrusExceptions
from hydrus.client import ClientParsing
from hydrus.client import ClientStrings
from hydrus.client import ClientTime
class DummyFormula( ClientParsing.ParseFormula ):
@ -221,10 +222,13 @@ class TestStringConverter( unittest.TestCase ):
#
string_converter = ClientStrings.StringConverter( conversions = [ ( ClientStrings.STRING_CONVERSION_DATEPARSER_DECODE, None ) ] )
self.assertEqual( string_converter.Convert( '1970-01-02 00:00:00 UTC' ), '86400' )
self.assertEqual( string_converter.Convert( 'January 12, 2012 10:00 PM EST' ), '1326423600' )
if ClientTime.DATEPARSER_OK:
string_converter = ClientStrings.StringConverter( conversions = [ ( ClientStrings.STRING_CONVERSION_DATEPARSER_DECODE, None ) ] )
self.assertEqual( string_converter.Convert( '1970-01-02 00:00:00 UTC' ), '86400' )
self.assertEqual( string_converter.Convert( 'January 12, 2012 10:00 PM EST' ), '1326423600' )
#

670
static/qss/e621.qss Normal file
View File

@ -0,0 +1,670 @@
/*******************************************************************************
An e621.net-inspired style/theme for Hydrus.
Created by IchHabs in February 2024.
Please enable darkmode and consider using the following colors:
- Thumbnail Background:
- local, normal: #152f56
- local, selected: #25477b
- not local, normal: #a0522d
- not local, selected: #ed5d1f
- Thumbnail Border:
- local, normal: #152f56
- local, selected: #b4c7d9
- not local, normal: #a0522d
- not local, selected: #b4c7d9
- Thumbnail Grid Background: #020f23
- Autocomplete Background: #ffffcc
- Media Viewer Background: #020f23
- Media Viewer Text: #b4c7d9
- Tabs Box Background: #152f56
If you want to change your tag namespace colors as well, try these:
- Character: #00aa00
- Copyright: #dd00dd
- Creator: #f2ac08
- Invalid: #ff0000
- Lore: #228822
- Meta: #ffffff
- Series: #3b81ea
- Species: #ed5d1f
- System: #ffffcc
- Unnamespaced: #b4c7d9
*******************************************************************************/
/*******************************************************************************
Development Notes:
- All default stylesheets mention QGroupBox,
but the actual Hydrus repository does not mention it once.
- Hydrus uses QTreeView/Widget for tables. (???)
- Qt reads hex colors as #RGB, #RRGGBB or #AARRGGBB. This file exclusivly uses
#RRGGBB and #AARRGGBB for all colors to aid with search/replace.
*******************************************************************************/
/********************************** Tool Tip **********************************/
/* Text that shows up when hovering over something. */
/* QToolTip {} */
/* Not included because the 'color' property is buggy. The default is fine. */
/******************************* Generic Stuff ********************************/
/* Pretty much everything is a widget. */
QWidget {
color: #ffffff;
background: #020f23;
}
.QWidget {
background-image: url("static/qss/e621/bg.svg");
background-position: center;
background-repeat: repeat-xy;
}
QWidget:disabled {
color: #767676;
}
QWidget::item {
color: #b4c7d9;
border-radius: 2px;
}
QWidget::item:hover {
color: #e9f2fa;
background: #1f3c67;
}
QWidget::item:selected {
color: #ffffff;
background: #2b538e;
}
QWidget::item:pressed {
color: #e8c446;
background: #2b538e;
}
QWidget#HydrusAnimationBar {
qproperty-hab_border: #44000000;
qproperty-hab_background: #44ffffff;
qproperty-hab_nub: #ffffff;
}
/* Most things are inside of a frame. */
QFrame {
border: 0;
padding: 0;
margin: 0;
}
/****************************** Menu + Menu Bar *******************************/
/* The bar at the top of the main window, with stuff like 'file' and 'help'. */
QMenuBar {
color: #ffffff;
background: #020f23;
spacing: 0;
}
QMenuBar::item {
color: #b4c7d9;
padding: 1px 0.5em;
}
QMenuBar::item:selected {
color: #ffffff;
background: #152f56;
}
QMenuBar::item:pressed {
color: #e8c446;
background: #152f56;
}
/* The list that pops up when you click on 'file', or right-click an image. */
QMenu {
color: #ffffff;
background: #1f3c67;
}
QMenu::item {
color: #b4c7d9;
background: #1f3c67;
padding: 0.25em 16px 0.25em 4px;
}
QMenu::item:selected {
color: #e9f2fa;
background: #2b538e;
}
QMenu::item:pressed {
color: #e8c446;
background: #2b538e;
}
QMenu::separator {
background: #152f56;
height: 2px;
margin: 2px 3px;
}
/******************************* Tables + Lists *******************************/
/* Table/List rows. */
QAbstractItemView {
background: #152f56;
alternate-background-color: #40020f23;
}
QAbstractItemView::item {
border-top: 1px solid #402B538E;
border-right: 1px solid #40020f23;
border-bottom: 1px solid #40020f23;
border-left: 1px solid #402B538E;
}
/* Table headers. */
QHeaderView::section {
color: #b4c7d9;
background: #193153;
border-bottom: 2px solid #020f23;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
padding-left: 4px;
padding-right: 4px;
}
QHeaderView::section:hover {
color: #e9f2fa;
}
QHeaderView::section:selected {
color: #ffffff;
}
QHeaderView::section:pressed {
color: #e8c446;
}
/* Intended for expandable folder UI. Used for tables instead. */
QTreeWidget, QTreeWidget::item {
border-radius: 2px;
padding: 1px;
}
/* Lists with (often) clickable items. */
QListView, QListView::item {
border: none;
padding: 1px;
}
/********************************** Buttons ***********************************/
/* Drop-down button field. The pop-up menu is a separate list object. */
QComboBox {
color: #000000;
background: #e9e9ed;
/* Undocumented property! combobox-popup: <boolean> */
/* Which style of popup list to use. Fusion defaults to 1. */
/* Disabled due to buggy behaviour. */
combobox-popup: 0;
border: none;
border-radius: 2px;
padding: 0 3px 0 5px;
margin: 0 1px;
}
QComboBox:hover {
background: #ffffff;
border: 2px solid #e59700;
padding: 0 1px 0 3px;
}
QComboBox:on {
background: #ffffcc;
selection-background-color: #0078d7;
border: 2px solid #e59700;
padding: 0 1px 0 3px;
padding-top: 2px;
}
/* Drop-down arrow icon. */
QComboBox::drop-down {
border: none;
width: 9px;
padding: 3px;
subcontrol-origin: content;
subcontrol-position: top right;
}
QComboBox::down-arrow {
image: url("static/qss/e621/dropdown.svg");
}
/* Generic buttons. */
QPushButton {
color: #000000;
background: #e9e9ed;
border: none;
border-radius: 6px;
min-height: 21px;
min-width: 24px;
padding: 0 3px;
}
QPushButton:disabled {
color: #767676;
background: #d0d0d7;
}
QPushButton:default {
border: 2px solid #e59700;
padding: 0 1px;
}
QPushButton:hover {
background: #ffffff;
border: 2px solid #e59700;
padding: 0 1px;
}
QPushButton:pressed {
background: #ffffcc;
padding-top: 2px;
}
QPushButton#HydrusAccept,
QPushButton#HydrusOnOffButton[hydrus_on=true] {
color: #ffffff;
background: #006400;
min-height: 21px;
min-width: 54px;
margin: 0 3px;
}
QPushButton#HydrusCancel,
QPushButton#HydrusOnOffButton[hydrus_on=false] {
color: #ffffff;
background: #800000;
min-height: 21px;
min-width: 54px;
margin: 0 3px;
}
QPushButton#HydrusAccept:default,
QPushButton#HydrusOnOffButton[hydrus_on=true]:default {
border: 2px solid #3e9e49;
}
QPushButton#HydrusCancel:default,
QPushButton#HydrusOnOffButton[hydrus_on=false]:default {
border: 2px solid #e45f5f;
}
QPushButton#HydrusAccept:hover,
QPushButton#HydrusOnOffButton[hydrus_on=true]:hover {
border: 2px solid #3e9e49;
background: #004b00;
}
QPushButton#HydrusCancel:hover,
QPushButton#HydrusOnOffButton[hydrus_on=false]:hover {
border: 2px solid #e45f5f;
background: #670000;
}
QPushButton#HydrusAccept:pressed,
QPushButton#HydrusOnOffButton[hydrus_on=true]:pressed {
padding-top: 2px;
}
QPushButton#HydrusCancel:pressed,
QPushButton#HydrusOnOffButton[hydrus_on=false]:pressed {
padding-top: 2px;
}
/* Square button with a checkmark. */
QCheckBox {
background: transparent;
spacing: 5px;
}
QCheckBox::indicator {
width: 14px;
height: 14px;
}
QCheckBox::indicator:unchecked {
image: url("static/qss/e621/cbox-un.svg");
}
QCheckBox::indicator:unchecked:hover {
image: url("static/qss/e621/cbox-un-ho.svg");
}
QCheckBox::indicator:unchecked:pressed {
image: url("static/qss/e621/cbox-un-pr.svg");
}
QCheckBox::indicator:checked {
image: url("static/qss/e621/cbox-ch.svg");
}
QCheckBox::indicator:checked:hover {
image: url("static/qss/e621/cbox-ch-ho.svg");
}
QCheckBox::indicator:checked:pressed {
image: url("static/qss/e621/cbox-ch-pr.svg");
}
QCheckBox::indicator:indeterminate {
image: url("static/qss/e621/cbox-in.svg");
}
QCheckBox::indicator:indeterminate:hover {
image: url("static/qss/e621/cbox-in-ho.svg");
}
QCheckBox::indicator:indeterminate:pressed {
image: url("static/qss/e621/cbox-in-pr.svg");
}
QCheckBox#HydrusWarning {
color: #ffffff;
background: #670000;
}
/* Circular button with a dot. */
QRadioButton {
background: transparent;
spacing: 5px;
}
QRadioButton::indicator {
width: 14px;
height: 14px;
}
QRadioButton::indicator:unchecked {
image: url("static/qss/e621/rbtn-un.svg");
}
QRadioButton::indicator:unchecked:hover {
image: url("static/qss/e621/rbtn-un-ho.svg");
}
QRadioButton::indicator:unchecked:pressed {
image: url("static/qss/e621/rbtn-un-pr.svg");
}
QRadioButton::indicator:checked {
image: url("static/qss/e621/rbtn-ch.svg");
}
QRadioButton::indicator:checked:hover {
image: url("static/qss/e621/rbtn-ch-ho.svg");
}
QRadioButton::indicator:checked:pressed {
image: url("static/qss/e621/rbtn-ch-pr.svg");
}
/********************************* Scroll Bar *********************************/
/* Visual indicator for scrolling vertically or horizontally. */
QScrollBar {
background: #071020;
}
QScrollBar:vertical {
width: 13px;
margin: 13px 0;
}
QScrollBar:horizontal {
height: 13px;
margin: 0 13px;
}
/* Handle - The bar in the middle that you can drag with your mouse. */
QScrollBar::handle {
background: #77ffffff;
border-radius: 2px;
}
QScrollBar::handle:hover {
background: #55ffffff;
}
QScrollBar::handle:pressed {
background: #33ffffff;
}
QScrollBar::handle:vertical {
min-height: 13px;
}
QScrollBar::handle:horizontal {
min-width: 13px;
}
/* Add/Subtract Line Buttons - The buttons at each end. */
QScrollBar::sub-line,
QScrollBar::add-line {
background: #00ffffff;
}
QScrollBar::sub-line:hover,
QScrollBar::add-line:hover {
background: #11ffffff;
}
QScrollBar::sub-line:pressed,
QScrollBar::add-line:pressed {
background: #22ffffff;
}
QScrollBar::add-line:vertical {
height: 13px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
height: 13px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:horizontal {
width: 13px;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
width: 13px;
subcontrol-position: left;
subcontrol-origin: margin;
}
/* Arrows - Decorative arrow inside the add/sub buttons. */
QScrollBar::up-arrow:vertical {
image: url("static/qss/e621/sbar-u.svg");
}
QScrollBar::down-arrow:vertical {
image: url("static/qss/e621/sbar-d.svg");
}
QScrollBar::right-arrow:horizontal {
image: url("static/qss/e621/sbar-r.svg");
}
QScrollBar::left-arrow:horizontal {
image: url("static/qss/e621/sbar-l.svg");
}
/* Add/Subtract Page Buttons - Area below/above the handle. */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical,
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
background: transparent;
}
/********************************** Spin Box **********************************/
/* Like a line edit box, but it only allows numbers. */
/* Also includes Date Time Edit as it's visually the same thing. */
QSpinBox,
QDateTimeEdit {
font-family: "Consolas","Liberation Mono","Courier New",monospace;
color: #000000;
background: #ffffff;
selection-color: #ffffff;
selection-background-color: #0078d7;
min-height: 21px;
min-width: 42px;
border-radius: 2px;
padding-right: 3px;
}
QSpinBox:disabled,
QDateTimeEdit:disabled {
color: #767676;
background: #d0d0d7;
}
QSpinBox:focus,
QDateTimeEdit:focus {
background: #ffffcc;
placeholder-text-color: #76765e;
}
QSpinBox::up-button,
QDateTimeEdit::up-button {
background: #e9e9ed;
width: 18px;
border-radius: 2px;
subcontrol-origin: border;
subcontrol-position: top right;
}
QSpinBox::down-button,
QDateTimeEdit::down-button {
background: #e9e9ed;
width: 18px;
border-radius: 2px;
subcontrol-origin: border;
subcontrol-position: bottom right;
}
QSpinBox::up-arrow,
QDateTimeEdit::up-arrow {
image: url("static/qss/e621/sbox-u.svg");
width: 8px;
height: 8px;
}
QSpinBox::down-arrow,
QDateTimeEdit::down-arrow {
image: url("static/qss/e621/sbox-d.svg");
width: 8px;
height: 8px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover,
QDateTimeEdit::up-button:hover, QDateTimeEdit::down-button:hover {
background: #d0d0d7;
}
QSpinBox::up-button:pressed, QSpinBox::down-button:pressed,
QDateTimeEdit::up-button:pressed, QDateTimeEdit::down-button:pressed {
background: #b4b4be;
}
QSpinBox::up-button:disabled, QSpinBox::up-button:off,
QSpinBox::down-button:disabled, QSpinBox::down-button:off,
QDateTimeEdit::up-button:disabled, QDateTimeEdit::up-button:off,
QDateTimeEdit::down-button:disabled, QDateTimeEdit::down-button:off {
background: #767676;
}
/*********************** Line, Text and Plain Text Edit ***********************/
QLineEdit {
font-family: "Consolas","Liberation Mono","Courier New",monospace;
color: #000000;
background: #ffffff;
selection-color: #ffffff;
selection-background-color: #0078d7;
placeholder-text-color: #767676;
border-radius: 2px;
min-height: 21px;
min-width: 42px;
}
QTextEdit, QPlainTextEdit {
font-family: "Consolas","Liberation Mono","Courier New",monospace;
color: #000000;
background: #ffffff;
selection-color: #ffffff;
selection-background-color: #0078d7;
placeholder-text-color: #767676;
border-radius: 2px;
min-height: 21px;
min-width: 42px;
}
QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled {
color: #767676;
background: #e9e9ed;
placeholder-text-color: #767676;
}
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
background: #ffffcc;
placeholder-text-color: #76765e;
}
QLineEdit:read-only, QTextEdit:read-only, QPlainTextEdit:read-only {
background: #d0d0d7;
}
QLineEdit#HydrusValid,
QTextEdit#HydrusValid,
QPlainTextEdit#HydrusValid {
background: #006400;
}
QLineEdit#HydrusIndeterminate,
QTextEdit#HydrusIndeterminate,
QPlainTextEdit#HydrusIndeterminate {
background: #5d4600;
}
QLineEdit#HydrusInvalid,
QTextEdit#HydrusInvalid,
QPlainTextEdit#HydrusInvalid {
background: #800000;
}
/******************************** Progress Bar ********************************/
QProgressBar {
color: #ffffff;
background: #152f56;
text-align: center;
}
QProgressBar::chunk {
background-color: #006400;
}
/**************************** Tab Widget + Tab Bar ****************************/
/* Areas with multiple tabs, like pages and tagging. */
/* QTabWidget: The area of a tab. */
QTabWidget::pane {
color: #ffffff;
background: #020f23;
border: none;
border: 3px solid #152f56;
border-radius: 3px;
}
QTabWidget::tab-bar {
left: 3px;
}
/* QTabBar: The clickable tab labels. */
QTabBar {
color: #b4c7d9;
background: #020f23;
border: none;
}
QTabBar::tab {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 0.25em 0.5em;
}
QTabBar::tab:hover {
color: #e9f2fa;
}
QTabBar::tab:selected {
color: #ffffff;
background: #152f56;
}
QTabBar::tab:pressed {
color: #e8c446;
background: #152f56;
}
/*********************************** Label ************************************/
/* Text labels. */
QLabel#HydrusHyperlink {
qproperty-link_color: #b4c7d9;
}
QLabel#HydrusValid {
color: #3e9e49;
}
QLabel#HydrusIndeterminate {
color: #ffe666;
}
QLabel#HydrusInvalid {
color: #e45f5f;
}
QLabel#HydrusWarning {
color: #ffffff;
background: #670000;
border-left: 3px solid #e45f5f;
padding: 0.5em;
}
/****************************** Hydrus: Locator *******************************/
/* The command palette. (Ctrl+P) */
QLocatorResultWidget#unselectedLocatorResult {
background: #1f3c67;
border-bottom: 1px solid#2b538e;
}
QLocatorResultWidget#selectedLocatorResult {
background: #2b538e;
border-bottom: 1px solid#2b538e;
}

1
static/qss/e621/bg.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="76" height="44"><path d="M27 41h22l11-19L49 3H27L16 22Zm38-22h22v6H65L54 44l11 19-5 3-11-19H27L16 66l-5-3 11-19-11-19h-22v-6h22L22 0 11-19l5-3L27-3h22l11-19 5 3L54 0z" style="fill:#fff;fill-opacity:.03137255;stroke:none"/><path d="M27 3 16 22l.4342.75L27 4.5h22l10.5658 18.25L60 22 49 3ZM11-17.5l.6445-.3867L22 0l-.4342.75ZM54 0l.4342.75L65-17.5l-.6504-.3766Zm33 26.5V25H65L54 44l.4342.75L65 26.5ZM21.5658 44.75 22 44 11 25h-22v1.5h22z" style="fill:#000;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:.26274511"/></svg>

After

Width:  |  Height:  |  Size: 641 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#2374ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><path d="m3 6 3 3H5l6-7h1v2l-6 7H5L2 8V6Z" style="fill:#fff;fill-opacity:1;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#3e90ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><path d="m3 6 3 3H5l6-7h1v2l-6 7H5L2 8V6Z" style="fill:#fff;fill-opacity:1;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#0060df;fill-opacity:1;fill-rule:evenodd;stroke:none"/><path d="m3 6 3 3H5l6-7h1v2l-6 7H5L2 8V6Z" style="fill:#fff;fill-opacity:1;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#2374ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="3" x="2" y="5.5" rx="1.5" ry="1.5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#3e90ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="3" x="2" y="5.5" rx="1.5" ry="1.5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#0060df;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="3" x="2" y="5.5" rx="1.5" ry="1.5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#676774;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="10" x="2" y="2" rx="0" ry="0" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#484851;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="10" x="2" y="2" rx="0" ry="0" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><rect width="14" height="14" rx="2" ry="2" style="fill:#8f8f9d;fill-opacity:1;fill-rule:evenodd;stroke:none"/><rect width="10" height="10" x="2" y="2" rx="0" ry="0" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="9" height="9"><path d="m2 2.5 3 3 3-3h1V4L5.5 7.5h-1L1 4V2.5Z" style="fill:#000;fill-opacity:.9;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#2374ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="3" style="fill:#2374ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#3e90ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="3" style="fill:#3e90ff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#0060df;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="3" style="fill:#0060df;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#676774;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#484851;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="14" height="14"><circle cx="7" cy="7" r="7" style="fill:#8f8f9d;fill-opacity:1;fill-rule:evenodd;stroke:none"/><circle cx="7" cy="7" r="5" style="fill:#fff;fill-opacity:1;fill-rule:evenodd;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="9" height="9"><path d="m1.5 2.5 3 3 3-3h1v2L5 8H4L.5 4.5v-2z" style="fill:#fff;fill-opacity:.333333;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="9" height="9"><path d="m6.5 1.5-3 3 3 3v1h-2L1 5V4L4.5.5h2z" style="fill:#fff;fill-opacity:.333333;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 187 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="9" height="9"><path d="m2.5 7.5 3-3-3-3v-1h2L8 4v1L4.5 8.5h-2z" style="fill:#fff;fill-opacity:.333333;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="9" height="9"><path d="m7.5 6.5-3-3-3 3h-1v-2L4 1h1l3.5 3.5v2z" style="fill:#fff;fill-opacity:.333333;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="8" height="8"><path d="M1.5 2 4 4.5 6.5 2h1v1l-3 3h-1l-3-3V2Z" style="fill:#000;fill-opacity:.8;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="8" height="8"><path d="M6.5 6 4 3.5 1.5 6h-1V5l3-3h1l3 3v1z" style="fill:#000;fill-opacity:.8;stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 182 B

View File

@ -1,2 +1,2 @@
QtPy==2.4.1
PySide6==6.6.3
PySide6==6.6.3.1