diff --git a/docs/changelog.md b/docs/changelog.md
index f5471f74..ec7d7609 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -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
diff --git a/docs/old_changelog.html b/docs/old_changelog.html
index 71b84044..2f6976d7 100644
--- a/docs/old_changelog.html
+++ b/docs/old_changelog.html
@@ -34,6 +34,38 @@
+
+
+
+ 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
+
+
diff --git a/hydrus/client/ClientApplicationCommand.py b/hydrus/client/ClientApplicationCommand.py
index bb4ba13f..f7356f02 100644
--- a/hydrus/client/ClientApplicationCommand.py
+++ b/hydrus/client/ClientApplicationCommand.py
@@ -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()
diff --git a/hydrus/client/ClientController.py b/hydrus/client/ClientController.py
index 191b4ca3..6eee474f 100644
--- a/hydrus/client/ClientController.py
+++ b/hydrus/client/ClientController.py
@@ -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 ):
diff --git a/hydrus/client/ClientDefaults.py b/hydrus/client/ClientDefaults.py
index ef6892d5..87f9b446 100644
--- a/hydrus/client/ClientDefaults.py
+++ b/hydrus/client/ClientDefaults.py
@@ -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 )
diff --git a/hydrus/client/ClientOptions.py b/hydrus/client/ClientOptions.py
index 63aa8168..05671151 100644
--- a/hydrus/client/ClientOptions.py
+++ b/hydrus/client/ClientOptions.py
@@ -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
diff --git a/hydrus/client/caches/ClientCaches.py b/hydrus/client/caches/ClientCaches.py
index 7dd1dec2..b014da37 100644
--- a/hydrus/client/caches/ClientCaches.py
+++ b/hydrus/client/caches/ClientCaches.py
@@ -848,7 +848,7 @@ class ThumbnailCache( object ):
- def GetThumbnail( self, media ):
+ def GetThumbnail( self, media ) -> ClientRendering.HydrusBitmap:
display_media = media.GetDisplayMedia()
diff --git a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
index cf2abfb9..d8f97e6d 100644
--- a/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
+++ b/hydrus/client/gui/ClientGUIScrolledPanelsManagement.py
@@ -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 )
diff --git a/hydrus/client/gui/ClientGUIShortcuts.py b/hydrus/client/gui/ClientGUIShortcuts.py
index 19263ced..7226bd07 100644
--- a/hydrus/client/gui/ClientGUIShortcuts.py
+++ b/hydrus/client/gui/ClientGUIShortcuts.py
@@ -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,
diff --git a/hydrus/client/gui/ClientGUIStyle.py b/hydrus/client/gui/ClientGUIStyle.py
index 29524ea2..d93c5f05 100644
--- a/hydrus/client/gui/ClientGUIStyle.py
+++ b/hydrus/client/gui/ClientGUIStyle.py
@@ -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 )
diff --git a/hydrus/client/gui/canvas/ClientGUICanvas.py b/hydrus/client/gui/canvas/ClientGUICanvas.py
index 5c9a7242..0aa8b6e2 100644
--- a/hydrus/client/gui/canvas/ClientGUICanvas.py
+++ b/hydrus/client/gui/canvas/ClientGUICanvas.py
@@ -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 )
diff --git a/hydrus/client/gui/media/ClientGUIMediaMenus.py b/hydrus/client/gui/media/ClientGUIMediaMenus.py
index 361f143b..ded120a3 100644
--- a/hydrus/client/gui/media/ClientGUIMediaMenus.py
+++ b/hydrus/client/gui/media/ClientGUIMediaMenus.py
@@ -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' )
+
diff --git a/hydrus/client/gui/media/ClientGUIMediaModalActions.py b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
index 22341cfd..5f6dabca 100644
--- a/hydrus/client/gui/media/ClientGUIMediaModalActions.py
+++ b/hydrus/client/gui/media/ClientGUIMediaModalActions.py
@@ -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()
diff --git a/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py b/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py
index 5533734c..c9e9b517 100644
--- a/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py
+++ b/hydrus/client/gui/media/ClientGUIMediaSimpleActions.py
@@ -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, ) ) )
diff --git a/hydrus/client/gui/pages/ClientGUIResults.py b/hydrus/client/gui/pages/ClientGUIResults.py
index f4c5a1b1..de4de2ec 100644
--- a/hydrus/client/gui/pages/ClientGUIResults.py
+++ b/hydrus/client/gui/pages/ClientGUIResults.py
@@ -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 ):
diff --git a/hydrus/client/gui/services/ClientGUIClientsideServices.py b/hydrus/client/gui/services/ClientGUIClientsideServices.py
index 790b7510..06b2b07d 100644
--- a/hydrus/client/gui/services/ClientGUIClientsideServices.py
+++ b/hydrus/client/gui/services/ClientGUIClientsideServices.py
@@ -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 )
diff --git a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
index c5b776d8..313ba588 100644
--- a/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
+++ b/hydrus/client/gui/widgets/ClientGUIApplicationCommand.py
@@ -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 ):
diff --git a/hydrus/client/media/ClientMedia.py b/hydrus/client/media/ClientMedia.py
index f351f1b5..3d0f991e 100644
--- a/hydrus/client/media/ClientMedia.py
+++ b/hydrus/client/media/ClientMedia.py
@@ -307,7 +307,7 @@ class Media( object ):
return self.__hash__() != other.__hash__()
- def GetDisplayMedia( self ) -> 'Media':
+ def GetDisplayMedia( self ) -> 'MediaSingleton':
raise NotImplementedError()
diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py
index ddeb950a..c47ae3fc 100644
--- a/hydrus/core/HydrusConstants.py
+++ b/hydrus/core/HydrusConstants.py
@@ -105,7 +105,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
-SOFTWARE_VERSION = 571
+SOFTWARE_VERSION = 572
CLIENT_API_VERSION = 64
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
diff --git a/hydrus/core/HydrusSerialisable.py b/hydrus/core/HydrusSerialisable.py
index 76f7b3e4..5a178572 100644
--- a/hydrus/core/HydrusSerialisable.py
+++ b/hydrus/core/HydrusSerialisable.py
@@ -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
diff --git a/hydrus/test/TestClientDB.py b/hydrus/test/TestClientDB.py
index 520b75f2..f3f7b8ec 100644
--- a/hydrus/test/TestClientDB.py
+++ b/hydrus/test/TestClientDB.py
@@ -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 )
diff --git a/hydrus/test/TestClientParsing.py b/hydrus/test/TestClientParsing.py
index 7a52ddf2..ff40729d 100644
--- a/hydrus/test/TestClientParsing.py
+++ b/hydrus/test/TestClientParsing.py
@@ -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' )
+
#
diff --git a/static/qss/e621.qss b/static/qss/e621.qss
new file mode 100644
index 00000000..5fefdd45
--- /dev/null
+++ b/static/qss/e621.qss
@@ -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: */
+ /* 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;
+}
diff --git a/static/qss/e621/bg.svg b/static/qss/e621/bg.svg
new file mode 100644
index 00000000..fca57af1
--- /dev/null
+++ b/static/qss/e621/bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-ch-ho.svg b/static/qss/e621/cbox-ch-ho.svg
new file mode 100644
index 00000000..7c741f70
--- /dev/null
+++ b/static/qss/e621/cbox-ch-ho.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-ch-pr.svg b/static/qss/e621/cbox-ch-pr.svg
new file mode 100644
index 00000000..c2bba388
--- /dev/null
+++ b/static/qss/e621/cbox-ch-pr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-ch.svg b/static/qss/e621/cbox-ch.svg
new file mode 100644
index 00000000..e1c692e6
--- /dev/null
+++ b/static/qss/e621/cbox-ch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-in-ho.svg b/static/qss/e621/cbox-in-ho.svg
new file mode 100644
index 00000000..36c7b49c
--- /dev/null
+++ b/static/qss/e621/cbox-in-ho.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-in-pr.svg b/static/qss/e621/cbox-in-pr.svg
new file mode 100644
index 00000000..0945485a
--- /dev/null
+++ b/static/qss/e621/cbox-in-pr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-in.svg b/static/qss/e621/cbox-in.svg
new file mode 100644
index 00000000..732f07cd
--- /dev/null
+++ b/static/qss/e621/cbox-in.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-un-ho.svg b/static/qss/e621/cbox-un-ho.svg
new file mode 100644
index 00000000..c78c0a09
--- /dev/null
+++ b/static/qss/e621/cbox-un-ho.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-un-pr.svg b/static/qss/e621/cbox-un-pr.svg
new file mode 100644
index 00000000..dd677533
--- /dev/null
+++ b/static/qss/e621/cbox-un-pr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/cbox-un.svg b/static/qss/e621/cbox-un.svg
new file mode 100644
index 00000000..a3c476de
--- /dev/null
+++ b/static/qss/e621/cbox-un.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/dropdown.svg b/static/qss/e621/dropdown.svg
new file mode 100644
index 00000000..6938264f
--- /dev/null
+++ b/static/qss/e621/dropdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-ch-ho.svg b/static/qss/e621/rbtn-ch-ho.svg
new file mode 100644
index 00000000..45e7133b
--- /dev/null
+++ b/static/qss/e621/rbtn-ch-ho.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-ch-pr.svg b/static/qss/e621/rbtn-ch-pr.svg
new file mode 100644
index 00000000..e5a37a07
--- /dev/null
+++ b/static/qss/e621/rbtn-ch-pr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-ch.svg b/static/qss/e621/rbtn-ch.svg
new file mode 100644
index 00000000..2e2b10cb
--- /dev/null
+++ b/static/qss/e621/rbtn-ch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-un-ho.svg b/static/qss/e621/rbtn-un-ho.svg
new file mode 100644
index 00000000..5d5d4444
--- /dev/null
+++ b/static/qss/e621/rbtn-un-ho.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-un-pr.svg b/static/qss/e621/rbtn-un-pr.svg
new file mode 100644
index 00000000..f1e48502
--- /dev/null
+++ b/static/qss/e621/rbtn-un-pr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/rbtn-un.svg b/static/qss/e621/rbtn-un.svg
new file mode 100644
index 00000000..08fd0109
--- /dev/null
+++ b/static/qss/e621/rbtn-un.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbar-d.svg b/static/qss/e621/sbar-d.svg
new file mode 100644
index 00000000..6f79b581
--- /dev/null
+++ b/static/qss/e621/sbar-d.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbar-l.svg b/static/qss/e621/sbar-l.svg
new file mode 100644
index 00000000..43522236
--- /dev/null
+++ b/static/qss/e621/sbar-l.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbar-r.svg b/static/qss/e621/sbar-r.svg
new file mode 100644
index 00000000..5cd4af60
--- /dev/null
+++ b/static/qss/e621/sbar-r.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbar-u.svg b/static/qss/e621/sbar-u.svg
new file mode 100644
index 00000000..e0d718b6
--- /dev/null
+++ b/static/qss/e621/sbar-u.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbox-d.svg b/static/qss/e621/sbox-d.svg
new file mode 100644
index 00000000..41e001a6
--- /dev/null
+++ b/static/qss/e621/sbox-d.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/qss/e621/sbox-u.svg b/static/qss/e621/sbox-u.svg
new file mode 100644
index 00000000..4c15172f
--- /dev/null
+++ b/static/qss/e621/sbox-u.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/requirements/advanced/requirements_qt6_test.txt b/static/requirements/advanced/requirements_qt6_test.txt
index 0826beaa..160623a4 100644
--- a/static/requirements/advanced/requirements_qt6_test.txt
+++ b/static/requirements/advanced/requirements_qt6_test.txt
@@ -1,2 +1,2 @@
QtPy==2.4.1
-PySide6==6.6.3
+PySide6==6.6.3.1