Version 440

closes #846
This commit is contained in:
Hydrus Network Developer 2021-05-19 16:30:28 -05:00
parent 4d9199bfaa
commit 9e3f691519
20 changed files with 412 additions and 117 deletions

View File

@ -8,6 +8,30 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_440"><a href="#version_440">version 440</a></h3></li>
<ul>
<li>tiled renderer:</li>
<li>the tiled renderer now has an additional error catching layer for tile rendering and coordinate calculation and _should_ be immune to to the crashes we have seen from unhandled errors inside Qt paint events</li>
<li>when a tile fails to render, a full black square will be used instead. additional error information is quickly printed to the log</li>
<li>fixed a tile coordinate bug related to viewer initialisation and shutdown. when the coordinate space is currently bugnuts, now nothing is drawn</li>
<li>if the image renderer encounters a file that appears to have a different resolution to that stored in the db, it now gives you a popup and automatically schedules a metadata regen job for that file. this should catch legacy files with EXIF rotation that were imported before hydrus understood that info</li>
<li>when a file completes a metadata regen, if the resolution changed it now schedules a force-regen of the thumbnail too</li>
<li>.</li>
<li>the rest:</li>
<li>added a prototype 'delete lock' for archived files to _options->files and trash_ (issue #846). this will be expanded in future when the metadata conditional object is made to lock various other file states, and there will be some better UI feedback, a padlock icon or similar, and some improved dialog texts. if you use this, let me know how you get on!</li>
<li>you can now set a custom namespace sort in the file sort menu. you have to type it manually, like when setting defaults in the options, but it will save with the page and should load up again nicely in the dialog if you edit it. this is an experiment in prep for better namespace sort edit UI</li>
<li>fixed an issue sorting by namespaces when one of those namespaces was hidden in the 'single media' tag context. now all 'display' tags are used for sort comparison groups. if users desire the old behaviour, we'll have to add an option, so let me know</li>
<li>the various service-level processing errors when update files are missing or janked out now report the actual hash of the bad update file. I am chasing down one of these errors with a couple of users and cannot quite figure out why the repair code is not auto-fixing things</li>
<li>fixed a problem when the system tray gets an activate event at unlucky moments</li>
<li>the default media viewer zoom centerpoint is now the mouse</li>
<li>fixed a typo in the client api with wildcard/namespace tag search--sorry for the trouble!</li>
<li>.</li>
<li>some boring multiple local file services cleanup:</li>
<li>if you have a mixture of trash and normal thumbnails selected, the right-click menu now has separate choices for 'delete trash' and 'delete selected' 'physically now'</li>
<li>if you have a mixture of trash and normal thumbnails selected, the advanced delete dialog now similarly provides separate 'physical delete' options for the trashed vs all</li>
<li>media viewer, preview viewer, and thumbnail view delete menu service actions are now populated dynamically. it should say 'delete from my files' instead of just 'delete'</li>
<li>in some file selection contexts, the 'remote' filter is renamed to 'not local'</li>
</ul>
<li><h3 id="version_439"><a href="#version_439">version 439</a></h3></li>
<ul>
<li>tiled image renderer improvements:</li>

View File

@ -528,8 +528,15 @@ class DuplicateActionOptions( HydrusSerialisable.SerialisableBase ):
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
for media in deletee_media:
if delete_lock_for_archived_files and not media.HasInbox():
continue
if media.GetLocationsManager().IsTrashed():
deletee_service_keys = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, )

View File

@ -517,8 +517,15 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
service_keys_to_deletee_hashes = collections.defaultdict( list )
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
for media_result in media_results:
if delete_lock_for_archived_files and not media_result.GetInbox():
continue
hash = media_result.GetHash()
deletee_service_keys = media_result.GetLocationsManager().GetCurrent().intersection( local_file_service_keys )

View File

@ -235,6 +235,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'booleans' ][ 'show_session_size_warnings' ] = True
self._dictionary[ 'booleans' ][ 'delete_lock_for_archived_files' ] = False
#
self._dictionary[ 'colours' ] = HydrusSerialisable.SerialisableDictionary()
@ -314,7 +316,7 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
from hydrus.client.gui.canvas import ClientGUICanvas
self._dictionary[ 'integers' ][ 'media_viewer_zoom_center' ] = ClientGUICanvas.ZOOM_CENTERPOINT_VIEWER_CENTER
self._dictionary[ 'integers' ][ 'media_viewer_zoom_center' ] = ClientGUICanvas.ZOOM_CENTERPOINT_MOUSE
self._dictionary[ 'integers' ][ 'last_session_save_period_minutes' ] = 5

View File

@ -83,7 +83,7 @@ def GenerateHydrusBitmapFromPILImage( pil_image, compressed = True ):
class ImageRenderer( object ):
def __init__( self, media ):
def __init__( self, media, this_is_for_metadata_alone = False ):
self._numpy_image = None
@ -95,6 +95,8 @@ class ImageRenderer( object ):
self._path = None
self._this_is_for_metadata_alone = this_is_for_metadata_alone
HG.client_controller.CallToThread( self._Initialise )
@ -192,6 +194,28 @@ class ImageRenderer( object ):
self._numpy_image = ClientImageHandling.GenerateNumPyImage( self._path, self._mime )
if not self._this_is_for_metadata_alone:
my_resolution_size = QC.QSize( self._resolution[0], self._resolution[1] )
my_numpy_size = QC.QSize( self._numpy_image.shape[1], self._numpy_image.shape[0] )
if my_resolution_size != my_numpy_size:
HG.client_controller.Write( 'file_maintenance_add_jobs_hashes', { self._hash }, ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_METADATA )
m = 'There was a problem rendering the image with hash {}! Hydrus thinks its resolution is {}, but it was actually {}. Maybe hydrus missed rotation data when the file first imported?'.format(
self._hash.hex(),
my_resolution_size,
my_numpy_size
)
m += os.linesep * 2
m += 'You may see some black squares in the image. A metadata regeneration has been scheduled, so with luck the image will fix itself soon.'
HydrusData.ShowText( m )
def GetEstimatedMemoryFootprint( self ):
@ -250,13 +274,26 @@ class ImageRenderer( object ):
target_resolution = clip_rect.size()
numpy_image = self._GetNumPyImage( clip_rect, target_resolution )
( height, width, depth ) = numpy_image.shape
data = numpy_image.data
return HG.client_controller.bitmap_manager.GetQtPixmapFromBuffer( width, height, depth * 8, data )
try:
numpy_image = self._GetNumPyImage( clip_rect, target_resolution )
( height, width, depth ) = numpy_image.shape
data = numpy_image.data
return HG.client_controller.bitmap_manager.GetQtPixmapFromBuffer( width, height, depth * 8, data )
except Exception as e:
HydrusData.PrintException( e, do_wait = False )
pixmap = QG.QPixmap( target_resolution )
pixmap.fill( QC.Qt.black )
return pixmap
def IsReady( self ):

View File

@ -1802,7 +1802,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file was missing. Your repository should be paused, and all update files have been scheduled for a presence check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) was missing. Your repository should be paused, and all update files have been scheduled for a presence check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.'.format( definition_hash.hex() ) )
with open( update_path, 'rb' ) as f:
@ -1820,7 +1820,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file was invalid. Your repository should be paused, and all update files have been scheduled for an integrity check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) was invalid. Your repository should be paused, and all update files have been scheduled for an integrity check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.'.format( definition_hash.hex() ) )
if not isinstance( definition_update, HydrusNetwork.DefinitionsUpdate ):
@ -1829,7 +1829,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file has incorrect metadata. Your repository should be paused, and all update files have been scheduled for a metadata rescan. Please permit file maintenance to fix them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) has incorrect metadata. Your repository should be paused, and all update files have been scheduled for a metadata rescan. Please permit file maintenance to fix them, or tell it to do so manually, before unpausing your repository.'.format( definition_hash.hex() ) )
rows_in_this_update = definition_update.GetNumRows()
@ -1936,7 +1936,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file was missing. Your repository should be paused, and all update files have been scheduled for a presence check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) was missing. Your repository should be paused, and all update files have been scheduled for a presence check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.'.format( content_hash.hex() ) )
with open( update_path, 'rb' ) as f:
@ -1954,7 +1954,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file was invalid. Your repository should be paused, and all update files have been scheduled for an integrity check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) was invalid. Your repository should be paused, and all update files have been scheduled for an integrity check. Please permit file maintenance to check them, or tell it to do so manually, before unpausing your repository.'.format( content_hash.hex() ) )
if not isinstance( content_update, HydrusNetwork.ContentUpdate ):
@ -1963,7 +1963,7 @@ class ServiceRepository( ServiceRestricted ):
self._do_a_full_metadata_resync = True
raise Exception( 'An unusual error has occured during repository processing: an update file has incorrect metadata. Your repository should be paused, and all update files have been scheduled for a metadata rescan. Please permit file maintenance to fix them, or tell it to do so manually, before unpausing your repository.' )
raise Exception( 'An unusual error has occured during repository processing: an update file ({}) has incorrect metadata. Your repository should be paused, and all update files have been scheduled for a metadata rescan. Please permit file maintenance to fix them, or tell it to do so manually, before unpausing your repository.'.format( content_hash.hex() ) )
rows_in_this_update = content_update.GetNumRows()
@ -2052,7 +2052,7 @@ class ServiceRepository( ServiceRestricted ):
with self._lock:
message = 'Failed to process updates for the ' + self._name + ' repository! The error follows:'
message = 'Failed to process updates for the {} repository! The error follows:'.format( self._name )
HydrusData.ShowText( message )

View File

@ -7528,6 +7528,8 @@ class DB( HydrusDB.HydrusDB ):
if job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FILE_METADATA:
( original_mr, ) = self._GetMediaResults( ( hash_id, ) )
( size, mime, width, height, duration, num_frames, has_audio, num_words ) = additional_data
files_rows = [ ( hash_id, size, mime, width, height, duration, num_frames, has_audio, num_words ) ]
@ -7551,6 +7553,14 @@ class DB( HydrusDB.HydrusDB ):
if mime in HC.MIMES_WITH_THUMBNAILS:
if original_mr.GetResolution() != ( width, height ):
self._FileMaintenanceAddJobs( { hash_id }, ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL )
elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_OTHER_HASHES:
( md5, sha1, sha512 ) = additional_data
@ -7793,6 +7803,23 @@ class DB( HydrusDB.HydrusDB ):
return culled_mappings_ids
def _FilterForFileDeleteLock( self, service_id, hash_ids ):
# eventually extend this to the metadata conditional object
if HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' ):
service = self.modules_services.GetService( service_id )
if service.GetServiceType() in HC.LOCAL_FILE_SERVICES:
hash_ids = set( hash_ids ).intersection( self.modules_files_metadata_basic.inbox_hash_ids )
return hash_ids
def _FilterHashesByService( self, file_service_key: bytes, hashes: typing.Sequence[ bytes ] ) -> typing.List[ bytes ]:
# returns hashes in order, to be nice to UI
@ -12788,6 +12815,8 @@ class DB( HydrusDB.HydrusDB ):
hash_ids = self._STS( self._c.execute( 'SELECT hash_id FROM current_files WHERE service_id = ?' + age_phrase + limit_phrase + ';', ( self.modules_services.trash_service_id, ) ) )
hash_ids = self._FilterForFileDeleteLock( self.modules_services.trash_service_id, hash_ids )
if HG.db_report_mode:
message = 'When asked for '
@ -14001,6 +14030,17 @@ class DB( HydrusDB.HydrusDB ):
elif action == HC.CONTENT_UPDATE_DELETE:
actual_delete_hash_ids = self._FilterForFileDeleteLock( service_id, hash_ids )
if len( actual_delete_hash_ids ) < len( hash_ids ):
hash_ids = actual_delete_hash_ids
hashes = self.modules_hashes_local_cache.GetHashes( hash_ids )
content_update.SetRow( hashes )
if service_id == self.modules_services.local_file_service_id:
reason = content_update.GetReason()

View File

@ -867,7 +867,16 @@ class ReviewExportFilesPanel( ClientGUIScrolledPanels.ReviewPanel ):
QP.CallAfter( qt_update_label, 'deleting' )
deletee_hashes = { media.GetHash() for ( ordering_index, media ) in to_do }
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
if delete_lock_for_archived_files:
deletee_hashes = { media.GetHash() for ( ordering_index, media ) in to_do if not media.HasArchive() }
else:
deletee_hashes = { media.GetHash() for ( ordering_index, media ) in to_do }
chunks_of_hashes = HydrusData.SplitListIntoChunks( deletee_hashes, 64 )

View File

@ -305,7 +305,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
def _Delete( self, file_service_key = None ):
def _Delete( self, file_service_key = None, only_those_in_file_service_key = None ):
media_to_delete = self._selected_media
if only_those_in_file_service_key is not None:
media_to_delete = ClientMedia.FlattenMedia( media_to_delete )
media_to_delete = [ m for m in media_to_delete if only_those_in_file_service_key in m.GetLocationsManager().GetCurrent() ]
if file_service_key is None or file_service_key in ( CC.LOCAL_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ):
@ -318,7 +327,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
try:
( involves_physical_delete, jobs ) = ClientGUIDialogsQuick.GetDeleteFilesJobs( self, self._selected_media, default_reason, suggested_file_service_key = file_service_key )
( involves_physical_delete, jobs ) = ClientGUIDialogsQuick.GetDeleteFilesJobs( self, media_to_delete, default_reason, suggested_file_service_key = file_service_key )
except HydrusExceptions.CancelledException:
@ -3259,6 +3268,9 @@ class MediaPanelThumbnails( MediaPanel ):
all_file_domains = HydrusData.MassUnion( locations_manager.GetCurrent() for locations_manager in all_locations_managers )
all_specific_file_domains = all_file_domains.difference( { CC.COMBINED_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY } )
all_local_file_domains = services_manager.Filter( all_specific_file_domains, ( HC.LOCAL_FILE_DOMAIN, ) )
all_local_file_domains_sorted = sorted( all_local_file_domains, key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
all_file_repos = services_manager.Filter( all_specific_file_domains, ( HC.FILE_REPOSITORY, ) )
has_local = True in ( locations_manager.IsLocal() for locations_manager in all_locations_managers )
@ -3636,14 +3648,19 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMenus.AppendSeparator( menu )
if selection_has_local_file_domain:
for file_service_key in all_local_file_domains_sorted:
ClientGUIMenus.AppendMenuItem( menu, local_delete_phrase, 'Delete the selected files from \'my files\'.', self._Delete )
ClientGUIMenus.AppendMenuItem( menu, '{} from {}'.format( local_delete_phrase, HG.client_controller.services_manager.GetName( file_service_key ) ), 'Delete the selected files.', self._Delete, file_service_key )
if selection_has_trash:
ClientGUIMenus.AppendMenuItem( menu, delete_physically_phrase, 'Delete the selected files from the trash, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
if selection_has_local_file_domain:
ClientGUIMenus.AppendMenuItem( menu, 'delete trash physically now', 'Completely delete the selected trashed files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY, only_those_in_file_service_key = CC.TRASH_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( menu, delete_physically_phrase, 'Completely delete the selected files, forcing an immediate physical delete from your hard drive.', self._Delete, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( menu, undelete_phrase, 'Restore the selected files back to \'my files\'.', self._Undelete )

View File

@ -11,6 +11,7 @@ from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUICore as CGC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIMenus
from hydrus.client.gui import ClientGUITags
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIMenuButton
@ -312,6 +313,11 @@ class MediaSortControl( QW.QWidget ):
if menu is not None:
ClientGUIMenus.AppendMenuItem( submenu, 'custom', 'Set a custom namespace sort', self._SetCustomNamespaceSort )
rating_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ) )
@ -358,6 +364,31 @@ class MediaSortControl( QW.QWidget ):
return sort_types
def _SetCustomNamespaceSort( self ):
if self._sort_type[0] == 'namespaces':
initial_namespaces = self._sort_type[1]
else:
initial_namespaces = [ 'series' ]
try:
edited_namespaces = ClientGUITags.EditNamespaceSort( self, initial_namespaces )
sort_type = ( 'namespaces', edited_namespaces )
self._SetSortType( sort_type )
except HydrusExceptions.VetoException:
return
def _SortTypeButtonClick( self ):
menu = QW.QMenu()

View File

@ -346,9 +346,9 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._media = ClientMedia.FlattenMedia( media )
self._media = self._FilterForDeleteLock( ClientMedia.FlattenMedia( media ), suggested_file_service_key )
self._question_is_already_resolved = False
self._question_is_already_resolved = len( self._media ) == 0
self._simple_description = ClientGUICommon.BetterStaticText( self, label = 'init' )
@ -430,6 +430,28 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
self.widget().setLayout( vbox )
def _FilterForDeleteLock( self, media, suggested_file_service_key ):
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
if delete_lock_for_archived_files:
if suggested_file_service_key is None:
suggested_file_service_key = CC.LOCAL_FILE_SERVICE_KEY
service = HG.client_controller.services_manager.GetService( suggested_file_service_key )
if service.GetServiceType() in HC.LOCAL_FILE_SERVICES:
media = [ m for m in media if m.HasInbox() ]
return media
def _GetReason( self ):
reason = self._reason_radio.GetValue()
@ -451,29 +473,42 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
suggested_file_service_key = CC.LOCAL_FILE_SERVICE_KEY
if suggested_file_service_key == CC.LOCAL_FILE_SERVICE_KEY:
if suggested_file_service_key not in ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ):
possible_file_service_keys.append( CC.LOCAL_FILE_SERVICE_KEY )
possible_file_service_keys.append( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
else:
possible_file_service_keys.append( suggested_file_service_key )
possible_file_service_keys.append( ( suggested_file_service_key, suggested_file_service_key ) )
keys_to_hashes = { possible_file_service_key : [ m.GetHash() for m in self._media if possible_file_service_key in m.GetLocationsManager().GetCurrent() ] for possible_file_service_key in possible_file_service_keys }
possible_file_service_keys.append( ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
possible_file_service_keys.append( ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
for possible_file_service_key in possible_file_service_keys:
keys_to_hashes = { ( selection_file_service_key, deletee_file_service_key ) : [ m.GetHash() for m in self._media if selection_file_service_key in m.GetLocationsManager().GetCurrent() ] for ( selection_file_service_key, deletee_file_service_key ) in possible_file_service_keys }
trashed_key = ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
combined_key = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
if trashed_key in keys_to_hashes and combined_key in keys_to_hashes and keys_to_hashes[ trashed_key ] == keys_to_hashes[ combined_key ]:
hashes = keys_to_hashes[ possible_file_service_key ]
del keys_to_hashes[ trashed_key ]
for fsk in possible_file_service_keys:
if fsk not in keys_to_hashes:
continue
hashes = keys_to_hashes[ fsk ]
num_to_delete = len( hashes )
if len( hashes ) > 0:
if num_to_delete > 0:
( selection_file_service_key, deletee_file_service_key ) = fsk
# update this stuff to say 'send to trash?' vs 'remove from blah? (it is still in bleh)'. for multiple local file services
if possible_file_service_key == CC.LOCAL_FILE_SERVICE_KEY:
if deletee_file_service_key == CC.LOCAL_FILE_SERVICE_KEY:
if not HC.options[ 'confirm_trash' ]:
@ -484,14 +519,22 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
if num_to_delete == 1: text = 'Send this file to the trash?'
else: text = 'Send these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files to the trash?'
elif possible_file_service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
elif deletee_file_service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
# do a physical delete now, skipping or force-removing from trash
possible_file_service_key = 'physical_delete'
deletee_file_service_key = 'physical_delete'
if num_to_delete == 1: text = 'Permanently delete this file?'
else: text = 'Permanently delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
if selection_file_service_key == CC.TRASH_SERVICE_KEY:
if num_to_delete == 1: text = 'Permanently delete this trashed file?'
else: text = 'Permanently delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' trashed files?'
else:
if num_to_delete == 1: text = 'Permanently delete this file?'
else: text = 'Permanently delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
else:
@ -499,7 +542,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
else: text = 'Admin-delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
self._permitted_action_choices.append( ( text, ( possible_file_service_key, hashes, text ) ) )
self._permitted_action_choices.append( ( text, ( deletee_file_service_key, hashes, text ) ) )
@ -509,7 +552,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
num_to_delete = len( hashes )
if len( hashes ) > 0:
if num_to_delete > 0:
if num_to_delete == 1:

View File

@ -939,6 +939,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._trash_max_age = ClientGUICommon.NoneableSpinCtrl( self, '', none_phrase = 'no age limit', min = 0, max = 8640 )
self._trash_max_size = ClientGUICommon.NoneableSpinCtrl( self, '', none_phrase = 'no size limit', min = 0, max = 20480 )
delete_lock_panel = ClientGUICommon.StaticBox( self, 'delete lock' )
self._delete_lock_for_archived_files = QW.QCheckBox( delete_lock_panel )
advanced_file_deletion_panel = ClientGUICommon.StaticBox( self, 'advanced file deletion and custom reasons' )
self._use_advanced_file_deletion_dialog = QW.QCheckBox( advanced_file_deletion_panel )
@ -971,6 +975,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._trash_max_age.SetValue( HC.options[ 'trash_max_age' ] )
self._trash_max_size.SetValue( HC.options[ 'trash_max_size' ] )
self._delete_lock_for_archived_files.setChecked( self._new_options.GetBoolean( 'delete_lock_for_archived_files' ) )
self._use_advanced_file_deletion_dialog.setChecked( self._new_options.GetBoolean( 'use_advanced_file_deletion_dialog' ) )
self._use_advanced_file_deletion_dialog.clicked.connect( self._UpdateAdvancedControls )
@ -1003,6 +1009,20 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
#
rows = []
rows.append( ( 'Do not permit archived files to be trashed or deleted: ', self._delete_lock_for_archived_files ) )
gridbox = ClientGUICommon.WrapInGrid( delete_lock_panel, rows )
delete_lock_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, delete_lock_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
#
rows = []
rows.append( ( 'Use the advanced file deletion dialog: ', self._use_advanced_file_deletion_dialog ) )
@ -1012,6 +1032,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
advanced_file_deletion_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
advanced_file_deletion_panel.Add( self._advanced_file_deletion_reasons, CC.FLAGS_EXPAND_BOTH_WAYS )
#
QP.AddToLayout( vbox, advanced_file_deletion_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
self.setLayout( vbox )
@ -1067,6 +1089,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
HC.options[ 'trash_max_age' ] = self._trash_max_age.GetValue()
HC.options[ 'trash_max_size' ] = self._trash_max_size.GetValue()
self._new_options.SetBoolean( 'delete_lock_for_archived_files', self._delete_lock_for_archived_files.isChecked() )
self._new_options.SetBoolean( 'use_advanced_file_deletion_dialog', self._use_advanced_file_deletion_dialog.isChecked() )
self._new_options.SetStringList( 'advanced_file_deletion_reasons', self._advanced_file_deletion_reasons.GetData() )
@ -2433,38 +2457,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _EditNamespaceSort( self, namespaces ):
# users might want to add a namespace with a hyphen in it, so in lieu of a nice list to edit we'll just escape for now mate
correct_char = '-'
escaped_char = '\\-'
escaped_namespaces = [ namespace.replace( correct_char, escaped_char ) for namespace in namespaces ]
edit_string = '-'.join( escaped_namespaces )
message = 'Write the namespaces you would like to sort by here, separated by hyphens. Any namespace in any of your sort definitions will be added to the collect-by menu.'
message += os.linesep * 2
message += 'If the namespace you want to add has a hyphen, like \'creator-id\', instead type it with a backslash escape, like \'creator\\-id-page\'.'
with ClientGUIDialogs.DialogTextEntry( self, message, allow_blank = False, default = edit_string ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
edited_string = dlg.GetValue()
edited_escaped_namespaces = re.split( r'(?<!\\)\-', edited_string )
edited_namespaces = [ namespace.replace( escaped_char, correct_char ) for namespace in edited_escaped_namespaces ]
edited_namespaces = [ HydrusTags.CleanTag( namespace ) for namespace in edited_namespaces if HydrusTags.TagOK( namespace ) ]
if len( edited_namespaces ) > 0:
return tuple( edited_namespaces )
raise HydrusExceptions.VetoException()
return ClientGUITags.EditNamespaceSort( self, namespaces )
def UpdateOptions( self ):

View File

@ -166,6 +166,11 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
def _WasActivated( self, activation_reason ):
if not QP.isValid( self ):
return
if activation_reason in ( QW.QSystemTrayIcon.Unknown, QW.QSystemTrayIcon.Trigger ):
if self._ui_is_currently_shown:

View File

@ -2,6 +2,7 @@ import collections
import itertools
import os
import random
import re
import time
import typing
@ -45,6 +46,41 @@ from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientTags
from hydrus.client.metadata import ClientTagsHandling
def EditNamespaceSort( win: QW.QWidget, namespaces: typing.Collection[ str ] ):
# users might want to add a namespace with a hyphen in it, so in lieu of a nice list to edit we'll just escape for now mate
correct_char = '-'
escaped_char = '\\-'
escaped_namespaces = [ namespace.replace( correct_char, escaped_char ) for namespace in namespaces ]
edit_string = '-'.join( escaped_namespaces )
message = 'Write the namespaces you would like to sort by here, separated by hyphens. Any namespace in any of your sort definitions will be added to the collect-by menu.'
message += os.linesep * 2
message += 'If the namespace you want to add has a hyphen, like \'creator-id\', instead type it with a backslash escape, like \'creator\\-id-page\'.'
with ClientGUIDialogs.DialogTextEntry( win, message, allow_blank = False, default = edit_string ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
edited_string = dlg.GetValue()
edited_escaped_namespaces = re.split( r'(?<!\\)\-', edited_string )
edited_namespaces = [ namespace.replace( escaped_char, correct_char ) for namespace in edited_escaped_namespaces ]
edited_namespaces = [ HydrusTags.CleanTag( namespace ) for namespace in edited_namespaces if HydrusTags.TagOK( namespace ) ]
if len( edited_namespaces ) > 0:
return tuple( edited_namespaces )
raise HydrusExceptions.VetoException()
class EditTagAutocompleteOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
def __init__( self, parent: QW.QWidget, tag_autocomplete_options: ClientTagsHandling.TagAutocompleteOptions ):

View File

@ -1897,11 +1897,14 @@ class CanvasPanel( Canvas ):
# brush this up to handle different service keys
# undelete do an optional service key too
if not locations_manager.GetCurrent().isdisjoint( local_file_service_keys ):
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
for file_service_key in local_file_service_keys_we_are_in:
ClientGUIMenus.AppendMenuItem( menu, 'delete', 'Delete this file.', self._Delete, file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( menu, 'delete from {}'.format( HG.client_controller.services_manager.GetName( file_service_key ) ), 'Delete this file.', self._Delete, file_service_key = file_service_key )
elif not locations_manager.GetDeleted().isdisjoint( local_file_service_keys ):
if locations_manager.IsTrashed():
ClientGUIMenus.AppendMenuItem( menu, 'delete completely', 'Physically delete this file from disk.', self._Delete, file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( menu, 'undelete', 'Take this file out of the trash.', self._Undelete )
@ -3719,7 +3722,20 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def TryToDoPreClose( self ):
if len( self._kept ) > 0 or len( self._deleted ) > 0:
kept_hashes = [ media.GetHash() for media in self._kept ]
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
if delete_lock_for_archived_files:
deleted_hashes = [ media.GetHash() for media in self._deleted if not media.HasArchive() ]
else:
deleted_hashes = [ media.GetHash() for media in self._deleted ]
if len( kept_hashes ) > 0 or len( deleted_hashes ) > 0:
label = 'keep ' + HydrusData.ToHumanInt( len( self._kept ) ) + ' and delete ' + HydrusData.ToHumanInt( len( self._deleted ) ) + ' files?'
@ -3741,21 +3757,18 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
elif result == QW.QDialog.Accepted:
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
self._kept_hashes = [ media.GetHash() for media in self._kept ]
service_keys_to_content_updates = {}
if len( self._deleted_hashes ) > 0:
if len( deleted_hashes ) > 0:
reason = 'Deleted in Archive/Delete filter.'
service_keys_to_content_updates[ CC.LOCAL_FILE_SERVICE_KEY ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, self._deleted_hashes, reason = reason ) ]
service_keys_to_content_updates[ CC.LOCAL_FILE_SERVICE_KEY ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, deleted_hashes, reason = reason ) ]
if len( self._kept_hashes ) > 0:
if len( kept_hashes ) > 0:
service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) ]
service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ] = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, kept_hashes ) ]
# do this in one go to ensure if the user hits F5 real quick, they won't see the files again
@ -3774,8 +3787,8 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
all_hashes = set()
all_hashes.update( self._deleted_hashes )
all_hashes.update( self._kept_hashes )
all_hashes.update( deleted_hashes )
all_hashes.update( kept_hashes )
HG.client_controller.pub( 'remove_media', self._page_key, all_hashes )
@ -4345,11 +4358,23 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendSeparator( menu )
if CC.LOCAL_FILE_SERVICE_KEY in locations_manager.GetCurrent():
ClientGUIMenus.AppendMenuItem( menu, 'delete', 'Send this file to the trash.', self._Delete, file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
#
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
# brush this up to handle different service keys
# undelete do an optional service key too
local_file_service_keys_we_are_in = sorted( locations_manager.GetCurrent().intersection( local_file_service_keys ), key = lambda fsk: HG.client_controller.services_manager.GetName( fsk ) )
for file_service_key in local_file_service_keys_we_are_in:
elif locations_manager.IsTrashed():
ClientGUIMenus.AppendMenuItem( menu, 'delete from {}'.format( HG.client_controller.services_manager.GetName( file_service_key ) ), 'Delete this file.', self._Delete, file_service_key = file_service_key )
#
if locations_manager.IsTrashed():
ClientGUIMenus.AppendMenuItem( menu, 'delete physically now', 'Delete this file immediately. This cannot be undone.', self._Delete, file_service_key = CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( menu, 'undelete', 'Take this file out of the trash, returning it to its original file service.', self._Undelete )

View File

@ -1756,6 +1756,11 @@ class StaticImage( QW.QWidget ):
def _GetTileCoordinateFromPoint( self, pos: QC.QPoint ):
if self._canvas_tile_size.width() == 0 or self._canvas_tile_size.height() == 0:
return ( 0, 0 )
tile_x = pos.x() // self._canvas_tile_size.width()
tile_y = pos.y() // self._canvas_tile_size.height()
@ -1764,7 +1769,7 @@ class StaticImage( QW.QWidget ):
def _GetTileCoordinatesInView( self, rect: QC.QRect ):
if self.width() == 0 or self.height() == 0:
if self.width() == 0 or self.height() == 0 or self._canvas_tile_size.width() == 0 or self._canvas_tile_size.height() == 0:
return []
@ -1801,37 +1806,46 @@ class StaticImage( QW.QWidget ):
return
dirty_tile_coordinates = self._GetTileCoordinatesInView( event.rect() )
for dirty_tile_coordinate in dirty_tile_coordinates:
try:
if dirty_tile_coordinate not in self._canvas_tiles:
dirty_tile_coordinates = self._GetTileCoordinatesInView( event.rect() )
for dirty_tile_coordinate in dirty_tile_coordinates:
self._DrawTile( dirty_tile_coordinate )
if dirty_tile_coordinate not in self._canvas_tiles:
self._DrawTile( dirty_tile_coordinate )
for dirty_tile_coordinate in dirty_tile_coordinates:
for dirty_tile_coordinate in dirty_tile_coordinates:
( tile, pos ) = self._canvas_tiles[ dirty_tile_coordinate ]
painter.drawPixmap( pos, tile )
( tile, pos ) = self._canvas_tiles[ dirty_tile_coordinate ]
all_visible_tile_coordinates = self._GetTileCoordinatesInView( self.visibleRegion().boundingRect() )
painter.drawPixmap( pos, tile )
deletee_tile_coordinates = set( self._canvas_tiles.keys() ).difference( all_visible_tile_coordinates )
all_visible_tile_coordinates = self._GetTileCoordinatesInView( self.visibleRegion().boundingRect() )
deletee_tile_coordinates = set( self._canvas_tiles.keys() ).difference( all_visible_tile_coordinates )
for deletee_tile_coordinate in deletee_tile_coordinates:
for deletee_tile_coordinate in deletee_tile_coordinates:
del self._canvas_tiles[ deletee_tile_coordinate ]
del self._canvas_tiles[ deletee_tile_coordinate ]
if not self._is_rendered:
self.readyForNeighbourPrefetch.emit()
self._is_rendered = True
if not self._is_rendered:
except Exception as e:
self.readyForNeighbourPrefetch.emit()
HydrusData.PrintException( e, do_wait = False )
self._is_rendered = True
return

View File

@ -1737,7 +1737,7 @@ file_filter_str_lookup[ FILE_FILTER_INBOX ] = 'inbox'
file_filter_str_lookup[ FILE_FILTER_ARCHIVE ] = 'archive'
file_filter_str_lookup[ FILE_FILTER_FILE_SERVICE ] = 'file service'
file_filter_str_lookup[ FILE_FILTER_LOCAL ] = 'local'
file_filter_str_lookup[ FILE_FILTER_REMOTE ] = 'remote'
file_filter_str_lookup[ FILE_FILTER_REMOTE ] = 'not local'
file_filter_str_lookup[ FILE_FILTER_TAGS ] = 'tags'
file_filter_str_lookup[ FILE_FILTER_MIME ] = 'filetype'
@ -3010,7 +3010,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
tags_manager = x.GetTagsManager()
return len( tags_manager.GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_SINGLE_MEDIA ) )
return len( tags_manager.GetCurrentAndPending( CC.COMBINED_TAG_SERVICE_KEY, ClientTags.TAG_DISPLAY_ACTUAL ) )
elif sort_data == CC.SORT_FILES_BY_MIME:
@ -3051,7 +3051,7 @@ class MediaSort( HydrusSerialisable.SerialisableBase ):
x_tags_manager = x.GetTagsManager()
return [ x_tags_manager.GetComparableNamespaceSlice( ( namespace, ), ClientTags.TAG_DISPLAY_SINGLE_MEDIA ) for namespace in namespaces ]
return [ x_tags_manager.GetComparableNamespaceSlice( ( namespace, ), ClientTags.TAG_DISPLAY_ACTUAL ) for namespace in namespaces ]
elif sort_metadata == 'rating':

View File

@ -229,7 +229,7 @@ def ParseClientAPISearchPredicates( request ):
predicate_type = ClientSearch.PREDICATE_TYPE_TAG
predicates.append( ClientSearch.Predicate( predicate_type = ClientSearch.PREDICATE_TYPE_TAG, value = tag, inclusive = inclusive ) )
predicates.append( ClientSearch.Predicate( predicate_type = predicate_type, value = tag, inclusive = inclusive ) )
if system_inbox:

View File

@ -81,7 +81,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 439
SOFTWARE_VERSION = 440
CLIENT_API_VERSION = 16
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -1763,6 +1763,11 @@ class ContentUpdate( object ):
return self._action in ( HC.CONTENT_UPDATE_ARCHIVE, HC.CONTENT_UPDATE_INBOX )
def SetRow( self, row ):
self._row = row
def ToTuple( self ):
return ( self._data_type, self._action, self._row )