Version 275
This commit is contained in:
parent
227d743f96
commit
781005f1d4
|
@ -8,6 +8,40 @@
|
|||
<div class="content">
|
||||
<h3>changelog</h3>
|
||||
<ul>
|
||||
<ul>
|
||||
<li><h3>version 275</h3></li>
|
||||
<li>if you hold shift down while dropping a page tab, the client will not 'chase' that page to show it (try it out!)</li>
|
||||
<li>the gui will be more snappy about dealing with drag-and-drop drops (of types file import, page tab rearrange, and url drop), returning the mouse to normal state instantly on drop and dealing with the event in a subsequent action</li>
|
||||
<li>dropping url text on the client will specifically inform the source program not to delete the dragged text (that the operation was copy, not move), if it is interested in hearing that</li>
|
||||
<li>page drag-and-drops should transition a little less flickerily</li>
|
||||
<li>all file import status objects can now track 'source time', typically to represent upload time</li>
|
||||
<li>file imports now populate 'source time' based on the earliest of creation/modified time! (this will be used later to parse as a tag)</li>
|
||||
<li>thread watchers now populate 'source time' based on post time!</li>
|
||||
<li>finished a watcher options object for the new thread checker system</li>
|
||||
<li>wrote a panel to edit watcher options</li>
|
||||
<li>converted the thread object to the new watcher system</li>
|
||||
<li>thread watchers now have two pause buttons--one for the file queue, one for the checker</li>
|
||||
<li>compressed thread watcher ui layout</li>
|
||||
<li>converted the thread left-panel ui and options->downloading page to reflect the new watcher system</li>
|
||||
<li>improved the watcher options to generate better timings for fresh threads</li>
|
||||
<li>cleaned up some thread watcher check time code</li>
|
||||
<li>the total/selected mime summary on the status bar is a little prettier and will now report by individual mime sometimes</li>
|
||||
<li>generating the total/select mime summaries are now faster on pages with >1,000 files (it'll just use 'file')</li>
|
||||
<li>a 'refresh' action on an import page now triggers a sort event</li>
|
||||
<li>added 'flip_darkmode' shortcut command to 'main_gui' and 'media_viewer' shortcut sets</li>
|
||||
<li>added copy_file/path/hash/bmp actions to the 'media' shortcut set, removed the hardcoded ctrl+c for copy_file. I added ctrl+c to the defaults, but existing users will have to re-add it manually if they want it!</li>
|
||||
<li>simplified some page ui update code</li>
|
||||
<li>import pages will no longer update their left-panel ui (which uses a bit of cpu) when they are not in view</li>
|
||||
<li>polished some new string and url matching code the domain engine will be using</li>
|
||||
<li>wrote a panel to edit string match objects</li>
|
||||
<li>wrote a new panel to handle simple ordered lists of data in a better way</li>
|
||||
<li>wrote most of a panel to edit url match objects</li>
|
||||
<li>misc domain manager work</li>
|
||||
<li>fixed an issue with the old listctrl where object name de-duplication was sometimes not permitting (1)-type names to be cleaned up</li>
|
||||
<li>fixed some inelegant time duration->text conversions</li>
|
||||
<li>improved complete process shutdown reliability when some downloads are waiting on bandwidth on shutdown</li>
|
||||
<li>did some misc listctrl update work</li>
|
||||
</ul>
|
||||
<li><h3>version 274</h3></li>
|
||||
<ul>
|
||||
<li>the help menu now has an easy on/off check entry for the darkmode colourset</li>
|
||||
|
|
|
@ -333,10 +333,10 @@ SHORTCUTS_RESERVED_NAMES = [ 'archive_delete_filter', 'duplicate_filter', 'media
|
|||
|
||||
# shortcut commands
|
||||
|
||||
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'archive_file', 'inbox_file', 'delete_file', 'remove_file_from_view', 'open_file_in_external_program', 'launch_the_archive_delete_filter' ]
|
||||
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom' ]
|
||||
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'archive_file', 'inbox_file', 'delete_file', 'remove_file_from_view', 'open_file_in_external_program', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_file', 'copy_path', 'copy_sha256_hash' ]
|
||||
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom', 'flip_darkmode' ]
|
||||
SHORTCUTS_MEDIA_VIEWER_BROWSER_ACTIONS = [ 'view_next', 'view_first', 'view_last', 'view_previous' ]
|
||||
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'new_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo' ]
|
||||
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'new_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo', 'flip_darkmode' ]
|
||||
SHORTCUTS_DUPLICATE_FILTER_ACTIONS = [ 'duplicate_filter_this_is_better', 'duplicate_filter_exactly_the_same', 'duplicate_filter_alternates', 'duplicate_filter_not_dupes', 'duplicate_filter_custom_action', 'duplicate_filter_skip', 'duplicate_filter_back' ]
|
||||
SHORTCUTS_ARCHIVE_DELETE_FILTER_ACTIONS = [ 'archive_delete_filter_keep', 'archive_delete_filter_delete', 'archive_delete_filter_skip', 'archive_delete_filter_back' ]
|
||||
|
||||
|
|
|
@ -1038,6 +1038,12 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
#
|
||||
|
||||
self._dictionary[ 'misc' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = WatcherOptions( intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
|
||||
|
||||
#
|
||||
|
||||
self._dictionary[ 'suggested_tags' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
self._dictionary[ 'suggested_tags' ][ 'favourites' ] = {}
|
||||
|
@ -1332,6 +1338,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def GetDefaultThreadWatcherOptions( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
return self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ]
|
||||
|
||||
|
||||
|
||||
def GetDefaultSort( self ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -1585,6 +1599,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def SetDefaultThreadWatcherOptions( self, watcher_options ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._dictionary[ 'misc' ][ 'default_thread_watcher_options' ] = watcher_options
|
||||
|
||||
|
||||
|
||||
def SetDefaultSort( self, media_sort ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -2690,3 +2712,139 @@ class TagCensor( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_TAG_CENSOR ] = TagCensor
|
||||
|
||||
class WatcherOptions( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_WATCHER_OPTIONS
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) ):
|
||||
|
||||
HydrusSerialisable.SerialisableBase.__init__( self )
|
||||
|
||||
self._intended_files_per_check = intended_files_per_check
|
||||
self._never_faster_than = never_faster_than
|
||||
self._never_slower_than = never_slower_than
|
||||
self._death_file_velocity = death_file_velocity
|
||||
|
||||
|
||||
def _GetCurrentFilesVelocity( self, seed_cache, last_check_time ):
|
||||
|
||||
( death_files_found, death_time_delta ) = self._death_file_velocity
|
||||
|
||||
since = last_check_time - death_time_delta
|
||||
|
||||
current_files_found = seed_cache.GetNumNewFilesSince( since )
|
||||
|
||||
if len( seed_cache ) == 0:
|
||||
|
||||
current_time_delta = death_time_delta
|
||||
|
||||
else:
|
||||
|
||||
# when a thread is only 30mins old (i.e. first file was posted 30 mins ago), we don't want to calculate based on a longer delete time delta
|
||||
# we want next check to be like 30mins from now, not 12 hours
|
||||
# so we'll say "5 files in 30 mins" rather than "5 files in 24 hours"
|
||||
|
||||
earliest_file_time = seed_cache.GetEarliestTimestamp()
|
||||
|
||||
early_time_delta = max( last_check_time - earliest_file_time, 30 )
|
||||
|
||||
current_time_delta = min( early_time_delta, death_time_delta )
|
||||
|
||||
|
||||
return ( current_files_found, current_time_delta )
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return ( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity ) = serialisable_info
|
||||
|
||||
|
||||
def GetNextCheckTime( self, seed_cache, last_check_time ):
|
||||
|
||||
if len( seed_cache ) == 0:
|
||||
|
||||
if last_check_time == 0:
|
||||
|
||||
return 0 # haven't checked yet, so should check immediately
|
||||
|
||||
else:
|
||||
|
||||
return HydrusData.GetNow() + self._never_slower_than
|
||||
|
||||
|
||||
else:
|
||||
|
||||
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
|
||||
|
||||
if current_files_found == 0:
|
||||
|
||||
check_period = self._never_slower_than
|
||||
|
||||
else:
|
||||
|
||||
approx_time_per_file = current_time_delta / current_files_found
|
||||
|
||||
ideal_check_period = self._intended_files_per_check * approx_time_per_file
|
||||
|
||||
check_period = min( max( self._never_faster_than, ideal_check_period ), self._never_slower_than )
|
||||
|
||||
|
||||
return last_check_time + check_period
|
||||
|
||||
|
||||
|
||||
def GetPrettyCurrentVelocity( self, seed_cache, last_check_time ):
|
||||
|
||||
if len( seed_cache ) == 0:
|
||||
|
||||
if last_check_time == 0:
|
||||
|
||||
pretty_current_velocity = 'no files yet'
|
||||
|
||||
else:
|
||||
|
||||
pretty_current_velocity = 'no files, unable to determine velocity'
|
||||
|
||||
|
||||
else:
|
||||
|
||||
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
|
||||
|
||||
pretty_current_velocity = 'at last check, found ' + HydrusData.ConvertIntToPrettyString( current_files_found ) + ' files in previous ' + HydrusData.ConvertTimeDeltaToPrettyString( current_time_delta )
|
||||
|
||||
|
||||
return pretty_current_velocity
|
||||
|
||||
|
||||
def IsDead( self, seed_cache, last_check_time ):
|
||||
|
||||
if len( seed_cache ) == 0 and last_check_time == 0:
|
||||
|
||||
return False
|
||||
|
||||
else:
|
||||
|
||||
( current_files_found, current_time_delta ) = self._GetCurrentFilesVelocity( seed_cache, last_check_time )
|
||||
|
||||
( death_files_found, deleted_time_delta ) = self._death_file_velocity
|
||||
|
||||
current_file_velocity_float = current_files_found / float( current_time_delta )
|
||||
death_file_velocity_float = death_files_found / float( deleted_time_delta )
|
||||
|
||||
return current_file_velocity_float < death_file_velocity_float
|
||||
|
||||
|
||||
|
||||
def ToTuple( self ):
|
||||
|
||||
return ( self._intended_files_per_check, self._never_faster_than, self._never_slower_than, self._death_file_velocity )
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_WATCHER_OPTIONS ] = WatcherOptions
|
||||
|
|
|
@ -628,6 +628,8 @@ def GetDefaultShortcuts():
|
|||
|
||||
media.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, wx.WXK_F12, [] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'launch_the_archive_delete_filter' ) )
|
||||
|
||||
media.SetCommand( ClientData.Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, ord( 'C' ), [ CC.SHORTCUT_MODIFIER_CTRL ] ), ClientData.ApplicationCommand( CC.APPLICATION_COMMAND_TYPE_SIMPLE, 'copy_file' ) )
|
||||
|
||||
shortcuts.append( media )
|
||||
|
||||
main_gui = ClientData.Shortcuts( 'main_gui' )
|
||||
|
|
|
@ -278,7 +278,7 @@ def Parse4chanPostScreen( html ):
|
|||
except: return ( 'error', 'unknown error' )
|
||||
|
||||
|
||||
def ParseImageboardFileURLFromPost( thread_url, post ):
|
||||
def ParseImageboardFileURLFromPost( thread_url, post, source_timestamp ):
|
||||
|
||||
url_filename = str( post[ 'tim' ] )
|
||||
url_ext = post[ 'ext' ]
|
||||
|
@ -295,7 +295,7 @@ def ParseImageboardFileURLFromPost( thread_url, post ):
|
|||
file_md5_base64 = None
|
||||
|
||||
|
||||
return ( file_url, file_md5_base64, file_original_filename )
|
||||
return ( file_url, file_md5_base64, file_original_filename, source_timestamp )
|
||||
|
||||
def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
|
||||
|
||||
|
@ -312,7 +312,16 @@ def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
|
|||
continue
|
||||
|
||||
|
||||
file_infos.append( ParseImageboardFileURLFromPost( thread_url, post ) )
|
||||
if 'time' in post:
|
||||
|
||||
source_timestamp = post[ 'time' ]
|
||||
|
||||
else:
|
||||
|
||||
source_timestamp = HydrusData.GetNow()
|
||||
|
||||
|
||||
file_infos.append( ParseImageboardFileURLFromPost( thread_url, post, source_timestamp ) )
|
||||
|
||||
if 'extra_files' in post:
|
||||
|
||||
|
@ -323,7 +332,7 @@ def ParseImageboardFileURLsFromJSON( thread_url, raw_json ):
|
|||
continue
|
||||
|
||||
|
||||
file_infos.append( ParseImageboardFileURLFromPost( thread_url, extra_file ) )
|
||||
file_infos.append( ParseImageboardFileURLFromPost( thread_url, extra_file, source_timestamp ) )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -38,13 +38,17 @@ class FileDropTarget( wx.PyDropTarget ):
|
|||
|
||||
paths = self._file_data_object.GetFilenames()
|
||||
|
||||
wx.CallAfter( self._filenames_callable, paths )
|
||||
wx.CallAfter( self._filenames_callable, paths ) # callafter to terminate dnd event now
|
||||
|
||||
result = wx.DragNone
|
||||
|
||||
elif received_format_type in ( wx.DF_TEXT, wx.DF_UNICODETEXT ) and self._url_callable is not None:
|
||||
|
||||
text = self._text_data_object.GetText()
|
||||
|
||||
self._url_callable( text )
|
||||
wx.CallAfter( self._url_callable, text ) # callafter to terminate dnd event now
|
||||
|
||||
result = wx.DragCopy
|
||||
|
||||
else:
|
||||
|
||||
|
@ -59,14 +63,16 @@ class FileDropTarget( wx.PyDropTarget ):
|
|||
|
||||
if format_id == 'application/hydrus-media':
|
||||
|
||||
pass
|
||||
result = wx.DragCancel
|
||||
|
||||
|
||||
if format_id == 'application/hydrus-page-tab' and self._page_callable is not None:
|
||||
|
||||
page_key = self._hydrus_page_tab_data_object.GetData()
|
||||
|
||||
self._page_callable( page_key )
|
||||
wx.CallAfter( self._page_callable, page_key ) # callafter to terminate dnd event now
|
||||
|
||||
result = wx.DragMove
|
||||
|
||||
|
||||
|
||||
|
@ -74,8 +80,4 @@ class FileDropTarget( wx.PyDropTarget ):
|
|||
return result
|
||||
|
||||
|
||||
def OnDragOver( self, x, y, result ):
|
||||
|
||||
return wx.DragCopy
|
||||
|
||||
|
||||
# setting OnDragOver to return copy gives Linux trouble with page tab drops with shift held down
|
||||
|
|
|
@ -834,22 +834,6 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
|
||||
|
||||
def _FlipDarkmode( self ):
|
||||
|
||||
current_colourset = self._new_options.GetString( 'current_colourset' )
|
||||
|
||||
if current_colourset == 'darkmode':
|
||||
|
||||
new_colourset = 'default'
|
||||
|
||||
elif current_colourset == 'default':
|
||||
|
||||
new_colourset = 'darkmode'
|
||||
|
||||
|
||||
self._new_options.SetString( 'current_colourset', new_colourset )
|
||||
|
||||
|
||||
def _GenerateMenuInfo( self, name ):
|
||||
|
||||
menu = wx.Menu()
|
||||
|
@ -1419,7 +1403,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
currently_darkmode = self._new_options.GetString( 'current_colourset' ) == 'darkmode'
|
||||
|
||||
ClientGUIMenus.AppendMenuCheckItem( self, menu, 'darkmode', 'Set the \'darkmode\' colourset on and off.', currently_darkmode, self._FlipDarkmode )
|
||||
ClientGUIMenus.AppendMenuCheckItem( self, menu, 'darkmode', 'Set the \'darkmode\' colourset on and off.', currently_darkmode, self.FlipDarkmode )
|
||||
|
||||
check_manager = ClientGUICommon.CheckboxManagerOptions( 'advanced_mode' )
|
||||
|
||||
|
@ -1954,6 +1938,10 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
self._UnclosePage()
|
||||
|
||||
elif action == 'flip_darkmode':
|
||||
|
||||
self.FlipDarkmode()
|
||||
|
||||
elif action == 'show_hide_splitters':
|
||||
|
||||
self._ShowHideSplitters()
|
||||
|
@ -2972,6 +2960,22 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
return True
|
||||
|
||||
|
||||
def FlipDarkmode( self ):
|
||||
|
||||
current_colourset = self._new_options.GetString( 'current_colourset' )
|
||||
|
||||
if current_colourset == 'darkmode':
|
||||
|
||||
new_colourset = 'default'
|
||||
|
||||
elif current_colourset == 'default':
|
||||
|
||||
new_colourset = 'darkmode'
|
||||
|
||||
|
||||
self._new_options.SetString( 'current_colourset', new_colourset )
|
||||
|
||||
|
||||
def FlushOutPredicates( self, predicates ):
|
||||
|
||||
good_predicates = []
|
||||
|
|
|
@ -1055,6 +1055,10 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizes ):
|
|||
|
||||
self.FullscreenSwitch()
|
||||
|
||||
elif action == 'flip_darkmode':
|
||||
|
||||
HG.client_controller.gui.FlipDarkmode()
|
||||
|
||||
else:
|
||||
|
||||
command_processed = False
|
||||
|
@ -1683,6 +1687,22 @@ class Canvas( wx.Window ):
|
|||
|
||||
self._Archive()
|
||||
|
||||
elif action == 'copy_bmp':
|
||||
|
||||
self._CopyBMPToClipboard()
|
||||
|
||||
elif action == 'copy_file':
|
||||
|
||||
self._CopyFileToClipboard()
|
||||
|
||||
elif action == 'copy_path':
|
||||
|
||||
self._CopyPathToClipboard()
|
||||
|
||||
elif action == 'copy_sha256_hash':
|
||||
|
||||
self._CopyHashToClipboard( 'sha256' )
|
||||
|
||||
elif action == 'delete_file':
|
||||
|
||||
self._Delete()
|
||||
|
@ -3591,7 +3611,6 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
|
|||
|
||||
if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete()
|
||||
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
|
||||
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
||||
else:
|
||||
|
||||
CanvasWithHovers.EventCharHook( self, event )
|
||||
|
@ -4229,8 +4248,7 @@ class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
|
|||
|
||||
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
|
||||
|
||||
if modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
||||
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
|
||||
if modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
|
||||
else:
|
||||
|
||||
CanvasMediaList.EventCharHook( self, event )
|
||||
|
@ -4579,7 +4597,6 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
|
|||
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
|
||||
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_SPACE, wx.WXK_NUMPAD_SPACE ): wx.CallAfter( self._PausePlaySlideshow )
|
||||
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self._Close()
|
||||
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
|
||||
else:
|
||||
|
||||
CanvasMediaListNavigable.EventCharHook( self, event )
|
||||
|
|
|
@ -623,6 +623,61 @@ class CheckboxCollect( wx.combo.ComboCtrl ):
|
|||
|
||||
|
||||
|
||||
class CheckboxManager( object ):
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CheckboxManagerCalls( CheckboxManager ):
|
||||
|
||||
def __init__( self, invert_call, value_call ):
|
||||
|
||||
CheckboxManager.__init__( self )
|
||||
|
||||
self._invert_call = invert_call
|
||||
self._value_call = value_call
|
||||
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
return self._value_call()
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
self._invert_call()
|
||||
|
||||
|
||||
class CheckboxManagerOptions( CheckboxManager ):
|
||||
|
||||
def __init__( self, boolean_name ):
|
||||
|
||||
CheckboxManager.__init__( self )
|
||||
|
||||
self._boolean_name = boolean_name
|
||||
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
new_options = HG.client_controller.GetNewOptions()
|
||||
|
||||
return new_options.GetBoolean( self._boolean_name )
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
new_options = HG.client_controller.GetNewOptions()
|
||||
|
||||
new_options.InvertBoolean( self._boolean_name )
|
||||
|
||||
|
||||
class ChoiceSort( wx.Panel ):
|
||||
|
||||
def __init__( self, parent, management_controller = None ):
|
||||
|
@ -1360,61 +1415,6 @@ class ListBook( wx.Panel ):
|
|||
|
||||
|
||||
|
||||
class CheckboxManager( object ):
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CheckboxManagerCalls( CheckboxManager ):
|
||||
|
||||
def __init__( self, invert_call, value_call ):
|
||||
|
||||
CheckboxManager.__init__( self )
|
||||
|
||||
self._invert_call = invert_call
|
||||
self._value_call = value_call
|
||||
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
return self._value_call()
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
self._invert_call()
|
||||
|
||||
|
||||
class CheckboxManagerOptions( CheckboxManager ):
|
||||
|
||||
def __init__( self, boolean_name ):
|
||||
|
||||
CheckboxManager.__init__( self )
|
||||
|
||||
self._boolean_name = boolean_name
|
||||
|
||||
|
||||
def GetCurrentValue( self ):
|
||||
|
||||
new_options = HG.client_controller.GetNewOptions()
|
||||
|
||||
return new_options.GetBoolean( self._boolean_name )
|
||||
|
||||
|
||||
def Invert( self ):
|
||||
|
||||
new_options = HG.client_controller.GetNewOptions()
|
||||
|
||||
new_options.InvertBoolean( self._boolean_name )
|
||||
|
||||
|
||||
class MenuBitmapButton( BetterBitmapButton ):
|
||||
|
||||
def __init__( self, parent, bitmap, menu_items ):
|
||||
|
@ -1554,8 +1554,13 @@ class NoneableSpinCtrl( wx.Panel ):
|
|||
def Bind( self, event_type, callback ):
|
||||
|
||||
self._checkbox.Bind( wx.EVT_CHECKBOX, callback )
|
||||
|
||||
self._one.Bind( wx.EVT_SPINCTRL, callback )
|
||||
if self._num_dimensions == 2: self._two.Bind( wx.EVT_SPINCTRL, callback )
|
||||
|
||||
if self._num_dimensions == 2:
|
||||
|
||||
self._two.Bind( wx.EVT_SPINCTRL, callback )
|
||||
|
||||
|
||||
|
||||
def EventCheckBox( self, event ):
|
||||
|
@ -2539,6 +2544,50 @@ class StaticBoxSorterForListBoxTags( StaticBox ):
|
|||
self._tags_box.SetTagsByMedia( media, force_reload = force_reload )
|
||||
|
||||
|
||||
class RadioBox( StaticBox ):
|
||||
|
||||
def __init__( self, parent, title, choice_pairs, initial_index = None ):
|
||||
|
||||
StaticBox.__init__( self, parent, title )
|
||||
|
||||
self._indices_to_radio_buttons = {}
|
||||
self._radio_buttons_to_data = {}
|
||||
|
||||
first_button = True
|
||||
|
||||
for ( index, ( text, data ) ) in enumerate( choice_pairs ):
|
||||
|
||||
if first_button:
|
||||
|
||||
style = wx.RB_GROUP
|
||||
|
||||
first_button = False
|
||||
|
||||
else: style = 0
|
||||
|
||||
radio_button = wx.RadioButton( self, label = text, style = style )
|
||||
|
||||
self.AddF( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self._indices_to_radio_buttons[ index ] = radio_button
|
||||
self._radio_buttons_to_data[ radio_button ] = data
|
||||
|
||||
|
||||
if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].SetValue( True )
|
||||
|
||||
|
||||
def GetSelectedClientData( self ):
|
||||
|
||||
for radio_button in self._radio_buttons_to_data.keys():
|
||||
|
||||
if radio_button.GetValue() == True: return self._radio_buttons_to_data[ radio_button ]
|
||||
|
||||
|
||||
|
||||
def SetSelection( self, index ): self._indices_to_radio_buttons[ index ].SetValue( True )
|
||||
|
||||
def SetString( self, index, text ): self._indices_to_radio_buttons[ index ].SetLabelText( text )
|
||||
|
||||
class TextAndGauge( wx.Panel ):
|
||||
|
||||
def __init__( self, parent ):
|
||||
|
@ -2727,7 +2776,7 @@ class TimeDeltaButton( wx.Button ):
|
|||
|
||||
class TimeDeltaCtrl( wx.Panel ):
|
||||
|
||||
def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, monthly_allowed = False ):
|
||||
def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False, monthly_allowed = False, monthly_label = 'monthly' ):
|
||||
|
||||
wx.Panel.__init__( self, parent )
|
||||
|
||||
|
@ -2782,7 +2831,7 @@ class TimeDeltaCtrl( wx.Panel ):
|
|||
self._monthly.Bind( wx.EVT_CHECKBOX, self.EventChange )
|
||||
|
||||
hbox.AddF( self._monthly, CC.FLAGS_VCENTER )
|
||||
hbox.AddF( BetterStaticText( self, 'monthly' ), CC.FLAGS_VCENTER )
|
||||
hbox.AddF( BetterStaticText( self, monthly_label ), CC.FLAGS_VCENTER )
|
||||
|
||||
|
||||
self.SetSizer( hbox )
|
||||
|
@ -2937,47 +2986,60 @@ class TimeDeltaCtrl( wx.Panel ):
|
|||
self._UpdateEnables()
|
||||
|
||||
|
||||
class RadioBox( StaticBox ):
|
||||
class VelocityCtrl( wx.Panel ):
|
||||
|
||||
def __init__( self, parent, title, choice_pairs, initial_index = None ):
|
||||
def __init__( self, parent, min_time_delta = 60, days = False, hours = False, minutes = False, seconds = False, per_phrase = 'per', unit = None ):
|
||||
|
||||
StaticBox.__init__( self, parent, title )
|
||||
wx.Panel.__init__( self, parent )
|
||||
|
||||
self._indices_to_radio_buttons = {}
|
||||
self._radio_buttons_to_data = {}
|
||||
self._num = wx.SpinCtrl( self, min = 0, max = 1000, size = ( 60, -1 ) )
|
||||
|
||||
first_button = True
|
||||
self._times = TimeDeltaCtrl( self, min = min_time_delta, days = days, hours = hours, minutes = minutes, seconds = seconds )
|
||||
|
||||
for ( index, ( text, data ) ) in enumerate( choice_pairs ):
|
||||
#
|
||||
|
||||
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
hbox.AddF( self._num, CC.FLAGS_VCENTER )
|
||||
|
||||
mid_text = per_phrase
|
||||
|
||||
if unit is not None:
|
||||
|
||||
if first_button:
|
||||
|
||||
style = wx.RB_GROUP
|
||||
|
||||
first_button = False
|
||||
|
||||
else: style = 0
|
||||
|
||||
radio_button = wx.RadioButton( self, label = text, style = style )
|
||||
|
||||
self.AddF( radio_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self._indices_to_radio_buttons[ index ] = radio_button
|
||||
self._radio_buttons_to_data[ radio_button ] = data
|
||||
mid_text = unit + ' ' + mid_text
|
||||
|
||||
|
||||
if initial_index is not None and initial_index in self._indices_to_radio_buttons: self._indices_to_radio_buttons[ initial_index ].SetValue( True )
|
||||
hbox.AddF( BetterStaticText( self, mid_text ), CC.FLAGS_VCENTER )
|
||||
|
||||
hbox.AddF( self._times, CC.FLAGS_VCENTER )
|
||||
|
||||
self.SetSizer( hbox )
|
||||
|
||||
|
||||
def GetSelectedClientData( self ):
|
||||
def GetValue( self ):
|
||||
|
||||
for radio_button in self._radio_buttons_to_data.keys():
|
||||
num = self._num.GetValue()
|
||||
time_delta = self._times.GetValue()
|
||||
|
||||
return ( num, time_delta )
|
||||
|
||||
|
||||
def SetToolTipString( self, text ):
|
||||
|
||||
wx.Panel.SetToolTipString( self, text )
|
||||
|
||||
for c in self.GetChildren():
|
||||
|
||||
if radio_button.GetValue() == True: return self._radio_buttons_to_data[ radio_button ]
|
||||
c.SetToolTipString( text )
|
||||
|
||||
|
||||
|
||||
def SetSelection( self, index ): self._indices_to_radio_buttons[ index ].SetValue( True )
|
||||
|
||||
def SetString( self, index, text ): self._indices_to_radio_buttons[ index ].SetLabelText( text )
|
||||
def SetValue( self, velocity ):
|
||||
|
||||
( num, time_delta ) = velocity
|
||||
|
||||
self._num.SetValue( num )
|
||||
|
||||
self._times.SetValue( time_delta )
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,225 @@ import HydrusTags
|
|||
import os
|
||||
import wx
|
||||
|
||||
class QueueListBox( wx.Panel ):
|
||||
|
||||
def __init__( self, parent, data_to_pretty_callable, add_callable, edit_callable ):
|
||||
|
||||
self._data_to_pretty_callable = data_to_pretty_callable
|
||||
self._add_callable = add_callable
|
||||
self._edit_callable = edit_callable
|
||||
|
||||
wx.Panel.__init__( self, parent )
|
||||
|
||||
self._listbox = wx.ListBox( self, style = wx.LB_MULTIPLE )
|
||||
|
||||
self._up_button = ClientGUICommon.BetterButton( self, u'\u2191', self._Up )
|
||||
|
||||
self._delete_button = ClientGUICommon.BetterButton( self, 'X', self._Delete )
|
||||
|
||||
self._down_button = ClientGUICommon.BetterButton( self, u'\u2193', self._Down )
|
||||
|
||||
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
|
||||
self._edit_button = ClientGUICommon.BetterButton( self, 'edit', self._Edit )
|
||||
|
||||
#
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
buttons_vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
buttons_vbox.AddF( self._up_button, CC.FLAGS_VCENTER )
|
||||
buttons_vbox.AddF( self._delete_button, CC.FLAGS_VCENTER )
|
||||
buttons_vbox.AddF( self._down_button, CC.FLAGS_VCENTER )
|
||||
|
||||
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
hbox.AddF( self._listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
hbox.AddF( buttons_vbox, CC.FLAGS_VCENTER )
|
||||
|
||||
buttons_hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
buttons_hbox.AddF( self._add_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
buttons_hbox.AddF( self._edit_button, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
vbox.AddF( hbox, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
vbox.AddF( buttons_hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._listbox.Bind( wx.EVT_LISTBOX, self.EventSelection )
|
||||
|
||||
|
||||
def _Add( self ):
|
||||
|
||||
( result, data ) = self._add_callable()
|
||||
|
||||
if result:
|
||||
|
||||
self._AddData( data )
|
||||
|
||||
|
||||
|
||||
def _AddData( self, data ):
|
||||
|
||||
pretty_data = self._data_to_pretty_callable( data )
|
||||
|
||||
self._listbox.Append( pretty_data, data )
|
||||
|
||||
|
||||
def _Delete( self ):
|
||||
|
||||
indices = self._listbox.GetSelections()
|
||||
|
||||
indices.sort( reverse = True )
|
||||
|
||||
if len( indices ) == 0:
|
||||
|
||||
return
|
||||
|
||||
|
||||
import ClientGUIDialogs
|
||||
|
||||
with ClientGUIDialogs.DialogYesNo( self, 'Remove all selected?' ) as dlg_yn:
|
||||
|
||||
if dlg_yn.ShowModal() == wx.ID_YES:
|
||||
|
||||
for i in indices:
|
||||
|
||||
self._listbox.Delete( i )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _Down( self ):
|
||||
|
||||
indices = self._listbox.GetSelections()
|
||||
|
||||
indices.sort( reverse = True )
|
||||
|
||||
for i in indices:
|
||||
|
||||
if i > 0:
|
||||
|
||||
if not self._listbox.IsSelected( i + 1 ): # is the one below not selected?
|
||||
|
||||
self._SwapRows( i, i + 1 )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
||||
for i in range( self._listbox.GetCount() ):
|
||||
|
||||
if not self._listbox.IsSelected( i ):
|
||||
|
||||
continue
|
||||
|
||||
|
||||
data = self._listbox.GetClientData( i )
|
||||
|
||||
( result, new_data ) = self._edit_callable( data )
|
||||
|
||||
if result:
|
||||
|
||||
self._listbox.Delete( i )
|
||||
|
||||
pretty_new_data = self._data_to_pretty_callable( new_data )
|
||||
|
||||
self._listbox.Set( pretty_new_data, i, new_data )
|
||||
|
||||
else:
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
def _SwapRows( self, index_a, index_b ):
|
||||
|
||||
data_a = self._listbox.GetClientData( index_a )
|
||||
data_b = self._listbox.GetClientData( index_b )
|
||||
|
||||
pretty_data_a = self._data_to_pretty_callable( data_a )
|
||||
pretty_data_b = self._data_to_pretty_callable( data_b )
|
||||
|
||||
self._listbox.Delete( index_a )
|
||||
self._listbox.Insert( pretty_data_b, index_a, data_b )
|
||||
|
||||
self._listbox.Delete( index_b )
|
||||
self._listbox.Insert( pretty_data_a, index_b, data_a )
|
||||
|
||||
|
||||
def _Up( self ):
|
||||
|
||||
indices = self._listbox.GetSelections()
|
||||
|
||||
for i in indices:
|
||||
|
||||
if i > 0:
|
||||
|
||||
if not self._listbox.IsSelected( i - 1 ): # is the one above not selected?
|
||||
|
||||
self._SwapRows( i, i - 1 )
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def AddDatas( self, datas ):
|
||||
|
||||
for data in datas:
|
||||
|
||||
self._AddData( data )
|
||||
|
||||
|
||||
|
||||
def Bind( self, event, handler ):
|
||||
|
||||
self._listbox.Bind( event, handler )
|
||||
|
||||
|
||||
def EventSelection( self, event ):
|
||||
|
||||
if self._listbox.GetSelection() == wx.NOT_FOUND:
|
||||
|
||||
self._up_button.Disable()
|
||||
self._delete_button.Disable()
|
||||
self._down_button.Disable()
|
||||
|
||||
self._edit_button.Disable()
|
||||
|
||||
else:
|
||||
|
||||
self._up_button.Enable()
|
||||
self._delete_button.Enable()
|
||||
self._down_button.Enable()
|
||||
|
||||
self._edit_button.Enable()
|
||||
|
||||
|
||||
event.Skip()
|
||||
|
||||
|
||||
def GetData( self, only_selected = False ):
|
||||
|
||||
datas = []
|
||||
|
||||
for i in range( self._listbox.GetCount() ):
|
||||
|
||||
data = self._listbox.GetClientData( i )
|
||||
|
||||
datas.append( data )
|
||||
|
||||
|
||||
return datas
|
||||
|
||||
|
||||
class ListBox( wx.ScrolledWindow ):
|
||||
|
||||
TEXT_X_PADDING = 3
|
||||
|
@ -914,7 +1133,7 @@ class ListBoxTags( ListBox ):
|
|||
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( self, menu, 'add parents to ' + text, 'Add a parent to this tag.', self._ProcessMenuTagEvent, 'parent' )
|
||||
ClientGUIMenus.AppendMenuItem( self, menu, 'add parents to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )
|
||||
ClientGUIMenus.AppendMenuItem( self, menu, 'add siblings to ' + text, 'Add a sibling to this tag.', self._ProcessMenuTagEvent, 'sibling' )
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -376,7 +376,7 @@ class SaneListCtrlForSingleObject( SaneListCtrl ):
|
|||
|
||||
name = obj.GetName()
|
||||
|
||||
current_names = { obj.GetName() for obj in self.GetObjects() }
|
||||
current_names = { o.GetName() for o in self.GetObjects() if o is not obj }
|
||||
|
||||
if name in current_names:
|
||||
|
||||
|
@ -492,7 +492,7 @@ class SaneListCtrlPanel( wx.Panel ):
|
|||
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
|
||||
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )
|
||||
|
||||
|
||||
|
||||
class BetterListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
|
||||
|
||||
def __init__( self, parent, name, height_num_chars, sizing_column_initial_width_num_chars, columns, data_to_tuples_func, delete_key_callback = None, activation_callback = None ):
|
||||
|
@ -881,3 +881,92 @@ class BetterListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin ):
|
|||
|
||||
|
||||
|
||||
|
||||
class BetterListCtrlPanel( wx.Panel ):
|
||||
|
||||
def __init__( self, parent ):
|
||||
|
||||
wx.Panel.__init__( self, parent )
|
||||
|
||||
self._vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
self._buttonbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
self._listctrl = None
|
||||
|
||||
self._button_infos = []
|
||||
|
||||
|
||||
def _HasSelected( self ):
|
||||
|
||||
return self._listctrl.HasSelected()
|
||||
|
||||
|
||||
def _UpdateButtons( self ):
|
||||
|
||||
for ( button, enabled_check_func ) in self._button_infos:
|
||||
|
||||
if enabled_check_func():
|
||||
|
||||
button.Enable()
|
||||
|
||||
else:
|
||||
|
||||
button.Disable()
|
||||
|
||||
|
||||
|
||||
|
||||
def AddButton( self, label, clicked_func, enabled_only_on_selection = False, enabled_check_func = None ):
|
||||
|
||||
button = ClientGUICommon.BetterButton( self, label, clicked_func )
|
||||
|
||||
self._buttonbox.AddF( button, CC.FLAGS_VCENTER )
|
||||
|
||||
if enabled_only_on_selection:
|
||||
|
||||
enabled_check_func = self._HasSelected
|
||||
|
||||
|
||||
if enabled_check_func is not None:
|
||||
|
||||
self._button_infos.append( ( button, enabled_check_func ) )
|
||||
|
||||
|
||||
|
||||
def AddWindow( self, window ):
|
||||
|
||||
self._buttonbox.AddF( window, CC.FLAGS_VCENTER )
|
||||
|
||||
|
||||
def EventContentChanged( self, event ):
|
||||
|
||||
self._UpdateButtons()
|
||||
|
||||
event.Skip()
|
||||
|
||||
|
||||
def EventSelectionChanged( self, event ):
|
||||
|
||||
self._UpdateButtons()
|
||||
|
||||
event.Skip()
|
||||
|
||||
|
||||
def SetListCtrl( self, listctrl ):
|
||||
|
||||
self._listctrl = listctrl
|
||||
|
||||
self._vbox.AddF( self._listctrl, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
self._vbox.AddF( self._buttonbox, CC.FLAGS_BUTTON_SIZER )
|
||||
|
||||
self.SetSizer( self._vbox )
|
||||
|
||||
self._listctrl.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelectionChanged )
|
||||
self._listctrl.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelectionChanged )
|
||||
|
||||
self._listctrl.Bind( wx.EVT_LIST_INSERT_ITEM, self.EventContentChanged )
|
||||
self._listctrl.Bind( wx.EVT_LIST_DELETE_ITEM, self.EventContentChanged )
|
||||
self._listctrl.Bind( wx.EVT_LIST_DELETE_ALL_ITEMS, self.EventContentChanged )
|
||||
|
||||
|
||||
|
|
|
@ -766,6 +766,16 @@ class ManagementPanel( wx.lib.scrolledpanel.ScrolledPanel ):
|
|||
pass
|
||||
|
||||
|
||||
def PageHidden( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def PageShown( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def SetSearchFocus( self, page_key ):
|
||||
|
||||
pass
|
||||
|
@ -1330,12 +1340,38 @@ class ManagementPanelImporter( ManagementPanel ):
|
|||
|
||||
self._import_update_timer.Start( 250, wx.TIMER_CONTINUOUS )
|
||||
|
||||
self._controller.sub( self, 'RefreshSort', 'refresh_query' )
|
||||
|
||||
|
||||
def _UpdateStatus( self ):
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def PageHidden( self ):
|
||||
|
||||
ManagementPanel.PageHidden( self )
|
||||
|
||||
self._import_update_timer.Stop()
|
||||
|
||||
|
||||
def PageShown( self ):
|
||||
|
||||
ManagementPanel.PageShown( self )
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
self._import_update_timer.Start()
|
||||
|
||||
|
||||
def RefreshSort( self, page_key ):
|
||||
|
||||
if page_key == self._page_key:
|
||||
|
||||
self._sort_by.BroadcastSort()
|
||||
|
||||
|
||||
|
||||
def TIMEREventImportUpdate( self, event ):
|
||||
|
||||
if self._controller.gui.IsCurrentPage( self._page_key ):
|
||||
|
@ -2213,12 +2249,12 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
self._options_panel = wx.Panel( self._thread_watcher_panel )
|
||||
|
||||
self._pause_button = wx.BitmapButton( self._options_panel, bitmap = CC.GlobalBMPs.pause )
|
||||
self._pause_button.Bind( wx.EVT_BUTTON, self.EventPause )
|
||||
|
||||
#
|
||||
|
||||
imports_panel = ClientGUICommon.StaticBox( self._options_panel, 'imports' )
|
||||
imports_panel = ClientGUICommon.StaticBox( self._options_panel, 'file imports' )
|
||||
|
||||
self._files_pause_button = wx.BitmapButton( imports_panel, bitmap = CC.GlobalBMPs.pause )
|
||||
self._files_pause_button.Bind( wx.EVT_BUTTON, self.EventPauseFiles )
|
||||
|
||||
self._current_action = ClientGUICommon.BetterStaticText( imports_panel )
|
||||
self._seed_cache_control = ClientGUISeedCache.SeedCacheStatusControl( imports_panel, self._controller )
|
||||
|
@ -2228,52 +2264,58 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
checker_panel = ClientGUICommon.StaticBox( self._options_panel, 'checker' )
|
||||
|
||||
self._file_velocity_status = ClientGUICommon.BetterStaticText( checker_panel )
|
||||
|
||||
self._thread_pause_button = wx.BitmapButton( checker_panel, bitmap = CC.GlobalBMPs.pause )
|
||||
self._thread_pause_button.Bind( wx.EVT_BUTTON, self.EventPauseThread )
|
||||
|
||||
self._watcher_status = ClientGUICommon.BetterStaticText( checker_panel )
|
||||
self._thread_download_control = ClientGUIControls.NetworkJobControl( checker_panel )
|
||||
|
||||
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
|
||||
|
||||
self._thread_times_to_check = wx.SpinCtrl( checker_panel, size = ( 60, -1 ), min = 0, max = 65536 )
|
||||
self._thread_times_to_check.SetValue( times_to_check )
|
||||
self._thread_times_to_check.Bind( wx.EVT_SPINCTRL, self.EventTimesToCheck )
|
||||
|
||||
self._thread_check_period = ClientGUICommon.TimeDeltaButton( checker_panel, min = 30, days = True, hours = True, minutes = True, seconds = True )
|
||||
self._thread_check_period.SetValue( check_period )
|
||||
self._thread_check_period.Bind( ClientGUICommon.EVT_TIME_DELTA, self.EventCheckPeriod )
|
||||
|
||||
self._thread_check_now_button = wx.Button( checker_panel, label = 'check now' )
|
||||
self._thread_check_now_button.Bind( wx.EVT_BUTTON, self.EventCheckNow )
|
||||
|
||||
self._watcher_options_button = ClientGUICommon.BetterButton( checker_panel, 'edit check timings', self._EditWatcherOptions )
|
||||
|
||||
self._thread_download_control = ClientGUIControls.NetworkJobControl( checker_panel )
|
||||
|
||||
#
|
||||
|
||||
self._thread_watcher_import = self._management_controller.GetVariable( 'thread_watcher_import' )
|
||||
|
||||
( thread_url, file_import_options, tag_import_options, times_to_check, check_period ) = self._thread_watcher_import.GetOptions()
|
||||
( thread_url, file_import_options, tag_import_options ) = self._thread_watcher_import.GetOptions()
|
||||
|
||||
self._file_import_options = ClientGUIImport.FileImportOptionsButton( self._thread_watcher_panel, file_import_options, self._thread_watcher_import.SetFileImportOptions )
|
||||
self._tag_import_options = ClientGUICollapsible.CollapsibleOptionsTags( self._thread_watcher_panel, namespaces = [ 'filename' ] )
|
||||
|
||||
#
|
||||
|
||||
imports_panel.AddF( self._current_action, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
hbox.AddF( self._current_action, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
||||
|
||||
hbox.AddF( self._files_pause_button, CC.FLAGS_LONE_BUTTON )
|
||||
|
||||
imports_panel.AddF( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
imports_panel.AddF( self._seed_cache_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
imports_panel.AddF( self._file_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
hbox_1 = wx.WrapSizer( wx.HORIZONTAL )
|
||||
#
|
||||
|
||||
hbox_1.AddF( ClientGUICommon.BetterStaticText( checker_panel, label = 'checking ' ), CC.FLAGS_VCENTER )
|
||||
hbox_1.AddF( self._thread_times_to_check, CC.FLAGS_VCENTER )
|
||||
hbox_1.AddF( ClientGUICommon.BetterStaticText( checker_panel, label = ' more times, every ' ), CC.FLAGS_VCENTER )
|
||||
hbox_1.AddF( self._thread_check_period, CC.FLAGS_VCENTER )
|
||||
hbox_1.AddF( self._thread_check_now_button, CC.FLAGS_VCENTER )
|
||||
gridbox = wx.FlexGridSizer( 0, 2 )
|
||||
|
||||
checker_panel.AddF( self._watcher_status, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
gridbox.AddGrowableCol( 0, 1 )
|
||||
|
||||
gridbox.AddF( self._file_velocity_status, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
||||
gridbox.AddF( self._thread_pause_button, CC.FLAGS_LONE_BUTTON )
|
||||
gridbox.AddF( self._watcher_status, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
|
||||
gridbox.AddF( self._thread_check_now_button, CC.FLAGS_VCENTER )
|
||||
|
||||
checker_panel.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
checker_panel.AddF( self._watcher_options_button, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
checker_panel.AddF( self._thread_download_control, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
checker_panel.AddF( hbox_1, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.AddF( self._pause_button, CC.FLAGS_LONE_BUTTON )
|
||||
vbox.AddF( imports_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
vbox.AddF( checker_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
|
@ -2302,8 +2344,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
self.Bind( wx.EVT_MENU, self.EventMenu )
|
||||
|
||||
self._controller.sub( self, 'DecrementTimesToCheck', 'decrement_times_to_check' )
|
||||
|
||||
seed_cache = self._thread_watcher_import.GetSeedCache()
|
||||
|
||||
self._seed_cache_control.SetSeedCache( seed_cache )
|
||||
|
@ -2315,12 +2355,30 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
self._tag_import_options.SetOptions( tag_import_options )
|
||||
|
||||
self._thread_times_to_check.SetValue( times_to_check )
|
||||
self._thread_check_period.SetValue( check_period )
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def _EditWatcherOptions( self ):
|
||||
|
||||
watcher_options = self._thread_watcher_import.GetWatcherOptions()
|
||||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self._watcher_options_button, 'edit check timings' ) as dlg:
|
||||
|
||||
panel = ClientGUIScrolledPanelsEdit.EditWatcherOptions( dlg, watcher_options )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
|
||||
new_watcher_options = panel.GetValue()
|
||||
|
||||
self._thread_watcher_import.SetWatcherOptions( new_watcher_options )
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
|
||||
|
||||
def _UpdateStatus( self ):
|
||||
|
||||
if self._thread_watcher_import.HasThread():
|
||||
|
@ -2344,37 +2402,63 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
|
||||
|
||||
( current_action, watcher_status, check_now, paused ) = self._thread_watcher_import.GetStatus()
|
||||
( current_action, files_paused, file_velocity_status, next_check_time, watcher_status, check_now, thread_paused ) = self._thread_watcher_import.GetStatus()
|
||||
|
||||
self._current_action.SetLabelText( current_action )
|
||||
|
||||
if paused:
|
||||
if files_paused:
|
||||
|
||||
if self._thread_times_to_check.GetValue() > 0 or check_now:
|
||||
if current_action == '':
|
||||
|
||||
watcher_status = 'paused'
|
||||
current_action = 'paused'
|
||||
|
||||
else:
|
||||
|
||||
current_action = 'pausing, ' + current_action
|
||||
|
||||
|
||||
if self._pause_button.GetBitmap() != CC.GlobalBMPs.play:
|
||||
if self._files_pause_button.GetBitmap() != CC.GlobalBMPs.play:
|
||||
|
||||
self._pause_button.SetBitmap( CC.GlobalBMPs.play )
|
||||
self._files_pause_button.SetBitmap( CC.GlobalBMPs.play )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
if self._pause_button.GetBitmap() != CC.GlobalBMPs.pause:
|
||||
if self._files_pause_button.GetBitmap() != CC.GlobalBMPs.pause:
|
||||
|
||||
self._pause_button.SetBitmap( CC.GlobalBMPs.pause )
|
||||
self._files_pause_button.SetBitmap( CC.GlobalBMPs.pause )
|
||||
|
||||
|
||||
|
||||
self._current_action.SetLabelText( current_action )
|
||||
|
||||
self._file_velocity_status.SetLabelText( file_velocity_status )
|
||||
|
||||
if thread_paused:
|
||||
|
||||
if watcher_status == '':
|
||||
|
||||
watcher_status = 'paused'
|
||||
|
||||
|
||||
if self._thread_pause_button.GetBitmap() != CC.GlobalBMPs.play:
|
||||
|
||||
self._thread_pause_button.SetBitmap( CC.GlobalBMPs.play )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
if watcher_status == '':
|
||||
|
||||
watcher_status = 'next check ' + HydrusData.ConvertTimestampToPrettyPending( next_check_time )
|
||||
|
||||
|
||||
if self._thread_pause_button.GetBitmap() != CC.GlobalBMPs.pause:
|
||||
|
||||
self._thread_pause_button.SetBitmap( CC.GlobalBMPs.pause )
|
||||
|
||||
|
||||
|
||||
self._watcher_status.SetLabelText( watcher_status )
|
||||
|
||||
if current_action == '' and paused:
|
||||
|
||||
current_action = 'paused'
|
||||
|
||||
|
||||
if check_now:
|
||||
|
||||
self._thread_check_now_button.Disable()
|
||||
|
@ -2385,18 +2469,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
|
||||
|
||||
def DecrementTimesToCheck( self, page_key ):
|
||||
|
||||
if page_key == self._page_key:
|
||||
|
||||
current_value = self._thread_times_to_check.GetValue()
|
||||
|
||||
new_value = max( 0, current_value - 1 )
|
||||
|
||||
self._thread_times_to_check.SetValue( new_value )
|
||||
|
||||
|
||||
|
||||
def EventCheckNow( self, event ):
|
||||
|
||||
self._thread_watcher_import.CheckNow()
|
||||
|
@ -2404,11 +2476,18 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
self._UpdateStatus()
|
||||
|
||||
|
||||
def EventCheckPeriod( self, event ):
|
||||
def EventPauseFiles( self, event ):
|
||||
|
||||
check_period = self._thread_check_period.GetValue()
|
||||
self._thread_watcher_import.PausePlayFiles()
|
||||
|
||||
self._thread_watcher_import.SetCheckPeriod( check_period )
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def EventPauseThread( self, event ):
|
||||
|
||||
self._thread_watcher_import.PausePlayThread()
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def EventKeyDown( self, event ):
|
||||
|
@ -2468,20 +2547,6 @@ class ManagementPanelImporterThreadWatcher( ManagementPanelImporter ):
|
|||
|
||||
|
||||
|
||||
def EventPause( self, event ):
|
||||
|
||||
self._thread_watcher_import.PausePlay()
|
||||
|
||||
self._UpdateStatus()
|
||||
|
||||
|
||||
def EventTimesToCheck( self, event ):
|
||||
|
||||
times_to_check = self._thread_times_to_check.GetValue()
|
||||
|
||||
self._thread_watcher_import.SetTimesToCheck( times_to_check )
|
||||
|
||||
|
||||
def SetSearchFocus( self, page_key ):
|
||||
|
||||
if page_key == self._page_key and self._thread_input.IsEditable():
|
||||
|
|
|
@ -118,8 +118,6 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
HG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' )
|
||||
HG.client_controller.sub( self, 'SetFocussedMedia', 'set_focus' )
|
||||
HG.client_controller.sub( self, 'PageHidden', 'page_hidden' )
|
||||
HG.client_controller.sub( self, 'PageShown', 'page_shown' )
|
||||
HG.client_controller.sub( self, 'Collect', 'collect_media' )
|
||||
HG.client_controller.sub( self, 'Sort', 'sort_media' )
|
||||
HG.client_controller.sub( self, 'FileDumped', 'file_dumped' )
|
||||
|
@ -547,34 +545,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
num_selected = self._GetNumSelected()
|
||||
|
||||
( sorted_mime_classes, selected_mime_classes ) = self._GetSortedSelectedMimeClasses()
|
||||
|
||||
if sorted_mime_classes == set( [ 'image' ] ):
|
||||
|
||||
num_files_descriptor = 'image'
|
||||
|
||||
elif sorted_mime_classes == set( [ 'video' ] ):
|
||||
|
||||
num_files_descriptor = 'video'
|
||||
|
||||
else:
|
||||
|
||||
num_files_descriptor = 'file'
|
||||
|
||||
|
||||
if selected_mime_classes == set( [ 'image' ] ):
|
||||
|
||||
selected_files_descriptor = 'image'
|
||||
|
||||
elif selected_mime_classes == set( [ 'video' ] ):
|
||||
|
||||
selected_files_descriptor = 'video'
|
||||
|
||||
else:
|
||||
|
||||
selected_files_descriptor = 'file'
|
||||
|
||||
|
||||
( num_files_descriptor, selected_files_descriptor ) = self._GetSortedSelectedMimeDescriptors()
|
||||
|
||||
if num_files == 1:
|
||||
|
||||
|
@ -585,28 +556,21 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
num_files_string = HydrusData.ConvertIntToPrettyString( num_files ) + ' ' + num_files_descriptor + 's'
|
||||
|
||||
|
||||
if selected_files_descriptor == num_files_descriptor:
|
||||
|
||||
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected )
|
||||
|
||||
else:
|
||||
|
||||
if num_selected == 1:
|
||||
|
||||
selected_files_string = '1 ' + selected_files_descriptor
|
||||
|
||||
else:
|
||||
|
||||
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected ) + ' ' + selected_files_descriptor + 's'
|
||||
|
||||
|
||||
|
||||
s = num_files_string # 23 files
|
||||
|
||||
if num_selected > 0:
|
||||
|
||||
s += ' - '
|
||||
|
||||
if num_selected == 1 or selected_files_descriptor == num_files_descriptor:
|
||||
|
||||
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected )
|
||||
|
||||
else:
|
||||
|
||||
selected_files_string = HydrusData.ConvertIntToPrettyString( num_selected ) + ' ' + selected_files_descriptor + 's'
|
||||
|
||||
|
||||
if num_selected == 1: # 23 files - 1 video selected, file_info
|
||||
|
||||
( selected_media, ) = self._selected_media
|
||||
|
@ -617,9 +581,18 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
num_inbox = sum( ( media.GetNumInbox() for media in self._selected_media ) )
|
||||
|
||||
if num_inbox == num_selected: inbox_phrase = 'all in inbox, '
|
||||
elif num_inbox == 0: inbox_phrase = 'all archived, '
|
||||
else: inbox_phrase = HydrusData.ConvertIntToPrettyString( num_inbox ) + ' in inbox and ' + HydrusData.ConvertIntToPrettyString( num_selected - num_inbox ) + ' archived, '
|
||||
if num_inbox == num_selected:
|
||||
|
||||
inbox_phrase = 'all in inbox, '
|
||||
|
||||
elif num_inbox == 0:
|
||||
|
||||
inbox_phrase = 'all archived, '
|
||||
|
||||
else:
|
||||
|
||||
inbox_phrase = HydrusData.ConvertIntToPrettyString( num_inbox ) + ' in inbox and ' + HydrusData.ConvertIntToPrettyString( num_selected - num_inbox ) + ' archived, '
|
||||
|
||||
|
||||
pretty_total_size = self._GetPrettyTotalSelectedSize()
|
||||
|
||||
|
@ -706,57 +679,63 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
|
||||
|
||||
def _GetSortedSelectedMimeClasses( self ):
|
||||
def _GetSortedSelectedMimeDescriptors( self ):
|
||||
|
||||
sorted_mimes = set()
|
||||
|
||||
for media in self._sorted_media:
|
||||
def GetDescriptor( classes ):
|
||||
|
||||
mime = media.GetMime()
|
||||
if len( classes ) == 0:
|
||||
|
||||
return 'file'
|
||||
|
||||
|
||||
if mime in HC.IMAGES:
|
||||
if len( classes ) == 1:
|
||||
|
||||
sorted_mimes.add( 'image' )
|
||||
( mime, ) = classes
|
||||
|
||||
elif mime in HC.VIDEO:
|
||||
return HC.mime_string_lookup[ mime ]
|
||||
|
||||
sorted_mimes.add( 'video' )
|
||||
|
||||
if len( classes.difference( HC.IMAGES ) ) == 0:
|
||||
|
||||
elif mime in HC.AUDIO:
|
||||
return 'image'
|
||||
|
||||
sorted_mimes.add( 'audio' )
|
||||
elif len( classes.difference( HC.VIDEO ) ) == 0:
|
||||
|
||||
return 'video'
|
||||
|
||||
elif len( classes.difference( HC.AUDIO ) ) == 0:
|
||||
|
||||
return 'audio file'
|
||||
|
||||
else:
|
||||
|
||||
sorted_mimes.add( 'misc' )
|
||||
return 'file'
|
||||
|
||||
|
||||
|
||||
selected_mimes = set()
|
||||
|
||||
for media in self._selected_media:
|
||||
if len( self._sorted_media ) > 1000:
|
||||
|
||||
mime = media.GetMime()
|
||||
sorted_mime_descriptor = 'file'
|
||||
|
||||
if mime in HC.IMAGES:
|
||||
|
||||
selected_mimes.add( 'image' )
|
||||
|
||||
elif mime in HC.VIDEO:
|
||||
|
||||
selected_mimes.add( 'video' )
|
||||
|
||||
elif mime in HC.AUDIO:
|
||||
|
||||
selected_mimes.add( 'audio' )
|
||||
|
||||
else:
|
||||
|
||||
selected_mimes.add( 'misc' )
|
||||
|
||||
else:
|
||||
|
||||
sorted_mimes = { media.GetMime() for media in self._sorted_media }
|
||||
|
||||
sorted_mime_descriptor = GetDescriptor( sorted_mimes )
|
||||
|
||||
|
||||
return ( sorted_mimes, selected_mimes )
|
||||
if len( self._selected_media ) > 1000:
|
||||
|
||||
selected_mime_descriptor = 'file'
|
||||
|
||||
else:
|
||||
|
||||
selected_mimes = { media.GetMime() for media in self._selected_media }
|
||||
|
||||
selected_mime_descriptor = GetDescriptor( selected_mimes )
|
||||
|
||||
|
||||
return ( sorted_mime_descriptor, selected_mime_descriptor )
|
||||
|
||||
|
||||
def _HitMedia( self, media, ctrl, shift ):
|
||||
|
@ -1031,7 +1010,23 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
action = data
|
||||
|
||||
if action == 'manage_file_ratings':
|
||||
if action == 'copy_bmp':
|
||||
|
||||
self._CopyBMPToClipboard()
|
||||
|
||||
elif action == 'copy_file':
|
||||
|
||||
self._CopyFilesToClipboard()
|
||||
|
||||
elif action == 'copy_path':
|
||||
|
||||
self._CopyPathsToClipboard()
|
||||
|
||||
elif action == 'copy_sha256_hash':
|
||||
|
||||
self._CopyHashesToClipboard( 'sha256' )
|
||||
|
||||
elif action == 'manage_file_ratings':
|
||||
|
||||
self._ManageRatings()
|
||||
|
||||
|
@ -1586,22 +1581,16 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
|
|||
|
||||
|
||||
|
||||
def PageHidden( self, page_key ):
|
||||
def PageHidden( self ):
|
||||
|
||||
if page_key == self._page_key:
|
||||
|
||||
HG.client_controller.pub( 'preview_changed', self._page_key, None )
|
||||
|
||||
HG.client_controller.pub( 'preview_changed', self._page_key, None )
|
||||
|
||||
|
||||
def PageShown( self, page_key ):
|
||||
def PageShown( self ):
|
||||
|
||||
if page_key == self._page_key:
|
||||
|
||||
HG.client_controller.pub( 'preview_changed', self._page_key, self._focussed_media )
|
||||
|
||||
self._PublishSelectionChange()
|
||||
|
||||
HG.client_controller.pub( 'preview_changed', self._page_key, self._focussed_media )
|
||||
|
||||
self._PublishSelectionChange()
|
||||
|
||||
|
||||
def ProcessContentUpdates( self, service_keys_to_content_updates ):
|
||||
|
@ -3594,7 +3583,6 @@ class MediaPanelThumbnails( MediaPanel ):
|
|||
( wx.ACCEL_SHIFT, wx.WXK_RIGHT, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'key_shift_right' ) ),
|
||||
( wx.ACCEL_SHIFT, wx.WXK_NUMPAD_RIGHT, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'key_shift_right' ) ),
|
||||
( wx.ACCEL_CTRL, ord( 'A' ), ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'select', 'all' ) ),
|
||||
( wx.ACCEL_CTRL, ord( 'c' ), ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'copy_files' ) ),
|
||||
( wx.ACCEL_CTRL, wx.WXK_SPACE, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'ctrl-space' ) )
|
||||
]
|
||||
|
||||
|
|
|
@ -576,12 +576,14 @@ class Page( wx.SplitterWindow ):
|
|||
|
||||
def PageHidden( self ):
|
||||
|
||||
self._controller.pub( 'page_hidden', self._page_key )
|
||||
self._management_panel.PageHidden()
|
||||
self._media_panel.PageHidden()
|
||||
|
||||
|
||||
def PageShown( self ):
|
||||
|
||||
self._controller.pub( 'page_shown', self._page_key )
|
||||
self._management_panel.PageShown()
|
||||
self._media_panel.PageShown()
|
||||
|
||||
|
||||
def PrepareToHide( self ):
|
||||
|
@ -1998,12 +2000,20 @@ class PagesNotebook( wx.Notebook ):
|
|||
|
||||
page.Reparent( dest_notebook )
|
||||
|
||||
self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )
|
||||
|
||||
|
||||
dest_notebook.InsertPage( insertion_tab_index, page, page.GetName() )
|
||||
shift_down = wx.GetKeyState( wx.WXK_SHIFT )
|
||||
|
||||
self.ShowPage( page )
|
||||
follow_dropped_page = not shift_down
|
||||
|
||||
dest_notebook.InsertPage( insertion_tab_index, page, page.GetName(), select = follow_dropped_page )
|
||||
|
||||
if follow_dropped_page:
|
||||
|
||||
self.ShowPage( page )
|
||||
|
||||
|
||||
self._controller.pub( 'refresh_page_name', source_notebook.GetPageKey() )
|
||||
self._controller.pub( 'refresh_page_name', page.GetPageKey() )
|
||||
|
||||
|
||||
|
|
|
@ -15,9 +15,12 @@ import ClientGUIMenus
|
|||
import ClientGUIScrolledPanels
|
||||
import ClientGUISeedCache
|
||||
import ClientGUITopLevelWindows
|
||||
import ClientNetworkingDomain
|
||||
import ClientParsing
|
||||
import ClientTags
|
||||
import HydrusConstants as HC
|
||||
import HydrusData
|
||||
import HydrusExceptions
|
||||
import HydrusGlobals as HG
|
||||
import HydrusNetwork
|
||||
import HydrusSerialisable
|
||||
|
@ -161,6 +164,47 @@ class EditBandwidthRulesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
return self._bandwidth_rules_ctrl.GetValue()
|
||||
|
||||
|
||||
class EditChooseMultiple( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, choice_tuples ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
self._checkboxes = wx.CheckListBox( self )
|
||||
|
||||
self._checkboxes.SetMinSize( ( 320, 420 ) )
|
||||
|
||||
for ( i, ( label, data, selected ) ) in enumerate( choice_tuples ):
|
||||
|
||||
self._checkboxes.Append( label, data )
|
||||
|
||||
if selected:
|
||||
|
||||
self._checkboxes.Check( i )
|
||||
|
||||
|
||||
|
||||
#
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.AddF( self._checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
datas = []
|
||||
|
||||
for index in self._checkboxes.GetChecked():
|
||||
|
||||
datas.append( self._checkboxes.GetClientData( index ) )
|
||||
|
||||
|
||||
return datas
|
||||
|
||||
|
||||
class EditDuplicateActionOptionsPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, duplicate_action, duplicate_action_options ):
|
||||
|
@ -1190,45 +1234,183 @@ class EditServersideService( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
|
||||
|
||||
class EditChooseMultiple( ClientGUIScrolledPanels.EditPanel ):
|
||||
class EditStringMatchPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, choice_tuples ):
|
||||
def __init__( self, parent, string_match ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
self._checkboxes = wx.CheckListBox( self )
|
||||
self._match_type = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._checkboxes.SetMinSize( ( 320, 420 ) )
|
||||
self._match_type.Append( ClientParsing.STRING_MATCH_ANY, 'any characters' )
|
||||
self._match_type.Append( ClientParsing.STRING_MATCH_FIXED, 'fixed characters' )
|
||||
self._match_type.Append( ClientParsing.STRING_MATCH_FLEXIBLE, 'character set' )
|
||||
self._match_type.Append( ClientParsing.STRING_MATCH_REGEX, 'regex' )
|
||||
|
||||
for ( i, ( label, data, selected ) ) in enumerate( choice_tuples ):
|
||||
|
||||
self._checkboxes.Append( label, data )
|
||||
|
||||
if selected:
|
||||
|
||||
self._checkboxes.Check( i )
|
||||
|
||||
|
||||
self._match_value_text_input = wx.TextCtrl( self )
|
||||
|
||||
self._match_value_flexible_input = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._match_value_flexible_input.Append( ClientParsing.ALPHA, 'alphabetic characters (a-zA-Z)' )
|
||||
self._match_value_flexible_input.Append( ClientParsing.ALPHANUMERIC, 'alphanumeric characters (a-zA-Z0-9)' )
|
||||
self._match_value_flexible_input.Append( ClientParsing.NUMERIC, 'numeric characters (0-9)' )
|
||||
|
||||
self._min_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )
|
||||
self._max_chars = ClientGUICommon.NoneableSpinCtrl( self, min = 1, max = 65535, unit = 'characters', none_phrase = 'no limit' )
|
||||
|
||||
self._example_string = wx.TextCtrl( self )
|
||||
|
||||
self._example_string_matches = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
#
|
||||
|
||||
( match_type, match_value, min_chars, max_chars, example_string ) = string_match.ToTuple()
|
||||
|
||||
self._match_type.SetClientData( match_type )
|
||||
|
||||
if match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
|
||||
|
||||
self._match_value_flexible_input.SetClientData( match_value )
|
||||
|
||||
else:
|
||||
|
||||
self._match_value_flexible_input.SetClientData( ClientParsing.ALPHA )
|
||||
|
||||
self._match_value_text_input.SetValue( match_value )
|
||||
|
||||
|
||||
self._UpdateControls()
|
||||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'match type: ', self._match_type ) )
|
||||
rows.append( ( 'match text: ', self._match_value_text_input ) )
|
||||
rows.append( ( 'match value (character set): ', self._match_value_flexible_input ) )
|
||||
rows.append( ( 'minumum allowed number of characters: ', self._min_chars ) )
|
||||
rows.append( ( 'maximum allowed number of characters: ', self._max_chars ) )
|
||||
rows.append( ( 'example string: ', self._example_string ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.AddF( self._checkboxes, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
vbox.AddF( gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
vbox.AddF( self._example_string_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._match_type.Bind( wx.EVT_CHOICE, self.EventUpdate )
|
||||
self._match_value_text_input.Bind( wx.EVT_TEXT, self.EventUpdate )
|
||||
self._match_value_flexible_input.Bind( wx.EVT_CHOICE, self.EventUpdate )
|
||||
self._min_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
|
||||
self._max_chars.Bind( wx.EVT_SPINCTRL, self.EventUpdate )
|
||||
self._example_string.Bind( wx.EVT_TEXT, self.EventUpdate )
|
||||
|
||||
|
||||
def _GetValue( self ):
|
||||
|
||||
match_type = self._match_type.GetChoice()
|
||||
|
||||
if match_type == ClientParsing.STRING_MATCH_ANY:
|
||||
|
||||
match_value = ''
|
||||
|
||||
elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
|
||||
|
||||
match_value = self._match_value_flexible_input.GetChoice()
|
||||
|
||||
else:
|
||||
|
||||
match_value = self._match_value_text_input.GetValue()
|
||||
|
||||
|
||||
min_chars = self._min_chars.GetValue()
|
||||
max_chars = self._max_chars.GetValue()
|
||||
|
||||
example_string = self._example_string.GetValue()
|
||||
|
||||
string_match = ClientParsing.StringMatch( match_type = match_type, match_value = match_value, min_chars = min_chars, max_chars = max_chars, example_string = example_string )
|
||||
|
||||
return string_match
|
||||
|
||||
|
||||
def _UpdateControls( self ):
|
||||
|
||||
match_type = self._match_type.GetChoice()
|
||||
|
||||
if match_type == ClientParsing.STRING_MATCH_ANY:
|
||||
|
||||
self._match_value_text_input.Disable()
|
||||
self._match_value_flexible_input.Disable()
|
||||
|
||||
elif match_type == ClientParsing.STRING_MATCH_FLEXIBLE:
|
||||
|
||||
self._match_value_text_input.Disable()
|
||||
self._match_value_flexible_input.Enable()
|
||||
|
||||
else:
|
||||
|
||||
self._match_value_text_input.Enable()
|
||||
self._match_value_flexible_input.Disable()
|
||||
|
||||
|
||||
if match_type == ClientParsing.STRING_MATCH_FIXED:
|
||||
|
||||
self._min_chars.SetValue( None )
|
||||
self._max_chars.SetValue( None )
|
||||
|
||||
self._min_chars.Disable()
|
||||
self._max_chars.Disable()
|
||||
|
||||
self._example_string.SetValue( self._match_value_text_input.GetValue() )
|
||||
|
||||
self._example_string_matches.SetLabelText( '' )
|
||||
|
||||
else:
|
||||
|
||||
self._min_chars.Enable()
|
||||
self._max_chars.Enable()
|
||||
|
||||
string_match = self._GetValue()
|
||||
|
||||
( result, reason ) = string_match.Test( self._example_string.GetValue() )
|
||||
|
||||
if result:
|
||||
|
||||
self._example_string_matches.SetLabelText( 'Example matches ok!' )
|
||||
self._example_string_matches.SetForegroundColour( ( 0, 128, 0 ) )
|
||||
|
||||
else:
|
||||
|
||||
self._example_string_matches.SetLabelText( 'Example does not match - ' + reason )
|
||||
self._example_string_matches.SetForegroundColour( ( 128, 0, 0 ) )
|
||||
|
||||
|
||||
|
||||
|
||||
def EventUpdate( self, event ):
|
||||
|
||||
self._UpdateControls()
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
datas = []
|
||||
string_match = self._GetValue()
|
||||
|
||||
for index in self._checkboxes.GetChecked():
|
||||
( result, reason ) = string_match.Test( self._example_string.GetValue() )
|
||||
|
||||
if not result:
|
||||
|
||||
datas.append( self._checkboxes.GetClientData( index ) )
|
||||
wx.MessageBox( 'Please enter an example text that matches the given rules!' )
|
||||
|
||||
raise HydrusExceptions.VetoException()
|
||||
|
||||
|
||||
return datas
|
||||
return string_match
|
||||
|
||||
|
||||
class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
@ -1673,7 +1855,6 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._UpdateSeedInfo()
|
||||
|
||||
|
||||
|
||||
class EditTagCensorPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, tag_censor ):
|
||||
|
@ -2123,3 +2304,310 @@ class EditTagImportOptions( ClientGUIScrolledPanels.EditPanel ):
|
|||
return tag_import_options
|
||||
|
||||
|
||||
class EditURLMatch( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, url_match ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
self._name = wx.TextCtrl( self )
|
||||
|
||||
self._preferred_scheme = ClientGUICommon.BetterChoice( self )
|
||||
|
||||
self._preferred_scheme.Append( 'http', 'http' )
|
||||
self._preferred_scheme.Append( 'https', 'https' )
|
||||
|
||||
self._netloc = wx.TextCtrl( self )
|
||||
|
||||
self._subdomain_is_important = wx.CheckBox( self )
|
||||
|
||||
#
|
||||
|
||||
path_components_panel = ClientGUICommon.StaticBox( self, 'path components' )
|
||||
|
||||
self._path_components = ClientGUIListBoxes.QueueListBox( path_components_panel, self._ConvertPathComponentToString, self._AddPathComponent, self._EditPathComponent )
|
||||
|
||||
#
|
||||
|
||||
parameters_panel = ClientGUICommon.StaticBox( self, 'parameters' )
|
||||
|
||||
self._parameters = ClientGUIListCtrl.BetterListCtrl( parameters_panel, 'url_match_path_components', 5, 20, [ ( 'key', 14 ), ( 'value', -1 ) ], self._ConvertParameterToListCtrlTuples, delete_key_callback = self._DeleteParameters, activation_callback = self._EditParameters )
|
||||
|
||||
# parameter buttons, do it with a new wrapper panel class
|
||||
|
||||
#
|
||||
|
||||
self._example_url = wx.TextCtrl( self )
|
||||
|
||||
self._example_url_matches = ClientGUICommon.BetterStaticText( self )
|
||||
|
||||
#
|
||||
|
||||
name = url_match.GetName()
|
||||
|
||||
self._name.SetValue( name )
|
||||
|
||||
( preferred_scheme, netloc, subdomain_is_important, path_components, parameters, example_url ) = url_match.ToTuple()
|
||||
|
||||
self._preferred_scheme.SelectClientData( preferred_scheme )
|
||||
|
||||
self._netloc.SetValue( netloc )
|
||||
|
||||
self._subdomain_is_important.SetValue( subdomain_is_important )
|
||||
|
||||
self._path_components.AddDatas( path_components )
|
||||
00
|
||||
self._parameters.AddDatas( parameters.items() )
|
||||
|
||||
self._example_url.SetValue( example_url )
|
||||
|
||||
self._UpdateControls()
|
||||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'name: ', self._name ) )
|
||||
rows.append( ( 'preferred scheme: ', self._preferred_scheme ) )
|
||||
rows.append( ( 'network location: ', self._netloc ) )
|
||||
rows.append( ( 'keep subdomains?: ', self._subdomain_is_important ) )
|
||||
|
||||
gridbox_1 = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'example url: ', self._example_url ) )
|
||||
|
||||
gridbox_2 = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.AddF( gridbox_1, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
vbox.AddF( path_components_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
vbox.AddF( parameters_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
vbox.AddF( self._example_url_matches, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
vbox.AddF( gridbox_2, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
#
|
||||
|
||||
self._preferred_scheme.Bind( wx.EVT_CHOICE, self.EventUpdate )
|
||||
self._netloc.Bind( wx.EVT_TEXT, self.EventUpdate )
|
||||
self._subdomain_is_important.Bind( wx.EVT_CHECKBOX, self.EventUpdate )
|
||||
self._example_url.Bind( wx.EVT_TEXT, self.EventUpdate )
|
||||
|
||||
|
||||
def _AddParameters( self ):
|
||||
|
||||
# throw up a dialog to take key text
|
||||
# warn on key conflict I guess
|
||||
|
||||
# throw up a string match dialog to take value
|
||||
|
||||
# add it
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _AddPathComponent( self ):
|
||||
|
||||
string_match = ClientParsing.StringMatch()
|
||||
|
||||
return self._EditPathComponent( string_match )
|
||||
|
||||
|
||||
def _ConvertParameterToListCtrlTuples( self, data ):
|
||||
|
||||
( key, string_match ) = data
|
||||
|
||||
pretty_key = key
|
||||
pretty_string_match = string_match.ToUnicode()
|
||||
|
||||
sort_key = pretty_key
|
||||
sort_string_match = pretty_string_match
|
||||
|
||||
display_tuple = ( pretty_key, pretty_string_match )
|
||||
sort_tuple = ( sort_key, sort_string_match )
|
||||
|
||||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _ConvertPathComponentToString( self, path_component ):
|
||||
|
||||
return path_component.ToUnicode()
|
||||
|
||||
|
||||
def _DeleteParameters( self ):
|
||||
|
||||
# ask for certain, then do it
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _EditParameters( self ):
|
||||
|
||||
# for each in list, throw up dialog for key, value
|
||||
# delete and readd
|
||||
# break on cancel, etc...
|
||||
# sort at the end
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _EditPathComponent( self, string_match ):
|
||||
|
||||
with ClientGUITopLevelWindows.DialogEdit( self, 'edit path component' ) as dlg:
|
||||
|
||||
panel = EditStringMatchPanel( dlg, string_match )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
if dlg.Showmodal() == wx.ID_OK:
|
||||
|
||||
new_string_match = panel.GetValue()
|
||||
|
||||
return ( True, new_string_match )
|
||||
|
||||
else:
|
||||
|
||||
return ( False, None )
|
||||
|
||||
|
||||
|
||||
|
||||
def _GetValue( self ):
|
||||
|
||||
name = self._name.GetValue()
|
||||
preferred_scheme = self._preferred_scheme.GetChoice()
|
||||
netloc = self._netloc.GetValue()
|
||||
subdomain_is_important = self._subdomain_is_important.GetValue()
|
||||
path_components = self._path_components.GetData()
|
||||
parameters = self._parameters.GetData()
|
||||
example_url = self._example_url.GetValue()
|
||||
|
||||
url_match = ClientNetworkingDomain.URLMatch( name, preferred_scheme = preferred_scheme, netloc = netloc, subdomain_is_important = subdomain_is_important, path_components = path_components, parameters = parameters, example_url = example_url )
|
||||
|
||||
return url_match
|
||||
|
||||
|
||||
def _UpdateControls( self ):
|
||||
|
||||
url_match = self._GetValue()
|
||||
|
||||
( result, reason ) = url_match.Test( self._example_url.GetValue() )
|
||||
|
||||
if result:
|
||||
|
||||
self._example_url_matches.SetLabelText( 'Example matches ok!' )
|
||||
self._example_url_matches.SetForegroundColour( ( 0, 128, 0 ) )
|
||||
|
||||
else:
|
||||
|
||||
self._example_url_matches.SetLabelText( 'Example does not match - ' + reason )
|
||||
self._example_url_matches.SetForegroundColour( ( 128, 0, 0 ) )
|
||||
|
||||
|
||||
|
||||
def EventUpdate( self, event ):
|
||||
|
||||
self._UpdateControls()
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
url_match = self._GetValue()
|
||||
|
||||
( result, reason ) = url_match.Test( self._example_url.GetValue() )
|
||||
|
||||
if not result:
|
||||
|
||||
wx.MessageBox( 'Please enter an example url that matches the given rules!' )
|
||||
|
||||
raise HydrusExceptions.VetoException()
|
||||
|
||||
|
||||
return url_match
|
||||
|
||||
|
||||
class EditWatcherOptions( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, watcher_options ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
help_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.help, self._ShowHelp )
|
||||
help_button.SetToolTipString( 'Show help regarding these checker options.' )
|
||||
|
||||
# add statictext or whatever that will update on any updates above to say 'given velocity of blah and last check at blah, next check in 5 mins'
|
||||
# or indeed this could just take the seed cache and last check of the caller, if there is one
|
||||
# this would be more useful to the user, to know 'right, on ok, it'll refresh in 30 mins'
|
||||
|
||||
self._intended_files_per_check = wx.SpinCtrl( self, min = 1, max = 1000 )
|
||||
|
||||
self._never_faster_than = ClientGUICommon.TimeDeltaCtrl( self, min = 30, days = True, hours = True, minutes = True, seconds = True )
|
||||
|
||||
self._never_slower_than = ClientGUICommon.TimeDeltaCtrl( self, min = 600, days = True, hours = True, minutes = True )
|
||||
|
||||
self._death_file_velocity = ClientGUICommon.VelocityCtrl( self, min_time_delta = 60, days = True, hours = True, minutes = True, per_phrase = 'in', unit = 'files' )
|
||||
|
||||
#
|
||||
|
||||
( intended_files_per_check, never_faster_than, never_slower_than, death_file_velocity ) = watcher_options.ToTuple()
|
||||
|
||||
self._intended_files_per_check.SetValue( intended_files_per_check )
|
||||
self._never_faster_than.SetValue( never_faster_than )
|
||||
self._never_slower_than.SetValue( never_slower_than )
|
||||
self._death_file_velocity.SetValue( death_file_velocity )
|
||||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'intended new files per check: ', self._intended_files_per_check ) )
|
||||
rows.append( ( 'stop checking if new files found falls below: ', self._death_file_velocity ) )
|
||||
rows.append( ( 'never check faster than once per: ', self._never_faster_than ) )
|
||||
rows.append( ( 'never check slower than once per: ', self._never_slower_than ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
help_hbox = wx.BoxSizer( wx.HORIZONTAL )
|
||||
|
||||
st = ClientGUICommon.BetterStaticText( self, 'help for this panel -->' )
|
||||
|
||||
st.SetForegroundColour( wx.Colour( 0, 0, 255 ) )
|
||||
|
||||
help_hbox.AddF( st, CC.FLAGS_VCENTER )
|
||||
help_hbox.AddF( help_button, CC.FLAGS_VCENTER )
|
||||
|
||||
vbox.AddF( help_hbox, CC.FLAGS_LONE_BUTTON )
|
||||
vbox.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
|
||||
def _ShowHelp( self ):
|
||||
|
||||
help = 'PROTIP: Do not change anything here unless you understand what it means!'
|
||||
help += os.linesep * 2
|
||||
help += 'After its initialisation check, the checker times future checks so that it will probably find the same specified number of new files each time. When files are being posted frequently, it will check more often. When things are slow, it will slow down as well.'
|
||||
help += os.linesep * 2
|
||||
help += 'For instance, if it were set to try for 5 new files with every check, and at the last check it knew that the last 24 hours had produced 10 new files, it would check again 12 hours later. When that check was done and any new files found, it would then recalculate and repeat the process.'
|
||||
help += os.linesep * 2
|
||||
help += 'If the \'file velocity\' drops below a certain amount, the checker considers the source of files dead and will stop checking. If it falls into this state but you think there might have been a rush of new files, hit the \'check now\' button in an attempt to revive the checker. If there are new files, it will start checking again until they drop off once more.'
|
||||
|
||||
wx.MessageBox( help )
|
||||
|
||||
|
||||
def GetValue( self ):
|
||||
|
||||
intended_files_per_check = self._intended_files_per_check.GetValue()
|
||||
never_faster_than = self._never_faster_than.GetValue()
|
||||
never_slower_than = self._never_slower_than.GetValue()
|
||||
death_file_velocity = self._death_file_velocity.GetValue()
|
||||
|
||||
return ClientData.WatcherOptions( intended_files_per_check, never_faster_than, never_slower_than, death_file_velocity )
|
||||
|
||||
|
||||
|
|
|
@ -1865,11 +1865,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
thread_checker = ClientGUICommon.StaticBox( self, 'thread checker' )
|
||||
|
||||
self._thread_times_to_check = wx.SpinCtrl( thread_checker, min = 0, max = 65536 )
|
||||
self._thread_times_to_check.SetToolTipString( 'how many times the thread checker will check' )
|
||||
watcher_options = self._new_options.GetDefaultThreadWatcherOptions()
|
||||
|
||||
self._thread_check_period = ClientGUICommon.TimeDeltaButton( thread_checker, min = 30, days = True, hours = True, minutes = True, seconds = True )
|
||||
self._thread_check_period.SetToolTipString( 'how long the checker will wait between checks' )
|
||||
self._thread_watcher_options = ClientGUIScrolledPanelsEdit.EditWatcherOptions( thread_checker, watcher_options )
|
||||
|
||||
#
|
||||
|
||||
|
@ -1877,12 +1875,6 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._gallery_file_limit.SetValue( HC.options[ 'gallery_file_limit' ] )
|
||||
|
||||
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
|
||||
|
||||
self._thread_times_to_check.SetValue( times_to_check )
|
||||
|
||||
self._thread_check_period.SetValue( check_period )
|
||||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
@ -1899,14 +1891,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'default number of times to check: ', self._thread_times_to_check ) )
|
||||
rows.append( ( 'default wait between checks: ', self._thread_check_period ) )
|
||||
|
||||
gridbox = ClientGUICommon.WrapInGrid( thread_checker, rows )
|
||||
|
||||
thread_checker.AddF( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
||||
thread_checker.AddF( self._thread_watcher_options, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
|
||||
#
|
||||
|
||||
|
@ -1923,7 +1908,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
self._new_options.SetBoolean( 'verify_regular_https', self._verify_regular_https.GetValue() )
|
||||
HC.options[ 'gallery_file_limit' ] = self._gallery_file_limit.GetValue()
|
||||
HC.options[ 'thread_checker_timings' ] = ( self._thread_times_to_check.GetValue(), self._thread_check_period.GetValue() )
|
||||
|
||||
self._new_options.SetDefaultThreadWatcherOptions( self._thread_watcher_options.GetValue() )
|
||||
|
||||
|
||||
|
||||
|
@ -6244,6 +6230,72 @@ class ManageURLsPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
|
||||
|
||||
class ManageURLMatchesPanel( ClientGUIScrolledPanels.ManagePanel ):
|
||||
|
||||
def __init__( self, parent ):
|
||||
|
||||
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
|
||||
|
||||
self._url_matches = ClientGUIListCtrl.BetterListCtrl( self, 'manage_url_matches', 15, 30, [ ( 'name', 20 ), ( 'example url', -1 ) ], self._ConvertURLMatchToListCtrlTuples, delete_key_callback = self._Delete, activation_callback = self._Edit )
|
||||
|
||||
# add, edit, delete buttons
|
||||
# it would be nice to wrap this up in a panel rather than writing it out manually
|
||||
|
||||
#
|
||||
|
||||
# load them from the db, populate the listctrl
|
||||
|
||||
self._original_names = set()
|
||||
# set self._original_names
|
||||
|
||||
#
|
||||
|
||||
vbox = wx.BoxSizer( wx.VERTICAL )
|
||||
|
||||
vbox.AddF( self._url_matches, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.SetSizer( vbox )
|
||||
|
||||
|
||||
def _Add( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _ConvertURLMatchToListCtrlTuples( self, url_match ):
|
||||
|
||||
name = url_match.GetName()
|
||||
example_url = url_match.GetExampleURL()
|
||||
|
||||
display_tuple = ( name, example_url )
|
||||
sort_tuple = display_tuple
|
||||
|
||||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
||||
def _Delete( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _Edit( self ):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def CommitChanges( self ):
|
||||
|
||||
datas = self._url_matches.GetData()
|
||||
|
||||
datas_names = { data.GetName() for data in datas }
|
||||
|
||||
to_delete = [ name for name in self._original_names if name not in datas_names ]
|
||||
|
||||
# save datas to db
|
||||
|
||||
# delete names from db
|
||||
|
||||
|
||||
class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
|
||||
|
||||
def __init__( self, parent, missing_locations ):
|
||||
|
@ -6409,3 +6461,4 @@ class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
|
|||
|
||||
HG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows )
|
||||
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
# 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', 20 ), ( 'last modified', 20 ), ( 'note', 30 ) ]
|
||||
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 )
|
||||
|
||||
|
@ -59,16 +59,26 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
sort_tuple = self._seed_cache.GetSeedInfo( seed )
|
||||
|
||||
( seed_index, seed, status, added_timestamp, last_modified_timestamp, note ) = sort_tuple
|
||||
( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note ) = sort_tuple
|
||||
|
||||
pretty_seed_index = HydrusData.ConvertIntToPrettyString( seed_index )
|
||||
pretty_seed = HydrusData.ToUnicode( seed )
|
||||
pretty_status = CC.status_string_lookup[ status ]
|
||||
pretty_added = HydrusData.ConvertTimestampToPrettyAgo( added_timestamp ) + ' ago'
|
||||
pretty_modified = HydrusData.ConvertTimestampToPrettyAgo( last_modified_timestamp ) + ' ago'
|
||||
|
||||
if source_timestamp is None:
|
||||
|
||||
pretty_source_time = 'unknown'
|
||||
|
||||
else:
|
||||
|
||||
pretty_source_time = HydrusData.ConvertTimestampToHumanPrettyTime( source_timestamp )
|
||||
|
||||
|
||||
pretty_note = note.split( os.linesep )[0]
|
||||
|
||||
display_tuple = ( pretty_seed_index, pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_note )
|
||||
display_tuple = ( pretty_seed_index, pretty_seed, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note )
|
||||
|
||||
return ( display_tuple, sort_tuple )
|
||||
|
||||
|
@ -79,7 +89,7 @@ class EditSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
for seed in self._list_ctrl.GetData( only_selected = True ):
|
||||
|
||||
( seed_index, seed, status, added_timestamp, last_modified_timestamp, note ) = self._seed_cache.GetSeedInfo( seed )
|
||||
( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note ) = self._seed_cache.GetSeedInfo( seed )
|
||||
|
||||
if note != '':
|
||||
|
||||
|
|
|
@ -1171,6 +1171,22 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
self._paths_cache.AddSeeds( paths )
|
||||
|
||||
for path in paths:
|
||||
|
||||
try:
|
||||
|
||||
s = os.stat( path )
|
||||
|
||||
source_time = min( s.st_mtime, s.st_ctime )
|
||||
|
||||
self._paths_cache.UpdateSeedSourceTime( path, source_time )
|
||||
|
||||
except:
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
self._file_import_options = file_import_options
|
||||
self._paths_to_tags = paths_to_tags
|
||||
|
@ -2438,7 +2454,7 @@ HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIAL
|
|||
class SeedCache( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SEED_CACHE
|
||||
SERIALISABLE_VERSION = 5
|
||||
SERIALISABLE_VERSION = 6
|
||||
|
||||
def __init__( self ):
|
||||
|
||||
|
@ -2496,20 +2512,6 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
self._dirty = False
|
||||
|
||||
|
||||
def _GetSeedTuple( self, seed ):
|
||||
|
||||
seed_index = self._seeds_to_indices[ seed ]
|
||||
|
||||
seed_info = self._seeds_to_info[ seed ]
|
||||
|
||||
status = seed_info[ 'status' ]
|
||||
added_timestamp = seed_info[ 'added_timestamp' ]
|
||||
last_modified_timestamp = seed_info[ 'last_modified_timestamp' ]
|
||||
note = seed_info[ 'note' ]
|
||||
|
||||
return ( seed_index, seed, status, added_timestamp, last_modified_timestamp, note )
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -2527,6 +2529,20 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def _GetSourceTimestamp( self, seed ):
|
||||
|
||||
seed_info = self._seeds_to_info[ seed ]
|
||||
|
||||
source_timestamp = seed_info[ 'source_timestamp' ]
|
||||
|
||||
if source_timestamp is None:
|
||||
|
||||
source_timestamp = seed_info[ 'added_timestamp' ] # decent fallback compromise
|
||||
|
||||
|
||||
return source_timestamp
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -2680,6 +2696,20 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
return ( 5, new_serialisable_info )
|
||||
|
||||
|
||||
if version == 5:
|
||||
|
||||
new_serialisable_info = []
|
||||
|
||||
for ( seed, seed_info ) in old_serialisable_info:
|
||||
|
||||
seed_info[ 'source_timestamp' ] = None
|
||||
|
||||
new_serialisable_info.append( ( seed, seed_info ) )
|
||||
|
||||
|
||||
return ( 6, new_serialisable_info )
|
||||
|
||||
|
||||
|
||||
def AddSeeds( self, seeds ):
|
||||
|
||||
|
@ -2719,6 +2749,7 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
seed_info[ 'status' ] = CC.STATUS_UNKNOWN
|
||||
seed_info[ 'added_timestamp' ] = now
|
||||
seed_info[ 'last_modified_timestamp' ] = now
|
||||
seed_info[ 'source_timestamp' ] = None
|
||||
seed_info[ 'note' ] = ''
|
||||
|
||||
self._seeds_to_info[ seed ] = seed_info
|
||||
|
@ -2774,6 +2805,16 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
HG.client_controller.pub( 'seed_cache_seeds_updated', self._seed_cache_key, ( seed, ) )
|
||||
|
||||
|
||||
def GetEarliestTimestamp( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
earliest_timestamp = min( ( self._GetSourceTimestamp( seed ) for seed in self._seeds_ordered ) )
|
||||
|
||||
|
||||
return earliest_timestamp
|
||||
|
||||
|
||||
def GetNextSeed( self, status ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -2792,6 +2833,26 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
return None
|
||||
|
||||
|
||||
def GetNumNewFilesSince( self, since ):
|
||||
|
||||
num_files = 0
|
||||
|
||||
with self._lock:
|
||||
|
||||
for seed in self._seeds_ordered:
|
||||
|
||||
source_timestamp = self._GetSourceTimestamp( seed )
|
||||
|
||||
if source_timestamp > since:
|
||||
|
||||
num_files += 1
|
||||
|
||||
|
||||
|
||||
|
||||
return num_files
|
||||
|
||||
|
||||
def GetSeedCacheKey( self ):
|
||||
|
||||
return self._seed_cache_key
|
||||
|
@ -2855,7 +2916,17 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
return self._GetSeedTuple( seed )
|
||||
seed_index = self._seeds_to_indices[ seed ]
|
||||
|
||||
seed_info = self._seeds_to_info[ seed ]
|
||||
|
||||
status = seed_info[ 'status' ]
|
||||
added_timestamp = seed_info[ 'added_timestamp' ]
|
||||
last_modified_timestamp = seed_info[ 'last_modified_timestamp' ]
|
||||
source_timestamp = seed_info[ 'source_timestamp' ]
|
||||
note = seed_info[ 'note' ]
|
||||
|
||||
return ( seed_index, seed, status, added_timestamp, last_modified_timestamp, source_timestamp, note )
|
||||
|
||||
|
||||
|
||||
|
@ -2948,6 +3019,18 @@ class SeedCache( HydrusSerialisable.SerialisableBase ):
|
|||
HG.client_controller.pub( 'seed_cache_seeds_updated', self._seed_cache_key, seeds_to_delete )
|
||||
|
||||
|
||||
def UpdateSeedSourceTime( self, seed, source_timestamp ):
|
||||
|
||||
# this is ugly--this should all be moved to the seed when it becomes a cleverer object, rather than jimmying it through the cache
|
||||
|
||||
with self._lock:
|
||||
|
||||
seed_info = self._seeds_to_info[ seed ]
|
||||
|
||||
seed_info[ 'source_timestamp' ] = source_timestamp
|
||||
|
||||
|
||||
|
||||
def UpdateSeedStatus( self, seed, status, note = '', exception = None ):
|
||||
|
||||
with self._lock:
|
||||
|
@ -3892,7 +3975,7 @@ HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIAL
|
|||
class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_THREAD_WATCHER_IMPORT
|
||||
SERIALISABLE_VERSION = 1
|
||||
SERIALISABLE_VERSION = 2
|
||||
|
||||
MIN_CHECK_PERIOD = 30
|
||||
|
||||
|
@ -3906,17 +3989,16 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
tag_import_options = new_options.GetDefaultTagImportOptions( ClientDownloading.GalleryIdentifier( HC.SITE_TYPE_THREAD_WATCHER ) )
|
||||
|
||||
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
|
||||
|
||||
self._thread_url = ''
|
||||
self._urls_cache = SeedCache()
|
||||
self._urls_to_filenames = {}
|
||||
self._urls_to_md5_base64 = {}
|
||||
self._watcher_options = new_options.GetDefaultThreadWatcherOptions()
|
||||
self._file_import_options = file_import_options
|
||||
self._tag_import_options = tag_import_options
|
||||
self._times_to_check = times_to_check
|
||||
self._check_period = check_period
|
||||
self._last_time_checked = 0
|
||||
self._last_check_time = 0
|
||||
|
||||
self._next_check_time = None
|
||||
|
||||
self._download_control_file_set = None
|
||||
self._download_control_file_clear = None
|
||||
|
@ -3924,10 +4006,12 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
self._download_control_thread_clear = None
|
||||
|
||||
self._check_now = False
|
||||
self._paused = False
|
||||
self._files_paused = False
|
||||
self._thread_paused = False
|
||||
|
||||
self._watcher_status = ''
|
||||
self._file_velocity_status = ''
|
||||
self._current_action = ''
|
||||
self._watcher_status = ''
|
||||
|
||||
self._thread_key = HydrusData.GenerateKey()
|
||||
|
||||
|
@ -3937,24 +4021,210 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
self._new_thread_event = threading.Event()
|
||||
|
||||
|
||||
def _CheckThread( self, page_key ):
|
||||
|
||||
error_occurred = False
|
||||
watcher_status_should_stick = True
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._watcher_status = 'checking thread'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
json_url = ClientDownloading.GetImageboardThreadJSONURL( self._thread_url )
|
||||
|
||||
network_job = ClientNetworking.NetworkJobThreadWatcher( self._thread_key, 'GET', json_url )
|
||||
|
||||
network_job.OverrideBandwidth()
|
||||
|
||||
HG.client_controller.network_engine.AddJob( network_job )
|
||||
|
||||
with self._lock:
|
||||
|
||||
if self._download_control_thread_set is not None:
|
||||
|
||||
wx.CallAfter( self._download_control_thread_set, network_job )
|
||||
|
||||
|
||||
|
||||
try:
|
||||
|
||||
network_job.WaitUntilDone()
|
||||
|
||||
finally:
|
||||
|
||||
if self._download_control_thread_clear is not None:
|
||||
|
||||
wx.CallAfter( self._download_control_thread_clear )
|
||||
|
||||
|
||||
|
||||
raw_json = network_job.GetContent()
|
||||
|
||||
file_infos = ClientDownloading.ParseImageboardFileURLsFromJSON( self._thread_url, raw_json )
|
||||
|
||||
new_urls = []
|
||||
new_urls_set = set()
|
||||
|
||||
file_urls_to_source_timestamps = {}
|
||||
|
||||
for ( file_url, file_md5_base64, file_original_filename, source_timestamp ) in file_infos:
|
||||
|
||||
if not self._urls_cache.HasSeed( file_url ) and not file_url in new_urls_set:
|
||||
|
||||
new_urls.append( file_url )
|
||||
new_urls_set.add( file_url )
|
||||
|
||||
self._urls_to_filenames[ file_url ] = file_original_filename
|
||||
|
||||
if file_md5_base64 is not None:
|
||||
|
||||
self._urls_to_md5_base64[ file_url ] = file_md5_base64
|
||||
|
||||
|
||||
file_urls_to_source_timestamps[ file_url ] = source_timestamp
|
||||
|
||||
|
||||
|
||||
self._urls_cache.AddSeeds( new_urls )
|
||||
|
||||
for ( file_url, source_timestamp ) in file_urls_to_source_timestamps.items():
|
||||
|
||||
self._urls_cache.UpdateSeedSourceTime( file_url, source_timestamp )
|
||||
|
||||
|
||||
num_new = len( new_urls )
|
||||
|
||||
watcher_status = 'thread checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls'
|
||||
watcher_status_should_stick = False
|
||||
|
||||
if num_new > 0:
|
||||
|
||||
self._new_files_event.set()
|
||||
|
||||
|
||||
except HydrusExceptions.NotFoundException:
|
||||
|
||||
error_occurred = True
|
||||
|
||||
watcher_status = 'thread 404'
|
||||
|
||||
except Exception as e:
|
||||
|
||||
error_occurred = True
|
||||
|
||||
watcher_status = HydrusData.ToUnicode( e )
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
|
||||
with self._lock:
|
||||
|
||||
if self._check_now:
|
||||
|
||||
self._check_now = False
|
||||
|
||||
|
||||
self._watcher_status = watcher_status
|
||||
|
||||
self._last_check_time = HydrusData.GetNow()
|
||||
|
||||
self._UpdateFileVelocityStatus()
|
||||
|
||||
self._UpdateNextCheckTime()
|
||||
|
||||
if error_occurred:
|
||||
|
||||
self._thread_paused = True
|
||||
|
||||
|
||||
|
||||
if error_occurred:
|
||||
|
||||
time.sleep( 5 )
|
||||
|
||||
|
||||
if not watcher_status_should_stick:
|
||||
|
||||
time.sleep( 5 )
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._watcher_status = ''
|
||||
|
||||
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
serialisable_url_cache = self._urls_cache.GetSerialisableTuple()
|
||||
serialisable_watcher_options = self._watcher_options.GetSerialisableTuple()
|
||||
serialisable_file_options = self._file_import_options.GetSerialisableTuple()
|
||||
serialisable_tag_options = self._tag_import_options.GetSerialisableTuple()
|
||||
|
||||
return ( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, self._times_to_check, self._check_period, self._last_time_checked, self._paused )
|
||||
return ( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, self._last_check_time, self._files_paused, self._thread_paused )
|
||||
|
||||
|
||||
def _HasThread( self ):
|
||||
|
||||
return self._thread_url != ''
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, self._times_to_check, self._check_period, self._last_time_checked, self._paused ) = serialisable_info
|
||||
( self._thread_url, serialisable_url_cache, self._urls_to_filenames, self._urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, self._last_check_time, self._files_paused, self._thread_paused ) = serialisable_info
|
||||
|
||||
self._urls_cache = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_url_cache )
|
||||
self._watcher_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_watcher_options )
|
||||
self._file_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_file_options )
|
||||
self._tag_import_options = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_tag_options )
|
||||
|
||||
|
||||
def _UpdateFileVelocityStatus( self ):
|
||||
|
||||
self._file_velocity_status = self._watcher_options.GetPrettyCurrentVelocity( self._urls_cache, self._last_check_time )
|
||||
|
||||
|
||||
def _UpdateNextCheckTime( self ):
|
||||
|
||||
if self._check_now:
|
||||
|
||||
self._next_check_time = self._last_check_time + self.MIN_CHECK_PERIOD
|
||||
|
||||
else:
|
||||
|
||||
if self._watcher_options.IsDead( self._urls_cache, self._last_check_time ):
|
||||
|
||||
self._watcher_status = 'thread is dead'
|
||||
|
||||
self._thread_paused = True
|
||||
|
||||
|
||||
self._next_check_time = self._watcher_options.GetNextCheckTime( self._urls_cache, self._last_check_time )
|
||||
|
||||
|
||||
|
||||
def _UpdateSerialisableInfo( self, version, old_serialisable_info ):
|
||||
|
||||
if version == 1:
|
||||
|
||||
( thread_url, serialisable_url_cache, urls_to_filenames, urls_to_md5_base64, serialisable_file_options, serialisable_tag_options, times_to_check, check_period, last_check_time, paused ) = old_serialisable_info
|
||||
|
||||
watcher_options = ClientData.WatcherOptions( intended_files_per_check = 8, never_faster_than = 300, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
|
||||
|
||||
serialisable_watcher_options = watcher_options.GetSerialisableTuple()
|
||||
|
||||
files_paused = paused
|
||||
thread_paused = paused
|
||||
|
||||
new_serialisable_info = ( thread_url, serialisable_url_cache, urls_to_filenames, urls_to_md5_base64, serialisable_watcher_options, serialisable_file_options, serialisable_tag_options, last_check_time, files_paused, thread_paused )
|
||||
|
||||
return ( 2, new_serialisable_info )
|
||||
|
||||
|
||||
|
||||
def _WorkOnFiles( self, page_key ):
|
||||
|
||||
file_url = self._urls_cache.GetNextSeed( CC.STATUS_UNKNOWN )
|
||||
|
@ -4139,168 +4409,11 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
return True
|
||||
|
||||
|
||||
def _WorkOnThread( self, page_key ):
|
||||
|
||||
error_occurred = False
|
||||
|
||||
with self._lock:
|
||||
|
||||
p1 = self._check_now and HydrusData.TimeHasPassed( self._last_time_checked + self.MIN_CHECK_PERIOD )
|
||||
p2 = self._times_to_check > 0 and HydrusData.TimeHasPassed( self._last_time_checked + self._check_period )
|
||||
|
||||
|
||||
if p1 or p2:
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._watcher_status = 'checking thread'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
json_url = ClientDownloading.GetImageboardThreadJSONURL( self._thread_url )
|
||||
|
||||
network_job = ClientNetworking.NetworkJobThreadWatcher( self._thread_key, 'GET', json_url )
|
||||
|
||||
network_job.OverrideBandwidth()
|
||||
|
||||
HG.client_controller.network_engine.AddJob( network_job )
|
||||
|
||||
with self._lock:
|
||||
|
||||
if self._download_control_thread_set is not None:
|
||||
|
||||
wx.CallAfter( self._download_control_thread_set, network_job )
|
||||
|
||||
|
||||
|
||||
try:
|
||||
|
||||
network_job.WaitUntilDone()
|
||||
|
||||
finally:
|
||||
|
||||
if self._download_control_thread_clear is not None:
|
||||
|
||||
wx.CallAfter( self._download_control_thread_clear )
|
||||
|
||||
|
||||
|
||||
raw_json = network_job.GetContent()
|
||||
|
||||
file_infos = ClientDownloading.ParseImageboardFileURLsFromJSON( self._thread_url, raw_json )
|
||||
|
||||
new_urls = []
|
||||
new_urls_set = set()
|
||||
|
||||
for ( file_url, file_md5_base64, file_original_filename ) in file_infos:
|
||||
|
||||
if not self._urls_cache.HasSeed( file_url ) and not file_url in new_urls_set:
|
||||
|
||||
new_urls.append( file_url )
|
||||
new_urls_set.add( file_url )
|
||||
|
||||
self._urls_to_filenames[ file_url ] = file_original_filename
|
||||
|
||||
if file_md5_base64 is not None:
|
||||
|
||||
self._urls_to_md5_base64[ file_url ] = file_md5_base64
|
||||
|
||||
|
||||
|
||||
|
||||
self._urls_cache.AddSeeds( new_urls )
|
||||
|
||||
num_new = len( new_urls )
|
||||
|
||||
watcher_status = 'thread checked OK - ' + HydrusData.ConvertIntToPrettyString( num_new ) + ' new urls'
|
||||
|
||||
if num_new > 0:
|
||||
|
||||
self._new_files_event.set()
|
||||
|
||||
|
||||
except HydrusExceptions.NotFoundException:
|
||||
|
||||
error_occurred = True
|
||||
|
||||
watcher_status = 'thread 404'
|
||||
|
||||
with self._lock:
|
||||
|
||||
for i in range( self._times_to_check ):
|
||||
|
||||
HG.client_controller.pub( 'decrement_times_to_check', page_key )
|
||||
|
||||
|
||||
self._times_to_check = 0
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
error_occurred = True
|
||||
|
||||
watcher_status = HydrusData.ToUnicode( e )
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
|
||||
with self._lock:
|
||||
|
||||
if self._check_now:
|
||||
|
||||
self._check_now = False
|
||||
|
||||
else:
|
||||
|
||||
self._times_to_check -= 1
|
||||
|
||||
HG.client_controller.pub( 'decrement_times_to_check', page_key )
|
||||
|
||||
|
||||
self._last_time_checked = HydrusData.GetNow()
|
||||
|
||||
|
||||
else:
|
||||
|
||||
with self._lock:
|
||||
|
||||
if self._check_now or self._times_to_check > 0:
|
||||
|
||||
if self._check_now:
|
||||
|
||||
delay = self.MIN_CHECK_PERIOD
|
||||
|
||||
else:
|
||||
|
||||
delay = self._check_period
|
||||
|
||||
|
||||
watcher_status = 'checking again ' + HydrusData.ConvertTimestampToPrettyPending( self._last_time_checked + delay )
|
||||
|
||||
else:
|
||||
|
||||
watcher_status = 'checking finished'
|
||||
|
||||
|
||||
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._watcher_status = watcher_status
|
||||
|
||||
|
||||
if error_occurred:
|
||||
|
||||
time.sleep( 5 )
|
||||
|
||||
|
||||
|
||||
def _THREADWorkOnFiles( self, page_key ):
|
||||
|
||||
while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ):
|
||||
|
||||
if self._paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
|
||||
if self._files_paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
|
||||
|
||||
self._new_files_event.wait( 5 )
|
||||
|
||||
|
@ -4344,7 +4457,15 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
while not ( HG.view_shutdown or HG.client_controller.PageCompletelyDestroyed( page_key ) ):
|
||||
|
||||
if self._paused or HG.client_controller.PageClosedButNotDestroyed( page_key ):
|
||||
with self._lock:
|
||||
|
||||
able_to_check = self._HasThread() and not self._thread_paused
|
||||
check_due = HydrusData.TimeHasPassed( self._next_check_time )
|
||||
|
||||
time_to_check = able_to_check and check_due
|
||||
|
||||
|
||||
if not time_to_check or HG.client_controller.PageClosedButNotDestroyed( page_key ):
|
||||
|
||||
self._new_thread_event.wait( 5 )
|
||||
|
||||
|
@ -4352,14 +4473,9 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
try:
|
||||
|
||||
if self._thread_url != '':
|
||||
|
||||
self._WorkOnThread( page_key )
|
||||
|
||||
self._CheckThread( page_key )
|
||||
|
||||
time.sleep( 1 )
|
||||
|
||||
# 1s wait here regardless so the text countdown status updates every sec
|
||||
time.sleep( 5 )
|
||||
|
||||
HG.client_controller.WaitUntilPubSubsEmpty()
|
||||
|
||||
|
@ -4381,6 +4497,10 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
self._check_now = True
|
||||
|
||||
self._thread_paused = False
|
||||
|
||||
self._UpdateNextCheckTime()
|
||||
|
||||
self._new_thread_event.set()
|
||||
|
||||
|
||||
|
@ -4391,7 +4511,7 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
finished = not self._urls_cache.WorkToDo()
|
||||
|
||||
return not finished and not self._paused
|
||||
return not finished and not self._files_paused
|
||||
|
||||
|
||||
|
||||
|
@ -4404,7 +4524,7 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
return ( self._thread_url, self._file_import_options, self._tag_import_options, self._times_to_check, self._check_period )
|
||||
return ( self._thread_url, self._file_import_options, self._tag_import_options )
|
||||
|
||||
|
||||
|
||||
|
@ -4412,9 +4532,15 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
finished = not self._urls_cache.WorkToDo()
|
||||
return ( self._current_action, self._files_paused, self._file_velocity_status, self._next_check_time, self._watcher_status, self._check_now, self._thread_paused )
|
||||
|
||||
return ( self._current_action, self._watcher_status, self._check_now, self._paused )
|
||||
|
||||
|
||||
def GetWatcherOptions( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
return self._watcher_options
|
||||
|
||||
|
||||
|
||||
|
@ -4422,26 +4548,34 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
with self._lock:
|
||||
|
||||
return self._thread_url != ''
|
||||
return self._HasThread()
|
||||
|
||||
|
||||
|
||||
def PausePlay( self ):
|
||||
def PausePlayFiles( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._paused = not self._paused
|
||||
self._files_paused = not self._files_paused
|
||||
|
||||
self._new_files_event.set()
|
||||
self._new_thread_event.set()
|
||||
|
||||
|
||||
|
||||
def SetCheckPeriod( self, check_period ):
|
||||
def PausePlayThread( self ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._check_period = max( self.MIN_CHECK_PERIOD, check_period )
|
||||
if self._thread_paused and self._watcher_options.IsDead( self._urls_cache, self._last_check_time ):
|
||||
|
||||
self._watcher_status = 'thread is dead--hit check now to try to revive'
|
||||
|
||||
else:
|
||||
|
||||
self._thread_paused = not self._thread_paused
|
||||
|
||||
self._new_thread_event.set()
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -4489,11 +4623,17 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
|
||||
|
||||
def SetTimesToCheck( self, times_to_check ):
|
||||
def SetWatcherOptions( self, watcher_options ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
self._times_to_check = times_to_check
|
||||
self._watcher_options = watcher_options
|
||||
|
||||
self._thread_paused = False
|
||||
|
||||
self._UpdateNextCheckTime()
|
||||
|
||||
self._UpdateFileVelocityStatus()
|
||||
|
||||
self._new_thread_event.set()
|
||||
|
||||
|
@ -4501,6 +4641,10 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
def Start( self, page_key ):
|
||||
|
||||
self._UpdateNextCheckTime()
|
||||
|
||||
self._UpdateFileVelocityStatus()
|
||||
|
||||
HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnThread, page_key )
|
||||
HG.client_controller.CallToThreadLongRunning( self._THREADWorkOnFiles, page_key )
|
||||
|
||||
|
|
|
@ -2472,7 +2472,15 @@ class NetworkJob( object ):
|
|||
|
||||
def WaitUntilDone( self ):
|
||||
|
||||
self._is_done_event.wait()
|
||||
while True:
|
||||
|
||||
self._is_done_event.wait( 5 )
|
||||
|
||||
if self.IsDone():
|
||||
|
||||
break
|
||||
|
||||
|
||||
|
||||
with self._lock:
|
||||
|
||||
|
|
|
@ -1,27 +1,63 @@
|
|||
import ClientConstants as CC
|
||||
import ClientParsing
|
||||
import HydrusConstants as HC
|
||||
import HydrusGlobals as HG
|
||||
import HydrusData
|
||||
import HydrusExceptions
|
||||
import HydrusSerialisable
|
||||
import threading
|
||||
import urlparse
|
||||
|
||||
# this should do network_contexts->user-agent as well, with some kind of approval system in place
|
||||
# approval needs a new queue in the network engine. this will eventually test downloader validity and so on. failable at that stage
|
||||
# user-agent info should be exportable/importable on the ui as well
|
||||
# eventually extend this to do urlmatch->downloader, I think.
|
||||
# eventually extend this to do urlmatch->downloader_key, I think.
|
||||
# hence we'll be able to do some kind of dnd_url->new thread watcher page
|
||||
# hide urls on media viewer based on domain
|
||||
# decide whether we want to add this to the dirtyobjects loop, and it which case, if anything is appropriate to store in the db separately
|
||||
# hence making this a serialisableobject itself.
|
||||
class DomainManager( object ):
|
||||
|
||||
def __init__( self, controller ):
|
||||
|
||||
self._controller = controller
|
||||
self._domains_to_url_matches = {}
|
||||
self._network_contexts_to_custom_headers = {} # user-agent here
|
||||
# ( header_key, header_value, approved, approval_reason )
|
||||
# approved is True for user created, None for imported and defaults
|
||||
|
||||
self._lock = threading.Lock()
|
||||
|
||||
self._Initialise()
|
||||
|
||||
|
||||
def _GetURLMatch( self, url ):
|
||||
|
||||
domain = 'blah' # get top urldomain
|
||||
|
||||
if domain in self._domains_to_url_matches:
|
||||
|
||||
url_matches = self._domains_to_url_matches[ domain ]
|
||||
|
||||
# it would be nice to somehow sort these based on descending complexity
|
||||
# maybe by length of example url
|
||||
# in this way, url matches can have overlapping desmaign
|
||||
# e.g. 'post url' vs 'post url, manga subpage'
|
||||
|
||||
for url_match in url_matches:
|
||||
|
||||
( result_bool, result_reason ) = url_match.Test( url )
|
||||
|
||||
if result_bool:
|
||||
|
||||
return url_match
|
||||
|
||||
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _Initialise( self ):
|
||||
|
||||
self._domains_to_url_matches = {}
|
||||
|
@ -32,28 +68,70 @@ class DomainManager( object ):
|
|||
pass
|
||||
|
||||
|
||||
def GetURLMatch( self, url ):
|
||||
def CanApprove( self, network_contexts ):
|
||||
|
||||
# if user selected false for any approval, return false
|
||||
# network job presumably throws a ValidationError at this point, which will cause any larger queue to pause.
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def DoApproval( self, network_contexts ):
|
||||
|
||||
# if false on validity check, it presents the user with a yes/no popup with the approval_reason and waits
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def GetCustomHeaders( self, network_contexts ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
domain = 'blah' # get top urldomain
|
||||
pass
|
||||
|
||||
if domain in self._domains_to_url_matches:
|
||||
# good order is global = least powerful, which I _think_ is how these come.
|
||||
|
||||
|
||||
|
||||
def GetDownloader( self, url ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
# this might be better as getdownloaderkey, but we'll see how it shakes out
|
||||
# might also be worth being a getifhasdownloader
|
||||
|
||||
# match the url to a url_match, then lookup that in a 'this downloader can handle this url_match type' dict that we'll manage
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def NeedsApproval( self, network_contexts ):
|
||||
|
||||
# this is called by the network engine in the new approval queue
|
||||
# if a job needs approval, it goes to a single step like the login one and waits, possibly failing.
|
||||
# checks for 'approved is None' on all ncs
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def NormaliseURL( self, url ):
|
||||
|
||||
# call this before an entry into a seed cache or the db
|
||||
# use it in the dialog to review mass db-level changes
|
||||
|
||||
with self._lock:
|
||||
|
||||
url_match = self._GetURLMatch( url )
|
||||
|
||||
if url_match is None:
|
||||
|
||||
url_matches = self._domains_to_url_matches[ domain ]
|
||||
|
||||
for url_match in url_matches:
|
||||
|
||||
( result_bool, result_reason ) = url_match.Test( url )
|
||||
|
||||
if result_bool:
|
||||
|
||||
return url_match
|
||||
|
||||
|
||||
return url
|
||||
|
||||
|
||||
return None
|
||||
normalised_url = url_match.Normalise( url )
|
||||
|
||||
return normalised_url
|
||||
|
||||
|
||||
|
||||
|
@ -69,21 +147,41 @@ class DomainManager( object ):
|
|||
# __hash__ for name? not sure
|
||||
# maybe all serialisable should return __hash__ of ( type, name ) if they don't already
|
||||
# that might lead to problems elsewhere, so careful
|
||||
class URLMatch( object ):
|
||||
class URLMatch( HydrusSerialisable.SerialisableBaseNamed ):
|
||||
|
||||
def __init__( self ):
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_URL_MATCH
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, name, preferred_scheme = 'https', netloc = 'hostname.com', subdomain_is_important = False, path_components = None, parameters = None, example_url = 'https://hostname.com' ):
|
||||
|
||||
if path_components is None:
|
||||
|
||||
path_components = HydrusSerialisable.SerialisableList()
|
||||
|
||||
path_components.append( ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'post', example_string = 'post' ) )
|
||||
path_components.append( ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'page.php', example_string = 'page.php' ) )
|
||||
|
||||
|
||||
if parameters is None:
|
||||
|
||||
parameters = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
parameters[ 's' ] = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FIXED, match_value = 'view', example_string = 'view' )
|
||||
parameters[ 'id' ] = ClientParsing.StringMatch( match_type = ClientParsing.STRING_MATCH_FLEXIBLE, match_value = ClientParsing.NUMERIC, example_string = '123456' )
|
||||
|
||||
|
||||
# an edit dialog panel for this that has example url and testing of current values
|
||||
# a parent panel or something that lists all current urls in the db that match and how they will be clipped, is this ok? kind of thing.
|
||||
|
||||
self._preferred_scheme = None
|
||||
self._netloc = None
|
||||
self._subdomain_is_important = False
|
||||
self._path_components = None
|
||||
self._parameters = {}
|
||||
HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
|
||||
|
||||
self._name = None
|
||||
self._example_url = None
|
||||
self._preferred_scheme = 'https'
|
||||
self._netloc = 'hostname.com'
|
||||
self._subdomain_is_important = False
|
||||
self._path_components = HydrusSerialisable.SerialisableList()
|
||||
self._parameters = HydrusSerialisable.SerialisableDictionary()
|
||||
|
||||
self._example_url = 'https://hostname.com/post/page.php?id=123456&s=view'
|
||||
|
||||
|
||||
def _ClipNetLoc( self, netloc ):
|
||||
|
@ -105,6 +203,22 @@ class URLMatch( object ):
|
|||
return netloc
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
serialisable_path_components = self._path_components.GetSerialisableTuple()
|
||||
serialisable_parameters = self._parameters.GetSerialisableTuple()
|
||||
|
||||
return ( self._preferred_scheme, self._netloc, self._subdomain_is_important, serialisable_path_components, serialisable_parameters, self._example_url )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._preferred_scheme, self._netloc, self._subdomain_is_important, serialisable_path_components, serialisable_parameters, self._example_url ) = serialisable_info
|
||||
|
||||
self._path_components = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_path_components )
|
||||
self._parameters = HydrusSerialisable.CreateFromSerialisableTuple( serialisable_parameters )
|
||||
|
||||
|
||||
def _ClipPath( self, path ):
|
||||
|
||||
# /post/show/1326143/akunim-anthro-armband-armwear-clothed-clothing-fem
|
||||
|
@ -151,7 +265,7 @@ class URLMatch( object ):
|
|||
return query
|
||||
|
||||
|
||||
def Clip( self, url ):
|
||||
def Normalise( self, url ):
|
||||
|
||||
p = urlparse.urlparse( url )
|
||||
|
||||
|
@ -225,3 +339,5 @@ class URLMatch( object ):
|
|||
return ( True, 'good' )
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_URLS_IMPORT ] = URLMatch
|
||||
|
||||
|
|
|
@ -1026,19 +1026,35 @@ STRING_MATCH_ANY = 3
|
|||
ALPHA = 0
|
||||
ALPHANUMERIC = 1
|
||||
NUMERIC = 2
|
||||
# make it serialisable as well
|
||||
class StringMatch( object ):
|
||||
|
||||
class StringMatch( HydrusSerialisable.SerialisableBase ):
|
||||
|
||||
def __init__( self, match_type = STRING_MATCH_FIXED, match_value = 'post' ):
|
||||
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH
|
||||
SERIALISABLE_VERSION = 1
|
||||
|
||||
def __init__( self, match_type = STRING_MATCH_FIXED, match_value = 'post', min_chars = None, max_chars = None, example_string = 'post' ):
|
||||
|
||||
HydrusSerialisable.SerialisableBase.__init__( self )
|
||||
# make a gui control that accepts one of these. displays expected input on the right and colours red/green (and does isvalid) based on current input
|
||||
# think about replacing the veto stuff above with this.
|
||||
|
||||
self._match_type = match_type
|
||||
self._match_value = match_value
|
||||
|
||||
self._min_chars = None
|
||||
self._max_chars = None
|
||||
self._min_chars = min_chars
|
||||
self._max_chars = max_chars
|
||||
|
||||
self._example_string = example_string
|
||||
|
||||
|
||||
def _GetSerialisableInfo( self ):
|
||||
|
||||
return ( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string )
|
||||
|
||||
|
||||
def _InitialiseFromSerialisableInfo( self, serialisable_info ):
|
||||
|
||||
( self._match_type, self._match_value, self._min_chars, self._max_chars, self._example_string ) = serialisable_info
|
||||
|
||||
|
||||
def SetMaxChars( self, max_chars ):
|
||||
|
@ -1059,12 +1075,12 @@ class StringMatch( object ):
|
|||
|
||||
if self._min_chars is not None and text_len < self._min_chars:
|
||||
|
||||
return ( False, presentation_text + ' had too few characters' )
|
||||
return ( False, presentation_text + ' had fewer than ' + HydrusData.ConvertIntToPrettyString( self._min_chars ) + ' characters' )
|
||||
|
||||
|
||||
if self._max_chars is not None and text_len > self._max_chars:
|
||||
|
||||
return ( False, presentation_text + ' had too many characters' )
|
||||
return ( False, presentation_text + ' had more than ' + HydrusData.ConvertIntToPrettyString( self._min_chars ) + ' characters' )
|
||||
|
||||
|
||||
if self._match_type == STRING_MATCH_FIXED:
|
||||
|
@ -1105,7 +1121,7 @@ class StringMatch( object ):
|
|||
fail_reason = ' did not match "' + r + '"'
|
||||
|
||||
|
||||
if re.search( r, text ) is None:
|
||||
if re.search( r, text, flags = re.UNICODE ) is None:
|
||||
|
||||
return ( False, presentation_text + fail_reason )
|
||||
|
||||
|
@ -1120,3 +1136,64 @@ class StringMatch( object ):
|
|||
|
||||
|
||||
|
||||
def ToUnicode( self ):
|
||||
|
||||
result = ''
|
||||
|
||||
if self._min_chars is not None:
|
||||
|
||||
if self._max_chars is not None:
|
||||
|
||||
result += 'between ' + HydrusData.ToUnicode( self._min_chars ) + ' and ' + HydrusData.ToUnicode( self._max_chars ) + ' '
|
||||
|
||||
else:
|
||||
|
||||
result += 'at least ' + HydrusData.ToUnicode( self._min_chars ) + ' '
|
||||
|
||||
|
||||
else:
|
||||
|
||||
result += 'at most ' + HydrusData.ToUnicode( self._max_chars ) + ' '
|
||||
|
||||
|
||||
show_example = True
|
||||
|
||||
if self._match_type == STRING_MATCH_ANY:
|
||||
|
||||
result += 'characters'
|
||||
|
||||
elif self._match_type == STRING_MATCH_FIXED:
|
||||
|
||||
result += self._match_value
|
||||
|
||||
show_example = False
|
||||
|
||||
elif self._match_type == STRING_MATCH_FLEXIBLE:
|
||||
|
||||
if self._match_value == ALPHA:
|
||||
|
||||
result += 'alphabetical characters'
|
||||
|
||||
elif self._match_value == ALPHANUMERIC:
|
||||
|
||||
result += 'alphanumeric characters'
|
||||
|
||||
elif self._match_value == NUMERIC:
|
||||
|
||||
result += 'numeric characters'
|
||||
|
||||
|
||||
elif self._match_type == STRING_MATCH_REGEX:
|
||||
|
||||
result += 'characters, matching regex "' + self._match_value + '"'
|
||||
|
||||
|
||||
if show_example:
|
||||
|
||||
result += ', such as ' + self._example_string
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_STRING_MATCH ] = StringMatch
|
||||
|
|
|
@ -49,7 +49,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 18
|
||||
SOFTWARE_VERSION = 274
|
||||
SOFTWARE_VERSION = 275
|
||||
|
||||
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
||||
|
|
|
@ -230,7 +230,7 @@ def ConvertTimeDeltaToPrettyString( seconds ):
|
|||
return 'per month'
|
||||
|
||||
|
||||
if seconds > 60:
|
||||
if seconds >= 60:
|
||||
|
||||
seconds = int( seconds )
|
||||
|
||||
|
@ -277,7 +277,14 @@ def ConvertTimeDeltaToPrettyString( seconds ):
|
|||
minutes = seconds / 60
|
||||
seconds = seconds % 60
|
||||
|
||||
result = '%d' % minutes + ' minutes'
|
||||
if minutes == 1:
|
||||
|
||||
result = '1 minute'
|
||||
|
||||
else:
|
||||
|
||||
result = '%d' % minutes + ' minutes'
|
||||
|
||||
|
||||
if seconds > 0:
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ SERIALISABLE_TYPE_NETWORK_SESSION_MANAGER = 46
|
|||
SERIALISABLE_TYPE_NETWORK_CONTEXT = 47
|
||||
SERIALISABLE_TYPE_NETWORK_LOGIN_MANAGER = 48
|
||||
SERIALISABLE_TYPE_MEDIA_SORT = 49
|
||||
SERIALISABLE_TYPE_URL_MATCH = 50
|
||||
SERIALISABLE_TYPE_STRING_MATCH = 51
|
||||
SERIALISABLE_TYPE_WATCHER_OPTIONS = 52
|
||||
|
||||
SERIALISABLE_TYPES_TO_OBJECT_TYPES = {}
|
||||
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import ClientData
|
||||
import ClientImporting
|
||||
import os
|
||||
import unittest
|
||||
|
||||
class TestData( unittest.TestCase ):
|
||||
|
||||
def test_watcher_options( self ):
|
||||
|
||||
regular_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 5, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
|
||||
fast_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 2, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
|
||||
slow_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 10, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 86400 ) )
|
||||
callous_watcher_options = ClientData.WatcherOptions( intended_files_per_check = 5, never_faster_than = 30, never_slower_than = 86400, death_file_velocity = ( 1, 60 ) )
|
||||
|
||||
empty_seed_cache = ClientImporting.SeedCache()
|
||||
|
||||
seed_cache = ClientImporting.SeedCache()
|
||||
|
||||
last_check_time = 10000000
|
||||
|
||||
one_day_before = last_check_time - 86400
|
||||
|
||||
for i in range( 50 ):
|
||||
|
||||
seed = os.urandom( 16 ).encode( 'hex' )
|
||||
|
||||
seed_cache.AddSeeds( ( seed, ) )
|
||||
|
||||
seed_cache.UpdateSeedSourceTime( seed, one_day_before - 10 )
|
||||
|
||||
|
||||
for i in range( 50 ):
|
||||
|
||||
seed = os.urandom( 16 ).encode( 'hex' )
|
||||
|
||||
seed_cache.AddSeeds( ( seed, ) )
|
||||
|
||||
seed_cache.UpdateSeedSourceTime( seed, one_day_before + 10 )
|
||||
|
||||
|
||||
bare_seed_cache = ClientImporting.SeedCache()
|
||||
|
||||
bare_seed_cache.AddSeeds( ( 'early', ) )
|
||||
bare_seed_cache.AddSeeds( ( 'in_time_delta', ) )
|
||||
|
||||
bare_seed_cache.UpdateSeedSourceTime( 'early', one_day_before - 10 )
|
||||
bare_seed_cache.UpdateSeedSourceTime( 'in_time_delta', one_day_before + 10 )
|
||||
|
||||
busy_seed_cache = ClientImporting.SeedCache()
|
||||
|
||||
busy_seed_cache.AddSeeds( ( 'early', ) )
|
||||
|
||||
busy_seed_cache.UpdateSeedSourceTime( 'early', one_day_before - 10 )
|
||||
|
||||
for i in range( 8640 ):
|
||||
|
||||
seed = os.urandom( 16 ).encode( 'hex' )
|
||||
|
||||
busy_seed_cache.AddSeeds( ( seed, ) )
|
||||
|
||||
busy_seed_cache.UpdateSeedSourceTime( seed, one_day_before + ( ( i + 1 ) * 10 ) - 1 )
|
||||
|
||||
|
||||
new_thread_seed_cache = ClientImporting.SeedCache()
|
||||
|
||||
for i in range( 10 ):
|
||||
|
||||
seed = os.urandom( 16 ).encode( 'hex' )
|
||||
|
||||
new_thread_seed_cache.AddSeeds( ( seed, ) )
|
||||
|
||||
new_thread_seed_cache.UpdateSeedSourceTime( seed, last_check_time - 600 )
|
||||
|
||||
|
||||
# empty
|
||||
# should say ok if last_check_time is 0, so it can initialise
|
||||
# otherwise sperg out safely
|
||||
|
||||
self.assertFalse( regular_watcher_options.IsDead( empty_seed_cache, 0 ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( empty_seed_cache, 0 ), 'no files yet' )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetNextCheckTime( empty_seed_cache, 0 ), 0 )
|
||||
|
||||
self.assertTrue( regular_watcher_options.IsDead( empty_seed_cache, last_check_time ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( empty_seed_cache, last_check_time ), 'no files, unable to determine velocity' )
|
||||
|
||||
# regular
|
||||
# current velocity should be 50 files per day for the day ones and 0 files per min for the callous minute one
|
||||
|
||||
self.assertFalse( regular_watcher_options.IsDead( seed_cache, last_check_time ) )
|
||||
self.assertFalse( fast_watcher_options.IsDead( seed_cache, last_check_time ) )
|
||||
self.assertFalse( slow_watcher_options.IsDead( seed_cache, last_check_time ) )
|
||||
self.assertTrue( callous_watcher_options.IsDead( seed_cache, last_check_time ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
|
||||
self.assertEqual( fast_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
|
||||
self.assertEqual( slow_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 50 files in previous 1 day' )
|
||||
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( seed_cache, last_check_time ), u'at last check, found 0 files in previous 1 minute' )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 8640 )
|
||||
self.assertEqual( fast_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 3456 )
|
||||
self.assertEqual( slow_watcher_options.GetNextCheckTime( seed_cache, last_check_time ), last_check_time + 17280 )
|
||||
|
||||
# bare
|
||||
# 1 files per day
|
||||
|
||||
self.assertFalse( regular_watcher_options.IsDead( bare_seed_cache, last_check_time ) )
|
||||
self.assertTrue( callous_watcher_options.IsDead( bare_seed_cache, last_check_time ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( bare_seed_cache, last_check_time ), u'at last check, found 1 files in previous 1 day' )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
|
||||
self.assertEqual( fast_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
|
||||
self.assertEqual( slow_watcher_options.GetNextCheckTime( bare_seed_cache, last_check_time ), last_check_time + 86400 )
|
||||
|
||||
# busy
|
||||
# 8640 files per day, 6 files per minute
|
||||
|
||||
self.assertFalse( regular_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
|
||||
self.assertFalse( fast_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
|
||||
self.assertFalse( slow_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
|
||||
self.assertFalse( callous_watcher_options.IsDead( busy_seed_cache, last_check_time ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( busy_seed_cache, last_check_time ), u'at last check, found 8,640 files in previous 1 day' )
|
||||
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( busy_seed_cache, last_check_time ), u'at last check, found 6 files in previous 1 minute' )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 50 )
|
||||
self.assertEqual( fast_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 30 )
|
||||
self.assertEqual( slow_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 100 )
|
||||
self.assertEqual( callous_watcher_options.GetNextCheckTime( busy_seed_cache, last_check_time ), last_check_time + 50 )
|
||||
|
||||
# new thread
|
||||
# only had files from ten mins ago, so timings are different
|
||||
|
||||
self.assertFalse( regular_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
|
||||
self.assertFalse( fast_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
|
||||
self.assertFalse( slow_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
|
||||
self.assertTrue( callous_watcher_options.IsDead( new_thread_seed_cache, last_check_time ) )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
|
||||
self.assertEqual( fast_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
|
||||
self.assertEqual( slow_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 10 files in previous 10 minutes' )
|
||||
self.assertEqual( callous_watcher_options.GetPrettyCurrentVelocity( new_thread_seed_cache, last_check_time ), u'at last check, found 0 files in previous 1 minute' )
|
||||
|
||||
self.assertEqual( regular_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 300 )
|
||||
self.assertEqual( fast_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 120 )
|
||||
self.assertEqual( slow_watcher_options.GetNextCheckTime( new_thread_seed_cache, last_check_time ), last_check_time + 600 )
|
||||
|
||||
|
2
test.py
2
test.py
|
@ -17,6 +17,7 @@ from include import HydrusTags
|
|||
from include import HydrusThreading
|
||||
from include import TestClientConstants
|
||||
from include import TestClientDaemons
|
||||
from include import TestClientData
|
||||
from include import TestClientListBoxes
|
||||
from include import TestClientNetworking
|
||||
from include import TestConstants
|
||||
|
@ -312,6 +313,7 @@ class Controller( object ):
|
|||
if run_all or only_run == 'data':
|
||||
|
||||
suites.append( unittest.TestLoader().loadTestsFromModule( TestClientConstants ) )
|
||||
suites.append( unittest.TestLoader().loadTestsFromModule( TestClientData ) )
|
||||
suites.append( unittest.TestLoader().loadTestsFromModule( TestFunctions ) )
|
||||
suites.append( unittest.TestLoader().loadTestsFromModule( TestHydrusSerialisable ) )
|
||||
suites.append( unittest.TestLoader().loadTestsFromModule( TestHydrusSessions ) )
|
||||
|
|
Loading…
Reference in New Issue