5809 lines
220 KiB
Python
5809 lines
220 KiB
Python
import collections
|
|
import os
|
|
import random
|
|
import threading
|
|
import typing
|
|
|
|
from qtpy import QtCore as QC
|
|
from qtpy import QtWidgets as QW
|
|
|
|
from hydrus.core import HydrusConstants as HC
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusLists
|
|
from hydrus.core import HydrusTags
|
|
from hydrus.core import HydrusTime
|
|
from hydrus.core.networking import HydrusNetwork
|
|
|
|
from hydrus.client import ClientConstants as CC
|
|
from hydrus.client import ClientDefaults
|
|
from hydrus.client import ClientDuplicates
|
|
from hydrus.client import ClientGlobals as CG
|
|
from hydrus.client import ClientLocation
|
|
from hydrus.client import ClientParsing
|
|
from hydrus.client import ClientPaths
|
|
from hydrus.client import ClientServices
|
|
from hydrus.client import ClientThreading
|
|
from hydrus.client import ClientTime
|
|
from hydrus.client.gui import ClientGUIAsync
|
|
from hydrus.client.gui import ClientGUICore as CGC
|
|
from hydrus.client.gui import ClientGUIDialogs
|
|
from hydrus.client.gui import ClientGUIDialogsMessage
|
|
from hydrus.client.gui import ClientGUIDialogsQuick
|
|
from hydrus.client.gui import ClientGUIFunctions
|
|
from hydrus.client.gui import ClientGUIMenus
|
|
from hydrus.client.gui import ClientGUIScrolledPanels
|
|
from hydrus.client.gui import ClientGUIFileSeedCache
|
|
from hydrus.client.gui import ClientGUIGallerySeedLog
|
|
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
|
|
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
|
|
from hydrus.client.gui import QtPorting as QP
|
|
from hydrus.client.gui.canvas import ClientGUICanvas
|
|
from hydrus.client.gui.canvas import ClientGUICanvasFrame
|
|
from hydrus.client.gui.importing import ClientGUIImport
|
|
from hydrus.client.gui.importing import ClientGUIImportOptions
|
|
from hydrus.client.gui.lists import ClientGUIListBoxes
|
|
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
|
|
from hydrus.client.gui.lists import ClientGUIListCtrl
|
|
from hydrus.client.gui.networking import ClientGUIHydrusNetwork
|
|
from hydrus.client.gui.networking import ClientGUINetworkJobControl
|
|
from hydrus.client.gui.pages import ClientGUIManagementController
|
|
from hydrus.client.gui.pages import ClientGUIResults
|
|
from hydrus.client.gui.pages import ClientGUIResultsSortCollect
|
|
from hydrus.client.gui.parsing import ClientGUIParsingFormulae
|
|
from hydrus.client.gui.search import ClientGUIACDropdown
|
|
from hydrus.client.gui.widgets import ClientGUICommon
|
|
from hydrus.client.gui.widgets import ClientGUIControls
|
|
from hydrus.client.gui.widgets import ClientGUIMenuButton
|
|
from hydrus.client.importing import ClientImporting
|
|
from hydrus.client.importing import ClientImportWatchers
|
|
from hydrus.client.importing import ClientImportLocal
|
|
from hydrus.client.importing import ClientImportSimpleURLs
|
|
from hydrus.client.importing.options import FileImportOptions
|
|
from hydrus.client.importing.options import PresentationImportOptions
|
|
from hydrus.client.media import ClientMedia
|
|
from hydrus.client.metadata import ClientContentUpdates
|
|
from hydrus.client.metadata import ClientTags
|
|
from hydrus.client.networking import ClientNetworkingFunctions
|
|
from hydrus.client.search import ClientSearch
|
|
|
|
management_panel_types_to_classes = {}
|
|
|
|
def AddPresentationSubmenu( menu: QW.QMenu, importer_name: str, single_selected_presentation_import_options: typing.Optional[ PresentationImportOptions.PresentationImportOptions ], callable ):
|
|
|
|
submenu = ClientGUIMenus.GenerateMenu( menu )
|
|
|
|
# inbox only
|
|
# detect single_selected_presentation_import_options and deal with it
|
|
|
|
description = 'Gather these files for the selected importers and show them.'
|
|
|
|
if single_selected_presentation_import_options is None:
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'default presented files', description, callable )
|
|
|
|
else:
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'default presented files ({})'.format( single_selected_presentation_import_options.GetSummary() ), description, callable )
|
|
|
|
|
|
sets_of_options = []
|
|
|
|
presentation_import_options = PresentationImportOptions.PresentationImportOptions()
|
|
|
|
presentation_import_options.SetPresentationStatus( PresentationImportOptions.PRESENTATION_STATUS_NEW_ONLY )
|
|
|
|
sets_of_options.append( presentation_import_options )
|
|
|
|
presentation_import_options = PresentationImportOptions.PresentationImportOptions()
|
|
|
|
presentation_import_options.SetPresentationInbox( PresentationImportOptions.PRESENTATION_INBOX_REQUIRE_INBOX )
|
|
|
|
sets_of_options.append( presentation_import_options )
|
|
|
|
presentation_import_options = PresentationImportOptions.PresentationImportOptions()
|
|
|
|
sets_of_options.append( presentation_import_options )
|
|
|
|
presentation_import_options = PresentationImportOptions.PresentationImportOptions()
|
|
|
|
presentation_import_options.SetLocationContext( ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
|
|
|
|
sets_of_options.append( presentation_import_options )
|
|
|
|
for presentation_import_options in sets_of_options:
|
|
|
|
if single_selected_presentation_import_options is not None and presentation_import_options == single_selected_presentation_import_options:
|
|
|
|
continue
|
|
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, presentation_import_options.GetSummary(), description, callable, presentation_import_options = presentation_import_options )
|
|
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'show files' )
|
|
|
|
|
|
class ListBoxTagsMediaManagementPanel( ClientGUIListBoxes.ListBoxTagsMedia ):
|
|
|
|
def __init__( self, parent, management_controller: ClientGUIManagementController.ManagementController, page_key, tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST, tag_autocomplete: typing.Optional[ ClientGUIACDropdown.AutoCompleteDropdownTagsRead ] = None ):
|
|
|
|
ClientGUIListBoxes.ListBoxTagsMedia.__init__( self, parent, tag_display_type, CC.TAG_PRESENTATION_SEARCH_PAGE, include_counts = True )
|
|
|
|
self._management_controller = management_controller
|
|
self._minimum_height_num_chars = 15
|
|
|
|
self._page_key = page_key
|
|
self._tag_autocomplete = tag_autocomplete
|
|
|
|
|
|
def _Activate( self, ctrl_down, shift_down ) -> bool:
|
|
|
|
predicates = self._GetPredicatesFromTerms( self._selected_terms )
|
|
|
|
if len( predicates ) > 0:
|
|
|
|
if ctrl_down:
|
|
|
|
predicates = [ predicate.GetInverseCopy() for predicate in predicates ]
|
|
|
|
|
|
if shift_down and len( predicates ) > 1:
|
|
|
|
predicates = ( ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_OR_CONTAINER, value = predicates ), )
|
|
|
|
|
|
CG.client_controller.pub( 'enter_predicates', self._page_key, predicates )
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _CanProvideCurrentPagePredicates( self ):
|
|
|
|
return self._tag_autocomplete is not None
|
|
|
|
|
|
def _GetCurrentLocationContext( self ):
|
|
|
|
return self._management_controller.GetLocationContext()
|
|
|
|
|
|
def _GetCurrentPagePredicates( self ) -> typing.Set[ ClientSearch.Predicate ]:
|
|
|
|
if self._tag_autocomplete is None:
|
|
|
|
return set()
|
|
|
|
else:
|
|
|
|
return self._tag_autocomplete.GetPredicates()
|
|
|
|
|
|
|
|
def _ProcessMenuPredicateEvent( self, command ):
|
|
|
|
( predicates, or_predicate, inverse_predicates, namespace_predicate, inverse_namespace_predicate ) = self._GetSelectedPredicatesAndInverseCopies()
|
|
|
|
p = None
|
|
permit_remove = True
|
|
permit_add = True
|
|
|
|
if command == 'add_predicates':
|
|
|
|
p = predicates
|
|
permit_remove = False
|
|
|
|
elif command == 'add_or_predicate':
|
|
|
|
p = ( or_predicate, )
|
|
permit_remove = False
|
|
|
|
elif command == 'remove_predicates':
|
|
|
|
p = predicates
|
|
permit_add = False
|
|
|
|
elif command == 'add_inverse_predicates':
|
|
|
|
p = inverse_predicates
|
|
permit_remove = False
|
|
|
|
elif command == 'add_namespace_predicate':
|
|
|
|
p = ( namespace_predicate, )
|
|
permit_remove = False
|
|
|
|
elif command == 'add_inverse_namespace_predicate':
|
|
|
|
p = ( inverse_namespace_predicate, )
|
|
permit_remove = False
|
|
|
|
|
|
if p is not None:
|
|
|
|
CG.client_controller.pub( 'enter_predicates', self._page_key, p, permit_remove = permit_remove, permit_add = permit_add )
|
|
|
|
|
|
|
|
|
|
class ManagementPanel( QW.QScrollArea ):
|
|
|
|
locationChanged = QC.Signal( ClientLocation.LocationContext )
|
|
|
|
SHOW_COLLECT = True
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
QW.QScrollArea.__init__( self, parent )
|
|
|
|
self.setFrameShape( QW.QFrame.NoFrame )
|
|
self.setWidget( QW.QWidget( self ) )
|
|
self.setWidgetResizable( True )
|
|
#self.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
|
|
#self.setLineWidth( 2 )
|
|
#self.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff )
|
|
self.setVerticalScrollBarPolicy( QC.Qt.ScrollBarAsNeeded )
|
|
|
|
self._controller = controller
|
|
self._management_controller = management_controller
|
|
|
|
self._last_seen_location_context = self._management_controller.GetLocationContext()
|
|
|
|
self._page = page
|
|
self._page_key = self._management_controller.GetVariable( 'page_key' )
|
|
|
|
self._page_state = CC.PAGE_STATE_NORMAL
|
|
|
|
self._current_selection_tags_list = None
|
|
|
|
self._media_sort_widget = ClientGUIResultsSortCollect.MediaSortControl( self, media_sort = self._management_controller.GetVariable( 'media_sort' ) )
|
|
|
|
if self._management_controller.HasVariable( 'media_collect' ):
|
|
|
|
media_collect = self._management_controller.GetVariable( 'media_collect' )
|
|
|
|
else:
|
|
|
|
media_collect = ClientMedia.MediaCollect()
|
|
|
|
|
|
self._media_collect_widget = ClientGUIResultsSortCollect.MediaCollectControl( self, media_collect = media_collect )
|
|
|
|
self._media_collect_widget.ListenForNewOptions()
|
|
|
|
if self.SHOW_COLLECT:
|
|
|
|
self._media_collect_widget.collectChanged.connect( self._CollectChanged )
|
|
|
|
else:
|
|
|
|
self._media_collect_widget.hide()
|
|
|
|
|
|
self._media_sort_widget.sortChanged.connect( self._SortChanged )
|
|
|
|
|
|
def _CollectChanged( self, media_collect ):
|
|
|
|
self._management_controller.SetVariable( 'media_collect', media_collect )
|
|
|
|
|
|
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
|
|
|
|
return 'empty page'
|
|
|
|
|
|
def _MakeCurrentSelectionTagsBox( self, sizer, tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST ):
|
|
|
|
self._current_selection_tags_box = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'selection tags', CC.TAG_PRESENTATION_SEARCH_PAGE )
|
|
|
|
self._current_selection_tags_list = ListBoxTagsMediaManagementPanel( self._current_selection_tags_box, self._management_controller, self._page_key, tag_display_type = tag_display_type )
|
|
|
|
self._current_selection_tags_box.SetTagsBox( self._current_selection_tags_list )
|
|
|
|
QP.AddToLayout( sizer, self._current_selection_tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
def _SetLocationContext( self, location_context: ClientLocation.LocationContext ):
|
|
|
|
if location_context != self._last_seen_location_context:
|
|
|
|
self._last_seen_location_context = location_context
|
|
|
|
self.locationChanged.emit( location_context )
|
|
|
|
|
|
|
|
def _SortChanged( self, media_sort ):
|
|
|
|
self._management_controller.SetVariable( 'media_sort', media_sort )
|
|
|
|
|
|
def ConnectMediaPanelSignals( self, media_panel: ClientGUIResults.MediaPanel ):
|
|
|
|
if self._current_selection_tags_list is not None:
|
|
|
|
media_panel.selectedMediaTagPresentationChanged.connect( self._current_selection_tags_list.SetTagsByMediaFromMediaPanel )
|
|
media_panel.selectedMediaTagPresentationIncremented.connect( self._current_selection_tags_list.IncrementTagsByMedia )
|
|
self._media_collect_widget.collectChanged.connect( media_panel.Collect )
|
|
self._media_sort_widget.sortChanged.connect( media_panel.Sort )
|
|
|
|
media_panel.PublishSelectionChange()
|
|
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
pass
|
|
|
|
|
|
def CleanBeforeClose( self ):
|
|
|
|
pass
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
pass
|
|
|
|
|
|
def GetDefaultEmptyMediaPanel( self ) -> ClientGUIResults.MediaPanel:
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, [] )
|
|
|
|
status = self._GetDefaultEmptyPageStatusOverride()
|
|
|
|
panel.SetEmptyPageStatusOverride( status )
|
|
|
|
return panel
|
|
|
|
|
|
def GetMediaCollect( self ):
|
|
|
|
if self.SHOW_COLLECT:
|
|
|
|
return self._media_collect_widget.GetValue()
|
|
|
|
else:
|
|
|
|
return ClientMedia.MediaCollect()
|
|
|
|
|
|
|
|
def GetMediaSort( self ):
|
|
|
|
return self._media_sort_widget.GetSort()
|
|
|
|
|
|
def GetPageState( self ) -> int:
|
|
|
|
return self._page_state
|
|
|
|
|
|
def PageHidden( self ):
|
|
|
|
pass
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
if self._controller.new_options.GetBoolean( 'set_search_focus_on_page_change' ):
|
|
|
|
self.SetSearchFocus()
|
|
|
|
|
|
|
|
def RefreshQuery( self ):
|
|
|
|
pass
|
|
|
|
|
|
def SetMediaSort( self, media_sort, do_sort = True ):
|
|
|
|
return self._media_sort_widget.SetSort( media_sort, do_sort = do_sort )
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
pass
|
|
|
|
|
|
def Start( self ):
|
|
|
|
pass
|
|
|
|
|
|
def REPEATINGPageUpdate( self ):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def CreateManagementPanel( parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ) -> ManagementPanel:
|
|
|
|
management_type = management_controller.GetType()
|
|
|
|
management_class = management_panel_types_to_classes[ management_type ]
|
|
|
|
management_panel = management_class( parent, page, controller, management_controller )
|
|
|
|
return management_panel
|
|
|
|
|
|
class ManagementPanelDuplicateFilter( ManagementPanel ):
|
|
|
|
SHOW_COLLECT = False
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanel.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._duplicates_manager = ClientDuplicates.DuplicatesManager.instance()
|
|
|
|
self._similar_files_maintenance_status = None
|
|
self._duplicates_manager_is_fetching_maintenance_numbers = False
|
|
self._potential_file_search_currently_happening = False
|
|
self._maintenance_numbers_need_redrawing = True
|
|
|
|
self._potential_duplicates_count = 0
|
|
|
|
self._have_done_first_maintenance_numbers_show = False
|
|
|
|
new_options = self._controller.new_options
|
|
|
|
self._dupe_count_numbers_dirty = True
|
|
|
|
self._currently_refreshing_dupe_count_numbers = False
|
|
|
|
#
|
|
|
|
self._main_notebook = ClientGUICommon.BetterNotebook( self )
|
|
|
|
self._main_left_panel = QW.QWidget( self._main_notebook )
|
|
self._main_right_panel = QW.QWidget( self._main_notebook )
|
|
|
|
#
|
|
|
|
self._refresh_maintenance_status = ClientGUICommon.BetterStaticText( self._main_left_panel, ellipsize_end = True )
|
|
self._refresh_maintenance_button = ClientGUICommon.BetterBitmapButton( self._main_left_panel, CC.global_pixmaps().refresh, self._duplicates_manager.RefreshMaintenanceNumbers )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'reset potential duplicates', 'This will delete all the discovered potential duplicate pairs. All files that may have potential pairs will be queued up for similar file search again.', self._ResetUnknown ) )
|
|
menu_items.append( ( 'separator', 0, 0, 0 ) )
|
|
|
|
check_manager = ClientGUICommon.CheckboxManagerOptions( 'maintain_similar_files_duplicate_pairs_during_idle' )
|
|
|
|
menu_items.append( ( 'check', 'search for potential duplicates at the current distance during idle time/shutdown', 'Tell the client to find duplicate pairs in its normal db maintenance cycles, whether you have that set to idle or shutdown time.', check_manager ) )
|
|
|
|
self._cog_button = ClientGUIMenuButton.MenuBitmapButton( self._main_left_panel, CC.global_pixmaps().cog, menu_items )
|
|
|
|
menu_items = []
|
|
|
|
page_func = HydrusData.Call( ClientGUIDialogsQuick.OpenDocumentation, self, HC.DOCUMENTATION_DUPLICATES )
|
|
|
|
menu_items.append( ( 'normal', 'open the html duplicates help', 'Open the help page for duplicates processing in your web browser.', page_func ) )
|
|
|
|
self._help_button = ClientGUIMenuButton.MenuBitmapButton( self._main_left_panel, CC.global_pixmaps().help, menu_items )
|
|
|
|
#
|
|
|
|
self._searching_panel = ClientGUICommon.StaticBox( self._main_left_panel, 'finding potential duplicates' )
|
|
|
|
self._eligible_files = ClientGUICommon.BetterStaticText( self._searching_panel, ellipsize_end = True )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'exact match', 'Search for exact matches.', HydrusData.Call( self._SetSearchDistance, CC.HAMMING_EXACT_MATCH ) ) )
|
|
menu_items.append( ( 'normal', 'very similar', 'Search for very similar files.', HydrusData.Call( self._SetSearchDistance, CC.HAMMING_VERY_SIMILAR ) ) )
|
|
menu_items.append( ( 'normal', 'similar', 'Search for similar files.', HydrusData.Call( self._SetSearchDistance, CC.HAMMING_SIMILAR ) ) )
|
|
menu_items.append( ( 'normal', 'speculative', 'Search for files that are probably similar.', HydrusData.Call( self._SetSearchDistance, CC.HAMMING_SPECULATIVE ) ) )
|
|
|
|
self._max_hamming_distance_for_potential_discovery_button = ClientGUIMenuButton.MenuButton( self._searching_panel, 'similarity', menu_items )
|
|
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl = ClientGUICommon.BetterSpinBox( self._searching_panel, min=0, max=64, width = 50 )
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.setSingleStep( 2 )
|
|
|
|
self._num_searched = ClientGUICommon.TextAndGauge( self._searching_panel )
|
|
|
|
self._search_button = ClientGUICommon.BetterBitmapButton( self._searching_panel, CC.global_pixmaps().play, self._duplicates_manager.StartPotentialsSearch )
|
|
|
|
#
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'edit duplicate metadata merge options for \'this is better\'', 'edit what content is merged when you filter files', HydrusData.Call( self._EditMergeOptions, HC.DUPLICATE_BETTER ) ) )
|
|
menu_items.append( ( 'normal', 'edit duplicate metadata merge options for \'same quality\'', 'edit what content is merged when you filter files', HydrusData.Call( self._EditMergeOptions, HC.DUPLICATE_SAME_QUALITY ) ) )
|
|
|
|
if new_options.GetBoolean( 'advanced_mode' ):
|
|
|
|
menu_items.append( ( 'normal', 'edit duplicate metadata merge options for \'alternates\' (advanced!)', 'edit what content is merged when you filter files', HydrusData.Call( self._EditMergeOptions, HC.DUPLICATE_ALTERNATE ) ) )
|
|
|
|
|
|
self._edit_merge_options = ClientGUIMenuButton.MenuButton( self._main_right_panel, 'edit default duplicate metadata merge options', menu_items )
|
|
|
|
#
|
|
|
|
self._filtering_panel = ClientGUICommon.StaticBox( self._main_right_panel, 'duplicate filter' )
|
|
|
|
file_search_context_1 = management_controller.GetVariable( 'file_search_context_1' )
|
|
file_search_context_2 = management_controller.GetVariable( 'file_search_context_2' )
|
|
|
|
file_search_context_1.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
|
|
file_search_context_2.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
|
|
|
|
if self._management_controller.HasVariable( 'synchronised' ):
|
|
|
|
synchronised = self._management_controller.GetVariable( 'synchronised' )
|
|
|
|
else:
|
|
|
|
synchronised = True
|
|
|
|
|
|
self._tag_autocomplete_1 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_1, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, synchronised = synchronised, force_system_everything = True )
|
|
self._tag_autocomplete_2 = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._filtering_panel, self._page_key, file_search_context_2, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, allow_all_known_files = False, synchronised = synchronised, force_system_everything = True )
|
|
|
|
self._dupe_search_type = ClientGUICommon.BetterChoice( self._filtering_panel )
|
|
|
|
self._dupe_search_type.addItem( 'at least one file matches the search', CC.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH )
|
|
self._dupe_search_type.addItem( 'both files match the search', CC.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH )
|
|
self._dupe_search_type.addItem( 'both files match different searches', CC.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
|
|
|
|
self._pixel_dupes_preference = ClientGUICommon.BetterChoice( self._filtering_panel )
|
|
|
|
for p in ( CC.SIMILAR_FILES_PIXEL_DUPES_REQUIRED, CC.SIMILAR_FILES_PIXEL_DUPES_ALLOWED, CC.SIMILAR_FILES_PIXEL_DUPES_EXCLUDED ):
|
|
|
|
self._pixel_dupes_preference.addItem( CC.similar_files_pixel_dupes_string_lookup[ p ], p )
|
|
|
|
|
|
self._max_hamming_distance_for_filter = ClientGUICommon.BetterSpinBox( self._filtering_panel, min = 0, max = 64 )
|
|
self._max_hamming_distance_for_filter.setSingleStep( 2 )
|
|
|
|
self._num_potential_duplicates = ClientGUICommon.BetterStaticText( self._filtering_panel, ellipsize_end = True )
|
|
self._refresh_dupe_counts_button = ClientGUICommon.BetterBitmapButton( self._filtering_panel, CC.global_pixmaps().refresh, self.RefreshDuplicateNumbers )
|
|
|
|
self._launch_filter = ClientGUICommon.BetterButton( self._filtering_panel, 'launch the filter', self._LaunchFilter )
|
|
|
|
#
|
|
|
|
random_filtering_panel = ClientGUICommon.StaticBox( self._main_right_panel, 'quick and dirty processing' )
|
|
|
|
self._show_some_dupes = ClientGUICommon.BetterButton( random_filtering_panel, 'show some random potential pairs', self._ShowRandomPotentialDupes )
|
|
|
|
self._set_random_as_same_quality_button = ClientGUICommon.BetterButton( random_filtering_panel, 'set current media as duplicates of the same quality', self._SetCurrentMediaAs, HC.DUPLICATE_SAME_QUALITY )
|
|
self._set_random_as_alternates_button = ClientGUICommon.BetterButton( random_filtering_panel, 'set current media as all related alternates', self._SetCurrentMediaAs, HC.DUPLICATE_ALTERNATE )
|
|
self._set_random_as_false_positives_button = ClientGUICommon.BetterButton( random_filtering_panel, 'set current media as not related/false positive', self._SetCurrentMediaAs, HC.DUPLICATE_FALSE_POSITIVE )
|
|
|
|
#
|
|
|
|
self._main_notebook.addTab( self._main_left_panel, 'preparation' )
|
|
self._main_notebook.addTab( self._main_right_panel, 'filtering' )
|
|
self._main_notebook.setCurrentWidget( self._main_right_panel )
|
|
|
|
#
|
|
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.setValue( new_options.GetInteger( 'similar_files_duplicate_pairs_search_distance' ) )
|
|
|
|
self._dupe_search_type.SetValue( management_controller.GetVariable( 'dupe_search_type' ) )
|
|
|
|
if not management_controller.HasVariable( 'pixel_dupes_preference' ):
|
|
|
|
management_controller.SetVariable( 'pixel_dupes_preference', CC.SIMILAR_FILES_PIXEL_DUPES_ALLOWED )
|
|
|
|
|
|
self._pixel_dupes_preference.SetValue( management_controller.GetVariable( 'pixel_dupes_preference' ) )
|
|
|
|
self._pixel_dupes_preference.currentIndexChanged.connect( self.FilterSearchDomainChanged )
|
|
|
|
if not management_controller.HasVariable( 'max_hamming_distance' ):
|
|
|
|
management_controller.SetVariable( 'max_hamming_distance', 4 )
|
|
|
|
|
|
self._max_hamming_distance_for_filter.setValue( management_controller.GetVariable( 'max_hamming_distance' ) )
|
|
|
|
self._max_hamming_distance_for_filter.valueChanged.connect( self.FilterSearchDomainChanged )
|
|
|
|
#
|
|
|
|
self._UpdateFilterSearchControls()
|
|
|
|
#
|
|
|
|
self._media_sort_widget.hide()
|
|
|
|
distance_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( distance_hbox, ClientGUICommon.BetterStaticText(self._searching_panel,label='search distance: '), CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( distance_hbox, self._max_hamming_distance_for_potential_discovery_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( distance_hbox, self._max_hamming_distance_for_potential_discovery_spinctrl, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
gridbox_2 = QP.GridLayout( cols = 2 )
|
|
|
|
gridbox_2.setColumnStretch( 0, 1 )
|
|
|
|
QP.AddToLayout( gridbox_2, self._num_searched, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
QP.AddToLayout( gridbox_2, self._search_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
self._searching_panel.Add( self._eligible_files, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._searching_panel.Add( distance_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._searching_panel.Add( gridbox_2, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._refresh_maintenance_status, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( hbox, self._refresh_maintenance_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( hbox, self._cog_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( hbox, self._help_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._searching_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.addStretch( 1 )
|
|
|
|
self._main_left_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
text_and_button_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( text_and_button_hbox, self._num_potential_duplicates, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( text_and_button_hbox, self._refresh_dupe_counts_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
rows = []
|
|
|
|
rows.append( ( 'maximum search distance of pair: ', self._max_hamming_distance_for_filter ) )
|
|
|
|
gridbox = ClientGUICommon.WrapInGrid( self._filtering_panel, rows )
|
|
|
|
self._filtering_panel.Add( self._dupe_search_type, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._filtering_panel.Add( self._tag_autocomplete_1, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._filtering_panel.Add( self._tag_autocomplete_2, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._filtering_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._filtering_panel.Add( self._pixel_dupes_preference, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._filtering_panel.Add( text_and_button_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._filtering_panel.Add( self._launch_filter, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
random_filtering_panel.Add( self._show_some_dupes, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
random_filtering_panel.Add( self._set_random_as_same_quality_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
random_filtering_panel.Add( self._set_random_as_alternates_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
random_filtering_panel.Add( self._set_random_as_false_positives_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._edit_merge_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._filtering_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, random_filtering_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self._main_right_panel.setLayout( vbox )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._main_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
self._controller.sub( self, 'NotifyNewMaintenanceNumbers', 'new_similar_files_maintenance_numbers' )
|
|
self._controller.sub( self, 'NotifyNewPotentialsSearchNumbers', 'new_similar_files_potentials_search_numbers' )
|
|
|
|
self._tag_autocomplete_1.searchChanged.connect( self.Search1Changed )
|
|
self._tag_autocomplete_2.searchChanged.connect( self.Search2Changed )
|
|
|
|
self._dupe_search_type.currentIndexChanged.connect( self.FilterDupeSearchTypeChanged )
|
|
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.valueChanged.connect( self.MaxHammingDistanceForPotentialDiscoveryChanged )
|
|
|
|
|
|
def _EditMergeOptions( self, duplicate_type ):
|
|
|
|
new_options = CG.client_controller.new_options
|
|
|
|
duplicate_content_merge_options = new_options.GetDuplicateContentMergeOptions( duplicate_type )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit duplicate merge options' ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanelsEdit.EditDuplicateContentMergeOptionsPanel( dlg, duplicate_type, duplicate_content_merge_options )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
duplicate_content_merge_options = panel.GetValue()
|
|
|
|
new_options.SetDuplicateContentMergeOptions( duplicate_type, duplicate_content_merge_options )
|
|
|
|
|
|
|
|
|
|
def _FilterSearchDomainUpdated( self ):
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData( optimise_for_search = False )
|
|
|
|
self._management_controller.SetVariable( 'file_search_context_1', file_search_context_1 )
|
|
self._management_controller.SetVariable( 'file_search_context_2', file_search_context_2 )
|
|
|
|
synchronised = self._tag_autocomplete_1.IsSynchronised()
|
|
|
|
self._management_controller.SetVariable( 'synchronised', synchronised )
|
|
|
|
self._management_controller.SetVariable( 'dupe_search_type', dupe_search_type )
|
|
self._management_controller.SetVariable( 'pixel_dupes_preference', pixel_dupes_preference )
|
|
self._management_controller.SetVariable( 'max_hamming_distance', max_hamming_distance )
|
|
|
|
self._SetLocationContext( file_search_context_1.GetLocationContext() )
|
|
|
|
self._UpdateFilterSearchControls()
|
|
|
|
if self._tag_autocomplete_1.IsSynchronised():
|
|
|
|
self._dupe_count_numbers_dirty = True
|
|
|
|
|
|
|
|
def _GetDuplicateFileSearchData( self, optimise_for_search = True ) -> typing.Tuple[ ClientSearch.FileSearchContext, ClientSearch.FileSearchContext, int, int, int ]:
|
|
|
|
file_search_context_1 = self._tag_autocomplete_1.GetFileSearchContext()
|
|
file_search_context_2 = self._tag_autocomplete_2.GetFileSearchContext()
|
|
|
|
dupe_search_type = self._dupe_search_type.GetValue()
|
|
|
|
if optimise_for_search:
|
|
|
|
if dupe_search_type == CC.DUPE_SEARCH_BOTH_FILES_MATCH_ONE_SEARCH and ( file_search_context_1.IsJustSystemEverything() or file_search_context_1.HasNoPredicates() ):
|
|
|
|
dupe_search_type = CC.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
|
|
|
|
elif dupe_search_type == CC.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES:
|
|
|
|
if file_search_context_1.IsJustSystemEverything() or file_search_context_1.HasNoPredicates():
|
|
|
|
f = file_search_context_1
|
|
file_search_context_1 = file_search_context_2
|
|
file_search_context_2 = f
|
|
|
|
dupe_search_type = CC.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
|
|
|
|
elif file_search_context_2.IsJustSystemEverything() or file_search_context_2.HasNoPredicates():
|
|
|
|
dupe_search_type = CC.DUPE_SEARCH_ONE_FILE_MATCHES_ONE_SEARCH
|
|
|
|
|
|
|
|
|
|
pixel_dupes_preference = self._pixel_dupes_preference.GetValue()
|
|
|
|
max_hamming_distance = self._max_hamming_distance_for_filter.value()
|
|
|
|
return ( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
|
|
def _LaunchFilter( self ):
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
|
|
|
|
canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() )
|
|
|
|
canvas_window = ClientGUICanvas.CanvasFilterDuplicates( canvas_frame, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
canvas_window.showPairInPage.connect( self._ShowPairInPage )
|
|
|
|
canvas_frame.SetCanvas( canvas_window )
|
|
|
|
|
|
def _RefreshDuplicateCounts( self ):
|
|
|
|
def qt_code( potential_duplicates_count ):
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
self._currently_refreshing_dupe_count_numbers = False
|
|
|
|
self._dupe_count_numbers_dirty = False
|
|
|
|
self._refresh_dupe_counts_button.setEnabled( True )
|
|
|
|
self._UpdatePotentialDuplicatesCount( potential_duplicates_count )
|
|
|
|
|
|
def thread_do_it( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ):
|
|
|
|
potential_duplicates_count = CG.client_controller.Read( 'potential_duplicates_count', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
QP.CallAfter( qt_code, potential_duplicates_count )
|
|
|
|
|
|
if not self._currently_refreshing_dupe_count_numbers:
|
|
|
|
self._currently_refreshing_dupe_count_numbers = True
|
|
|
|
self._refresh_dupe_counts_button.setEnabled( False )
|
|
|
|
self._num_potential_duplicates.setText( 'updating' + HC.UNICODE_ELLIPSIS )
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
|
|
|
|
CG.client_controller.CallToThread( thread_do_it, file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
|
|
|
|
def _ResetUnknown( self ):
|
|
|
|
text = 'ADVANCED TOOL: This will delete all the current potential duplicate pairs. All files that may be similar will be queued for search again.'
|
|
text += os.linesep * 2
|
|
text += 'This can be useful if you know you have database damage and need to reset and re-search everything, or if you have accidentally searched too broadly and are now swamped with too many false positives. It is not useful for much else.'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, text )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
self._controller.Write( 'delete_potential_duplicate_pairs' )
|
|
|
|
self._duplicates_manager.RefreshMaintenanceNumbers()
|
|
|
|
|
|
|
|
def _SetCurrentMediaAs( self, duplicate_type ):
|
|
|
|
media_panel = self._page.GetMediaPanel()
|
|
|
|
change_made = media_panel.SetDuplicateStatusForAll( duplicate_type )
|
|
|
|
if change_made:
|
|
|
|
self._dupe_count_numbers_dirty = True
|
|
|
|
if self._potential_duplicates_count > 1:
|
|
|
|
self._ShowRandomPotentialDupes()
|
|
|
|
else:
|
|
|
|
self._ShowPotentialDupes( [] )
|
|
|
|
|
|
|
|
|
|
def _SetSearchDistance( self, value ):
|
|
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.setValue( value )
|
|
|
|
self._UpdateMaintenanceStatus()
|
|
|
|
|
|
def _ShowPairInPage( self, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
|
|
|
|
media_results = [ m.GetMediaResult() for m in media ]
|
|
|
|
self._page.GetMediaPanel().AddMediaResults( self._page_key, media_results )
|
|
|
|
|
|
def _ShowPotentialDupes( self, hashes ):
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
|
|
|
|
location_context = file_search_context_1.GetLocationContext()
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
if len( hashes ) > 0:
|
|
|
|
media_results = self._controller.Read( 'media_results', hashes, sorted = True )
|
|
|
|
else:
|
|
|
|
media_results = []
|
|
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no dupes found' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._page_state = CC.PAGE_STATE_NORMAL
|
|
|
|
|
|
def _ShowRandomPotentialDupes( self ):
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData()
|
|
|
|
self._page_state = CC.PAGE_STATE_SEARCHING
|
|
|
|
hashes = self._controller.Read( 'random_potential_duplicate_hashes', file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance )
|
|
|
|
if len( hashes ) == 0:
|
|
|
|
HydrusData.ShowText( 'No random potential duplicates were found. Try refreshing the count, and if this keeps happening, please let hydrus_dev know.' )
|
|
|
|
|
|
self._ShowPotentialDupes( hashes )
|
|
|
|
|
|
def _UpdateMaintenanceStatus( self ):
|
|
|
|
self._refresh_maintenance_button.setEnabled( not ( self._duplicates_manager_is_fetching_maintenance_numbers or self._potential_file_search_currently_happening ) )
|
|
|
|
if self._similar_files_maintenance_status is None:
|
|
|
|
self._search_button.setEnabled( False )
|
|
|
|
return
|
|
|
|
|
|
searched_distances_to_count = self._similar_files_maintenance_status
|
|
|
|
self._cog_button.setEnabled( True )
|
|
|
|
total_num_files = sum( searched_distances_to_count.values() )
|
|
|
|
self._eligible_files.setText( '{} eligible files in the system.'.format(HydrusData.ToHumanInt(total_num_files)) )
|
|
|
|
self._max_hamming_distance_for_potential_discovery_button.setEnabled( True )
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.setEnabled( True )
|
|
|
|
options_search_distance = self._controller.new_options.GetInteger( 'similar_files_duplicate_pairs_search_distance' )
|
|
|
|
if self._max_hamming_distance_for_potential_discovery_spinctrl.value() != options_search_distance:
|
|
|
|
self._max_hamming_distance_for_potential_discovery_spinctrl.setValue( options_search_distance )
|
|
|
|
|
|
search_distance = self._max_hamming_distance_for_potential_discovery_spinctrl.value()
|
|
|
|
if search_distance in CC.hamming_string_lookup:
|
|
|
|
button_label = CC.hamming_string_lookup[ search_distance ]
|
|
|
|
else:
|
|
|
|
button_label = 'custom'
|
|
|
|
|
|
self._max_hamming_distance_for_potential_discovery_button.setText( button_label )
|
|
|
|
num_searched = sum( ( count for ( value, count ) in searched_distances_to_count.items() if value is not None and value >= search_distance ) )
|
|
|
|
not_all_files_searched = num_searched < total_num_files
|
|
|
|
we_can_start_work = not_all_files_searched and not self._potential_file_search_currently_happening
|
|
|
|
self._search_button.setEnabled( we_can_start_work )
|
|
|
|
if not_all_files_searched:
|
|
|
|
if num_searched == 0:
|
|
|
|
self._num_searched.SetValue( 'Have not yet searched at this distance.', 0, total_num_files )
|
|
|
|
else:
|
|
|
|
self._num_searched.SetValue( 'Searched ' + HydrusData.ConvertValueRangeToPrettyString( num_searched, total_num_files ) + ' files at this distance.', num_searched, total_num_files )
|
|
|
|
|
|
page_name = 'preparation (needs work)'
|
|
|
|
else:
|
|
|
|
self._num_searched.SetValue( 'All potential duplicates found at this distance.', total_num_files, total_num_files )
|
|
|
|
page_name = 'preparation'
|
|
|
|
|
|
self._main_notebook.setTabText( 0, page_name )
|
|
|
|
|
|
def _UpdatePotentialDuplicatesCount( self, potential_duplicates_count ):
|
|
|
|
self._potential_duplicates_count = potential_duplicates_count
|
|
|
|
self._num_potential_duplicates.setText( '{} potential pairs.'.format( HydrusData.ToHumanInt( potential_duplicates_count ) ) )
|
|
|
|
if self._potential_duplicates_count > 0:
|
|
|
|
self._show_some_dupes.setEnabled( True )
|
|
self._launch_filter.setEnabled( True )
|
|
|
|
else:
|
|
|
|
self._show_some_dupes.setEnabled( False )
|
|
self._launch_filter.setEnabled( False )
|
|
|
|
|
|
|
|
def _UpdateFilterSearchControls( self ):
|
|
|
|
( file_search_context_1, file_search_context_2, dupe_search_type, pixel_dupes_preference, max_hamming_distance ) = self._GetDuplicateFileSearchData( optimise_for_search = False )
|
|
|
|
self._tag_autocomplete_2.setVisible( dupe_search_type == CC.DUPE_SEARCH_BOTH_FILES_MATCH_DIFFERENT_SEARCHES )
|
|
|
|
self._max_hamming_distance_for_filter.setEnabled( self._pixel_dupes_preference.GetValue() != CC.SIMILAR_FILES_PIXEL_DUPES_REQUIRED )
|
|
|
|
|
|
def FilterDupeSearchTypeChanged( self ):
|
|
|
|
self._FilterSearchDomainUpdated()
|
|
|
|
|
|
def FilterSearchDomainChanged( self ):
|
|
|
|
self._FilterSearchDomainUpdated()
|
|
|
|
|
|
def MaxHammingDistanceForPotentialDiscoveryChanged( self ):
|
|
|
|
search_distance = self._max_hamming_distance_for_potential_discovery_spinctrl.value()
|
|
|
|
self._controller.new_options.SetInteger( 'similar_files_duplicate_pairs_search_distance', search_distance )
|
|
|
|
self._controller.pub( 'new_similar_files_maintenance_numbers' )
|
|
|
|
self._UpdateMaintenanceStatus()
|
|
|
|
|
|
def NotifyNewMaintenanceNumbers( self ):
|
|
|
|
self._maintenance_numbers_need_redrawing = True
|
|
|
|
|
|
def NotifyNewPotentialsSearchNumbers( self ):
|
|
|
|
self._dupe_count_numbers_dirty = True
|
|
|
|
|
|
def PageHidden( self ):
|
|
|
|
ManagementPanel.PageHidden( self )
|
|
|
|
self._tag_autocomplete_1.SetForceDropdownHide( True )
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
ManagementPanel.PageShown( self )
|
|
|
|
self._tag_autocomplete_1.SetForceDropdownHide( False )
|
|
|
|
|
|
def RefreshDuplicateNumbers( self ):
|
|
|
|
self._dupe_count_numbers_dirty = True
|
|
|
|
|
|
def RefreshQuery( self ):
|
|
|
|
self._FilterSearchDomainUpdated()
|
|
|
|
|
|
def REPEATINGPageUpdate( self ):
|
|
|
|
if self._maintenance_numbers_need_redrawing:
|
|
|
|
( self._similar_files_maintenance_status, self._duplicates_manager_is_fetching_maintenance_numbers, self._potential_file_search_currently_happening ) = self._duplicates_manager.GetMaintenanceNumbers()
|
|
|
|
self._maintenance_numbers_need_redrawing = False
|
|
|
|
self._UpdateMaintenanceStatus()
|
|
|
|
|
|
if self._dupe_count_numbers_dirty:
|
|
|
|
self._RefreshDuplicateCounts()
|
|
|
|
|
|
self._tag_autocomplete_1.REPEATINGPageUpdate()
|
|
self._tag_autocomplete_2.REPEATINGPageUpdate()
|
|
|
|
|
|
def Search1Changed( self, file_search_context: ClientSearch.FileSearchContext ):
|
|
|
|
self._tag_autocomplete_2.blockSignals( True )
|
|
|
|
self._tag_autocomplete_2.SetLocationContext( self._tag_autocomplete_1.GetLocationContext() )
|
|
self._tag_autocomplete_2.SetSynchronised( self._tag_autocomplete_1.IsSynchronised() )
|
|
|
|
self._tag_autocomplete_2.blockSignals( False )
|
|
|
|
self._FilterSearchDomainUpdated()
|
|
|
|
|
|
def Search2Changed( self, file_search_context: ClientSearch.FileSearchContext ):
|
|
|
|
self._tag_autocomplete_1.blockSignals( True )
|
|
|
|
self._tag_autocomplete_1.SetLocationContext( self._tag_autocomplete_2.GetLocationContext() )
|
|
self._tag_autocomplete_1.SetSynchronised( self._tag_autocomplete_2.IsSynchronised() )
|
|
|
|
self._tag_autocomplete_1.blockSignals( False )
|
|
|
|
self._FilterSearchDomainUpdated()
|
|
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_DUPLICATE_FILTER ] = ManagementPanelDuplicateFilter
|
|
|
|
class ManagementPanelImporter( ManagementPanel ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanel.__init__( self, parent, page, controller, management_controller )
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
ManagementPanel.PageShown( self )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def RefreshQuery( self ):
|
|
|
|
self._media_sort_widget.BroadcastSort()
|
|
|
|
|
|
def REPEATINGPageUpdate( self ):
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
class ManagementPanelImporterHDD( ManagementPanelImporter ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanelImporter.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._import_queue_panel = ClientGUICommon.StaticBox( self, 'imports' )
|
|
|
|
self._current_action = ClientGUICommon.BetterStaticText( self._import_queue_panel, ellipsize_end = True )
|
|
|
|
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self._import_queue_panel, self._controller, self._page_key )
|
|
|
|
self._pause_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.Pause )
|
|
self._pause_button.setToolTip( 'pause/play imports' )
|
|
|
|
self._hdd_import: ClientImportLocal.HDDImport = self._management_controller.GetVariable( 'hdd_import' )
|
|
|
|
file_import_options = self._hdd_import.GetFileImportOptions()
|
|
|
|
show_downloader_options = False
|
|
allow_default_selection = True
|
|
|
|
self._import_options_button = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
|
|
|
|
self._import_options_button.SetFileImportOptions( file_import_options )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._current_action, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( hbox, self._pause_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
self._import_queue_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._import_queue_panel.Add( self._file_seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._import_queue_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._import_queue_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
file_seed_cache = self._hdd_import.GetFileSeedCache()
|
|
|
|
self._file_seed_cache_control.SetFileSeedCache( file_seed_cache )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
self._import_options_button.fileImportOptionsChanged.connect( self._hdd_import.SetFileImportOptions )
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
( current_action, paused ) = self._hdd_import.GetStatus()
|
|
|
|
if paused:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_button, CC.global_pixmaps().file_play )
|
|
|
|
else:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_button, CC.global_pixmaps().file_pause )
|
|
|
|
|
|
self._current_action.setText( current_action )
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
if self._hdd_import.CurrentlyWorking():
|
|
|
|
raise HydrusExceptions.VetoException( 'This page is still importing.' )
|
|
|
|
|
|
|
|
def Pause( self ):
|
|
|
|
self._hdd_import.PausePlay()
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def Start( self ):
|
|
|
|
self._hdd_import.Start( self._page_key )
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_IMPORT_HDD ] = ManagementPanelImporterHDD
|
|
|
|
class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanelImporter.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._last_time_imports_changed = 0
|
|
self._next_update_time = 0
|
|
|
|
self._multiple_gallery_import = self._management_controller.GetVariable( 'multiple_gallery_import' )
|
|
|
|
self._highlighted_gallery_import = self._multiple_gallery_import.GetHighlightedGalleryImport()
|
|
|
|
self._loading_highlight_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
self._loading_highlight_job_status.Finish()
|
|
|
|
#
|
|
|
|
self._gallery_downloader_panel = ClientGUICommon.StaticBox( self, 'gallery downloader' )
|
|
|
|
#
|
|
|
|
self._gallery_importers_status_st_top = ClientGUICommon.BetterStaticText( self._gallery_downloader_panel, ellipsize_end = True )
|
|
self._gallery_importers_status_st_bottom = ClientGUICommon.BetterStaticText( self._gallery_downloader_panel, ellipsize_end = True )
|
|
|
|
self._gallery_importers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._gallery_downloader_panel )
|
|
|
|
self._gallery_importers_listctrl = ClientGUIListCtrl.BetterListCtrl( self._gallery_importers_listctrl_panel, CGLC.COLUMN_LIST_GALLERY_IMPORTERS.ID, 4, self._ConvertDataToListCtrlTuples, delete_key_callback = self._RemoveGalleryImports, activation_callback = self._HighlightSelectedGalleryImport )
|
|
|
|
self._gallery_importers_listctrl_panel.SetListCtrl( self._gallery_importers_listctrl )
|
|
|
|
self._gallery_importers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().highlight, self._HighlightSelectedGalleryImport, tooltip = 'highlight', enabled_check_func = self._CanHighlight )
|
|
self._gallery_importers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().clear_highlight, self._ClearExistingHighlightAndPanel, tooltip = 'clear highlight', enabled_check_func = self._CanClearHighlight )
|
|
self._gallery_importers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().file_pause, self._PausePlayFiles, tooltip = 'pause/play files', enabled_only_on_selection = True )
|
|
self._gallery_importers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().gallery_pause, self._PausePlayGallery, tooltip = 'pause/play search', enabled_only_on_selection = True )
|
|
self._gallery_importers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().trash, self._RemoveGalleryImports, tooltip = 'remove selected', enabled_only_on_selection = True )
|
|
|
|
self._gallery_importers_listctrl_panel.NewButtonRow()
|
|
|
|
self._gallery_importers_listctrl_panel.AddButton( 'retry failed', self._RetryFailed, enabled_check_func = self._CanRetryFailed )
|
|
self._gallery_importers_listctrl_panel.AddButton( 'retry ignored', self._RetryIgnored, enabled_check_func = self._CanRetryIgnored )
|
|
|
|
self._gallery_importers_listctrl_panel.NewButtonRow()
|
|
|
|
self._gallery_importers_listctrl_panel.AddButton( 'set options to queries', self._SetOptionsToGalleryImports, enabled_only_on_selection = True )
|
|
|
|
self._gallery_importers_listctrl.Sort()
|
|
|
|
#
|
|
|
|
self._query_input = ClientGUIControls.TextAndPasteCtrl( self._gallery_downloader_panel, self._PendQueries )
|
|
|
|
self._cog_button = ClientGUICommon.BetterBitmapButton( self._gallery_downloader_panel, CC.global_pixmaps().cog, self._ShowCogMenu )
|
|
|
|
self._gug_key_and_name = ClientGUIImport.GUGKeyAndNameSelector( self._gallery_downloader_panel, self._multiple_gallery_import.GetGUGKeyAndName(), update_callable = self._SetGUGKeyAndName )
|
|
|
|
self._file_limit = ClientGUICommon.NoneableSpinCtrl( self._gallery_downloader_panel, 'stop after this many files', min = 1, none_phrase = 'no limit' )
|
|
self._file_limit.valueChanged.connect( self.EventFileLimit )
|
|
self._file_limit.setToolTip( 'per query, stop searching the gallery once this many files has been reached' )
|
|
|
|
file_import_options = self._multiple_gallery_import.GetFileImportOptions()
|
|
tag_import_options = self._multiple_gallery_import.GetTagImportOptions()
|
|
note_import_options = self._multiple_gallery_import.GetNoteImportOptions()
|
|
file_limit = self._multiple_gallery_import.GetFileLimit()
|
|
|
|
show_downloader_options = True
|
|
allow_default_selection = True
|
|
|
|
self._import_options_button = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
|
|
|
|
self._import_options_button.SetFileImportOptions( file_import_options )
|
|
self._import_options_button.SetTagImportOptions( tag_import_options )
|
|
self._import_options_button.SetNoteImportOptions( note_import_options )
|
|
|
|
#
|
|
|
|
input_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( input_hbox, self._query_input, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( input_hbox, self._cog_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
self._gallery_downloader_panel.Add( self._gallery_importers_status_st_top, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._gallery_downloader_panel.Add( self._gallery_importers_status_st_bottom, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._gallery_downloader_panel.Add( self._gallery_importers_listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
self._gallery_downloader_panel.Add( input_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._gallery_downloader_panel.Add( self._gug_key_and_name, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._gallery_downloader_panel.Add( self._file_limit, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._gallery_downloader_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
self._highlighted_gallery_import_panel = ClientGUIImport.GalleryImportPanel( self, self._page_key, name = 'highlighted query' )
|
|
|
|
self._highlighted_gallery_import_panel.SetGalleryImport( self._highlighted_gallery_import )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._gallery_downloader_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._highlighted_gallery_import_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
initial_search_text = self._multiple_gallery_import.GetInitialSearchText()
|
|
|
|
self._query_input.setPlaceholderText( initial_search_text )
|
|
|
|
self._file_limit.SetValue( file_limit )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
self._gallery_importers_listctrl.AddRowsMenuCallable( self._GetListCtrlMenu )
|
|
|
|
self._import_options_button.fileImportOptionsChanged.connect( self._multiple_gallery_import.SetFileImportOptions )
|
|
self._import_options_button.noteImportOptionsChanged.connect( self._multiple_gallery_import.SetNoteImportOptions )
|
|
self._import_options_button.tagImportOptionsChanged.connect( self._multiple_gallery_import.SetTagImportOptions )
|
|
|
|
|
|
def _CanClearHighlight( self ):
|
|
|
|
return self._highlighted_gallery_import is not None or not self._loading_highlight_job_status.IsDone()
|
|
|
|
|
|
def _CanHighlight( self ):
|
|
|
|
selected = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected ) != 1:
|
|
|
|
return False
|
|
|
|
|
|
gallery_import = selected[0]
|
|
|
|
return not self._ThisIsTheCurrentOrLoadingHighlight( gallery_import )
|
|
|
|
|
|
def _CanRetryFailed( self ):
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
if gallery_import.CanRetryFailed():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _CanRetryIgnored( self ):
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
if gallery_import.CanRetryIgnored():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _ClearExistingHighlight( self ):
|
|
|
|
if not self._loading_highlight_job_status.IsDone():
|
|
|
|
self._loading_highlight_job_status.Cancel()
|
|
|
|
|
|
if self._highlighted_gallery_import is not None:
|
|
|
|
self._highlighted_gallery_import.PublishToPage( False )
|
|
|
|
self._highlighted_gallery_import = None
|
|
|
|
self._multiple_gallery_import.ClearHighlightedGalleryImport()
|
|
|
|
self._gallery_importers_listctrl_panel.UpdateButtons()
|
|
|
|
self._highlighted_gallery_import_panel.SetGalleryImport( None )
|
|
|
|
|
|
|
|
def _ClearExistingHighlightAndPanel( self ):
|
|
|
|
self._ClearExistingHighlight()
|
|
|
|
media_results = []
|
|
|
|
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no highlighted query' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _ConvertDataToListCtrlTuples( self, gallery_import ):
|
|
|
|
query_text = gallery_import.GetQueryText()
|
|
|
|
pretty_query_text = query_text
|
|
|
|
if gallery_import == self._highlighted_gallery_import:
|
|
|
|
pretty_query_text = f'* {pretty_query_text}'
|
|
|
|
elif not self._loading_highlight_job_status.IsDone():
|
|
|
|
downloader = self._loading_highlight_job_status.GetIfHasVariable( 'downloader' )
|
|
|
|
if downloader is not None and gallery_import == downloader:
|
|
|
|
pretty_query_text = f'> {pretty_query_text}'
|
|
|
|
|
|
|
|
source = gallery_import.GetSourceName()
|
|
|
|
pretty_source = source
|
|
|
|
files_finished = gallery_import.FilesFinished()
|
|
files_paused = gallery_import.FilesPaused()
|
|
|
|
if files_finished:
|
|
|
|
pretty_files_paused = CG.client_controller.new_options.GetString( 'stop_character' )
|
|
|
|
sort_files_paused = -1
|
|
|
|
elif files_paused:
|
|
|
|
pretty_files_paused = CG.client_controller.new_options.GetString( 'pause_character' )
|
|
|
|
sort_files_paused = 0
|
|
|
|
else:
|
|
|
|
pretty_files_paused = ''
|
|
|
|
sort_files_paused = 1
|
|
|
|
|
|
gallery_finished = gallery_import.GalleryFinished()
|
|
gallery_paused = gallery_import.GalleryPaused()
|
|
|
|
if gallery_finished:
|
|
|
|
pretty_gallery_paused = CG.client_controller.new_options.GetString( 'stop_character' )
|
|
|
|
sort_gallery_paused = -1
|
|
|
|
elif gallery_paused:
|
|
|
|
pretty_gallery_paused = CG.client_controller.new_options.GetString( 'pause_character' )
|
|
|
|
sort_gallery_paused = 0
|
|
|
|
else:
|
|
|
|
pretty_gallery_paused = ''
|
|
|
|
sort_gallery_paused = 1
|
|
|
|
|
|
( status_enum, pretty_status ) = gallery_import.GetSimpleStatus()
|
|
|
|
sort_status = ClientImporting.downloader_enum_sort_lookup[ status_enum ]
|
|
|
|
file_seed_cache_status = gallery_import.GetFileSeedCache().GetStatus()
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
progress = ( num_total, num_done )
|
|
|
|
pretty_progress = file_seed_cache_status.GetStatusText( simple = True )
|
|
|
|
added = gallery_import.GetCreationTime()
|
|
|
|
pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False )
|
|
|
|
display_tuple = ( pretty_query_text, pretty_source, pretty_files_paused, pretty_gallery_paused, pretty_status, pretty_progress, pretty_added )
|
|
sort_tuple = ( query_text, pretty_source, sort_files_paused, sort_gallery_paused, sort_status, progress, added )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _CopySelectedQueries( self ):
|
|
|
|
gallery_importers = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( gallery_importers ) > 0:
|
|
|
|
text = os.linesep.join( ( gallery_importer.GetQueryText() for gallery_importer in gallery_importers ) )
|
|
|
|
CG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
|
|
|
|
return 'no highlighted query'
|
|
|
|
|
|
def _GetListCtrlMenu( self ):
|
|
|
|
selected_importers = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected_importers ) == 0:
|
|
|
|
raise HydrusExceptions.DataMissing()
|
|
|
|
|
|
menu = ClientGUIMenus.GenerateMenu( self )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'copy queries', 'Copy all the selected downloaders\' queries to clipboard.', self._CopySelectedQueries )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
single_selected_presentation_import_options = None
|
|
|
|
if len( selected_importers ) == 1:
|
|
|
|
( importer, ) = selected_importers
|
|
|
|
fio = importer.GetFileImportOptions()
|
|
|
|
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
|
|
|
|
|
|
AddPresentationSubmenu( menu, 'downloader', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if len( selected_importers ) == 1:
|
|
|
|
( importer, ) = selected_importers
|
|
|
|
file_seed_cache = importer.GetFileSeedCache()
|
|
|
|
submenu = ClientGUIMenus.GenerateMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'show file log', 'Show the file log windows for the selected query.', self._ShowSelectedImportersFileSeedCaches )
|
|
|
|
ClientGUIMenus.AppendSeparator( submenu )
|
|
|
|
ClientGUIFileSeedCache.PopulateFileSeedCacheMenu( self, submenu, file_seed_cache )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'file log' )
|
|
|
|
gallery_seed_log = importer.GetGallerySeedLog()
|
|
|
|
submenu = ClientGUIMenus.GenerateMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'show search log', 'Show the search log windows for the selected query.', self._ShowSelectedImportersGallerySeedLogs )
|
|
|
|
ClientGUIMenus.AppendSeparator( submenu )
|
|
|
|
ClientGUIGallerySeedLog.PopulateGallerySeedLogButton( self, submenu, gallery_seed_log, False, True, 'search' )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'search log' )
|
|
|
|
else:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'show file logs', 'Show the file log windows for the selected queries.', self._ShowSelectedImportersFileSeedCaches )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'show search log', 'Show the search log windows for the selected query.', self._ShowSelectedImportersGallerySeedLogs )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'remove', 'Remove the selected queries.', self._RemoveGalleryImports )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'pause/play files', 'Pause/play all the selected downloaders\' file queues.', self._PausePlayFiles )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'pause/play search', 'Pause/play all the selected downloaders\' gallery searches.', self._PausePlayGallery )
|
|
|
|
return menu
|
|
|
|
|
|
def _HighlightGalleryImport( self, new_highlight ):
|
|
|
|
if self._ThisIsTheCurrentOrLoadingHighlight( new_highlight ):
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
else:
|
|
|
|
self._ClearExistingHighlight()
|
|
|
|
self._loading_highlight_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
name = new_highlight.GetQueryText()
|
|
|
|
self._loading_highlight_job_status.SetStatusTitle( f'Loading {name}' )
|
|
|
|
self._loading_highlight_job_status.SetVariable( 'downloader', new_highlight )
|
|
|
|
self._gallery_importers_listctrl_panel.UpdateButtons()
|
|
|
|
self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
job_status = self._loading_highlight_job_status
|
|
hashes = new_highlight.GetPresentedHashes()
|
|
|
|
num_to_do = len( hashes )
|
|
|
|
if num_to_do > 0:
|
|
|
|
panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
|
|
def work_callable():
|
|
|
|
BLOCK_SIZE = 256
|
|
|
|
start_time = HydrusTime.GetNowFloat()
|
|
have_published_job_status = False
|
|
|
|
all_media_results = []
|
|
|
|
for ( i, block_of_hashes ) in enumerate( HydrusData.SplitIteratorIntoChunks( hashes, BLOCK_SIZE ) ):
|
|
|
|
num_done = i * BLOCK_SIZE
|
|
|
|
job_status.SetStatusText( 'Loading files: {}'.format( HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ) ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( num_done, num_to_do ) )
|
|
|
|
if not have_published_job_status and HydrusTime.TimeHasPassedFloat( start_time + 3 ):
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
have_published_job_status = True
|
|
|
|
|
|
if job_status.IsCancelled():
|
|
|
|
return all_media_results
|
|
|
|
|
|
block_of_media_results = CG.client_controller.Read( 'media_results', block_of_hashes, sorted = True )
|
|
|
|
all_media_results.extend( block_of_media_results )
|
|
|
|
|
|
job_status.SetStatusText( 'Done!' )
|
|
job_status.DeleteVariable( 'popup_gauge_1' )
|
|
|
|
return all_media_results
|
|
|
|
|
|
def publish_callable( media_results ):
|
|
|
|
try:
|
|
|
|
if job_status != self._loading_highlight_job_status or job_status.IsCancelled():
|
|
|
|
return
|
|
|
|
|
|
self._highlighted_gallery_import = new_highlight
|
|
|
|
self._multiple_gallery_import.SetHighlightedGalleryImport( self._highlighted_gallery_import )
|
|
|
|
self._highlighted_gallery_import.PublishToPage( True )
|
|
|
|
location_context = FileImportOptions.GetRealFileImportOptions( self._highlighted_gallery_import.GetFileImportOptions(), FileImportOptions.IMPORT_TYPE_LOUD ).GetDestinationLocationContext()
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no files for this query and its publishing settings' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._highlighted_gallery_import_panel.SetGalleryImport( self._highlighted_gallery_import )
|
|
|
|
finally:
|
|
|
|
self._gallery_importers_listctrl_panel.UpdateButtons()
|
|
|
|
self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
|
|
job = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
job.start()
|
|
|
|
|
|
|
|
def _HighlightSelectedGalleryImport( self ):
|
|
|
|
selected = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected ) == 1:
|
|
|
|
new_highlight = selected[0]
|
|
|
|
self._HighlightGalleryImport( new_highlight )
|
|
|
|
|
|
|
|
def _PausePlayFiles( self ):
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
gallery_import.PausePlayFiles()
|
|
|
|
|
|
self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _PausePlayGallery( self ):
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
gallery_import.PausePlayGallery()
|
|
|
|
|
|
self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _PendQueries( self, queries ):
|
|
|
|
results = self._multiple_gallery_import.PendQueries( queries )
|
|
|
|
if len( results ) > 0 and self._highlighted_gallery_import is None and CG.client_controller.new_options.GetBoolean( 'highlight_new_query' ):
|
|
|
|
first_result = results[ 0 ]
|
|
|
|
self._HighlightGalleryImport( first_result )
|
|
|
|
|
|
self._UpdateImportStatusNow()
|
|
|
|
|
|
def _RemoveGalleryImports( self ):
|
|
|
|
removees = list( self._gallery_importers_listctrl.GetData( only_selected = True ) )
|
|
|
|
if len( removees ) == 0:
|
|
|
|
return
|
|
|
|
|
|
num_working = 0
|
|
|
|
for gallery_import in removees:
|
|
|
|
if gallery_import.CurrentlyWorking():
|
|
|
|
num_working += 1
|
|
|
|
|
|
|
|
message = 'Remove the ' + HydrusData.ToHumanInt( len( removees ) ) + ' selected queries?'
|
|
|
|
if num_working > 0:
|
|
|
|
message += os.linesep * 2
|
|
message += HydrusData.ToHumanInt( num_working ) + ' are still working.'
|
|
|
|
|
|
if self._highlighted_gallery_import is not None and self._highlighted_gallery_import in removees:
|
|
|
|
message += os.linesep * 2
|
|
message += 'The currently highlighted query will be removed, and the media panel cleared.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
highlight_was_included = False
|
|
|
|
for gallery_import in removees:
|
|
|
|
if self._ThisIsTheCurrentOrLoadingHighlight( gallery_import ):
|
|
|
|
highlight_was_included = True
|
|
|
|
|
|
self._multiple_gallery_import.RemoveGalleryImport( gallery_import.GetGalleryImportKey() )
|
|
|
|
|
|
if highlight_was_included:
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
|
|
|
|
self._UpdateImportStatusNow()
|
|
|
|
|
|
def _RetryFailed( self ):
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
gallery_import.RetryFailed()
|
|
|
|
|
|
|
|
def _RetryIgnored( self ):
|
|
|
|
try:
|
|
|
|
ignored_regex = ClientGUIFileSeedCache.GetRetryIgnoredParam( self )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
for gallery_import in self._gallery_importers_listctrl.GetData( only_selected = True ):
|
|
|
|
gallery_import.RetryIgnored( ignored_regex = ignored_regex )
|
|
|
|
|
|
|
|
def _SetGUGKeyAndName( self, gug_key_and_name ):
|
|
|
|
current_initial_search_text = self._multiple_gallery_import.GetInitialSearchText()
|
|
|
|
current_input_value = self._query_input.GetValue()
|
|
|
|
should_initialise_new_text = current_input_value in ( current_initial_search_text, '' )
|
|
|
|
self._multiple_gallery_import.SetGUGKeyAndName( gug_key_and_name )
|
|
|
|
if should_initialise_new_text:
|
|
|
|
new_initial_search_text = self._multiple_gallery_import.GetInitialSearchText()
|
|
|
|
self._query_input.setPlaceholderText( new_initial_search_text )
|
|
|
|
|
|
self._query_input.setFocus( QC.Qt.OtherFocusReason )
|
|
|
|
|
|
def _SetOptionsToGalleryImports( self ):
|
|
|
|
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( gallery_imports ) == 0:
|
|
|
|
return
|
|
|
|
|
|
message = 'Set the current file limit, file import, and tag import options to all the selected queries? (by default, these options are only applied to new queries)'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
file_limit = self._file_limit.GetValue()
|
|
file_import_options = self._import_options_button.GetFileImportOptions()
|
|
tag_import_options = self._import_options_button.GetTagImportOptions()
|
|
note_import_options = self._import_options_button.GetNoteImportOptions()
|
|
|
|
for gallery_import in gallery_imports:
|
|
|
|
gallery_import.SetFileLimit( file_limit )
|
|
gallery_import.SetFileImportOptions( file_import_options )
|
|
gallery_import.SetTagImportOptions( tag_import_options )
|
|
gallery_import.SetNoteImportOptions( note_import_options )
|
|
|
|
|
|
|
|
|
|
def _ShowCogMenu( self ):
|
|
|
|
menu = ClientGUIMenus.GenerateMenu( self )
|
|
|
|
start_file_queues_paused = self._multiple_gallery_import.GetStartFileQueuesPaused()
|
|
start_gallery_queues_paused = self._multiple_gallery_import.GetStartGalleryQueuesPaused()
|
|
do_not_allow_new_dupes = self._multiple_gallery_import.GetDoNotAllowNewDupes()
|
|
merge_simultaneous_pends_to_one_importer = self._multiple_gallery_import.GetMergeSimultaneousPendsToOneImporter()
|
|
|
|
ClientGUIMenus.AppendMenuCheckItem( menu, 'start new importers\' files paused', 'Start any new importers in a file import-paused state.', start_file_queues_paused, self._multiple_gallery_import.SetStartFileQueuesPaused, not start_file_queues_paused )
|
|
ClientGUIMenus.AppendMenuCheckItem( menu, 'start new importers\' search paused', 'Start any new importers in a gallery search-paused state.', start_gallery_queues_paused, self._multiple_gallery_import.SetStartGalleryQueuesPaused, not start_gallery_queues_paused )
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
ClientGUIMenus.AppendMenuCheckItem( menu, 'do not allow new duplicates', 'This will discard any query/source pair you try to add that is already in the list.', do_not_allow_new_dupes, self._multiple_gallery_import.SetDoNotAllowNewDupes, not do_not_allow_new_dupes )
|
|
ClientGUIMenus.AppendMenuCheckItem( menu, 'bundle multiple pasted queries into one importer (advanced)', 'If you are pasting many small queries at once (such as md5 lookups), check this to smooth out the workflow.', merge_simultaneous_pends_to_one_importer, self._multiple_gallery_import.SetMergeSimultaneousPendsToOneImporter, not merge_simultaneous_pends_to_one_importer )
|
|
|
|
CGC.core().PopupMenu( self._cog_button, menu )
|
|
|
|
|
|
def _ShowSelectedImportersFileSeedCaches( self ):
|
|
|
|
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( gallery_imports ) == 0:
|
|
|
|
return
|
|
|
|
|
|
gallery_import = gallery_imports[0]
|
|
|
|
file_seed_cache = gallery_import.GetFileSeedCache()
|
|
|
|
title = 'file log'
|
|
frame_key = 'file_import_status'
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = ClientGUIFileSeedCache.EditFileSeedCachePanel( frame, CG.client_controller, file_seed_cache )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def _ShowSelectedImportersFiles( self, presentation_import_options = None ):
|
|
|
|
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( gallery_imports ) == 0:
|
|
|
|
return
|
|
|
|
|
|
hashes = list()
|
|
seen_hashes = set()
|
|
|
|
for gallery_import in gallery_imports:
|
|
|
|
gallery_hashes = gallery_import.GetPresentedHashes( presentation_import_options = presentation_import_options )
|
|
|
|
new_hashes = [ hash for hash in gallery_hashes if hash not in seen_hashes ]
|
|
|
|
hashes.extend( new_hashes )
|
|
seen_hashes.update( new_hashes )
|
|
|
|
|
|
if len( hashes ) > 0:
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
media_results = self._controller.Read( 'media_results', hashes, sorted = True )
|
|
|
|
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
else:
|
|
|
|
ClientGUIDialogsMessage.ShowWarning( self, 'No presented files for that selection!' )
|
|
|
|
|
|
|
|
def _ShowSelectedImportersGallerySeedLogs( self ):
|
|
|
|
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( gallery_imports ) == 0:
|
|
|
|
return
|
|
|
|
|
|
gallery_import = gallery_imports[0]
|
|
|
|
gallery_seed_log = gallery_import.GetGallerySeedLog()
|
|
|
|
title = 'search log'
|
|
frame_key = 'gallery_import_log'
|
|
|
|
read_only = False
|
|
can_generate_more_pages = True
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = ClientGUIGallerySeedLog.EditGallerySeedLogPanel( frame, CG.client_controller, read_only, can_generate_more_pages, 'search', gallery_seed_log )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def _ThisIsTheCurrentOrLoadingHighlight( self, gallery_import ):
|
|
|
|
if self._highlighted_gallery_import is not None and gallery_import == self._highlighted_gallery_import:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
if not self._loading_highlight_job_status.IsDone():
|
|
|
|
downloader = self._loading_highlight_job_status.GetIfHasVariable( 'downloader' )
|
|
|
|
if downloader is not None and downloader == gallery_import:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
if HydrusTime.TimeHasPassed( self._next_update_time ):
|
|
|
|
num_items = len( self._gallery_importers_listctrl.GetData() )
|
|
|
|
update_period = max( 1, int( ( num_items / 10 ) ** 0.33 ) )
|
|
|
|
self._next_update_time = HydrusTime.GetNow() + update_period
|
|
|
|
#
|
|
|
|
last_time_imports_changed = self._multiple_gallery_import.GetLastTimeImportsChanged()
|
|
|
|
num_gallery_imports = self._multiple_gallery_import.GetNumGalleryImports()
|
|
|
|
#
|
|
|
|
if num_gallery_imports == 0:
|
|
|
|
text_top = 'waiting for new queries'
|
|
text_bottom = ''
|
|
|
|
else:
|
|
|
|
file_seed_cache_status = self._multiple_gallery_import.GetTotalStatus()
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
text_top = '{} queries - {}'.format( HydrusData.ToHumanInt( num_gallery_imports ), HydrusData.ConvertValueRangeToPrettyString( num_done, num_total ) )
|
|
text_bottom = file_seed_cache_status.GetStatusText()
|
|
|
|
|
|
self._gallery_importers_status_st_top.setText( text_top )
|
|
self._gallery_importers_status_st_bottom.setText( text_bottom )
|
|
|
|
#
|
|
|
|
if self._last_time_imports_changed != last_time_imports_changed:
|
|
|
|
self._last_time_imports_changed = last_time_imports_changed
|
|
|
|
gallery_imports = self._multiple_gallery_import.GetGalleryImports()
|
|
|
|
self._gallery_importers_listctrl.SetData( gallery_imports )
|
|
|
|
ideal_rows = len( gallery_imports )
|
|
ideal_rows = max( 4, ideal_rows )
|
|
ideal_rows = min( ideal_rows, 24 )
|
|
|
|
self._gallery_importers_listctrl.ForceHeight( ideal_rows )
|
|
|
|
else:
|
|
|
|
sort_data_has_changed = self._gallery_importers_listctrl.UpdateDatas()
|
|
|
|
if sort_data_has_changed:
|
|
|
|
self._gallery_importers_listctrl.Sort()
|
|
|
|
|
|
|
|
|
|
|
|
def _UpdateImportStatusNow( self ):
|
|
|
|
self._next_update_time = 0
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
num_working = 0
|
|
|
|
for gallery_import in self._multiple_gallery_import.GetGalleryImports():
|
|
|
|
if gallery_import.CurrentlyWorking():
|
|
|
|
num_working += 1
|
|
|
|
|
|
|
|
if num_working > 0:
|
|
|
|
raise HydrusExceptions.VetoException( HydrusData.ToHumanInt( num_working ) + ' queries are still importing.' )
|
|
|
|
|
|
|
|
def EventFileLimit( self ):
|
|
|
|
self._multiple_gallery_import.SetFileLimit( self._file_limit.GetValue() )
|
|
|
|
|
|
def PendSubscriptionGapDownloader( self, gug_key_and_name, query_text, file_import_options, tag_import_options, note_import_options, file_limit ):
|
|
|
|
new_query = self._multiple_gallery_import.PendSubscriptionGapDownloader( gug_key_and_name, query_text, file_import_options, tag_import_options, note_import_options, file_limit )
|
|
|
|
if new_query is not None and self._highlighted_gallery_import is None and CG.client_controller.new_options.GetBoolean( 'highlight_new_query' ):
|
|
|
|
self._HighlightGalleryImport( new_query )
|
|
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._query_input )
|
|
|
|
|
|
def Start( self ):
|
|
|
|
self._multiple_gallery_import.Start( self._page_key )
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_IMPORT_MULTIPLE_GALLERY ] = ManagementPanelImporterMultipleGallery
|
|
|
|
class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanelImporter.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._last_time_watchers_changed = 0
|
|
self._next_update_time = 0
|
|
|
|
self._multiple_watcher_import = self._management_controller.GetVariable( 'multiple_watcher_import' )
|
|
|
|
self._highlighted_watcher = self._multiple_watcher_import.GetHighlightedWatcher()
|
|
|
|
self._loading_highlight_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
self._loading_highlight_job_status.Finish()
|
|
|
|
checker_options = self._multiple_watcher_import.GetCheckerOptions()
|
|
file_import_options = self._multiple_watcher_import.GetFileImportOptions()
|
|
tag_import_options = self._multiple_watcher_import.GetTagImportOptions()
|
|
note_import_options = self._multiple_watcher_import.GetNoteImportOptions()
|
|
|
|
#
|
|
|
|
self._watchers_panel = ClientGUICommon.StaticBox( self, 'watchers' )
|
|
|
|
self._watchers_status_st_top = ClientGUICommon.BetterStaticText( self._watchers_panel, ellipsize_end = True )
|
|
self._watchers_status_st_bottom = ClientGUICommon.BetterStaticText( self._watchers_panel, ellipsize_end = True )
|
|
|
|
self._watchers_listctrl_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._watchers_panel )
|
|
|
|
self._watchers_listctrl = ClientGUIListCtrl.BetterListCtrl( self._watchers_listctrl_panel, CGLC.COLUMN_LIST_WATCHERS.ID, 4, self._ConvertDataToListCtrlTuples, delete_key_callback = self._RemoveWatchers, activation_callback = self._HighlightSelectedWatcher )
|
|
|
|
self._watchers_listctrl_panel.SetListCtrl( self._watchers_listctrl )
|
|
|
|
self._watchers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().highlight, self._HighlightSelectedWatcher, tooltip = 'highlight', enabled_check_func = self._CanHighlight )
|
|
self._watchers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().clear_highlight, self._ClearExistingHighlightAndPanel, tooltip = 'clear highlight', enabled_check_func = self._CanClearHighlight )
|
|
self._watchers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().file_pause, self._PausePlayFiles, tooltip = 'pause/play files', enabled_only_on_selection = True )
|
|
self._watchers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().gallery_pause, self._PausePlayChecking, tooltip = 'pause/play checking', enabled_only_on_selection = True )
|
|
self._watchers_listctrl_panel.AddBitmapButton( CC.global_pixmaps().trash, self._RemoveWatchers, tooltip = 'remove selected', enabled_only_on_selection = True )
|
|
self._watchers_listctrl_panel.AddButton( 'check now', self._CheckNow, enabled_only_on_selection = True )
|
|
|
|
self._watchers_listctrl_panel.NewButtonRow()
|
|
|
|
self._watchers_listctrl_panel.AddButton( 'retry failed', self._RetryFailed, enabled_check_func = self._CanRetryFailed )
|
|
self._watchers_listctrl_panel.AddButton( 'retry ignored', self._RetryIgnored, enabled_check_func = self._CanRetryIgnored )
|
|
|
|
self._watchers_listctrl_panel.NewButtonRow()
|
|
|
|
self._watchers_listctrl_panel.AddButton( 'set options to watchers', self._SetOptionsToWatchers, enabled_only_on_selection = True )
|
|
|
|
self._watchers_listctrl.Sort()
|
|
|
|
self._watcher_url_input = ClientGUIControls.TextAndPasteCtrl( self._watchers_panel, self._AddURLs )
|
|
|
|
self._watcher_url_input.setPlaceholderText( 'watcher url' )
|
|
|
|
self._checker_options = ClientGUIImport.CheckerOptionsButton( self._watchers_panel, checker_options )
|
|
|
|
show_downloader_options = True
|
|
allow_default_selection = True
|
|
|
|
self._import_options_button = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
|
|
|
|
self._import_options_button.SetFileImportOptions( file_import_options )
|
|
self._import_options_button.SetTagImportOptions( tag_import_options )
|
|
self._import_options_button.SetNoteImportOptions( note_import_options )
|
|
|
|
# suck up watchers from elsewhere in the program (presents a checkboxlistdialog)
|
|
|
|
#
|
|
|
|
self._highlighted_watcher_panel = ClientGUIImport.WatcherReviewPanel( self, self._page_key, name = 'highlighted watcher' )
|
|
|
|
self._highlighted_watcher_panel.SetWatcher( self._highlighted_watcher )
|
|
|
|
#
|
|
|
|
self._watchers_panel.Add( self._watchers_status_st_top, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._watchers_panel.Add( self._watchers_status_st_bottom, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._watchers_panel.Add( self._watchers_listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
self._watchers_panel.Add( self._watcher_url_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._watchers_panel.Add( self._checker_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._watchers_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._watchers_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( vbox, self._highlighted_watcher_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._watchers_listctrl.AddRowsMenuCallable( self._GetListCtrlMenu )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
CG.client_controller.sub( self, '_ClearExistingHighlightAndPanel', 'clear_multiwatcher_highlights' )
|
|
|
|
self._import_options_button.fileImportOptionsChanged.connect( self._OptionsUpdated )
|
|
self._import_options_button.noteImportOptionsChanged.connect( self._OptionsUpdated )
|
|
self._import_options_button.tagImportOptionsChanged.connect( self._OptionsUpdated )
|
|
|
|
self._checker_options.valueChanged.connect( self._OptionsUpdated )
|
|
|
|
|
|
def _AddURLs( self, urls, filterable_tags = None, additional_service_keys_to_tags = None ):
|
|
|
|
if filterable_tags is None:
|
|
|
|
filterable_tags = set()
|
|
|
|
|
|
if additional_service_keys_to_tags is None:
|
|
|
|
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
|
|
|
|
|
|
first_result = None
|
|
|
|
for url in urls:
|
|
|
|
result = self._multiple_watcher_import.AddURL( url, filterable_tags = filterable_tags, additional_service_keys_to_tags = additional_service_keys_to_tags )
|
|
|
|
if result is not None and first_result is None:
|
|
|
|
first_result = result
|
|
|
|
|
|
|
|
if first_result is not None and self._highlighted_watcher is None and CG.client_controller.new_options.GetBoolean( 'highlight_new_watcher' ):
|
|
|
|
self._HighlightWatcher( first_result )
|
|
|
|
|
|
self._UpdateImportStatusNow()
|
|
|
|
|
|
def _CanClearHighlight( self ):
|
|
|
|
return self._highlighted_watcher is not None or not self._loading_highlight_job_status.IsDone()
|
|
|
|
|
|
def _CanHighlight( self ):
|
|
|
|
selected = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected ) != 1:
|
|
|
|
return False
|
|
|
|
|
|
watcher = selected[0]
|
|
|
|
return not self._ThisIsTheCurrentOrLoadingHighlight( watcher )
|
|
|
|
|
|
def _CanRetryFailed( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
if watcher.CanRetryFailed():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _CanRetryIgnored( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
if watcher.CanRetryIgnored():
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
def _CheckNow( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
watcher.CheckNow()
|
|
|
|
|
|
|
|
def _ClearExistingHighlight( self ):
|
|
|
|
if not self._loading_highlight_job_status.IsDone():
|
|
|
|
self._loading_highlight_job_status.Cancel()
|
|
|
|
|
|
if self._highlighted_watcher is not None:
|
|
|
|
self._highlighted_watcher.PublishToPage( False )
|
|
|
|
self._highlighted_watcher = None
|
|
|
|
self._multiple_watcher_import.ClearHighlightedWatcher()
|
|
|
|
self._watchers_listctrl_panel.UpdateButtons()
|
|
|
|
self._highlighted_watcher_panel.SetWatcher( None )
|
|
|
|
|
|
|
|
def _ClearExistingHighlightAndPanel( self ):
|
|
|
|
self._ClearExistingHighlight()
|
|
|
|
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
|
|
|
|
media_results = []
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no highlighted watcher' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._watchers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _ConvertDataToListCtrlTuples( self, watcher: ClientImportWatchers.WatcherImport ):
|
|
|
|
subject = watcher.GetSubject()
|
|
|
|
pretty_subject = subject
|
|
|
|
if watcher == self._highlighted_watcher:
|
|
|
|
pretty_subject = f'* {pretty_subject}'
|
|
|
|
elif not self._loading_highlight_job_status.IsDone():
|
|
|
|
downloader = self._loading_highlight_job_status.GetIfHasVariable( 'downloader' )
|
|
|
|
if downloader is not None and downloader == watcher:
|
|
|
|
pretty_subject = f'> {pretty_subject}'
|
|
|
|
|
|
|
|
files_paused = watcher.FilesPaused()
|
|
|
|
if files_paused:
|
|
|
|
pretty_files_paused = CG.client_controller.new_options.GetString( 'pause_character' )
|
|
|
|
else:
|
|
|
|
pretty_files_paused = ''
|
|
|
|
|
|
checking_dead = watcher.IsDead()
|
|
checking_paused = watcher.CheckingPaused()
|
|
|
|
if checking_dead:
|
|
|
|
pretty_checking_paused = CG.client_controller.new_options.GetString( 'stop_character' )
|
|
|
|
sort_checking_paused = -1
|
|
|
|
elif checking_paused:
|
|
|
|
pretty_checking_paused = CG.client_controller.new_options.GetString( 'pause_character' )
|
|
|
|
sort_checking_paused = 0
|
|
|
|
else:
|
|
|
|
pretty_checking_paused = ''
|
|
|
|
sort_checking_paused = 1
|
|
|
|
|
|
file_seed_cache_status = watcher.GetFileSeedCache().GetStatus()
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
progress = ( num_total, num_done )
|
|
|
|
pretty_progress = file_seed_cache_status.GetStatusText( simple = True )
|
|
|
|
added = watcher.GetCreationTime()
|
|
|
|
pretty_added = ClientTime.TimestampToPrettyTimeDelta( added, show_seconds = False )
|
|
|
|
( status_enum, pretty_watcher_status ) = self._multiple_watcher_import.GetWatcherSimpleStatus( watcher )
|
|
|
|
checking_status = watcher.GetCheckingStatus()
|
|
|
|
if checking_status == ClientImporting.CHECKER_STATUS_OK:
|
|
|
|
next_check_time = watcher.GetNextCheckTime()
|
|
|
|
if next_check_time is None:
|
|
|
|
next_check_time = 0
|
|
|
|
|
|
sort_watcher_status = ( ClientImporting.downloader_enum_sort_lookup[ status_enum ], next_check_time )
|
|
|
|
else:
|
|
|
|
# this lets 404 and DEAD sort different
|
|
|
|
sort_watcher_status = ( ClientImporting.downloader_enum_sort_lookup[ status_enum ], checking_status )
|
|
|
|
|
|
display_tuple = ( pretty_subject, pretty_files_paused, pretty_checking_paused, pretty_watcher_status, pretty_progress, pretty_added )
|
|
sort_tuple = ( subject, files_paused, sort_checking_paused, sort_watcher_status, progress, added )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _CopySelectedSubjects( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) > 0:
|
|
|
|
text = os.linesep.join( ( watcher.GetSubject() for watcher in watchers ) )
|
|
|
|
CG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _CopySelectedURLs( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) > 0:
|
|
|
|
text = os.linesep.join( ( watcher.GetURL() for watcher in watchers ) )
|
|
|
|
CG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
|
|
|
|
return 'no highlighted watcher'
|
|
|
|
|
|
def _GetListCtrlMenu( self ):
|
|
|
|
selected_watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected_watchers ) == 0:
|
|
|
|
raise HydrusExceptions.DataMissing()
|
|
|
|
|
|
menu = ClientGUIMenus.GenerateMenu( self )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'copy urls', 'Copy all the selected watchers\' urls to clipboard.', self._CopySelectedURLs )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'open urls', 'Open all the selected watchers\' urls in your browser.', self._OpenSelectedURLs )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'copy subjects', 'Copy all the selected watchers\' subjects to clipboard.', self._CopySelectedSubjects )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
single_selected_presentation_import_options = None
|
|
|
|
if len( selected_watchers ) == 1:
|
|
|
|
( watcher, ) = selected_watchers
|
|
|
|
fio = watcher.GetFileImportOptions()
|
|
|
|
single_selected_presentation_import_options = FileImportOptions.GetRealPresentationImportOptions( fio, FileImportOptions.IMPORT_TYPE_LOUD )
|
|
|
|
|
|
AddPresentationSubmenu( menu, 'watcher', single_selected_presentation_import_options, self._ShowSelectedImportersFiles )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if len( selected_watchers ) == 1:
|
|
|
|
( watcher, ) = selected_watchers
|
|
|
|
file_seed_cache = watcher.GetFileSeedCache()
|
|
|
|
submenu = ClientGUIMenus.GenerateMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'show file log', 'Show the file log windows for the selected watcher.', self._ShowSelectedImportersFileSeedCaches )
|
|
|
|
ClientGUIMenus.AppendSeparator( submenu )
|
|
|
|
ClientGUIFileSeedCache.PopulateFileSeedCacheMenu( self, submenu, file_seed_cache )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'file log' )
|
|
|
|
gallery_seed_log = watcher.GetGallerySeedLog()
|
|
|
|
submenu = ClientGUIMenus.GenerateMenu( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( submenu, 'show check log', 'Show the check log windows for the selected watcher.', self._ShowSelectedImportersGallerySeedLogs )
|
|
|
|
ClientGUIMenus.AppendSeparator( submenu )
|
|
|
|
ClientGUIGallerySeedLog.PopulateGallerySeedLogButton( self, submenu, gallery_seed_log, True, False, 'check' )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'check log' )
|
|
|
|
else:
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'show file logs', 'Show the file log windows for the selected queries.', self._ShowSelectedImportersFileSeedCaches )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'show check log', 'Show the checker log windows for the selected watcher.', self._ShowSelectedImportersGallerySeedLogs )
|
|
|
|
|
|
if self._CanRetryFailed() or self._CanRetryIgnored():
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if self._CanRetryFailed():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'retry failed', 'Retry all the failed downloads.', self._RetryFailed )
|
|
|
|
|
|
if self._CanRetryIgnored():
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'retry ignored', 'Retry all the ignored downloads.', self._RetryIgnored )
|
|
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'remove selected', 'Remove the selected watchers.', self._RemoveWatchers )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'pause/play files', 'Pause/play all the selected watchers\' file queues.', self._PausePlayFiles )
|
|
ClientGUIMenus.AppendMenuItem( menu, 'pause/play checking', 'Pause/play all the selected watchers\' checking routines.', self._PausePlayChecking )
|
|
|
|
return menu
|
|
|
|
|
|
def _HighlightWatcher( self, new_highlight ):
|
|
|
|
if self._ThisIsTheCurrentOrLoadingHighlight( new_highlight ):
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
else:
|
|
|
|
self._ClearExistingHighlight()
|
|
|
|
self._loading_highlight_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
name = new_highlight.GetSubject()
|
|
|
|
self._loading_highlight_job_status.SetStatusTitle( f'Loading {name}' )
|
|
|
|
self._loading_highlight_job_status.SetVariable( 'downloader', new_highlight )
|
|
|
|
self._watchers_listctrl_panel.UpdateButtons()
|
|
|
|
self._watchers_listctrl.UpdateDatas()
|
|
|
|
job_status = self._loading_highlight_job_status
|
|
hashes = new_highlight.GetPresentedHashes()
|
|
|
|
num_to_do = len( hashes )
|
|
|
|
if num_to_do > 0:
|
|
|
|
panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
|
|
def work_callable():
|
|
|
|
BLOCK_SIZE = 256
|
|
|
|
start_time = HydrusTime.GetNowFloat()
|
|
have_published_job_status = False
|
|
|
|
all_media_results = []
|
|
|
|
for ( i, block_of_hashes ) in enumerate( HydrusData.SplitIteratorIntoChunks( hashes, BLOCK_SIZE ) ):
|
|
|
|
num_done = i * BLOCK_SIZE
|
|
|
|
job_status.SetStatusText( 'Loading files: {}'.format( HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ) ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( num_done, num_to_do ) )
|
|
|
|
if not have_published_job_status and HydrusTime.TimeHasPassedFloat( start_time + 3 ):
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
have_published_job_status = True
|
|
|
|
|
|
if job_status.IsCancelled():
|
|
|
|
return all_media_results
|
|
|
|
|
|
block_of_media_results = CG.client_controller.Read( 'media_results', block_of_hashes, sorted = True )
|
|
|
|
all_media_results.extend( block_of_media_results )
|
|
|
|
|
|
job_status.SetStatusText( 'Done!' )
|
|
job_status.DeleteVariable( 'popup_gauge_1' )
|
|
|
|
return all_media_results
|
|
|
|
|
|
def publish_callable( media_results ):
|
|
|
|
try:
|
|
|
|
if job_status != self._loading_highlight_job_status or job_status.IsCancelled():
|
|
|
|
return
|
|
|
|
|
|
self._highlighted_watcher = new_highlight
|
|
|
|
self._multiple_watcher_import.SetHighlightedWatcher( self._highlighted_watcher )
|
|
|
|
self._highlighted_watcher.PublishToPage( True )
|
|
|
|
location_context = FileImportOptions.GetRealFileImportOptions( self._highlighted_watcher.GetFileImportOptions(), FileImportOptions.IMPORT_TYPE_LOUD ).GetDestinationLocationContext()
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no files for this watcher and its publishing settings' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._highlighted_watcher_panel.SetWatcher( self._highlighted_watcher )
|
|
|
|
finally:
|
|
|
|
self._watchers_listctrl_panel.UpdateButtons()
|
|
|
|
self._watchers_listctrl.UpdateDatas()
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
|
|
job = ClientGUIAsync.AsyncQtJob( self, work_callable, publish_callable )
|
|
|
|
job.start()
|
|
|
|
|
|
|
|
def _HighlightSelectedWatcher( self ):
|
|
|
|
selected = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( selected ) == 1:
|
|
|
|
new_highlight = selected[0]
|
|
|
|
self._HighlightWatcher( new_highlight )
|
|
|
|
|
|
|
|
def _OpenSelectedURLs( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) > 0:
|
|
|
|
if len( watchers ) > 10:
|
|
|
|
message = 'You have many watchers selected--are you sure you want to open them all?'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
|
|
for watcher in watchers:
|
|
|
|
ClientPaths.LaunchURLInWebBrowser( watcher.GetURL() )
|
|
|
|
|
|
|
|
|
|
def _OptionsUpdated( self, *args, **kwargs ):
|
|
|
|
self._multiple_watcher_import.SetCheckerOptions( self._checker_options.GetValue() )
|
|
self._multiple_watcher_import.SetFileImportOptions( self._import_options_button.GetFileImportOptions() )
|
|
self._multiple_watcher_import.SetNoteImportOptions( self._import_options_button.GetNoteImportOptions() )
|
|
self._multiple_watcher_import.SetTagImportOptions( self._import_options_button.GetTagImportOptions() )
|
|
|
|
|
|
def _PausePlayChecking( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
watcher.PausePlayChecking()
|
|
|
|
|
|
self._watchers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _PausePlayFiles( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
watcher.PausePlayFiles()
|
|
|
|
|
|
self._watchers_listctrl.UpdateDatas()
|
|
|
|
|
|
def _RemoveWatchers( self ):
|
|
|
|
removees = list( self._watchers_listctrl.GetData( only_selected = True ) )
|
|
|
|
if len( removees ) == 0:
|
|
|
|
return
|
|
|
|
|
|
num_working = 0
|
|
num_alive = 0
|
|
|
|
for watcher in removees:
|
|
|
|
if watcher.CurrentlyWorking():
|
|
|
|
num_working += 1
|
|
|
|
|
|
if watcher.CurrentlyAlive():
|
|
|
|
num_alive += 1
|
|
|
|
|
|
|
|
message = 'Remove the ' + HydrusData.ToHumanInt( len( removees ) ) + ' selected watchers?'
|
|
|
|
if num_working > 0:
|
|
|
|
message += os.linesep * 2
|
|
message += HydrusData.ToHumanInt( num_working ) + ' are still working.'
|
|
|
|
|
|
if num_alive > 0:
|
|
|
|
message += os.linesep * 2
|
|
message += HydrusData.ToHumanInt( num_alive ) + ' are not yet DEAD.'
|
|
|
|
|
|
if self._highlighted_watcher is not None and self._highlighted_watcher in removees:
|
|
|
|
message += os.linesep * 2
|
|
message += 'The currently highlighted watcher will be removed, and the media panel cleared.'
|
|
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
highlight_was_included = False
|
|
|
|
for watcher in removees:
|
|
|
|
if self._ThisIsTheCurrentOrLoadingHighlight( watcher ):
|
|
|
|
highlight_was_included = True
|
|
|
|
|
|
self._multiple_watcher_import.RemoveWatcher( watcher.GetWatcherKey() )
|
|
|
|
|
|
if highlight_was_included:
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
|
|
|
|
self._UpdateImportStatusNow()
|
|
|
|
|
|
def _RetryFailed( self ):
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
watcher.RetryFailed()
|
|
|
|
|
|
|
|
def _RetryIgnored( self ):
|
|
|
|
try:
|
|
|
|
ignored_regex = ClientGUIFileSeedCache.GetRetryIgnoredParam( self )
|
|
|
|
except HydrusExceptions.CancelledException:
|
|
|
|
return
|
|
|
|
|
|
for watcher in self._watchers_listctrl.GetData( only_selected = True ):
|
|
|
|
watcher.RetryIgnored( ignored_regex = ignored_regex )
|
|
|
|
|
|
|
|
def _SetOptionsToWatchers( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) == 0:
|
|
|
|
return
|
|
|
|
|
|
message = 'Set the current checker, file import, and tag import options to all the selected watchers? (by default, these options are only applied to new watchers)'
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
checker_options = self._checker_options.GetValue()
|
|
file_import_options = self._import_options_button.GetFileImportOptions()
|
|
tag_import_options = self._import_options_button.GetTagImportOptions()
|
|
|
|
for watcher in watchers:
|
|
|
|
watcher.SetCheckerOptions( checker_options )
|
|
watcher.SetFileImportOptions( file_import_options )
|
|
watcher.SetTagImportOptions( tag_import_options )
|
|
|
|
|
|
|
|
|
|
def _ShowSelectedImportersFileSeedCaches( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) == 0:
|
|
|
|
return
|
|
|
|
|
|
watcher = watchers[0]
|
|
|
|
file_seed_cache = watcher.GetFileSeedCache()
|
|
|
|
title = 'file log'
|
|
frame_key = 'file_import_status'
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = ClientGUIFileSeedCache.EditFileSeedCachePanel( frame, CG.client_controller, file_seed_cache )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def _ShowSelectedImportersFiles( self, presentation_import_options = None ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) == 0:
|
|
|
|
return
|
|
|
|
|
|
hashes = list()
|
|
seen_hashes = set()
|
|
|
|
for watcher in watchers:
|
|
|
|
watcher_hashes = watcher.GetPresentedHashes( presentation_import_options = presentation_import_options )
|
|
|
|
new_hashes = [ hash for hash in watcher_hashes if hash not in seen_hashes ]
|
|
|
|
hashes.extend( new_hashes )
|
|
seen_hashes.update( new_hashes )
|
|
|
|
|
|
if len( hashes ) > 0:
|
|
|
|
self._ClearExistingHighlightAndPanel()
|
|
|
|
media_results = self._controller.Read( 'media_results', hashes, sorted = True )
|
|
|
|
location_context = ClientLocation.LocationContext.STATICCreateSimple( CC.COMBINED_LOCAL_MEDIA_SERVICE_KEY )
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
else:
|
|
|
|
ClientGUIDialogsMessage.ShowWarning( self, 'No presented files for that selection!' )
|
|
|
|
|
|
|
|
def _ShowSelectedImportersGallerySeedLogs( self ):
|
|
|
|
watchers = self._watchers_listctrl.GetData( only_selected = True )
|
|
|
|
if len( watchers ) == 0:
|
|
|
|
return
|
|
|
|
|
|
watcher = watchers[0]
|
|
|
|
gallery_seed_log = watcher.GetGallerySeedLog()
|
|
|
|
title = 'check log'
|
|
frame_key = 'gallery_import_log'
|
|
|
|
read_only = True
|
|
can_generate_more_pages = False
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = ClientGUIGallerySeedLog.EditGallerySeedLogPanel( frame, CG.client_controller, read_only, can_generate_more_pages, 'check', gallery_seed_log )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def _ThisIsTheCurrentOrLoadingHighlight( self, watcher ):
|
|
|
|
if self._highlighted_watcher is not None and watcher == self._highlighted_watcher:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
if not self._loading_highlight_job_status.IsDone():
|
|
|
|
downloader = self._loading_highlight_job_status.GetIfHasVariable( 'downloader' )
|
|
|
|
if downloader is not None and downloader == watcher:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
if HydrusTime.TimeHasPassed( self._next_update_time ):
|
|
|
|
num_items = len( self._watchers_listctrl.GetData() )
|
|
|
|
update_period = max( 1, int( ( num_items / 10 ) ** 0.33 ) )
|
|
|
|
self._next_update_time = HydrusTime.GetNow() + update_period
|
|
|
|
#
|
|
|
|
last_time_watchers_changed = self._multiple_watcher_import.GetLastTimeWatchersChanged()
|
|
num_watchers = self._multiple_watcher_import.GetNumWatchers()
|
|
|
|
#
|
|
|
|
if num_watchers == 0:
|
|
|
|
text_top = 'waiting for new watchers'
|
|
text_bottom = ''
|
|
|
|
else:
|
|
|
|
num_dead = self._multiple_watcher_import.GetNumDead()
|
|
|
|
if num_dead == 0:
|
|
|
|
num_dead_text = ''
|
|
|
|
else:
|
|
|
|
num_dead_text = HydrusData.ToHumanInt( num_dead ) + ' DEAD - '
|
|
|
|
|
|
file_seed_cache_status = self._multiple_watcher_import.GetTotalStatus()
|
|
|
|
( num_done, num_total ) = file_seed_cache_status.GetValueRange()
|
|
|
|
text_top = '{} watchers - {}'.format( HydrusData.ToHumanInt( num_watchers ), HydrusData.ConvertValueRangeToPrettyString( num_done, num_total ) )
|
|
text_bottom = file_seed_cache_status.GetStatusText()
|
|
|
|
|
|
self._watchers_status_st_top.setText( text_top )
|
|
self._watchers_status_st_bottom.setText( text_bottom )
|
|
|
|
#
|
|
|
|
if self._last_time_watchers_changed != last_time_watchers_changed:
|
|
|
|
self._last_time_watchers_changed = last_time_watchers_changed
|
|
|
|
watchers = self._multiple_watcher_import.GetWatchers()
|
|
|
|
self._watchers_listctrl.SetData( watchers )
|
|
|
|
ideal_rows = len( watchers )
|
|
ideal_rows = max( 4, ideal_rows )
|
|
ideal_rows = min( ideal_rows, 24 )
|
|
|
|
self._watchers_listctrl.ForceHeight( ideal_rows )
|
|
|
|
else:
|
|
|
|
sort_data_has_changed = self._watchers_listctrl.UpdateDatas()
|
|
|
|
if sort_data_has_changed:
|
|
|
|
self._watchers_listctrl.Sort()
|
|
|
|
|
|
|
|
|
|
|
|
def _UpdateImportStatusNow( self ):
|
|
|
|
self._next_update_time = 0
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
num_working = 0
|
|
|
|
for watcher in self._multiple_watcher_import.GetWatchers():
|
|
|
|
if watcher.CurrentlyWorking():
|
|
|
|
num_working += 1
|
|
|
|
|
|
|
|
if num_working > 0:
|
|
|
|
raise HydrusExceptions.VetoException( HydrusData.ToHumanInt( num_working ) + ' watchers are still importing.' )
|
|
|
|
|
|
|
|
def PendURL( self, url, filterable_tags = None, additional_service_keys_to_tags = None ):
|
|
|
|
if filterable_tags is None:
|
|
|
|
filterable_tags = set()
|
|
|
|
|
|
if additional_service_keys_to_tags is None:
|
|
|
|
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
|
|
|
|
|
|
self._AddURLs( ( url, ), filterable_tags = filterable_tags, additional_service_keys_to_tags = additional_service_keys_to_tags )
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._watcher_url_input )
|
|
|
|
|
|
def Start( self ):
|
|
|
|
self._multiple_watcher_import.Start( self._page_key )
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_IMPORT_MULTIPLE_WATCHER ] = ManagementPanelImporterMultipleWatcher
|
|
|
|
class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanelImporter.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._simple_downloader_import: ClientImportSimpleURLs.SimpleDownloaderImport = self._management_controller.GetVariable( 'simple_downloader_import' )
|
|
|
|
#
|
|
|
|
self._simple_downloader_panel = ClientGUICommon.StaticBox( self, 'simple downloader' )
|
|
|
|
#
|
|
|
|
self._import_queue_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'imports' )
|
|
|
|
self._pause_files_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.PauseFiles )
|
|
self._pause_files_button.setToolTip( 'pause/play files' )
|
|
self._current_action = ClientGUICommon.BetterStaticText( self._import_queue_panel, ellipsize_end = True )
|
|
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self._import_queue_panel, self._controller, self._page_key )
|
|
self._file_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._import_queue_panel )
|
|
|
|
#
|
|
|
|
#
|
|
|
|
self._simple_parsing_jobs_panel = ClientGUICommon.StaticBox( self._simple_downloader_panel, 'parsing' )
|
|
|
|
self._pause_queue_button = ClientGUICommon.BetterBitmapButton( self._simple_parsing_jobs_panel, CC.global_pixmaps().gallery_pause, self.PauseQueue )
|
|
self._pause_queue_button.setToolTip( 'pause/play queue' )
|
|
|
|
self._parser_status = ClientGUICommon.BetterStaticText( self._simple_parsing_jobs_panel, ellipsize_end = True )
|
|
|
|
self._gallery_seed_log_control = ClientGUIGallerySeedLog.GallerySeedLogStatusControl( self._simple_parsing_jobs_panel, self._controller, True, False, 'parsing', self._page_key )
|
|
|
|
self._page_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._simple_parsing_jobs_panel )
|
|
|
|
self._pending_jobs_listbox = ClientGUIListBoxes.BetterQListWidget( self._simple_parsing_jobs_panel )
|
|
|
|
self._pending_jobs_listbox.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
|
|
|
|
self._advance_button = QW.QPushButton( '\u2191', self._simple_parsing_jobs_panel )
|
|
self._advance_button.clicked.connect( self.EventAdvance )
|
|
|
|
self._delete_button = QW.QPushButton( 'X', self._simple_parsing_jobs_panel )
|
|
self._delete_button.clicked.connect( self.EventDelete )
|
|
|
|
self._delay_button = QW.QPushButton( '\u2193', self._simple_parsing_jobs_panel )
|
|
self._delay_button.clicked.connect( self.EventDelay )
|
|
|
|
self._page_url_input = ClientGUIControls.TextAndPasteCtrl( self._simple_parsing_jobs_panel, self._PendPageURLs )
|
|
|
|
self._page_url_input.setPlaceholderText( 'url to be parsed by the selected formula' )
|
|
|
|
self._formulae = ClientGUICommon.BetterChoice( self._simple_parsing_jobs_panel )
|
|
|
|
formulae_width = ClientGUIFunctions.ConvertTextToPixelWidth( self._formulae, 10 )
|
|
|
|
self._formulae.setMinimumWidth( formulae_width )
|
|
|
|
menu_items = []
|
|
|
|
menu_items.append( ( 'normal', 'edit formulae', 'Edit these parsing formulae.', self._EditFormulae ) )
|
|
|
|
self._formula_cog = ClientGUIMenuButton.MenuBitmapButton( self._simple_parsing_jobs_panel, CC.global_pixmaps().cog, menu_items )
|
|
|
|
self._RefreshFormulae()
|
|
|
|
file_import_options = self._simple_downloader_import.GetFileImportOptions()
|
|
|
|
show_downloader_options = True
|
|
allow_default_selection = True
|
|
|
|
self._import_options_button = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
|
|
|
|
self._import_options_button.SetFileImportOptions( file_import_options )
|
|
|
|
#
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._current_action, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( hbox, self._pause_files_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
self._import_queue_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._import_queue_panel.Add( self._file_seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._import_queue_panel.Add( self._file_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
queue_buttons_vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( queue_buttons_vbox, self._advance_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( queue_buttons_vbox, self._delete_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
QP.AddToLayout( queue_buttons_vbox, self._delay_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
queue_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( queue_hbox, self._pending_jobs_listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( queue_hbox, queue_buttons_vbox, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
formulae_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( formulae_hbox, self._formulae, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
QP.AddToLayout( formulae_hbox, self._formula_cog, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._parser_status, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( hbox, self._pause_queue_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
self._simple_parsing_jobs_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._simple_parsing_jobs_panel.Add( self._gallery_seed_log_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._simple_parsing_jobs_panel.Add( self._page_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._simple_parsing_jobs_panel.Add( queue_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
self._simple_parsing_jobs_panel.Add( self._page_url_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._simple_parsing_jobs_panel.Add( formulae_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
self._simple_downloader_panel.Add( self._import_queue_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._simple_downloader_panel.Add( self._simple_parsing_jobs_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._simple_downloader_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._simple_downloader_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
self._formulae.currentIndexChanged.connect( self.EventFormulaChanged )
|
|
|
|
file_seed_cache = self._simple_downloader_import.GetFileSeedCache()
|
|
|
|
self._file_seed_cache_control.SetFileSeedCache( file_seed_cache )
|
|
|
|
gallery_seed_log = self._simple_downloader_import.GetGallerySeedLog()
|
|
|
|
self._gallery_seed_log_control.SetGallerySeedLog( gallery_seed_log )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
self._import_options_button.fileImportOptionsChanged.connect( self._simple_downloader_import.SetFileImportOptions )
|
|
|
|
|
|
def _EditFormulae( self ):
|
|
|
|
def data_to_pretty_callable( data ):
|
|
|
|
simple_downloader_formula = data
|
|
|
|
return simple_downloader_formula.GetName()
|
|
|
|
|
|
def edit_callable( data ):
|
|
|
|
simple_downloader_formula = data
|
|
|
|
name = simple_downloader_formula.GetName()
|
|
|
|
with ClientGUIDialogs.DialogTextEntry( dlg, 'edit name', default = name ) as dlg_2:
|
|
|
|
if dlg_2.exec() == QW.QDialog.Accepted:
|
|
|
|
name = dlg_2.GetValue()
|
|
|
|
else:
|
|
|
|
raise HydrusExceptions.VetoException()
|
|
|
|
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( dlg, 'edit formula' ) as dlg_3:
|
|
|
|
panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg_3 )
|
|
|
|
formula = simple_downloader_formula.GetFormula()
|
|
|
|
control = ClientGUIParsingFormulae.EditFormulaPanel( panel, formula, lambda: ClientParsing.ParsingTestData( {}, ( '', ) ) )
|
|
|
|
panel.SetControl( control )
|
|
|
|
dlg_3.SetPanel( panel )
|
|
|
|
if dlg_3.exec() == QW.QDialog.Accepted:
|
|
|
|
formula = control.GetValue()
|
|
|
|
simple_downloader_formula = ClientParsing.SimpleDownloaderParsingFormula( name = name, formula = formula )
|
|
|
|
return simple_downloader_formula
|
|
|
|
else:
|
|
|
|
raise HydrusExceptions.VetoException()
|
|
|
|
|
|
|
|
|
|
def add_callable():
|
|
|
|
data = ClientParsing.SimpleDownloaderParsingFormula()
|
|
|
|
return edit_callable( data )
|
|
|
|
|
|
formulae = list( self._controller.new_options.GetSimpleDownloaderFormulae() )
|
|
|
|
formulae.sort( key = lambda o: o.GetName() )
|
|
|
|
with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit simple downloader formulae' ) as dlg:
|
|
|
|
panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg )
|
|
|
|
height_num_chars = 20
|
|
|
|
control = ClientGUIListBoxes.AddEditDeleteListBoxUniqueNamedObjects( panel, height_num_chars, data_to_pretty_callable, add_callable, edit_callable )
|
|
|
|
control.AddSeparator()
|
|
control.AddImportExportButtons( ( ClientParsing.SimpleDownloaderParsingFormula, ) )
|
|
control.AddSeparator()
|
|
control.AddDefaultsButton( ClientDefaults.GetDefaultSimpleDownloaderFormulae )
|
|
|
|
control.AddDatas( formulae )
|
|
|
|
panel.SetControl( control )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.exec() == QW.QDialog.Accepted:
|
|
|
|
formulae = control.GetData()
|
|
|
|
self._controller.new_options.SetSimpleDownloaderFormulae( formulae )
|
|
|
|
|
|
|
|
self._RefreshFormulae()
|
|
|
|
|
|
def _PendPageURLs( self, unclean_urls ):
|
|
|
|
urls = [ ClientNetworkingFunctions.WashURL( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
|
|
|
|
simple_downloader_formula = self._formulae.GetValue()
|
|
|
|
for url in urls:
|
|
|
|
job = ( url, simple_downloader_formula )
|
|
|
|
self._simple_downloader_import.PendJob( job )
|
|
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def _RefreshFormulae( self ):
|
|
|
|
self._formulae.blockSignals( True )
|
|
|
|
self._formulae.clear()
|
|
|
|
to_select = None
|
|
|
|
select_name = self._simple_downloader_import.GetFormulaName()
|
|
|
|
simple_downloader_formulae = list( self._controller.new_options.GetSimpleDownloaderFormulae() )
|
|
|
|
simple_downloader_formulae.sort( key = lambda o: o.GetName() )
|
|
|
|
for ( i, simple_downloader_formula ) in enumerate( simple_downloader_formulae ):
|
|
|
|
name = simple_downloader_formula.GetName()
|
|
|
|
self._formulae.addItem( name, simple_downloader_formula )
|
|
|
|
if name == select_name:
|
|
|
|
to_select = i
|
|
|
|
|
|
|
|
self._formulae.blockSignals( False )
|
|
|
|
if to_select is not None:
|
|
|
|
self._formulae.setCurrentIndex( to_select )
|
|
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
( pending_jobs, parser_status, current_action, queue_paused, files_paused ) = self._simple_downloader_import.GetStatus()
|
|
|
|
current_pending_jobs = self._pending_jobs_listbox.GetData()
|
|
|
|
if current_pending_jobs != pending_jobs:
|
|
|
|
selected_jobs = set( self._pending_jobs_listbox.GetData( only_selected = True ) )
|
|
|
|
self._pending_jobs_listbox.clear()
|
|
|
|
for job in pending_jobs:
|
|
|
|
( url, simple_downloader_formula ) = job
|
|
|
|
pretty_job = simple_downloader_formula.GetName() + ': ' + url
|
|
|
|
self._pending_jobs_listbox.Append( pretty_job, job )
|
|
|
|
|
|
self._pending_jobs_listbox.SelectData( selected_jobs )
|
|
|
|
|
|
self._parser_status.setText( parser_status )
|
|
|
|
self._current_action.setText( current_action )
|
|
|
|
if files_paused:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_files_button, CC.global_pixmaps().file_play )
|
|
|
|
else:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_files_button, CC.global_pixmaps().file_pause )
|
|
|
|
|
|
if queue_paused:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_queue_button, CC.global_pixmaps().gallery_play )
|
|
|
|
else:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_queue_button, CC.global_pixmaps().gallery_pause )
|
|
|
|
|
|
( file_network_job, page_network_job ) = self._simple_downloader_import.GetNetworkJobs()
|
|
|
|
if file_network_job is None:
|
|
|
|
self._file_download_control.ClearNetworkJob()
|
|
|
|
else:
|
|
|
|
self._file_download_control.SetNetworkJob( file_network_job )
|
|
|
|
|
|
if page_network_job is None:
|
|
|
|
self._page_download_control.ClearNetworkJob()
|
|
|
|
else:
|
|
|
|
self._page_download_control.SetNetworkJob( page_network_job )
|
|
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
if self._simple_downloader_import.CurrentlyWorking():
|
|
|
|
raise HydrusExceptions.VetoException( 'This page is still importing.' )
|
|
|
|
|
|
|
|
def EventAdvance( self ):
|
|
|
|
selected_jobs = self._pending_jobs_listbox.GetData( only_selected = True )
|
|
|
|
for job in selected_jobs:
|
|
|
|
self._simple_downloader_import.AdvanceJob( job )
|
|
|
|
|
|
if len( selected_jobs ) > 0:
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
|
|
def EventDelay( self ):
|
|
|
|
selected_jobs = list( self._pending_jobs_listbox.GetData( only_selected = True ) )
|
|
|
|
selected_jobs.reverse()
|
|
|
|
for job in selected_jobs:
|
|
|
|
self._simple_downloader_import.DelayJob( job )
|
|
|
|
|
|
if len( selected_jobs ) > 0:
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
|
|
def EventDelete( self ):
|
|
|
|
selected_jobs = self._pending_jobs_listbox.GetData( only_selected = True )
|
|
|
|
message = 'Delete {} jobs?'.format( HydrusData.ToHumanInt( len( selected_jobs ) ) )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
|
|
|
if result != QW.QDialog.Accepted:
|
|
|
|
return
|
|
|
|
|
|
for job in selected_jobs:
|
|
|
|
self._simple_downloader_import.DeleteJob( job )
|
|
|
|
|
|
if len( selected_jobs ) > 0:
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
|
|
def EventFormulaChanged( self ):
|
|
|
|
formula = self._formulae.GetValue()
|
|
|
|
formula_name = formula.GetName()
|
|
|
|
self._simple_downloader_import.SetFormulaName( formula_name )
|
|
self._controller.new_options.SetString( 'favourite_simple_downloader_formula', formula_name )
|
|
|
|
|
|
def PauseQueue( self ):
|
|
|
|
self._simple_downloader_import.PausePlayQueue()
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def PauseFiles( self ):
|
|
|
|
self._simple_downloader_import.PausePlayFiles()
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._page_url_input )
|
|
|
|
|
|
def Start( self ):
|
|
|
|
self._simple_downloader_import.Start( self._page_key )
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_IMPORT_SIMPLE_DOWNLOADER ] = ManagementPanelImporterSimpleDownloader
|
|
|
|
class ManagementPanelImporterURLs( ManagementPanelImporter ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanelImporter.__init__( self, parent, page, controller, management_controller )
|
|
|
|
#
|
|
|
|
self._url_panel = ClientGUICommon.StaticBox( self, 'url downloader' )
|
|
|
|
#
|
|
|
|
self._import_queue_panel = ClientGUICommon.StaticBox( self._url_panel, 'imports' )
|
|
|
|
self._pause_button = ClientGUICommon.BetterBitmapButton( self._import_queue_panel, CC.global_pixmaps().file_pause, self.Pause )
|
|
self._pause_button.setToolTip( 'pause/play files' )
|
|
|
|
self._file_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._import_queue_panel )
|
|
|
|
self._urls_import: ClientImportSimpleURLs.URLsImport = self._management_controller.GetVariable( 'urls_import' )
|
|
|
|
self._file_seed_cache_control = ClientGUIFileSeedCache.FileSeedCacheStatusControl( self._import_queue_panel, self._controller, page_key = self._page_key )
|
|
|
|
#
|
|
|
|
self._gallery_panel = ClientGUICommon.StaticBox( self._url_panel, 'search' )
|
|
|
|
self._gallery_download_control = ClientGUINetworkJobControl.NetworkJobControl( self._gallery_panel )
|
|
|
|
self._gallery_seed_log_control = ClientGUIGallerySeedLog.GallerySeedLogStatusControl( self._gallery_panel, self._controller, False, False, 'search', page_key = self._page_key )
|
|
|
|
#
|
|
|
|
self._url_input = ClientGUIControls.TextAndPasteCtrl( self._url_panel, self._PendURLs )
|
|
|
|
self._url_input.setPlaceholderText( 'any url hydrus recognises, or a raw file url' )
|
|
|
|
file_import_options = self._urls_import.GetFileImportOptions()
|
|
tag_import_options = self._urls_import.GetTagImportOptions()
|
|
note_import_options = self._urls_import.GetNoteImportOptions()
|
|
|
|
show_downloader_options = True
|
|
allow_default_selection = True
|
|
|
|
self._import_options_button = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
|
|
|
|
self._import_options_button.SetFileImportOptions( file_import_options )
|
|
self._import_options_button.SetTagImportOptions( tag_import_options )
|
|
self._import_options_button.SetNoteImportOptions( note_import_options )
|
|
|
|
#
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, self._pause_button, CC.FLAGS_ON_RIGHT )
|
|
|
|
self._import_queue_panel.Add( hbox, CC.FLAGS_ON_RIGHT )
|
|
self._import_queue_panel.Add( self._file_seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._import_queue_panel.Add( self._file_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._gallery_panel.Add( self._gallery_seed_log_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._gallery_panel.Add( self._gallery_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._url_panel.Add( self._import_queue_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._url_panel.Add( self._gallery_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._url_panel.Add( self._url_input, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._url_panel.Add( self._import_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
#
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._url_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
#
|
|
|
|
file_seed_cache = self._urls_import.GetFileSeedCache()
|
|
|
|
self._file_seed_cache_control.SetFileSeedCache( file_seed_cache )
|
|
|
|
gallery_seed_log = self._urls_import.GetGallerySeedLog()
|
|
|
|
self._gallery_seed_log_control.SetGallerySeedLog( gallery_seed_log )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
self._import_options_button.fileImportOptionsChanged.connect( self._urls_import.SetFileImportOptions )
|
|
self._import_options_button.noteImportOptionsChanged.connect( self._urls_import.SetNoteImportOptions )
|
|
self._import_options_button.tagImportOptionsChanged.connect( self._urls_import.SetTagImportOptions )
|
|
|
|
|
|
def _PendURLs( self, unclean_urls, filterable_tags = None, additional_service_keys_to_tags = None ):
|
|
|
|
if filterable_tags is None:
|
|
|
|
filterable_tags = set()
|
|
|
|
|
|
if additional_service_keys_to_tags is None:
|
|
|
|
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
|
|
|
|
|
|
urls = [ ClientNetworkingFunctions.WashURL( unclean_url ) for unclean_url in unclean_urls if ClientNetworkingFunctions.LooksLikeAFullURL( unclean_url ) ]
|
|
|
|
self._urls_import.PendURLs( urls, filterable_tags = filterable_tags, additional_service_keys_to_tags = additional_service_keys_to_tags )
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def _UpdateImportStatus( self ):
|
|
|
|
paused = self._urls_import.IsPaused()
|
|
|
|
if paused:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_button, CC.global_pixmaps().file_play )
|
|
|
|
else:
|
|
|
|
ClientGUIFunctions.SetBitmapButtonBitmap( self._pause_button, CC.global_pixmaps().file_pause )
|
|
|
|
|
|
( file_network_job, gallery_network_job ) = self._urls_import.GetNetworkJobs()
|
|
|
|
if file_network_job is None:
|
|
|
|
self._file_download_control.ClearNetworkJob()
|
|
|
|
else:
|
|
|
|
self._file_download_control.SetNetworkJob( file_network_job )
|
|
|
|
|
|
if gallery_network_job is None:
|
|
|
|
self._gallery_download_control.ClearNetworkJob()
|
|
|
|
else:
|
|
|
|
self._gallery_download_control.SetNetworkJob( gallery_network_job )
|
|
|
|
|
|
|
|
def CheckAbleToClose( self ):
|
|
|
|
if self._urls_import.CurrentlyWorking():
|
|
|
|
raise HydrusExceptions.VetoException( 'This page is still importing.' )
|
|
|
|
|
|
|
|
def Pause( self ):
|
|
|
|
self._urls_import.PausePlay()
|
|
|
|
self._UpdateImportStatus()
|
|
|
|
|
|
def PendURL( self, url, filterable_tags = None, additional_service_keys_to_tags = None ):
|
|
|
|
if filterable_tags is None:
|
|
|
|
filterable_tags = set()
|
|
|
|
|
|
if additional_service_keys_to_tags is None:
|
|
|
|
additional_service_keys_to_tags = ClientTags.ServiceKeysToTags()
|
|
|
|
|
|
self._PendURLs( ( url, ), filterable_tags = filterable_tags, additional_service_keys_to_tags = additional_service_keys_to_tags )
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._url_input )
|
|
|
|
|
|
def Start( self ):
|
|
|
|
self._urls_import.Start( self._page_key )
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_IMPORT_URLS ] = ManagementPanelImporterURLs
|
|
|
|
def GetPetitionActionInfo( petition: HydrusNetwork.Petition ):
|
|
|
|
add_contents = petition.GetContents( HC.CONTENT_UPDATE_PEND )
|
|
delete_contents = petition.GetContents( HC.CONTENT_UPDATE_PETITION )
|
|
|
|
have_add = len( add_contents ) > 0
|
|
have_delete = len( delete_contents ) > 0
|
|
|
|
action_text = 'UNKNOWN'
|
|
hydrus_text = 'default'
|
|
object_name = 'normal'
|
|
|
|
if have_add or have_delete:
|
|
|
|
if have_add and have_delete:
|
|
|
|
action_text = 'REPLACE'
|
|
|
|
elif have_add:
|
|
|
|
action_text = 'ADD'
|
|
hydrus_text = 'valid'
|
|
object_name = 'HydrusValid'
|
|
|
|
else:
|
|
|
|
action_text = 'DELETE'
|
|
hydrus_text = 'invalid'
|
|
object_name = 'HydrusInvalid'
|
|
|
|
|
|
|
|
return ( action_text, hydrus_text, object_name )
|
|
|
|
|
|
class ManagementPanelPetitions( ManagementPanel ):
|
|
|
|
TAG_DISPLAY_TYPE = ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
self._petition_service_key = management_controller.GetVariable( 'petition_service_key' )
|
|
|
|
ManagementPanel.__init__( self, parent, page, controller, management_controller )
|
|
|
|
self._service = self._controller.services_manager.GetService( self._petition_service_key )
|
|
self._can_ban = self._service.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_MODERATE )
|
|
|
|
service_type = self._service.GetServiceType()
|
|
|
|
self._petition_types_to_count = collections.Counter()
|
|
self._current_petition = None
|
|
|
|
content_type = management_controller.GetVariable( 'petition_type_content_type' )
|
|
status = management_controller.GetVariable( 'petition_type_status' )
|
|
|
|
if content_type is None or status is None:
|
|
|
|
self._last_petition_type_fetched = None
|
|
|
|
else:
|
|
|
|
self._last_petition_type_fetched = ( content_type, status )
|
|
|
|
|
|
self._last_fetched_subject_account_key = None
|
|
|
|
self._petition_headers_to_fetched_petitions_cache = {}
|
|
self._petition_headers_we_failed_to_fetch = set()
|
|
self._petition_headers_we_are_fetching = []
|
|
self._outgoing_petition_headers_to_petitions = {}
|
|
self._failed_outgoing_petition_headers_to_petitions = {}
|
|
|
|
self._petition_fetcher_and_uploader_work_lock = threading.Lock()
|
|
|
|
#
|
|
|
|
self._petition_numbers_panel = ClientGUICommon.StaticBox( self, 'counts' )
|
|
|
|
self._petition_account_key = QW.QLineEdit( self._petition_numbers_panel )
|
|
self._petition_account_key.setPlaceholderText( 'account id filter' )
|
|
|
|
self._num_petitions_to_fetch = ClientGUICommon.BetterSpinBox( self._petition_numbers_panel, min = 1, max = 10000 )
|
|
|
|
self._num_petitions_to_fetch.setValue( management_controller.GetVariable( 'num_petitions_to_fetch' ) )
|
|
|
|
self._refresh_num_petitions_button = ClientGUICommon.BetterButton( self._petition_numbers_panel, 'refresh counts', self._StartFetchNumPetitions )
|
|
|
|
self._petition_types_to_controls = {}
|
|
|
|
content_type_hboxes = []
|
|
|
|
self._my_petition_types = []
|
|
|
|
if service_type == HC.FILE_REPOSITORY:
|
|
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_FILES, HC.CONTENT_STATUS_PETITIONED ) )
|
|
|
|
elif service_type == HC.TAG_REPOSITORY:
|
|
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_STATUS_PETITIONED ) )
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_STATUS_PENDING ) )
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_STATUS_PETITIONED ) )
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_STATUS_PENDING ) )
|
|
self._my_petition_types.append( ( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_STATUS_PETITIONED ) )
|
|
|
|
|
|
for petition_type in self._my_petition_types:
|
|
|
|
( content_type, status ) = petition_type
|
|
|
|
func = HydrusData.Call( self._FetchPetitionsSummary, petition_type )
|
|
|
|
st = ClientGUICommon.BetterStaticText( self._petition_numbers_panel )
|
|
button = ClientGUICommon.BetterButton( self._petition_numbers_panel, 'fetch ' + HC.content_status_string_lookup[ status ] + ' ' + HC.content_type_string_lookup[ content_type ] + ' petitions', func )
|
|
|
|
button.setEnabled( False )
|
|
|
|
self._petition_types_to_controls[ ( content_type, status ) ] = ( st, button )
|
|
|
|
hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( hbox, st, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( hbox, button, CC.FLAGS_CENTER_PERPENDICULAR )
|
|
|
|
content_type_hboxes.append( hbox )
|
|
|
|
|
|
#
|
|
|
|
self._petitions_panel = ClientGUICommon.StaticBox( self, 'petitions' )
|
|
|
|
self._petitions_summary_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( self._petitions_panel )
|
|
|
|
self._petitions_summary_list = ClientGUIListCtrl.BetterListCtrl( self._petitions_summary_list_panel, CGLC.COLUMN_LIST_PETITIONS_SUMMARY.ID, 12, self._ConvertDataToListCtrlTuples, activation_callback = self._ActivateToHighlightPetition )
|
|
|
|
self._petitions_summary_list_panel.SetListCtrl( self._petitions_summary_list )
|
|
|
|
self._petitions_summary_list_panel.AddButton( 'mass-approve', self._ApproveSelected, enabled_check_func = self._OnlySelectingLoadedPetitions, tooltip = 'Approve the selected petitions' )
|
|
self._petitions_summary_list_panel.AddButton( 'mass-deny', self._DenySelected, enabled_check_func = self._OnlySelectingLoadedPetitions, tooltip = 'Deny the selected petitions' )
|
|
|
|
#
|
|
|
|
self._petition_panel = ClientGUICommon.StaticBox( self, 'highlighted petition' )
|
|
|
|
self._num_files_to_show = ClientGUICommon.NoneableSpinCtrl( self._petition_panel, message = 'number of files to show', min = 1 )
|
|
|
|
self._num_files_to_show.SetValue( management_controller.GetVariable( 'num_files_to_show' ) )
|
|
|
|
self._action_text = ClientGUICommon.BetterStaticText( self._petition_panel, label = '' )
|
|
|
|
self._reason_text = QW.QTextEdit( self._petition_panel )
|
|
self._reason_text.setReadOnly( True )
|
|
|
|
( min_width, min_height ) = ClientGUIFunctions.ConvertTextToPixels( self._reason_text, ( 16, 6 ) )
|
|
|
|
self._reason_text.setFixedHeight( min_height )
|
|
|
|
check_all = ClientGUICommon.BetterButton( self._petition_panel, 'check all', self._CheckAll )
|
|
flip_selected = ClientGUICommon.BetterButton( self._petition_panel, 'flip selected', self._FlipSelected )
|
|
check_none = ClientGUICommon.BetterButton( self._petition_panel, 'check none', self._CheckNone )
|
|
|
|
self._sort_by_left = ClientGUICommon.BetterButton( self._petition_panel, 'sort by left', self._SortBy, 'left' )
|
|
self._sort_by_right = ClientGUICommon.BetterButton( self._petition_panel, 'sort by right', self._SortBy, 'right' )
|
|
|
|
self._sort_by_left.setEnabled( False )
|
|
self._sort_by_right.setEnabled( False )
|
|
|
|
self._contents_add = ClientGUICommon.BetterCheckBoxList( self._petition_panel )
|
|
self._contents_add.itemDoubleClicked.connect( self.ContentsAddDoubleClick )
|
|
self._contents_add.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff )
|
|
|
|
( min_width, min_height ) = ClientGUIFunctions.ConvertTextToPixels( self._contents_add, ( 16, 20 ) )
|
|
|
|
self._contents_add.setFixedHeight( min_height )
|
|
|
|
self._contents_delete = ClientGUICommon.BetterCheckBoxList( self._petition_panel )
|
|
self._contents_delete.itemDoubleClicked.connect( self.ContentsDeleteDoubleClick )
|
|
self._contents_delete.setHorizontalScrollBarPolicy( QC.Qt.ScrollBarAlwaysOff )
|
|
|
|
( min_width, min_height ) = ClientGUIFunctions.ConvertTextToPixels( self._contents_delete, ( 16, 20 ) )
|
|
|
|
self._contents_delete.setFixedHeight( min_height )
|
|
|
|
self._process = QW.QPushButton( 'process', self._petition_panel )
|
|
self._process.clicked.connect( self.ProcessCurrentPetition )
|
|
self._process.setObjectName( 'HydrusAccept' )
|
|
|
|
self._copy_account_key_button = ClientGUICommon.BetterButton( self._petition_panel, 'copy petitioner account id', self._CopyAccountKey )
|
|
|
|
self._modify_petitioner = QW.QPushButton( 'modify petitioner', self._petition_panel )
|
|
self._modify_petitioner.clicked.connect( self.EventModifyPetitioner )
|
|
self._modify_petitioner.setEnabled( False )
|
|
if not self._can_ban: self._modify_petitioner.setVisible( False )
|
|
|
|
#
|
|
|
|
self._petition_numbers_panel.Add( self._petition_account_key, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_numbers_panel.Add( self._refresh_num_petitions_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_numbers_panel.Add( ClientGUICommon.WrapInText( self._num_petitions_to_fetch, self, 'number of petitions to fetch' ), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
for hbox in content_type_hboxes:
|
|
|
|
self._petition_numbers_panel.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
|
|
#
|
|
|
|
self._petitions_panel.Add( self._petitions_summary_list_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
#
|
|
|
|
check_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( check_hbox, check_all, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( check_hbox, flip_selected, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( check_hbox, check_none, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
|
|
sort_hbox = QP.HBoxLayout()
|
|
|
|
QP.AddToLayout( sort_hbox, self._sort_by_left, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
QP.AddToLayout( sort_hbox, self._sort_by_right, CC.FLAGS_CENTER_PERPENDICULAR_EXPAND_DEPTH )
|
|
|
|
self._petition_panel.Add( ClientGUICommon.BetterStaticText( self._petition_panel, label = 'Double-click a petition row to see its files, if it has them.' ), CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._num_files_to_show, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._action_text, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._reason_text, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( sort_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._petition_panel.Add( self._contents_add, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._contents_delete, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( check_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._petition_panel.Add( self._process, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._copy_account_key_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
self._petition_panel.Add( self._modify_petitioner, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
QP.AddToLayout( vbox, self._petition_numbers_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._petitions_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._petition_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
if service_type == HC.TAG_REPOSITORY:
|
|
|
|
tag_display_type = ClientTags.TAG_DISPLAY_STORAGE
|
|
|
|
else:
|
|
|
|
tag_display_type = ClientTags.TAG_DISPLAY_SELECTION_LIST
|
|
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox, tag_display_type = tag_display_type )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
self._contents_add.rightClicked.connect( self.EventAddRowRightClick )
|
|
self._contents_delete.rightClicked.connect( self.EventDeleteRowRightClick )
|
|
|
|
self._petition_account_key.textChanged.connect( self._UpdateAccountKey )
|
|
|
|
self._num_files_to_show.valueChanged.connect( self._NotifyNumsUpdated )
|
|
self._num_petitions_to_fetch.valueChanged.connect( self._NotifyNumsUpdated )
|
|
|
|
self._UpdateAccountKey()
|
|
self._DrawCurrentPetition()
|
|
|
|
|
|
def _ActivateToHighlightPetition( self ):
|
|
|
|
for eligible_petition_header in self._petitions_summary_list.GetData( only_selected = True ):
|
|
|
|
if self._CanHighlight( eligible_petition_header ):
|
|
|
|
self._HighlightPetition( eligible_petition_header )
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
def _ApproveSelected( self ):
|
|
|
|
selected_petition_headers = self._petitions_summary_list.GetData( only_selected = True )
|
|
|
|
viable_petitions = [ self._petition_headers_to_fetched_petitions_cache[ petition_header ] for petition_header in selected_petition_headers if self._CanHighlight( petition_header ) ]
|
|
|
|
if len( viable_petitions ) > 0:
|
|
|
|
text = 'Approve all the content in these {} petitions?'.format( HydrusData.ToHumanInt( len( viable_petitions ) ) )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, text )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
for petition in viable_petitions:
|
|
|
|
petition.ApproveAll()
|
|
|
|
|
|
self._StartUploadingCompletedPetitions( viable_petitions )
|
|
|
|
|
|
|
|
|
|
def _CanHighlight( self, petition_header: HydrusNetwork.PetitionHeader ):
|
|
|
|
if petition_header in self._outgoing_petition_headers_to_petitions:
|
|
|
|
return False
|
|
|
|
|
|
if petition_header in self._failed_outgoing_petition_headers_to_petitions:
|
|
|
|
return False
|
|
|
|
|
|
return petition_header in self._petition_headers_to_fetched_petitions_cache
|
|
|
|
|
|
def _CheckAll( self ):
|
|
|
|
for i in range( self._contents_add.count() ):
|
|
|
|
self._contents_add.Check( i, True )
|
|
|
|
|
|
for i in range( self._contents_delete.count() ):
|
|
|
|
self._contents_delete.Check( i, True )
|
|
|
|
|
|
|
|
def _CheckNone( self ):
|
|
|
|
for i in range( self._contents_add.count() ):
|
|
|
|
self._contents_add.Check( i, False )
|
|
|
|
|
|
for i in range( self._contents_delete.count() ):
|
|
|
|
self._contents_delete.Check( i, False )
|
|
|
|
|
|
|
|
def _ClearCurrentPetition( self ):
|
|
|
|
if self._current_petition is not None:
|
|
|
|
petition_header = self._current_petition.GetPetitionHeader()
|
|
|
|
self._current_petition = None
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petitions_summary_list.UpdateDatas( ( petition_header, ) )
|
|
|
|
|
|
self._DrawCurrentPetition()
|
|
|
|
self._ShowHashes( [] )
|
|
|
|
|
|
|
|
def _ClearPetitionsSummary( self ):
|
|
|
|
self._petitions_summary_list.DeleteDatas( self._petitions_summary_list.GetData() )
|
|
|
|
self._petition_headers_we_are_fetching = []
|
|
self._petition_headers_we_failed_to_fetch = set()
|
|
self._failed_outgoing_petition_headers_to_petitions = {}
|
|
|
|
self._ClearCurrentPetition()
|
|
|
|
|
|
def _ConvertDataToListCtrlTuples( self, petition_header: HydrusNetwork.PetitionHeader ):
|
|
|
|
pretty_action = ''
|
|
pretty_content = 'fetching' + HC.UNICODE_ELLIPSIS
|
|
|
|
sort_content = 1
|
|
|
|
petition = None
|
|
this_is_current_petition = False
|
|
|
|
if petition_header in self._outgoing_petition_headers_to_petitions:
|
|
|
|
petition = self._outgoing_petition_headers_to_petitions[ petition_header ]
|
|
|
|
pretty_content = 'uploading' + HC.UNICODE_ELLIPSIS
|
|
|
|
elif petition_header in self._failed_outgoing_petition_headers_to_petitions:
|
|
|
|
petition = self._failed_outgoing_petition_headers_to_petitions[ petition_header ]
|
|
|
|
pretty_content = 'failed to upload!'
|
|
|
|
elif petition_header in self._petition_headers_to_fetched_petitions_cache:
|
|
|
|
petition = self._petition_headers_to_fetched_petitions_cache[ petition_header ]
|
|
|
|
pretty_content = petition.GetContentSummary()
|
|
|
|
this_is_current_petition = False
|
|
|
|
if self._current_petition is not None and petition_header == self._current_petition.GetPetitionHeader():
|
|
|
|
this_is_current_petition = True
|
|
|
|
|
|
elif petition_header in self._petition_headers_we_failed_to_fetch:
|
|
|
|
pretty_content = 'failed to fetch!'
|
|
|
|
|
|
if petition is not None:
|
|
|
|
pretty_action = GetPetitionActionInfo( petition )[0]
|
|
|
|
sort_content = petition.GetActualContentWeight()
|
|
|
|
|
|
if this_is_current_petition:
|
|
|
|
pretty_action = f'* {pretty_action}'
|
|
|
|
|
|
pretty_account_key = petition_header.account_key.hex()
|
|
pretty_reason = petition_header.reason
|
|
|
|
sort_action = pretty_action
|
|
sort_account_key = pretty_account_key
|
|
sort_reason = pretty_reason
|
|
|
|
display_tuple = ( pretty_action, pretty_account_key, pretty_reason, pretty_content )
|
|
sort_tuple = ( sort_action, sort_account_key, sort_reason, sort_content )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _CopyAccountKey( self ):
|
|
|
|
if self._current_petition is None:
|
|
|
|
return
|
|
|
|
|
|
account_key = self._current_petition.GetPetitionerAccount().GetAccountKey()
|
|
|
|
CG.client_controller.pub( 'clipboard', 'text', account_key.hex() )
|
|
|
|
|
|
def _DenySelected( self ):
|
|
|
|
selected_petition_headers = self._petitions_summary_list.GetData( only_selected = True )
|
|
|
|
viable_petitions = [ self._petition_headers_to_fetched_petitions_cache[ petition_header ] for petition_header in selected_petition_headers if self._CanHighlight( petition_header ) ]
|
|
|
|
if len( viable_petitions ) > 0:
|
|
|
|
text = 'Deny all the content in these {} petitions?'.format( HydrusData.ToHumanInt( len( viable_petitions ) ) )
|
|
|
|
result = ClientGUIDialogsQuick.GetYesNo( self, text )
|
|
|
|
if result == QW.QDialog.Accepted:
|
|
|
|
for petition in viable_petitions:
|
|
|
|
petition.DenyAll()
|
|
|
|
|
|
self._StartUploadingCompletedPetitions( viable_petitions )
|
|
|
|
|
|
|
|
|
|
def _DrawCurrentPetition( self ):
|
|
|
|
if self._current_petition is None:
|
|
|
|
self._action_text.clear()
|
|
self._action_text.setProperty( 'hydrus_text', 'default' )
|
|
|
|
self._reason_text.clear()
|
|
self._reason_text.setProperty( 'hydrus_text', 'default' )
|
|
|
|
self._contents_add.clear()
|
|
self._contents_delete.clear()
|
|
|
|
self._contents_add.hide()
|
|
self._contents_delete.hide()
|
|
|
|
self._process.setEnabled( False )
|
|
self._copy_account_key_button.setEnabled( False )
|
|
|
|
self._sort_by_left.setEnabled( False )
|
|
self._sort_by_right.setEnabled( False )
|
|
|
|
if self._can_ban:
|
|
|
|
self._modify_petitioner.setEnabled( False )
|
|
|
|
|
|
else:
|
|
|
|
add_contents = self._current_petition.GetContents( HC.CONTENT_UPDATE_PEND )
|
|
delete_contents = self._current_petition.GetContents( HC.CONTENT_UPDATE_PETITION )
|
|
|
|
have_add = len( add_contents ) > 0
|
|
have_delete = len( delete_contents ) > 0
|
|
|
|
( action_text, hydrus_text, object_name ) = GetPetitionActionInfo( self._current_petition )
|
|
|
|
self._action_text.setText( action_text )
|
|
self._action_text.setObjectName( object_name )
|
|
#self._action_text.setProperty( 'hydrus_text', hydrus_text )
|
|
|
|
reason = self._current_petition.GetReason()
|
|
|
|
self._reason_text.setPlainText( reason )
|
|
self._reason_text.setObjectName( object_name )
|
|
#self._reason_text.setProperty( 'hydrus_text', hydrus_text )
|
|
|
|
if self._last_petition_type_fetched[0] in ( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_TYPE_TAG_PARENTS ):
|
|
|
|
self._sort_by_left.setEnabled( True )
|
|
self._sort_by_right.setEnabled( True )
|
|
|
|
else:
|
|
|
|
self._sort_by_left.setEnabled( False )
|
|
self._sort_by_right.setEnabled( False )
|
|
|
|
|
|
self._contents_add.setVisible( have_add )
|
|
self._contents_delete.setVisible( have_delete )
|
|
|
|
contents_and_checks = [ ( c, True ) for c in add_contents ]
|
|
|
|
self._SetContentsAndChecks( HC.CONTENT_UPDATE_PEND, contents_and_checks, 'right' )
|
|
|
|
contents_and_checks = [ ( c, True ) for c in delete_contents ]
|
|
|
|
self._SetContentsAndChecks( HC.CONTENT_UPDATE_PETITION, contents_and_checks, 'right' )
|
|
|
|
self._process.setEnabled( True )
|
|
self._copy_account_key_button.setEnabled( True )
|
|
|
|
if self._can_ban:
|
|
|
|
self._modify_petitioner.setEnabled( True )
|
|
|
|
|
|
|
|
self._action_text.style().polish( self._action_text )
|
|
self._reason_text.style().polish( self._reason_text )
|
|
|
|
|
|
def _DrawNumPetitions( self ):
|
|
|
|
for ( petition_type, ( st, button ) ) in self._petition_types_to_controls.items():
|
|
|
|
count = self._petition_types_to_count[ petition_type ]
|
|
|
|
( st, button ) = self._petition_types_to_controls[ petition_type ]
|
|
|
|
st.setText( '{} petitions'.format( HydrusData.ToHumanInt( count ) ) )
|
|
|
|
button.setEnabled( count > 0 )
|
|
|
|
|
|
|
|
def _FetchBestPetitionsSummary( self ):
|
|
|
|
top_petition_type_with_count = None
|
|
|
|
for petition_type in self._my_petition_types:
|
|
|
|
count = self._petition_types_to_count[ petition_type ]
|
|
|
|
if count == 0:
|
|
|
|
continue
|
|
|
|
|
|
if self._last_petition_type_fetched is not None and self._last_petition_type_fetched == petition_type:
|
|
|
|
self._FetchPetitionsSummary( petition_type )
|
|
|
|
return
|
|
|
|
|
|
if top_petition_type_with_count is None:
|
|
|
|
top_petition_type_with_count = petition_type
|
|
|
|
|
|
|
|
if top_petition_type_with_count is not None:
|
|
|
|
self._FetchPetitionsSummary( top_petition_type_with_count )
|
|
|
|
|
|
|
|
def _FetchPetitionsSummary( self, petition_type ):
|
|
|
|
( st, button ) = self._petition_types_to_controls[ petition_type ]
|
|
|
|
( content_type, status ) = petition_type
|
|
|
|
num_to_fetch = self._num_petitions_to_fetch.value()
|
|
|
|
subject_account_key = self._GetSubjectAccountKey()
|
|
|
|
def qt_set_petitions_summary( petitions_summary ):
|
|
|
|
if self._last_petition_type_fetched != petition_type:
|
|
|
|
last_petition_type = self._last_petition_type_fetched
|
|
|
|
self._last_petition_type_fetched = petition_type
|
|
|
|
self._management_controller.SetVariable( 'petition_type_content_type', content_type )
|
|
self._management_controller.SetVariable( 'petition_type_status', status )
|
|
|
|
self._UpdateFetchButtonText( last_petition_type )
|
|
|
|
|
|
self._SetPetitionsSummary( petitions_summary )
|
|
|
|
|
|
def qt_done():
|
|
|
|
button.setEnabled( True )
|
|
|
|
self._UpdateFetchButtonText( self._last_petition_type_fetched )
|
|
|
|
|
|
def do_it( service ):
|
|
|
|
try:
|
|
|
|
if subject_account_key is None:
|
|
|
|
response = service.Request( HC.GET, 'petitions_summary', { 'content_type' : content_type, 'status' : status, 'num' : num_to_fetch } )
|
|
|
|
else:
|
|
|
|
response = service.Request( HC.GET, 'petitions_summary', { 'content_type' : content_type, 'status' : status, 'num' : num_to_fetch, 'subject_account_key' : subject_account_key } )
|
|
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_set_petitions_summary, response[ 'petitions_summary' ] )
|
|
|
|
except HydrusExceptions.NotFoundException:
|
|
|
|
job_status = ClientThreading.JobStatus()
|
|
|
|
job_status.SetStatusText( 'Hey, the server did not have that type of petition after all. Please hit refresh counts.' )
|
|
|
|
job_status.FinishAndDismiss( 5 )
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
finally:
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_done )
|
|
|
|
|
|
|
|
if petition_type != self._last_petition_type_fetched:
|
|
|
|
self._ClearPetitionsSummary()
|
|
|
|
|
|
button.setEnabled( False )
|
|
button.setText( 'Fetching' + HC.UNICODE_ELLIPSIS )
|
|
|
|
self._controller.CallToThread( do_it, self._service )
|
|
|
|
|
|
def _FlipSelected( self ):
|
|
|
|
for i in self._contents_add.GetSelectedIndices():
|
|
|
|
flipped_state = not self._contents_add.IsChecked( i )
|
|
|
|
self._contents_add.Check( i, flipped_state )
|
|
|
|
|
|
for i in self._contents_delete.GetSelectedIndices():
|
|
|
|
flipped_state = not self._contents_delete.IsChecked( i )
|
|
|
|
self._contents_delete.Check( i, flipped_state )
|
|
|
|
|
|
|
|
def _GetContentsAndChecks( self, action ):
|
|
|
|
if action == HC.CONTENT_UPDATE_PEND:
|
|
|
|
contents = self._contents_add
|
|
|
|
else:
|
|
|
|
contents = self._contents_delete
|
|
|
|
|
|
contents_and_checks = []
|
|
|
|
for i in range( contents.count() ):
|
|
|
|
content = contents.GetData( i )
|
|
check = contents.IsChecked( i )
|
|
|
|
contents_and_checks.append( ( content, check ) )
|
|
|
|
|
|
return contents_and_checks
|
|
|
|
|
|
def _GetSubjectAccountKey( self ):
|
|
|
|
account_key_hex = self._petition_account_key.text()
|
|
|
|
if len( account_key_hex ) == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
account_key_bytes = bytes.fromhex( account_key_hex )
|
|
|
|
if len( account_key_bytes ) != 32:
|
|
|
|
raise Exception()
|
|
|
|
|
|
return account_key_bytes
|
|
|
|
except Exception as e:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _HighlightAPetitionIfNeeded( self ):
|
|
|
|
if self._current_petition is None:
|
|
|
|
for eligible_petition_header in self._petitions_summary_list.GetData():
|
|
|
|
if self._CanHighlight( eligible_petition_header ):
|
|
|
|
self._HighlightPetition( eligible_petition_header )
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
def _HighlightPetition( self, petition_header ):
|
|
|
|
if not self._CanHighlight( petition_header ):
|
|
|
|
return
|
|
|
|
|
|
if self._current_petition is not None and petition_header == self._current_petition.GetPetitionHeader():
|
|
|
|
self._ClearCurrentPetition()
|
|
|
|
elif petition_header in self._petition_headers_to_fetched_petitions_cache:
|
|
|
|
petition = self._petition_headers_to_fetched_petitions_cache[ petition_header ]
|
|
|
|
self._SetCurrentPetition( petition )
|
|
|
|
|
|
|
|
def _OnlySelectingLoadedPetitions( self ):
|
|
|
|
petition_headers = self._petitions_summary_list.GetData( only_selected = True )
|
|
|
|
if len( petition_headers ) == 0:
|
|
|
|
return False
|
|
|
|
|
|
for petition_header in petition_headers:
|
|
|
|
if petition_header not in self._petition_headers_to_fetched_petitions_cache:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
def _NotifyNumsUpdated( self ):
|
|
|
|
self._management_controller.SetVariable( 'num_petitions_to_fetch', self._num_petitions_to_fetch.value() )
|
|
self._management_controller.SetVariable( 'num_files_to_show', self._num_files_to_show.GetValue() )
|
|
|
|
|
|
def _SetContentsAndChecks( self, action, contents_and_checks, sort_type ):
|
|
|
|
def key( c_and_s ):
|
|
|
|
( c, s ) = c_and_s
|
|
|
|
if c.GetContentType() in ( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_TYPE_TAG_PARENTS ):
|
|
|
|
( left, right ) = c.GetContentData()
|
|
|
|
if sort_type == 'left':
|
|
|
|
( part_one, part_two ) = ( HydrusTags.SplitTag( left ), HydrusTags.SplitTag( right ) )
|
|
|
|
elif sort_type == 'right':
|
|
|
|
( part_one, part_two ) = ( HydrusTags.SplitTag( right ), HydrusTags.SplitTag( left ) )
|
|
|
|
|
|
elif c.GetContentType() == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
( tag, hashes ) = c.GetContentData()
|
|
|
|
part_one = HydrusTags.SplitTag( tag )
|
|
part_two = None
|
|
|
|
else:
|
|
|
|
part_one = None
|
|
part_two = None
|
|
|
|
|
|
return ( -c.GetVirtualWeight(), part_one, part_two )
|
|
|
|
|
|
contents_and_checks.sort( key = key )
|
|
|
|
if action == HC.CONTENT_UPDATE_PEND:
|
|
|
|
contents = self._contents_add
|
|
|
|
string_template = 'ADD: {}'
|
|
|
|
else:
|
|
|
|
contents = self._contents_delete
|
|
|
|
string_template = 'DELETE: {}'
|
|
|
|
|
|
contents.clear()
|
|
|
|
for ( i, ( content, check ) ) in enumerate( contents_and_checks ):
|
|
|
|
content_string = string_template.format( content.ToString() )
|
|
|
|
contents.Append( content_string, content, starts_checked = check )
|
|
|
|
|
|
if contents.count() > 0:
|
|
|
|
ideal_height_in_rows = max( 1, min( 20, len( contents_and_checks ) ) )
|
|
|
|
pixels_per_row = contents.sizeHintForRow( 0 )
|
|
|
|
else:
|
|
|
|
ideal_height_in_rows = 1
|
|
pixels_per_row = 16
|
|
|
|
|
|
ideal_height_in_pixels = ( ideal_height_in_rows * pixels_per_row ) + ( contents.frameWidth() * 2 )
|
|
|
|
contents.setFixedHeight( ideal_height_in_pixels )
|
|
|
|
|
|
def _SetCurrentPetition( self, petition: HydrusNetwork.Petition ):
|
|
|
|
self._ClearCurrentPetition()
|
|
|
|
self._current_petition = petition
|
|
|
|
self._petitions_summary_list.UpdateDatas( ( self._current_petition.GetPetitionHeader(), ) )
|
|
|
|
self._DrawCurrentPetition()
|
|
|
|
self._ShowHashes( [] )
|
|
|
|
|
|
def _SetPetitionsSummary( self, petitions_summary: typing.List[ HydrusNetwork.PetitionHeader ] ):
|
|
|
|
# note we can't make this a nice 'append' so easily, since we still need to cull petitions that were processed without us looking
|
|
# we'll keep the current since the user is looking, but otherwise we'll be good for now
|
|
# maybe add a hard refresh button in future? we'll see how common these issues are
|
|
|
|
if self._current_petition is not None and len( petitions_summary ) > 0:
|
|
|
|
current_petition_header = self._current_petition.GetPetitionHeader()
|
|
|
|
if current_petition_header not in petitions_summary:
|
|
|
|
petitions_summary.append( current_petition_header )
|
|
|
|
|
|
|
|
self._petitions_summary_list.SetData( petitions_summary )
|
|
|
|
sorted_petition_headers = self._petitions_summary_list.GetData()
|
|
|
|
self._petition_headers_we_are_fetching = [ petition_header for petition_header in sorted_petition_headers if petition_header not in self._petition_headers_to_fetched_petitions_cache ]
|
|
|
|
if len( self._petition_headers_we_are_fetching ) > 0:
|
|
|
|
CG.client_controller.CallToThread( self.THREADPetitionFetcherAndUploader, self._petition_fetcher_and_uploader_work_lock, self._service )
|
|
|
|
|
|
self._HighlightAPetitionIfNeeded()
|
|
|
|
|
|
def _ShowHashes( self, hashes ):
|
|
|
|
with ClientGUICommon.BusyCursor():
|
|
|
|
media_results = self._controller.Read( 'media_results', hashes )
|
|
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.Collect( self._media_collect_widget.GetValue() )
|
|
|
|
panel.Sort( self._media_sort_widget.GetSort() )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
|
|
def _SortBy( self, sort_type ):
|
|
|
|
for action in [ HC.CONTENT_UPDATE_PEND, HC.CONTENT_UPDATE_PETITION ]:
|
|
|
|
contents_and_checks = self._GetContentsAndChecks( action )
|
|
|
|
self._SetContentsAndChecks( action, contents_and_checks, sort_type )
|
|
|
|
|
|
|
|
def _StartFetchNumPetitions( self ):
|
|
|
|
def do_it( service, subject_account_key = None ):
|
|
|
|
def qt_draw( petition_count_rows ):
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
num_petitions_currently_listed = len( self._petitions_summary_list.GetData() )
|
|
|
|
old_petition_types_to_count = self._petition_types_to_count
|
|
|
|
self._petition_types_to_count = collections.Counter()
|
|
|
|
# we had a whole thing here that did 'if count dropped by more than 1, refresh summary' and 'if we only have 20% left of our desired count, refresh summary'
|
|
# but the count from the server and the count of what we see differs for mappings, where petitions are bunched, and it was just a pain
|
|
# maybe try again later, with better counting tech and more experience of what is actually wanted here
|
|
|
|
for ( content_type, status, count ) in petition_count_rows:
|
|
|
|
petition_type = ( content_type, status )
|
|
|
|
self._petition_types_to_count[ petition_type ] = count
|
|
|
|
|
|
self._DrawNumPetitions()
|
|
|
|
if num_petitions_currently_listed == 0:
|
|
|
|
self._FetchBestPetitionsSummary()
|
|
|
|
|
|
|
|
def qt_reset():
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
self._refresh_num_petitions_button.setText( 'refresh counts' )
|
|
|
|
|
|
try:
|
|
|
|
if subject_account_key is None:
|
|
|
|
response = service.Request( HC.GET, 'num_petitions' )
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
response = service.Request( HC.GET, 'num_petitions', { 'subject_account_key' : subject_account_key } )
|
|
|
|
except HydrusExceptions.NotFoundException:
|
|
|
|
HydrusData.ShowText( 'That account id was not found!' )
|
|
|
|
QP.CallAfter( qt_draw, [] )
|
|
|
|
return
|
|
|
|
|
|
|
|
num_petition_info = response[ 'num_petitions' ]
|
|
|
|
QP.CallAfter( qt_draw, num_petition_info )
|
|
|
|
finally:
|
|
|
|
QP.CallAfter( qt_reset )
|
|
|
|
|
|
|
|
self._refresh_num_petitions_button.setText( 'Fetching' + HC.UNICODE_ELLIPSIS )
|
|
|
|
subject_account_key = self._GetSubjectAccountKey()
|
|
|
|
self._last_fetched_subject_account_key = subject_account_key
|
|
|
|
self._controller.CallToThread( do_it, self._service, subject_account_key )
|
|
|
|
|
|
def _StartUploadingCompletedPetitions( self, petitions: typing.Collection[ HydrusNetwork.Petition ] ):
|
|
|
|
for petition in petitions:
|
|
|
|
self._outgoing_petition_headers_to_petitions[ petition.GetPetitionHeader() ] = petition
|
|
|
|
if petition == self._current_petition:
|
|
|
|
self._ClearCurrentPetition()
|
|
|
|
|
|
|
|
self._HighlightAPetitionIfNeeded()
|
|
|
|
CG.client_controller.CallToThread( self.THREADPetitionFetcherAndUploader, self._petition_fetcher_and_uploader_work_lock, self._service )
|
|
|
|
|
|
def _UpdateAccountKey( self ):
|
|
|
|
account_key_hex = self._petition_account_key.text()
|
|
|
|
if len( account_key_hex ) == 0:
|
|
|
|
valid = True
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
account_key_bytes = bytes.fromhex( account_key_hex )
|
|
|
|
if len( account_key_bytes ) != 32:
|
|
|
|
raise Exception()
|
|
|
|
|
|
valid = True
|
|
|
|
except Exception as e:
|
|
|
|
valid = False
|
|
|
|
|
|
|
|
if valid:
|
|
|
|
self._petition_account_key.setObjectName( 'HydrusValid' )
|
|
|
|
if self._GetSubjectAccountKey() != self._last_fetched_subject_account_key:
|
|
|
|
self._StartFetchNumPetitions()
|
|
|
|
|
|
else:
|
|
|
|
self._petition_account_key.setObjectName( 'HydrusInvalid' )
|
|
|
|
|
|
self._petition_account_key.style().polish( self._petition_account_key )
|
|
|
|
|
|
def _UpdateFetchButtonText( self, petition_type ):
|
|
|
|
if petition_type is not None:
|
|
|
|
( st, button ) = self._petition_types_to_controls[ petition_type ]
|
|
|
|
( content_type, status ) = petition_type
|
|
|
|
label = 'fetch {} {} petitions'.format( HC.content_status_string_lookup[ status ], HC.content_type_string_lookup[ content_type ] )
|
|
|
|
if petition_type == self._last_petition_type_fetched:
|
|
|
|
label = f'{label} (*)'
|
|
|
|
|
|
button.setText( label )
|
|
|
|
|
|
|
|
def ContentsAddDoubleClick( self, item ):
|
|
|
|
selected_indices = self._contents_add.GetSelectedIndices()
|
|
|
|
if len( selected_indices ) > 0:
|
|
|
|
selection = selected_indices[0]
|
|
|
|
content = self._contents_add.GetData( selection )
|
|
|
|
self.EventContentsDoubleClick( content )
|
|
|
|
|
|
|
|
def ContentsDeleteDoubleClick( self, item ):
|
|
|
|
selected_indices = self._contents_delete.GetSelectedIndices()
|
|
|
|
if len( selected_indices ) > 0:
|
|
|
|
selection = selected_indices[0]
|
|
|
|
content = self._contents_delete.GetData( selection )
|
|
|
|
self.EventContentsDoubleClick( content )
|
|
|
|
|
|
|
|
def EventContentsDoubleClick( self, content ):
|
|
|
|
if content.HasHashes():
|
|
|
|
hashes = content.GetHashes()
|
|
|
|
num_files_to_show = self._num_files_to_show.GetValue()
|
|
|
|
if num_files_to_show is not None and len( hashes ) > num_files_to_show:
|
|
|
|
hashes = random.sample( hashes, num_files_to_show )
|
|
|
|
|
|
self._ShowHashes( hashes )
|
|
|
|
|
|
|
|
def EventModifyPetitioner( self ):
|
|
|
|
subject_account_key = self._current_petition.GetPetitionerAccount().GetAccountKey()
|
|
|
|
subject_account_identifiers = [ HydrusNetwork.AccountIdentifier( account_key = subject_account_key ) ]
|
|
|
|
frame = ClientGUITopLevelWindowsPanels.FrameThatTakesScrollablePanel( self, 'manage accounts' )
|
|
|
|
panel = ClientGUIHydrusNetwork.ModifyAccountsPanel( frame, self._petition_service_key, subject_account_identifiers )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
def EventAddRowRightClick( self ):
|
|
|
|
selected_indices = self._contents_add.GetSelectedIndices()
|
|
|
|
selected_contents = []
|
|
|
|
for i in selected_indices:
|
|
|
|
content = self._contents_add.GetData( i )
|
|
|
|
selected_contents.append( content )
|
|
|
|
|
|
self.EventContentsRightClick( selected_contents )
|
|
|
|
|
|
def EventDeleteRowRightClick( self ):
|
|
|
|
selected_indices = self._contents_delete.GetSelectedIndices()
|
|
|
|
selected_contents = []
|
|
|
|
for i in selected_indices:
|
|
|
|
content = self._contents_delete.GetData( i )
|
|
|
|
selected_contents.append( content )
|
|
|
|
|
|
self.EventContentsRightClick( selected_contents )
|
|
|
|
|
|
def EventContentsRightClick( self, contents ):
|
|
|
|
copyable_items_a = []
|
|
copyable_items_b = []
|
|
|
|
for content in contents:
|
|
|
|
content_type = content.GetContentType()
|
|
|
|
if content_type == HC.CONTENT_TYPE_MAPPINGS:
|
|
|
|
( tag, hashes ) = content.GetContentData()
|
|
|
|
copyable_items_a.append( tag )
|
|
|
|
elif content_type in ( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_TYPE_TAG_PARENTS ):
|
|
|
|
( tag_a, tag_b ) = content.GetContentData()
|
|
|
|
copyable_items_a.append( tag_a )
|
|
copyable_items_b.append( tag_b )
|
|
|
|
|
|
|
|
copyable_items_a = HydrusData.DedupeList( copyable_items_a )
|
|
copyable_items_b = HydrusData.DedupeList( copyable_items_b )
|
|
|
|
if len( copyable_items_a ) + len( copyable_items_b ) > 0:
|
|
|
|
menu = ClientGUIMenus.GenerateMenu( self )
|
|
|
|
for copyable_items in [ copyable_items_a, copyable_items_b ]:
|
|
|
|
if len( copyable_items ) > 0:
|
|
|
|
if len( copyable_items ) == 1:
|
|
|
|
tag = copyable_items[0]
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'copy {}'.format( tag ), 'Copy this tag.', CG.client_controller.pub, 'clipboard', 'text', tag )
|
|
|
|
else:
|
|
|
|
text = os.linesep.join( copyable_items )
|
|
|
|
ClientGUIMenus.AppendMenuItem( menu, 'copy {} tags'.format( HydrusData.ToHumanInt( len( copyable_items ) ) ), 'Copy this tag.', CG.client_controller.pub, 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
|
|
CGC.core().PopupMenu( self, menu )
|
|
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
ManagementPanel.PageShown( self )
|
|
|
|
CG.client_controller.CallToThread( self.THREADPetitionFetcherAndUploader, self._petition_fetcher_and_uploader_work_lock, self._service )
|
|
|
|
|
|
def ProcessCurrentPetition( self ):
|
|
|
|
if self._current_petition is None:
|
|
|
|
return
|
|
|
|
|
|
jobs = [
|
|
( self._contents_add, HC.CONTENT_UPDATE_PEND ),
|
|
( self._contents_delete, HC.CONTENT_UPDATE_PETITION )
|
|
]
|
|
|
|
for ( contents_list, action ) in jobs:
|
|
|
|
for index in range( contents_list.count() ):
|
|
|
|
content = contents_list.GetData( index )
|
|
|
|
if contents_list.IsChecked( index ):
|
|
|
|
self._current_petition.Approve( action, content )
|
|
|
|
else:
|
|
|
|
self._current_petition.Deny( action, content )
|
|
|
|
|
|
|
|
|
|
self._StartUploadingCompletedPetitions( ( self._current_petition, ) )
|
|
|
|
|
|
def RefreshQuery( self ):
|
|
|
|
self._DrawCurrentPetition()
|
|
|
|
|
|
def Start( self ):
|
|
|
|
QP.CallAfter( self._StartFetchNumPetitions )
|
|
|
|
|
|
def THREADPetitionFetcherAndUploader( self, work_lock: threading.Lock, service: ClientServices.ServiceRepository ):
|
|
|
|
def qt_get_work():
|
|
|
|
fetch_petition_header = None
|
|
outgoing_petition = None
|
|
|
|
if len( self._petition_headers_we_are_fetching ) > 0:
|
|
|
|
if CG.client_controller.PageAliveAndNotClosed( self._page_key ):
|
|
|
|
fetch_petition_header = self._petition_headers_we_are_fetching[0]
|
|
|
|
elif CG.client_controller.PageDestroyed( self._page_key ):
|
|
|
|
self._petition_headers_we_are_fetching = []
|
|
|
|
|
|
|
|
if len( self._outgoing_petition_headers_to_petitions ) > 0:
|
|
|
|
item = list( self._outgoing_petition_headers_to_petitions.keys() )[0]
|
|
|
|
outgoing_petition = self._outgoing_petition_headers_to_petitions[ item ]
|
|
|
|
|
|
return ( fetch_petition_header, outgoing_petition )
|
|
|
|
|
|
def qt_petition_cleared( petition: HydrusNetwork.Petition ):
|
|
|
|
petition_header = petition.GetPetitionHeader()
|
|
|
|
if petition_header in self._outgoing_petition_headers_to_petitions:
|
|
|
|
del self._outgoing_petition_headers_to_petitions[ petition_header ]
|
|
|
|
|
|
if petition_header in self._failed_outgoing_petition_headers_to_petitions:
|
|
|
|
del self._failed_outgoing_petition_headers_to_petitions[ petition_header ]
|
|
|
|
|
|
if petition_header in self._petition_headers_to_fetched_petitions_cache:
|
|
|
|
del self._petition_headers_to_fetched_petitions_cache[ petition_header ]
|
|
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petitions_summary_list.DeleteDatas( ( petition_header, ) )
|
|
|
|
|
|
self._StartFetchNumPetitions()
|
|
|
|
|
|
def qt_petition_clear_failed( petition: HydrusNetwork.Petition ):
|
|
|
|
petition_header = petition.GetPetitionHeader()
|
|
|
|
if petition_header in self._outgoing_petition_headers_to_petitions:
|
|
|
|
del self._outgoing_petition_headers_to_petitions[ petition_header ]
|
|
|
|
|
|
self._failed_outgoing_petition_headers_to_petitions[ petition_header ] = petition
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petitions_summary_list.UpdateDatas( ( petition_header, ) )
|
|
|
|
|
|
|
|
def qt_petition_fetch_404( petition_header: HydrusNetwork.PetitionHeader ):
|
|
|
|
if petition_header in self._petition_headers_we_are_fetching:
|
|
|
|
self._petition_headers_we_are_fetching.remove( petition_header )
|
|
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petitions_summary_list.DeleteDatas( ( petition_header, ) )
|
|
|
|
|
|
|
|
def qt_petition_fetch_failed( petition_header: HydrusNetwork.PetitionHeader ):
|
|
|
|
if petition_header in self._petition_headers_we_are_fetching:
|
|
|
|
self._petition_headers_we_are_fetching.remove( petition_header )
|
|
|
|
|
|
self._petition_headers_we_failed_to_fetch.add( petition_header )
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petitions_summary_list.UpdateDatas( ( petition_header, ) )
|
|
|
|
|
|
|
|
def qt_petition_fetched( petition: HydrusNetwork.Petition ):
|
|
|
|
petition_header = petition.GetPetitionHeader()
|
|
|
|
if petition_header in self._petition_headers_we_are_fetching:
|
|
|
|
self._petition_headers_we_are_fetching.remove( petition_header )
|
|
|
|
|
|
self._petition_headers_we_failed_to_fetch.discard( petition_header )
|
|
|
|
if self._petitions_summary_list.HasData( petition_header ):
|
|
|
|
self._petition_headers_to_fetched_petitions_cache[ petition_header ] = petition
|
|
|
|
self._petitions_summary_list.UpdateDatas( ( petition_header, ) )
|
|
|
|
|
|
if self._current_petition is None:
|
|
|
|
self._HighlightAPetitionIfNeeded()
|
|
|
|
|
|
|
|
with work_lock:
|
|
|
|
while True:
|
|
|
|
fetch_petition_header: typing.Optional[ HydrusNetwork.PetitionHeader ] = None
|
|
outgoing_petition: typing.Optional[ HydrusNetwork.Petition ] = None
|
|
|
|
( fetch_petition_header, outgoing_petition ) = CG.client_controller.CallBlockingToQt( self, qt_get_work )
|
|
|
|
if fetch_petition_header is None and outgoing_petition is None:
|
|
|
|
break
|
|
|
|
|
|
if fetch_petition_header is not None:
|
|
|
|
try:
|
|
|
|
request_dict = {
|
|
'content_type' : fetch_petition_header.content_type,
|
|
'status' : fetch_petition_header.status,
|
|
'subject_account_key' : fetch_petition_header.account_key,
|
|
'reason' : fetch_petition_header.reason
|
|
}
|
|
|
|
response = service.Request( HC.GET, 'petition', request_dict )
|
|
|
|
petition = response[ 'petition' ]
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_petition_fetched, petition )
|
|
|
|
except HydrusExceptions.NotFoundException:
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_petition_fetch_404, fetch_petition_header )
|
|
|
|
except Exception as e:
|
|
|
|
HydrusData.ShowText( 'Failed to fetch a petition!' )
|
|
HydrusData.ShowException( e )
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_petition_fetch_failed, fetch_petition_header )
|
|
|
|
|
|
|
|
if outgoing_petition is not None:
|
|
|
|
try:
|
|
|
|
job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
job_status.SetStatusTitle( 'committing petition' )
|
|
|
|
time_started = HydrusTime.GetNowFloat()
|
|
|
|
try:
|
|
|
|
updates = outgoing_petition.GetCompletedUploadableClientToServerUpdates()
|
|
|
|
num_to_do = len( updates )
|
|
|
|
for ( num_done, update ) in enumerate( updates ):
|
|
|
|
if HydrusTime.TimeHasPassed( time_started + 3 ):
|
|
|
|
CG.client_controller.pub( 'message', job_status )
|
|
|
|
|
|
( i_paused, should_quit ) = job_status.WaitIfNeeded()
|
|
|
|
if should_quit:
|
|
|
|
return
|
|
|
|
|
|
service.Request( HC.POST, 'update', { 'client_to_server_update' : update } )
|
|
|
|
content_updates = ClientContentUpdates.ConvertClientToServerUpdateToContentUpdates( update )
|
|
|
|
if len( content_updates ) > 0:
|
|
|
|
CG.client_controller.WriteSynchronous( 'content_updates', ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( service.GetServiceKey(), content_updates ) )
|
|
|
|
|
|
job_status.SetStatusText( HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ) )
|
|
job_status.SetVariable( 'popup_gauge_1', ( num_done, num_to_do ) )
|
|
|
|
|
|
finally:
|
|
|
|
job_status.FinishAndDismiss()
|
|
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_petition_cleared, outgoing_petition )
|
|
|
|
except Exception as e:
|
|
|
|
HydrusData.ShowText( 'Failed to upload a petition!' )
|
|
HydrusData.ShowException( e )
|
|
|
|
CG.client_controller.CallBlockingToQt( self, qt_petition_clear_failed, outgoing_petition )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_PETITIONS ] = ManagementPanelPetitions
|
|
|
|
class ManagementPanelQuery( ManagementPanel ):
|
|
|
|
def __init__( self, parent, page, controller, management_controller: ClientGUIManagementController.ManagementController ):
|
|
|
|
ManagementPanel.__init__( self, parent, page, controller, management_controller )
|
|
|
|
file_search_context = self._management_controller.GetVariable( 'file_search_context' )
|
|
|
|
file_search_context.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
|
|
|
|
self._search_enabled = self._management_controller.GetVariable( 'search_enabled' )
|
|
|
|
self._query_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
self._query_job_status.Finish()
|
|
|
|
if self._search_enabled:
|
|
|
|
self._search_panel = ClientGUICommon.StaticBox( self, 'search' )
|
|
|
|
synchronised = self._management_controller.GetVariable( 'synchronised' )
|
|
|
|
self._tag_autocomplete = ClientGUIACDropdown.AutoCompleteDropdownTagsRead( self._search_panel, self._page_key, file_search_context, media_sort_widget = self._media_sort_widget, media_collect_widget = self._media_collect_widget, media_callable = self._page.GetMedia, synchronised = synchronised )
|
|
|
|
self._tag_autocomplete.searchCancelled.connect( self._CancelSearch )
|
|
|
|
self._search_panel.Add( self._tag_autocomplete, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
vbox = QP.VBoxLayout()
|
|
|
|
QP.AddToLayout( vbox, self._media_sort_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
QP.AddToLayout( vbox, self._media_collect_widget, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
if self._search_enabled:
|
|
|
|
QP.AddToLayout( vbox, self._search_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
self._MakeCurrentSelectionTagsBox( vbox )
|
|
|
|
self.widget().setLayout( vbox )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.searchChanged.connect( self.SearchChanged )
|
|
|
|
self._tag_autocomplete.locationChanged.connect( self.SetLocationContext )
|
|
|
|
|
|
|
|
def _CancelSearch( self ):
|
|
|
|
if self._search_enabled:
|
|
|
|
self._query_job_status.Cancel()
|
|
|
|
file_search_context = self._tag_autocomplete.GetFileSearchContext()
|
|
|
|
location_context = file_search_context.GetLocationContext()
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, [] )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'search cancelled!' )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._page_state = CC.PAGE_STATE_SEARCHING_CANCELLED
|
|
|
|
self._UpdateCancelButton()
|
|
|
|
|
|
|
|
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
|
|
|
|
return 'no search done yet'
|
|
|
|
|
|
def _MakeCurrentSelectionTagsBox( self, sizer ):
|
|
|
|
self._current_selection_tags_box = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'selection tags', CC.TAG_PRESENTATION_SEARCH_PAGE )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._current_selection_tags_list = ListBoxTagsMediaManagementPanel( self._current_selection_tags_box, self._management_controller, self._page_key, tag_autocomplete = self._tag_autocomplete )
|
|
|
|
else:
|
|
|
|
self._current_selection_tags_list = ListBoxTagsMediaManagementPanel( self._current_selection_tags_box, self._management_controller, self._page_key )
|
|
|
|
|
|
self._current_selection_tags_box.SetTagsBox( self._current_selection_tags_list )
|
|
|
|
if self._search_enabled:
|
|
|
|
file_search_context = self._management_controller.GetVariable( 'file_search_context' )
|
|
|
|
file_search_context.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
|
|
|
|
tag_service_key = file_search_context.GetTagContext().service_key
|
|
|
|
self._current_selection_tags_box.SetTagServiceKey( tag_service_key )
|
|
|
|
self._tag_autocomplete.tagServiceChanged.connect( self._current_selection_tags_box.SetTagServiceKey )
|
|
|
|
|
|
QP.AddToLayout( sizer, self._current_selection_tags_box, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
|
|
def _RefreshQuery( self ):
|
|
|
|
self._controller.ResetIdleTimer()
|
|
|
|
if self._search_enabled:
|
|
|
|
file_search_context = self._tag_autocomplete.GetFileSearchContext()
|
|
|
|
synchronised = self._tag_autocomplete.IsSynchronised()
|
|
|
|
# a query refresh now undoes paused sync
|
|
if not synchronised:
|
|
|
|
# this will trigger a refresh of search
|
|
self._tag_autocomplete.SetSynchronised( True )
|
|
|
|
return
|
|
|
|
|
|
interrupting_current_search = not self._query_job_status.IsDone()
|
|
|
|
self._query_job_status.Cancel()
|
|
|
|
if len( file_search_context.GetPredicates() ) > 0:
|
|
|
|
self._query_job_status = ClientThreading.JobStatus( cancellable = True )
|
|
|
|
sort_by = self._media_sort_widget.GetSort()
|
|
|
|
self._controller.CallToThread( self.THREADDoQuery, self._controller, self._page_key, self._query_job_status, file_search_context, sort_by )
|
|
|
|
panel = ClientGUIResults.MediaPanelLoading( self._page, self._page_key, self._management_controller )
|
|
|
|
self._page_state = CC.PAGE_STATE_SEARCHING
|
|
|
|
else:
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, [] )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no search' )
|
|
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
else:
|
|
|
|
self._media_sort_widget.BroadcastSort()
|
|
|
|
|
|
|
|
def _UpdateCancelButton( self ):
|
|
|
|
if self._search_enabled:
|
|
|
|
if self._query_job_status.IsDone():
|
|
|
|
self._tag_autocomplete.ShowCancelSearchButton( False )
|
|
|
|
else:
|
|
|
|
# don't show it immediately to save on flickeriness on short queries
|
|
|
|
WAIT_PERIOD = 3.0
|
|
|
|
search_is_lagging = HydrusTime.TimeHasPassedFloat( self._query_job_status.GetCreationTime() + WAIT_PERIOD )
|
|
|
|
self._tag_autocomplete.ShowCancelSearchButton( search_is_lagging )
|
|
|
|
|
|
|
|
|
|
def ConnectMediaPanelSignals( self, media_panel: ClientGUIResults.MediaPanel ):
|
|
|
|
ManagementPanel.ConnectMediaPanelSignals( self, media_panel )
|
|
|
|
media_panel.newMediaAdded.connect( self.PauseSearching )
|
|
|
|
|
|
def SetLocationContext( self, location_context: ClientLocation.LocationContext ):
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
|
|
def CleanBeforeClose( self ):
|
|
|
|
ManagementPanel.CleanBeforeClose( self )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.CancelCurrentResultsFetchJob()
|
|
|
|
|
|
self._query_job_status.Cancel()
|
|
|
|
|
|
def CleanBeforeDestroy( self ):
|
|
|
|
ManagementPanel.CleanBeforeDestroy( self )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.CancelCurrentResultsFetchJob()
|
|
|
|
|
|
self._query_job_status.Cancel()
|
|
|
|
|
|
def GetPredicates( self ):
|
|
|
|
if self._search_enabled:
|
|
|
|
return self._tag_autocomplete.GetPredicates()
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
def PageHidden( self ):
|
|
|
|
ManagementPanel.PageHidden( self )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.SetForceDropdownHide( True )
|
|
|
|
|
|
|
|
def PageShown( self ):
|
|
|
|
ManagementPanel.PageShown( self )
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.SetForceDropdownHide( False )
|
|
|
|
|
|
|
|
def PauseSearching( self ):
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.SetSynchronised( False )
|
|
|
|
|
|
|
|
def RefreshQuery( self ):
|
|
|
|
self._RefreshQuery()
|
|
|
|
|
|
def SearchChanged( self, file_search_context: ClientSearch.FileSearchContext ):
|
|
|
|
if self._search_enabled:
|
|
|
|
file_search_context = self._tag_autocomplete.GetFileSearchContext()
|
|
|
|
self._management_controller.SetVariable( 'file_search_context', file_search_context.Duplicate() )
|
|
|
|
location_context = file_search_context.GetLocationContext()
|
|
|
|
self._SetLocationContext( location_context )
|
|
|
|
synchronised = self._tag_autocomplete.IsSynchronised()
|
|
|
|
self._management_controller.SetVariable( 'synchronised', synchronised )
|
|
|
|
self._management_controller.SetDirty()
|
|
|
|
if synchronised:
|
|
|
|
self._RefreshQuery()
|
|
|
|
else:
|
|
|
|
interrupting_current_search = not self._query_job_status.IsDone()
|
|
|
|
if interrupting_current_search:
|
|
|
|
self._CancelSearch()
|
|
|
|
|
|
|
|
|
|
|
|
def SetSearchFocus( self ):
|
|
|
|
if self._search_enabled:
|
|
|
|
ClientGUIFunctions.SetFocusLater( self._tag_autocomplete )
|
|
|
|
|
|
|
|
def ShowFinishedQuery( self, query_job_status, media_results ):
|
|
|
|
if query_job_status == self._query_job_status:
|
|
|
|
location_context = self._management_controller.GetLocationContext()
|
|
|
|
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, self._management_controller, media_results )
|
|
|
|
panel.SetEmptyPageStatusOverride( 'no files found for this search' )
|
|
|
|
panel.Collect( self._media_collect_widget.GetValue() )
|
|
|
|
panel.Sort( self._media_sort_widget.GetSort() )
|
|
|
|
self._page.SwapMediaPanel( panel )
|
|
|
|
self._page_state = CC.PAGE_STATE_NORMAL
|
|
|
|
|
|
|
|
def Start( self ):
|
|
|
|
file_search_context = self._management_controller.GetVariable( 'file_search_context' )
|
|
|
|
file_search_context.FixMissingServices( CG.client_controller.services_manager.FilterValidServiceKeys )
|
|
|
|
initial_predicates = file_search_context.GetPredicates()
|
|
|
|
if len( initial_predicates ) > 0 and not file_search_context.IsComplete():
|
|
|
|
QP.CallAfter( self.RefreshQuery )
|
|
|
|
|
|
|
|
def THREADDoQuery( self, controller, page_key, query_job_status, file_search_context: ClientSearch.FileSearchContext, sort_by ):
|
|
|
|
def qt_code():
|
|
|
|
query_job_status.Finish()
|
|
|
|
if not self or not QP.isValid( self ):
|
|
|
|
return
|
|
|
|
|
|
self.ShowFinishedQuery( query_job_status, media_results )
|
|
|
|
|
|
QUERY_CHUNK_SIZE = 256
|
|
|
|
CG.client_controller.file_viewing_stats_manager.Flush()
|
|
|
|
query_hash_ids = controller.Read( 'file_query_ids', file_search_context, job_status = query_job_status, limit_sort_by = sort_by )
|
|
|
|
if query_job_status.IsCancelled():
|
|
|
|
return
|
|
|
|
|
|
media_results = []
|
|
|
|
for sub_query_hash_ids in HydrusLists.SplitListIntoChunks( query_hash_ids, QUERY_CHUNK_SIZE ):
|
|
|
|
if query_job_status.IsCancelled():
|
|
|
|
return
|
|
|
|
|
|
more_media_results = controller.Read( 'media_results_from_ids', sub_query_hash_ids )
|
|
|
|
media_results.extend( more_media_results )
|
|
|
|
controller.pub( 'set_num_query_results', page_key, len( media_results ), len( query_hash_ids ) )
|
|
|
|
controller.WaitUntilViewFree()
|
|
|
|
|
|
file_search_context.SetComplete()
|
|
|
|
self._management_controller.SetVariable( 'file_search_context', file_search_context.Duplicate() )
|
|
|
|
self._management_controller.SetDirty()
|
|
|
|
QP.CallAfter( qt_code )
|
|
|
|
|
|
def REPEATINGPageUpdate( self ):
|
|
|
|
self._UpdateCancelButton()
|
|
|
|
if self._search_enabled:
|
|
|
|
self._tag_autocomplete.REPEATINGPageUpdate()
|
|
|
|
|
|
|
|
management_panel_types_to_classes[ ClientGUIManagementController.MANAGEMENT_TYPE_QUERY ] = ManagementPanelQuery
|