659 lines
21 KiB
Python
659 lines
21 KiB
Python
import ClientConstants as CC
|
|
import ClientGUICommon
|
|
import ClientGUIDialogs
|
|
import ClientGUIListCtrl
|
|
import ClientGUIMenus
|
|
import ClientGUISerialisable
|
|
import ClientGUIScrolledPanels
|
|
import ClientGUITopLevelWindows
|
|
import ClientSerialisable
|
|
import ClientThreading
|
|
import HydrusConstants as HC
|
|
import HydrusData
|
|
import HydrusGlobals as HG
|
|
import HydrusPaths
|
|
import HydrusText
|
|
import os
|
|
import webbrowser
|
|
import wx
|
|
|
|
class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
|
|
|
|
def __init__( self, parent, controller, seed_cache ):
|
|
|
|
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
|
|
|
self._controller = controller
|
|
self._seed_cache = seed_cache
|
|
|
|
self._text = ClientGUICommon.BetterStaticText( self, 'initialising' )
|
|
|
|
# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on seed add/remove
|
|
|
|
columns = [ ( '#', 3 ), ( 'source', -1 ), ( 'status', 12 ), ( 'added', 23 ), ( 'last modified', 23 ), ( 'source time', 23 ), ( 'note', 20 ) ]
|
|
|
|
self._list_ctrl = ClientGUIListCtrl.BetterListCtrl( self, 'seed_cache', 30, 30, columns, self._ConvertSeedToListCtrlTuples )
|
|
|
|
#
|
|
|
|
self._AddSeeds( self._seed_cache.GetSeeds() )
|
|
|
|
self._list_ctrl.Sort( 0 )
|
|
|
|
#
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.Add( self._text, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.Add( self._list_ctrl, CC.FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
self._list_ctrl.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )
|
|
|
|
self._controller.sub( self, 'NotifySeedsUpdated', 'seed_cache_seeds_updated' )
|
|
|
|
wx.CallAfter( self._UpdateText )
|
|
|
|
|
|
def _AddSeeds( self, seeds ):
|
|
|
|
self._list_ctrl.AddDatas( seeds )
|
|
|
|
|
|
def _ConvertSeedToListCtrlTuples( self, seed ):
|
|
|
|
seed_index = self._seed_cache.GetSeedIndex( seed )
|
|
|
|
seed_data = seed.seed_data
|
|
status = seed.status
|
|
added = seed.created
|
|
modified = seed.modified
|
|
source_time = seed.source_time
|
|
note = seed.note
|
|
|
|
pretty_seed_index = HydrusData.ConvertIntToPrettyString( seed_index )
|
|
pretty_seed_data = HydrusData.ToUnicode( seed_data )
|
|
pretty_status = CC.status_string_lookup[ status ]
|
|
pretty_added = HydrusData.ConvertTimestampToPrettyAgo( added ) + ' ago'
|
|
pretty_modified = HydrusData.ConvertTimestampToPrettyAgo( modified ) + ' ago'
|
|
|
|
if source_time is None:
|
|
|
|
pretty_source_time = 'unknown'
|
|
|
|
else:
|
|
|
|
pretty_source_time = HydrusData.ConvertTimestampToHumanPrettyTime( source_time )
|
|
|
|
|
|
pretty_note = note.split( os.linesep )[0]
|
|
|
|
display_tuple = ( pretty_seed_index, pretty_seed_data, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note )
|
|
sort_tuple = ( seed_index, seed_data, status, added, modified, source_time, note )
|
|
|
|
return ( display_tuple, sort_tuple )
|
|
|
|
|
|
def _CopySelectedNotes( self ):
|
|
|
|
notes = []
|
|
|
|
for seed in self._list_ctrl.GetData( only_selected = True ):
|
|
|
|
note = seed.note
|
|
|
|
if note != '':
|
|
|
|
notes.append( note )
|
|
|
|
|
|
|
|
if len( notes ) > 0:
|
|
|
|
separator = os.linesep * 2
|
|
|
|
text = separator.join( notes )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _CopySelectedSeedData( self ):
|
|
|
|
seeds = self._list_ctrl.GetData( only_selected = True )
|
|
|
|
if len( seeds ) > 0:
|
|
|
|
separator = os.linesep * 2
|
|
|
|
text = separator.join( ( seed.seed_data for seed in seeds ) )
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', text )
|
|
|
|
|
|
|
|
def _DeleteSelected( self ):
|
|
|
|
seeds_to_delete = self._list_ctrl.GetData( only_selected = True )
|
|
|
|
if len( seeds_to_delete ) > 0:
|
|
|
|
message = 'Are you sure you want to delete all the selected entries?'
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_YES:
|
|
|
|
self._seed_cache.RemoveSeeds( seeds_to_delete )
|
|
|
|
|
|
|
|
|
|
|
|
def _OpenSelectedSeedData( self ):
|
|
|
|
seeds = self._list_ctrl.GetData( only_selected = True )
|
|
|
|
if len( seeds ) > 0:
|
|
|
|
if len( seeds ) > 10:
|
|
|
|
message = 'You have many objects selected--are you sure you want to open them all?'
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
|
|
|
|
if dlg.ShowModal() != wx.ID_YES:
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if seeds[0].seed_data.startswith( 'http' ):
|
|
|
|
for seed in seeds:
|
|
|
|
webbrowser.open( seed.seed_data )
|
|
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
for seed in seeds:
|
|
|
|
HydrusPaths.OpenFileLocation( seed.seed_data )
|
|
|
|
|
|
except Exception as e:
|
|
|
|
wx.MessageBox( unicode( e ) )
|
|
|
|
|
|
|
|
|
|
|
|
def _SetSelected( self, status_to_set ):
|
|
|
|
seeds = self._list_ctrl.GetData( only_selected = True )
|
|
|
|
for seed in seeds:
|
|
|
|
seed.SetStatus( status_to_set )
|
|
|
|
|
|
self._seed_cache.NotifySeedsUpdated( seeds )
|
|
|
|
|
|
def _ShowMenuIfNeeded( self ):
|
|
|
|
if self._list_ctrl.HasSelected() > 0:
|
|
|
|
menu = wx.Menu()
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'copy sources', 'Copy all the selected sources to clipboard.', self._CopySelectedSeedData )
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'open sources', 'Open all the selected sources in your file explorer or web browser.', self._OpenSelectedSeedData )
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'try again', 'Reset the progress of all the selected imports.', HydrusData.Call( self._SetSelected, CC.STATUS_UNKNOWN ) )
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'skip', 'Skip all the selected imports.', HydrusData.Call( self._SetSelected, CC.STATUS_SKIPPED ) )
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'delete from list', 'Remove all the selected imports.', self._DeleteSelected )
|
|
|
|
HG.client_controller.PopupMenu( self, menu )
|
|
|
|
|
|
|
|
def _UpdateListCtrl( self, seeds ):
|
|
|
|
seeds_to_add = []
|
|
seeds_to_update = []
|
|
seeds_to_delete = []
|
|
|
|
for seed in seeds:
|
|
|
|
if self._seed_cache.HasSeed( seed ):
|
|
|
|
if self._list_ctrl.HasData( seed ):
|
|
|
|
seeds_to_update.append( seed )
|
|
|
|
else:
|
|
|
|
seeds_to_add.append( seed )
|
|
|
|
|
|
else:
|
|
|
|
if self._list_ctrl.HasData( seed ):
|
|
|
|
seeds_to_delete.append( seed )
|
|
|
|
|
|
|
|
|
|
self._list_ctrl.DeleteDatas( seeds_to_delete )
|
|
|
|
self._list_ctrl.UpdateDatas( seeds_to_update )
|
|
|
|
self._AddSeeds( seeds_to_add )
|
|
|
|
|
|
def _UpdateText( self ):
|
|
|
|
( status, ( total_processed, total ) ) = self._seed_cache.GetStatus()
|
|
|
|
self._text.SetLabelText( status )
|
|
|
|
self.Layout()
|
|
|
|
|
|
def EventShowMenu( self, event ):
|
|
|
|
wx.CallAfter( self._ShowMenuIfNeeded )
|
|
|
|
event.Skip() # let the right click event go through before doing menu, in case selection should happen
|
|
|
|
|
|
def GetValue( self ):
|
|
|
|
return self._seed_cache
|
|
|
|
|
|
def NotifySeedsUpdated( self, seed_cache_key, seeds ):
|
|
|
|
if seed_cache_key == self._seed_cache.GetSeedCacheKey():
|
|
|
|
self._UpdateText()
|
|
self._UpdateListCtrl( seeds )
|
|
|
|
|
|
|
|
class SeedCacheButton( ClientGUICommon.BetterBitmapButton ):
|
|
|
|
def __init__( self, parent, controller, seed_cache_get_callable, seed_cache_set_callable = None ):
|
|
|
|
ClientGUICommon.BetterBitmapButton.__init__( self, parent, CC.GlobalBMPs.seed_cache, self._ShowSeedCacheFrame )
|
|
|
|
self._controller = controller
|
|
self._seed_cache_get_callable = seed_cache_get_callable
|
|
self._seed_cache_set_callable = seed_cache_set_callable
|
|
|
|
self.SetToolTip( 'open detailed file import status--right-click for quick actions, if applicable' )
|
|
|
|
self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )
|
|
|
|
|
|
def _ClearProcessed( self ):
|
|
|
|
message = 'Are you sure you want to delete all the processed (i.e. anything with a non-blank status in the larger window) file imports? This is useful for cleaning up and de-laggifying a very large list, but not much else.'
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_YES:
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
seed_cache.RemoveProcessedSeeds()
|
|
|
|
|
|
|
|
|
|
def _ClearSuccessful( self ):
|
|
|
|
message = 'Are you sure you want to delete all the successful/already in db file imports? This is useful for cleaning up and de-laggifying a very large list and leaving only failed and otherwise skipped entries.'
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_YES:
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
seed_cache.RemoveSuccessfulSeeds()
|
|
|
|
|
|
|
|
|
|
def _GetExportableSourcesString( self ):
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
seeds = seed_cache.GetSeeds()
|
|
|
|
sources = [ seed.seed_data for seed in seeds ]
|
|
|
|
return os.linesep.join( sources )
|
|
|
|
|
|
def _GetSourcesFromSourcesString( self, sources_string ):
|
|
|
|
sources_string = HydrusData.ToUnicode( sources_string )
|
|
|
|
sources = HydrusText.DeserialiseNewlinedTexts( sources_string )
|
|
|
|
return sources
|
|
|
|
|
|
def _ImportFromClipboard( self ):
|
|
|
|
raw_text = HG.client_controller.GetClipboardText()
|
|
|
|
sources = self._GetSourcesFromSourcesString( raw_text )
|
|
|
|
try:
|
|
|
|
self._ImportSources( sources )
|
|
|
|
except:
|
|
|
|
wx.MessageBox( 'Could not import!' )
|
|
|
|
raise
|
|
|
|
|
|
|
|
def _ImportFromPng( self ):
|
|
|
|
with wx.FileDialog( self, 'select the png with the sources', wildcard = 'PNG (*.png)|*.png' ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
|
|
path = HydrusData.ToUnicode( dlg.GetPath() )
|
|
|
|
payload = ClientSerialisable.LoadFromPng( path )
|
|
|
|
try:
|
|
|
|
sources = self._GetSourcesFromSourcesString( payload )
|
|
|
|
self._ImportSources( sources )
|
|
|
|
except:
|
|
|
|
wx.MessageBox( 'Could not import!' )
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def _ImportSources( self, sources ):
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
if sources[0].startswith( 'http' ):
|
|
|
|
seed_cache.AddURLs( sources )
|
|
|
|
else:
|
|
|
|
seed_cache.AddPaths( sources )
|
|
|
|
|
|
|
|
def _ExportToPng( self ):
|
|
|
|
payload = self._GetExportableSourcesString()
|
|
|
|
with ClientGUITopLevelWindows.DialogNullipotent( self, 'export to png' ) as dlg:
|
|
|
|
panel = ClientGUISerialisable.PngExportPanel( dlg, payload )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.ShowModal()
|
|
|
|
|
|
|
|
def _ExportToClipboard( self ):
|
|
|
|
payload = self._GetExportableSourcesString()
|
|
|
|
HG.client_controller.pub( 'clipboard', 'text', payload )
|
|
|
|
|
|
def _RetryFailures( self ):
|
|
|
|
message = 'Are you sure you want to retry all the failed files?'
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_YES:
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
seed_cache.RetryFailures()
|
|
|
|
|
|
|
|
|
|
def _ShowSeedCacheFrame( self ):
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
tlp = ClientGUICommon.GetTLP( self )
|
|
|
|
if isinstance( tlp, wx.Dialog ):
|
|
|
|
if self._seed_cache_set_callable is None: # throw up a dialog that edits the seed cache in place
|
|
|
|
with ClientGUITopLevelWindows.DialogNullipotent( self, 'file import status' ) as dlg:
|
|
|
|
panel = EditSeedCachePanel( dlg, self._controller, seed_cache )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
dlg.ShowModal()
|
|
|
|
|
|
else: # throw up a dialog that edits the seed cache but can be cancelled
|
|
|
|
dupe_seed_cache = seed_cache.Duplicate()
|
|
|
|
with ClientGUITopLevelWindows.DialogEdit( self, 'file import status' ) as dlg:
|
|
|
|
panel = EditSeedCachePanel( dlg, self._controller, dupe_seed_cache )
|
|
|
|
dlg.SetPanel( panel )
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
|
|
self._seed_cache_set_callable( dupe_seed_cache )
|
|
|
|
|
|
|
|
|
|
else: # throw up a frame that edits the seed cache in place
|
|
|
|
title = 'file import status'
|
|
frame_key = 'file_import_status'
|
|
|
|
frame = ClientGUITopLevelWindows.FrameThatTakesScrollablePanel( self, title, frame_key )
|
|
|
|
panel = EditSeedCachePanel( frame, self._controller, seed_cache )
|
|
|
|
frame.SetPanel( panel )
|
|
|
|
|
|
|
|
def EventShowMenu( self, event ):
|
|
|
|
menu = wx.Menu()
|
|
|
|
seed_cache = self._seed_cache_get_callable()
|
|
|
|
num_failures = seed_cache.GetSeedCount( CC.STATUS_FAILED )
|
|
|
|
if num_failures > 0:
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'retry ' + HydrusData.ConvertIntToPrettyString( num_failures ) + ' failures', 'Tell this cache to reattempt all its failures.', self._RetryFailures )
|
|
|
|
|
|
num_unknown = seed_cache.GetSeedCount( CC.STATUS_UNKNOWN )
|
|
|
|
num_successful = seed_cache.GetSeedCount( CC.STATUS_SUCCESSFUL ) + seed_cache.GetSeedCount( CC.STATUS_REDUNDANT )
|
|
|
|
if num_successful > 0:
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'delete ' + HydrusData.ConvertIntToPrettyString( num_successful ) + ' \'successful\' file imports from the queue', 'Tell this cache to clear out successful/already in db files, reducing the size of the queue.', self._ClearSuccessful )
|
|
|
|
|
|
num_processed = len( seed_cache ) - num_unknown
|
|
|
|
if num_processed > 0 and num_processed != num_successful:
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, menu, 'delete ' + HydrusData.ConvertIntToPrettyString( num_processed ) + ' \'processed\' file imports from the queue', 'Tell this cache to clear out processed files, reducing the size of the queue.', self._ClearProcessed )
|
|
|
|
|
|
ClientGUIMenus.AppendSeparator( menu )
|
|
|
|
if len( seed_cache ) > 0:
|
|
|
|
submenu = wx.Menu()
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, submenu, 'to clipboard', 'Copy all the sources in this list to the clipboard.', self._ExportToClipboard )
|
|
ClientGUIMenus.AppendMenuItem( self, submenu, 'to png', 'Export all the sources in this list to a png file.', self._ExportToPng )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'export all sources' )
|
|
|
|
|
|
submenu = wx.Menu()
|
|
|
|
ClientGUIMenus.AppendMenuItem( self, submenu, 'from clipboard', 'Import new urls or paths to this list from the clipboard.', self._ImportFromClipboard )
|
|
ClientGUIMenus.AppendMenuItem( self, submenu, 'from png', 'Import new urls or paths to this list from a png file.', self._ImportFromPng )
|
|
|
|
ClientGUIMenus.AppendMenu( menu, submenu, 'import new sources' )
|
|
|
|
HG.client_controller.PopupMenu( self, menu )
|
|
|
|
|
|
class SeedCacheStatusControl( wx.Panel ):
|
|
|
|
def __init__( self, parent, controller ):
|
|
|
|
wx.Panel.__init__( self, parent, style = wx.BORDER_DOUBLE )
|
|
|
|
self._controller = controller
|
|
|
|
self._seed_cache = None
|
|
|
|
self._import_summary_st = ClientGUICommon.BetterStaticText( self )
|
|
self._progress_st = ClientGUICommon.BetterStaticText( self )
|
|
|
|
self._seed_cache_button = SeedCacheButton( self, self._controller, self._GetSeedCache )
|
|
|
|
self._progress_gauge = ClientGUICommon.Gauge( self )
|
|
|
|
#
|
|
|
|
self._Update()
|
|
|
|
#
|
|
|
|
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
hbox.Add( self._progress_st, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
|
hbox.Add( self._seed_cache_button, CC.FLAGS_VCENTER )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.Add( self._import_summary_st, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
vbox.Add( self._progress_gauge, CC.FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
#
|
|
|
|
HG.client_controller.gui.RegisterUIUpdateWindow( self )
|
|
|
|
|
|
def _GetSeedCache( self ):
|
|
|
|
return self._seed_cache
|
|
|
|
|
|
def _Update( self ):
|
|
|
|
if self._seed_cache is None:
|
|
|
|
self._import_summary_st.SetLabelText( '' )
|
|
self._progress_st.SetLabelText( '' )
|
|
self._progress_gauge.SetRange( 1 )
|
|
self._progress_gauge.SetValue( 0 )
|
|
|
|
if self._seed_cache_button.IsEnabled():
|
|
|
|
self._seed_cache_button.Disable()
|
|
|
|
|
|
else:
|
|
|
|
( import_summary, ( num_done, num_to_do ) ) = self._seed_cache.GetStatus()
|
|
|
|
self._import_summary_st.SetLabelText( import_summary )
|
|
|
|
if num_to_do == 0:
|
|
|
|
self._progress_st.SetLabelText( '' )
|
|
|
|
else:
|
|
|
|
self._progress_st.SetLabelText( HydrusData.ConvertValueRangeToPrettyString( num_done, num_to_do ) )
|
|
|
|
|
|
self._progress_gauge.SetRange( num_to_do )
|
|
self._progress_gauge.SetValue( num_done )
|
|
|
|
if not self._seed_cache_button.IsEnabled():
|
|
|
|
self._seed_cache_button.Enable()
|
|
|
|
|
|
|
|
|
|
def SetSeedCache( self, seed_cache ):
|
|
|
|
if not self:
|
|
|
|
return
|
|
|
|
|
|
self._seed_cache = seed_cache
|
|
|
|
|
|
def TIMERUIUpdate( self ):
|
|
|
|
if self._controller.gui.IShouldRegularlyUpdate( self ):
|
|
|
|
self._Update()
|
|
|
|
|
|
|