Version 84

This commit is contained in:
Hydrus 2013-09-11 16:28:19 -05:00
parent fc8ed382ec
commit 6a35a41190
18 changed files with 1073 additions and 315 deletions

View File

@ -8,6 +8,37 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 84</h3></li>
<ul>
<li>switch fullscreen button added to fullscreen canvases</li>
<li>messaging stuff is disabled for now</li>
<li>gif scanbars now fill up as the frames render</li>
<li>pngs with transparency are no longer drawn with black background after first viewing</li>
<li>made a small change to listbooks to correct some gui-weirdness, particularly in local options</li>
<li>autocomplete dropdown windows will now hide-and-reposition on parent scroll events</li>
<li>autocomplete hide-and-reposition waits 250ms rather than 100ms, making it a little less flickery</li>
<li>a graphical bug related to hitting end on a large search is fixed</li>
<li>upnp framework started</li>
<li>services->manage local upnp started</li>
<li>new messagegauge added</li>
<li>fixed mp4 import</li>
<li>youtube url->formats added</li>
<li>youtube format chooser dialog added</li>
<li>youtube downloader thread added</li>
<li>youtube gauge popup added</li>
<li>youtube error handling improved</li>
<li>youtube num_bytes_done added</li>
<li>youtube unknown total_num_bytes handled</li>
<li>youtube cancel button added</li>
<li>youtube right click dismiss throws up a yes/no dialog to decide whether to cancel the download</li>
<li>started manage upnp dialog</li>
<li>closed page undo added</li>
<li>closed pages will be paused and quiet</li>
<li>pause import folders option added</li>
<li>fixed an invalid index drawing bug after removing certain media</li>
<li>thumbnail waterfall improved</li>
<li>tags are limited to 1024 characters</li>
</ul>
<li><h3>version 83</h3></li>
<ul>
<li>sort by longest fixed for files with no duration</li>

View File

@ -20,7 +20,7 @@
<li><a href="getting_started_tags.html">getting started with tags</a></li>
<li><a href="getting_started_ratings.html">getting started with ratings</a></li>
<li><a href="getting_started_subscriptions.html">getting started with subscriptions</a></li>
<li><a href="getting_started_messages.html">getting started with messages</a></li>
<li><a><del>getting started with messages</del></a> (currently reworking this)</li>
<li><a href="registration_keys.html">registering new accounts</a></li>
<li><a href="access_keys.html">access keys to my server's services</a></li>
<li><a href="tagging_schema.html">thoughts on a public tagging schema</a></li>

View File

@ -13,6 +13,7 @@ import HydrusTags
import itertools
import multipart
import os
import Queue
import random
import sqlite3
import sys
@ -281,6 +282,7 @@ CLIENT_DEFAULT_OPTIONS[ 'confirm_client_exit' ] = False
CLIENT_DEFAULT_OPTIONS[ 'default_tag_repository' ] = HC.LOCAL_TAG_SERVICE_IDENTIFIER
CLIENT_DEFAULT_OPTIONS[ 'default_tag_sort' ] = SORT_BY_LEXICOGRAPHIC_ASC
CLIENT_DEFAULT_OPTIONS[ 'pause_import_folders_sync' ] = False
CLIENT_DEFAULT_OPTIONS[ 'pause_repo_sync' ] = False
CLIENT_DEFAULT_OPTIONS[ 'pause_subs_sync' ] = False
@ -2629,8 +2631,12 @@ class ThumbnailCache():
self._data_cache = DataCache( 'thumbnail_cache_size' )
self._queue = Queue.Queue()
self.Clear()
threading.Thread( target = self.DAEMONWaterfall, name = 'Waterfall Daemon' ).start()
HC.pubsub.sub( self, 'Clear', 'thumbnail_resize' )
@ -2690,7 +2696,42 @@ class ThumbnailCache():
else: return self._special_thumbs[ 'hydrus' ]
def Waterfall( self, page_key, medias ): threading.Thread( target = self.THREADWaterfall, args = ( page_key, medias ) ).start()
def Waterfall( self, page_key, medias ):
self._queue.put( ( page_key, medias ) )
#threading.Thread( target = self.THREADWaterfall, args = ( page_key, medias ) ).start()
def DAEMONWaterfall( self ):
last_paused = time.clock()
while not HC.shutdown:
try:
( page_key, medias ) = self._queue.get( timeout = 1 )
random.shuffle( medias )
for media in medias:
thumbnail = self.GetThumbnail( media )
HC.pubsub.pub( 'waterfall_thumbnail', page_key, media, thumbnail )
if time.clock() - last_paused > 0.005:
time.sleep( 0.0001 )
last_paused = time.clock()
except: pass
def THREADWaterfall( self, page_key, medias ):
@ -2704,7 +2745,7 @@ class ThumbnailCache():
HC.pubsub.pub( 'waterfall_thumbnail', page_key, media, thumbnail )
if time.clock() - last_paused > 0.005:
if time.clock() - last_paused > 1.0 / 15:
time.sleep( 0.0001 )

View File

@ -6541,7 +6541,6 @@ class DB( ServiceDB ):
HC.DAEMONWorker( 'DownloadThumbnails', DAEMONDownloadThumbnails, ( 'notify_new_permissions', 'notify_new_thumbnails' ) )
HC.DAEMONWorker( 'ResizeThumbnails', DAEMONResizeThumbnails, init_wait = 600 )
HC.DAEMONWorker( 'SynchroniseAccounts', DAEMONSynchroniseAccounts, ( 'notify_new_services', 'permissions_are_stale' ) )
HC.DAEMONWorker( 'SynchroniseMessages', DAEMONSynchroniseMessages, ( 'notify_new_permissions', 'notify_check_messages' ), period = 60 )
HC.DAEMONWorker( 'SynchroniseRepositoriesAndSubscriptions', DAEMONSynchroniseRepositoriesAndSubscriptions, ( 'notify_new_permissions', 'notify_new_subscriptions' ) )
HC.DAEMONQueue( 'FlushRepositoryUpdates', DAEMONFlushServiceUpdates, 'service_updates_delayed', period = 2 )
@ -6598,114 +6597,119 @@ class DB( ServiceDB ):
def DAEMONCheckImportFolders():
import_folders = HC.app.ReadDaemon( 'import_folders' )
for ( folder_path, details ) in import_folders:
if not HC.options[ 'pause_import_folders_sync' ]:
now = HC.GetNow()
import_folders = HC.app.ReadDaemon( 'import_folders' )
if now > details[ 'last_checked' ] + details[ 'check_period' ]:
for ( folder_path, details ) in import_folders:
if os.path.exists( folder_path ) and os.path.isdir( folder_path ):
now = HC.GetNow()
if now > details[ 'last_checked' ] + details[ 'check_period' ]:
filenames = dircache.listdir( folder_path )
raw_paths = [ folder_path + os.path.sep + filename for filename in filenames ]
all_paths = CC.GetAllPaths( raw_paths, quiet = True )
HC.pubsub.pub( 'service_status', 'Found ' + HC.u( len( all_paths ) ) + ' files to import from ' + folder_path )
if details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_SYNCHRONISE:
if os.path.exists( folder_path ) and os.path.isdir( folder_path ):
all_paths = [ path for path in all_paths if path not in details[ 'cached_imported_paths' ] ]
filenames = dircache.listdir( folder_path )
all_paths = [ path for path in all_paths if path not in details[ 'failed_imported_paths' ] ]
successful_hashes = set()
for ( i, path ) in enumerate( all_paths ):
raw_paths = [ folder_path + os.path.sep + filename for filename in filenames ]
should_import = True
should_action = True
all_paths = CC.GetAllPaths( raw_paths, quiet = True )
HC.pubsub.pub( 'service_status', 'Importing ' + HC.u( i ) + ' of ' + HC.u( len( all_paths ) ) )
HC.pubsub.pub( 'service_status', 'Found ' + HC.u( len( all_paths ) ) + ' files to import from ' + folder_path )
temp_path = HC.GetTempPath()
try:
if details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_SYNCHRONISE:
# make read only perms to make sure it isn't being written/downloaded right now
os.chmod( path, stat.S_IREAD )
os.chmod( path, stat.S_IWRITE )
shutil.copy( path, temp_path )
os.chmod( temp_path, stat.S_IWRITE )
except:
# could not lock, so try again later
should_import = False
should_action = False
all_paths = [ path for path in all_paths if path not in details[ 'cached_imported_paths' ] ]
if should_import:
all_paths = [ path for path in all_paths if path not in details[ 'failed_imported_paths' ] ]
successful_hashes = set()
for ( i, path ) in enumerate( all_paths ):
if HC.options[ 'pause_import_folders_sync' ]: return
should_import = True
should_action = True
HC.pubsub.pub( 'service_status', 'Importing ' + HC.u( i ) + ' of ' + HC.u( len( all_paths ) ) )
temp_path = HC.GetTempPath()
try:
if details[ 'local_tag' ] is not None: service_identifiers_to_tags = { HC.LOCAL_TAG_SERVICE_IDENTIFIER : { details[ 'local_tag' ] } }
else: service_identifiers_to_tags = {}
# make read only perms to make sure it isn't being written/downloaded right now
( result, hash ) = HC.app.WriteSynchronous( 'import_file', temp_path, service_identifiers_to_tags = service_identifiers_to_tags )
os.chmod( path, stat.S_IREAD )
if result in ( 'successful', 'redundant' ): successful_hashes.add( hash )
elif result == 'deleted':
details[ 'failed_imported_paths' ].add( path )
os.chmod( path, stat.S_IWRITE )
shutil.copy( path, temp_path )
os.chmod( temp_path, stat.S_IWRITE )
except:
details[ 'failed_imported_paths' ].add( path )
message = 'Import folder failed to import a file: ' + os.linesep + path + os.linesep + traceback.format_exc()
HC.Message( HC.MESSAGE_TYPE_ERROR, Exception( message ) )
# could not lock, so try again later
should_import = False
should_action = False
os.remove( temp_path )
if should_action:
if details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_DELETE:
if should_import:
try: os.remove( path )
except: details[ 'failed_imported_paths' ].add( path )
try:
if details[ 'local_tag' ] is not None: service_identifiers_to_tags = { HC.LOCAL_TAG_SERVICE_IDENTIFIER : { details[ 'local_tag' ] } }
else: service_identifiers_to_tags = {}
( result, hash ) = HC.app.WriteSynchronous( 'import_file', temp_path, service_identifiers_to_tags = service_identifiers_to_tags )
if result in ( 'successful', 'redundant' ): successful_hashes.add( hash )
elif result == 'deleted':
details[ 'failed_imported_paths' ].add( path )
except:
details[ 'failed_imported_paths' ].add( path )
message = 'Import folder failed to import a file: ' + os.linesep + path + os.linesep + traceback.format_exc()
HC.Message( HC.MESSAGE_TYPE_ERROR, Exception( message ) )
should_action = False
os.remove( temp_path )
if should_action:
if details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_DELETE:
try: os.remove( path )
except: details[ 'failed_imported_paths' ].add( path )
elif details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_SYNCHRONISE: details[ 'cached_imported_paths' ].add( path )
elif details[ 'type' ] == HC.IMPORT_FOLDER_TYPE_SYNCHRONISE: details[ 'cached_imported_paths' ].add( path )
if len( successful_hashes ) > 0:
if len( successful_hashes ) > 0:
message_text = HC.u( len( successful_hashes ) ) + ' files imported from ' + folder_path
HC.pubsub.pub( 'message', HC.Message( HC.MESSAGE_TYPE_FILES, ( message_text, successful_hashes ) ) )
message_text = HC.u( len( successful_hashes ) ) + ' files imported from ' + folder_path
details[ 'last_checked' ] = now
HC.pubsub.pub( 'message', HC.Message( HC.MESSAGE_TYPE_FILES, ( message_text, successful_hashes ) ) )
HC.pubsub.pub( 'service_status', '' )
HC.app.WriteSynchronous( 'import_folder', folder_path, details )
details[ 'last_checked' ] = now
HC.pubsub.pub( 'service_status', '' )
HC.app.WriteSynchronous( 'import_folder', folder_path, details )

View File

@ -1,12 +1,11 @@
import httplib
import HydrusConstants as HC
import ClientConstants as CC
import ClientConstantsMessages
import ClientGUICommon
import ClientGUIDialogs
import ClientGUIDialogsManage
import ClientGUIMessages
import ClientGUIPages
import HydrusDownloading
import os
import random
import subprocess
@ -56,6 +55,10 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
self._statusbar_downloads = ''
self._statusbar_db_locked = ''
self._focus_holder = wx.Window( self, size = ( 0, 0 ) )
self._closed_pages = []
self._notebook = wx.Notebook( self )
self._notebook.Bind( wx.EVT_MIDDLE_DOWN, self.EventNotebookMiddleClick )
self._notebook.Bind( wx.EVT_RIGHT_DCLICK, self.EventNotebookMiddleClick )
@ -77,7 +80,6 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
HC.pubsub.sub( self, 'NewPageImportHDD', 'new_hdd_import' )
HC.pubsub.sub( self, 'NewPageImportThreadWatcher', 'new_page_import_thread_watcher' )
HC.pubsub.sub( self, 'NewPageImportURL', 'new_page_import_url' )
HC.pubsub.sub( self, 'NewPageMessages', 'new_page_messages' )
HC.pubsub.sub( self, 'NewPagePetitions', 'new_page_petitions' )
HC.pubsub.sub( self, 'NewPageQuery', 'new_page_query' )
HC.pubsub.sub( self, 'NewPageThreadDumper', 'new_thread_dumper' )
@ -97,6 +99,12 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
self._RefreshStatusBar()
vbox = wx.BoxSizer( wx.HORIZONTAL )
vbox.AddF( self._notebook, FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.SetSizer( vbox )
self.Show( True )
wx.CallAfter( self._NewPageQuery, HC.LOCAL_FILE_SERVICE_IDENTIFIER )
@ -417,15 +425,40 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
selection = self._notebook.GetSelection()
if selection != wx.NOT_FOUND:
page = self._notebook.GetPage( selection )
try: page.TryToClose()
except: return
self._notebook.DeletePage( selection )
if selection != wx.NOT_FOUND: self._ClosePage( selection )
def _ClosePage( self, selection ):
page = self._notebook.GetPage( selection )
try: page.TryToClose()
except: return
page.Pause()
page.Hide()
name = self._notebook.GetPageText( selection )
self._closed_pages.append( ( selection, name, page ) )
self._notebook.RemovePage( selection )
if self._notebook.GetPageCount() == 0: self._focus_holder.SetFocus()
self.RefreshMenuBar()
def _DeleteAllPages( self ):
for ( selection, name, page ) in self._closed_pages: page.Destroy()
self._closed_pages = []
self._focus_holder.SetFocus()
self.RefreshMenuBar()
def _DeletePending( self, service_identifier ):
@ -588,6 +621,11 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
with ClientGUIDialogsManage.DialogManageTagSiblings( self ) as dlg: dlg.ShowModal()
def _ManageUPnP( self, service_identifier ):
with ClientGUIDialogsManage.DialogManageUPnP( self, service_identifier ) as dlg: dlg.ShowModal()
def _ModifyAccount( self, service_identifier ):
service = HC.app.Read( 'service', service_identifier )
@ -684,17 +722,6 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
self._notebook.SetSelection( self._notebook.GetPageCount() - 1 )
def _NewPageMessages( self, identity ):
new_page = ClientGUIPages.PageMessages( self._notebook, identity )
self._notebook.AddPage( new_page, identity.GetName() + ' messages', select = True )
self._notebook.SetSelection( self._notebook.GetPageCount() - 1 )
new_page.SetSearchFocus()
def _NewPagePetitions( self, service_identifier = None ):
if service_identifier is None: service_identifier = ClientGUIDialogs.SelectServiceIdentifier( service_types = HC.REPOSITORIES, permission = HC.RESOLVE_PETITIONS )
@ -743,6 +770,7 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
if sync_type == 'repo': HC.options[ 'pause_repo_sync' ] = not HC.options[ 'pause_repo_sync' ]
elif sync_type == 'subs': HC.options[ 'pause_subs_sync' ] = not HC.options[ 'pause_subs_sync' ]
elif sync_type == 'import_folders': HC.options[ 'pause_import_folders_sync' ] = not HC.options[ 'pause_import_folders_sync' ]
try: HC.app.Write( 'save_options' )
except: wx.MessageBox( traceback.format_exc() )
@ -835,6 +863,38 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if page is not None: page.SetSynchronisedWait()
def _StartYoutubeDownload( self ):
with wx.TextEntryDialog( self, 'Enter YouTube URL' ) as dlg:
result = dlg.ShowModal()
if result == wx.ID_OK:
url = dlg.GetValue()
info = HydrusDownloading.GetYoutubeFormats( url )
with ClientGUIDialogs.DialogSelectYoutubeURL( self, info ) as select_dlg: select_dlg.ShowModal()
def _UnclosePage( self, closed_page_index ):
( index, name, page ) = self._closed_pages.pop( closed_page_index )
page.Unpause()
page.Show()
index = min( index, self._notebook.GetPageCount() )
self._notebook.InsertPage( index, page, name, True )
self.RefreshMenuBar()
def _UploadPending( self, service_identifier ):
job_key = os.urandom( 32 )
@ -892,6 +952,8 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
except: return
self._DeleteAllPages()
self._message_manager.CleanUp()
self._message_manager.Destroy()
@ -961,6 +1023,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
print( 'garbage: ' + HC.u( gc.garbage ) )
elif command == 'delete_all_pages': self._DeleteAllPages()
elif command == 'delete_pending': self._DeletePending( data )
elif command == 'exit': self.EventExit( event )
elif command == 'fetch_ip': self._FetchIP( data )
@ -982,18 +1045,19 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
elif command == 'manage_tag_parents': self._ManageTagParents()
elif command == 'manage_tag_service_precedence': self._ManageTagServicePrecedence()
elif command == 'manage_tag_siblings': self._ManageTagSiblings()
elif command == 'manage_upnp': self._ManageUPnP( data )
elif command == 'modify_account': self._ModifyAccount( data )
elif command == 'new_accounts': self._NewAccounts( data )
elif command == 'new_import_booru': self._NewPageImportBooru()
elif command == 'new_import_thread_watcher': self._NewPageImportThreadWatcher()
elif command == 'new_import_url': self._NewPageImportURL()
elif command == 'new_log_page': self._NewPageLog()
elif command == 'new_messages_page': self._NewPageMessages( data )
elif command == 'new_page': FramePageChooser()
elif command == 'new_page_query': self._NewPageQuery( data )
elif command == 'news': self._News( data )
elif command == 'open_export_folder': self._OpenExportFolder()
elif command == 'options': self._ManageOptions( data )
elif command == 'pause_import_folders_sync': self._PauseSync( 'import_folders' )
elif command == 'pause_repo_sync': self._PauseSync( 'repo' )
elif command == 'pause_subs_sync': self._PauseSync( 'subs' )
elif command == 'petitions': self._NewPagePetitions( data )
@ -1015,10 +1079,12 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
elif command == 'set_media_focus': self._SetMediaFocus()
elif command == 'set_search_focus': self._SetSearchFocus()
elif command == 'site': webbrowser.open( 'http://hydrusnetwork.github.io/hydrus/' )
elif command == 'start_youtube_download': self._StartYoutubeDownload()
elif command == 'stats': self._Stats( data )
elif command == 'synchronised_wait_switch': self._SetSynchronisedWait()
elif command == 'tumblr': webbrowser.open( 'http://hydrus.tumblr.com/' )
elif command == 'twitter': webbrowser.open( 'http://twitter.com/#!/hydrusnetwork' )
elif command == 'unclose_page': self._UnclosePage( data )
elif command == 'upload_pending': self._UploadPending( data )
elif command == 'vacuum_db': self._VacuumDatabase()
else: event.Skip()
@ -1029,12 +1095,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
( tab_index, flags ) = self._notebook.HitTest( ( event.GetX(), event.GetY() ) )
page = self._notebook.GetPage( tab_index )
try: page.TryToClose()
except: return
self._notebook.DeletePage( tab_index )
self._ClosePage( tab_index )
def EventNotebookPageChanged( self, event ):
@ -1086,8 +1147,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def NewPageImportURL( self ): self._NewPageImportURL()
def NewPageMessages( self, identity ): self._NewPageMessages( identity )
def NewPagePetitions( self, service_identifier ): self._NewPagePetitions( service_identifier )
def NewPageQuery( self, service_identifier, initial_media_results = [], initial_predicates = [] ): self._NewPageQuery( service_identifier, initial_media_results = initial_media_results, initial_predicates = initial_predicates )
@ -1146,8 +1205,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
admin_message_depots = [ message_depot.GetServiceIdentifier() for message_depot in message_depots if message_depot.GetAccount().HasPermission( HC.GENERAL_ADMIN ) ]
identities = HC.app.Read( 'identities' )
servers_admin = [ service for service in services if service.GetServiceIdentifier().GetType() == HC.SERVER_ADMIN ]
server_admin_identifiers = [ service.GetServiceIdentifier() for service in servers_admin if service.GetAccount().HasPermission( HC.GENERAL_ADMIN ) ]
@ -1165,6 +1222,35 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
menu.Append( file, p( '&File' ) )
if len( self._closed_pages ) > 0:
undo = wx.Menu()
if len( self._closed_pages ) > 0:
undo_pages = wx.Menu()
undo_pages.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete_all_pages' ), 'clear all' )
undo_pages.AppendSeparator()
args = []
for ( i, ( index, name, page ) ) in enumerate( self._closed_pages ):
args.append( ( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'unclose_page', i ), name + ' - ' + page.GetPrettyStatus() ) )
args.reverse() # so that recently closed are at the top
for a in args: undo_pages.Append( *a )
undo.AppendMenu( CC.ID_NULL, p( 'Closed Pages' ), undo_pages )
menu.Append( undo, p( '&Undo' ) )
view = wx.Menu()
view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'refresh' ), p( '&Refresh' ), p( 'Refresh the current view.' ) )
view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'show_hide_splitters' ), p( 'Show/Hide Splitters' ), p( 'Show or hide the current page\'s splitters.' ) )
@ -1184,15 +1270,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'new_import_booru' ), p( '&New Booru Download Page' ), p( 'Open a new tab to download files from a booru.' ) )
view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'new_import_thread_watcher' ), p( '&New Thread Watcher Page' ), p( 'Open a new tab to watch a thread.' ) )
view.AppendSeparator()
if len( identities ) > 0:
for identity in identities: view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'new_messages_page', identity ), p( identity.GetName() + ' Message Page' ), p( 'Open a new tab to review the messages for ' + identity.GetName() ) )
view.AppendSeparator()
view.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'new_log_page' ), p( '&New Log Page' ), p( 'Open a new tab to show recently logged events.' ) )
menu.Append( view, p( '&View' ) )
download = wx.Menu()
download.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'start_youtube_download' ), p( '&Download a YouTube Video' ), p( 'Enter a YouTube URL and choose which formats you would like to download' ) )
menu.Append( download, p( 'Do&wnload' ) )
database = wx.Menu()
database.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'set_password' ), p( 'Set a &Password' ), p( 'Set a password for the database so only you can access it.' ) )
database.AppendSeparator()
@ -1244,12 +1330,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
submenu = wx.Menu()
pause_import_folders_sync_id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'pause_import_folders_sync' )
pause_repo_sync_id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'pause_repo_sync' )
pause_subs_sync_id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'pause_subs_sync' )
submenu.AppendCheckItem( pause_import_folders_sync_id, p( '&Import Folders Synchronisation' ), p( 'Pause the client\'s import folders.' ) )
submenu.AppendCheckItem( pause_repo_sync_id, p( '&Repositories Synchronisation' ), p( 'Pause the client\'s synchronisation with hydrus repositories.' ) )
submenu.AppendCheckItem( pause_subs_sync_id, p( '&Subscriptions Synchronisation' ), p( 'Pause the client\'s synchronisation with website subscriptions.' ) )
submenu.Check( pause_import_folders_sync_id, HC.options[ 'pause_import_folders_sync' ] )
submenu.Check( pause_repo_sync_id, HC.options[ 'pause_repo_sync' ] )
submenu.Check( pause_subs_sync_id, HC.options[ 'pause_subs_sync' ] )
@ -1270,7 +1359,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
services.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'manage_pixiv_account' ), p( 'Manage &Pixiv Account' ), p( 'Set up your pixiv username and password.' ) )
services.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'manage_subscriptions' ), p( 'Manage &Subscriptions' ), p( 'Change the queries you want the client to regularly import from.' ) )
services.AppendSeparator()
services.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'manage_contacts' ), p( 'Manage &Contacts and Identities' ), p( 'Change the names and addresses of the people you talk to.' ) )
services.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'manage_upnp', HC.LOCAL_FILE_SERVICE_IDENTIFIER ), p( 'Manage Local UPnP' ) )
services.AppendSeparator()
submenu = wx.Menu()
for s_i in tag_service_identifiers: submenu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'news', s_i ), p( s_i.GetName() ), p( 'Review ' + s_i.GetName() + '\'s past news.' ) )
@ -1533,8 +1622,6 @@ class FramePageChooser( ClientGUICommon.Frame ):
self._petition_service_identifiers = [ service.GetServiceIdentifier() for service in self._services if service.GetServiceIdentifier().GetType() in HC.REPOSITORIES and service.GetAccount().HasPermission( HC.RESOLVE_PETITIONS ) ]
self._identities = HC.app.Read( 'identities' )
self._InitButtons( 'home' )
self.Bind( wx.EVT_BUTTON, self.EventButton )
@ -1562,12 +1649,6 @@ class FramePageChooser( ClientGUICommon.Frame ):
elif entry_type == 'page_import_booru': button.SetLabel( 'booru' )
elif entry_type == 'page_import_gallery': button.SetLabel( obj )
elif entry_type == 'page_messages':
name = obj.GetName()
button.SetLabel( name )
elif entry_type == 'page_import_thread_watcher': button.SetLabel( 'thread watcher' )
elif entry_type == 'page_import_url': button.SetLabel( 'url' )
@ -1584,8 +1665,6 @@ class FramePageChooser( ClientGUICommon.Frame ):
if len( self._petition_service_identifiers ) > 0: entries.append( ( 'menu', 'petitions' ) )
if len( self._identities ) > 0: entries.append( ( 'menu', 'messages' ) )
elif menu_keyword == 'files':
file_repos = [ ( 'page_query', service_identifier ) for service_identifier in [ service.GetServiceIdentifier() for service in self._services ] if service_identifier.GetType() == HC.FILE_REPOSITORY ]
@ -1605,7 +1684,6 @@ class FramePageChooser( ClientGUICommon.Frame ):
elif menu_keyword == 'hentai foundry': entries = [ ( 'page_import_gallery', 'hentai foundry by artist' ), ( 'page_import_gallery', 'hentai foundry by tags' ) ]
elif menu_keyword == 'pixiv': entries = [ ( 'page_import_gallery', 'pixiv by artist' ), ( 'page_import_gallery', 'pixiv by tag' ) ]
elif menu_keyword == 'messages': entries = [ ( 'page_messages', identity ) for identity in self._identities ]
elif menu_keyword == 'petitions': entries = [ ( 'page_petitions', service_identifier ) for service_identifier in self._petition_service_identifiers ]
if len( entries ) <= 4:
@ -1648,7 +1726,6 @@ class FramePageChooser( ClientGUICommon.Frame ):
elif entry_type == 'page_import_gallery': HC.pubsub.pub( 'new_page_import_gallery', obj )
elif entry_type == 'page_import_thread_watcher': HC.pubsub.pub( 'new_page_import_thread_watcher' )
elif entry_type == 'page_import_url': HC.pubsub.pub( 'new_page_import_url' )
elif entry_type == 'page_messages': HC.pubsub.pub( 'new_page_messages', obj )
elif entry_type == 'page_petitions': HC.pubsub.pub( 'new_page_petitions', obj )
self.Destroy()

View File

@ -69,6 +69,165 @@ def GetExtraDimensions( media ):
return ( extra_width, extra_height )
class AnimationBar( wx.Window ):
def __init__( self, parent, media, media_window ):
( parent_width, parent_height ) = parent.GetClientSize()
wx.Window.__init__( self, parent, size = ( parent_width, ANIMATED_SCANBAR_HEIGHT ), pos = ( 0, parent_height - ANIMATED_SCANBAR_HEIGHT ) )
self._canvas_bmp = wx.EmptyBitmap( parent_width, ANIMATED_SCANBAR_HEIGHT, 24 )
self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
self._media = media
self._media_window = media_window
self._num_frames = self._media.GetNumFrames()
self._current_frame_index = 0
self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse )
self.Bind( wx.EVT_TIMER, self.EventTimerFlash, id = ID_TIMER_FLASH )
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
self.Bind( wx.EVT_PAINT, self.EventPaint )
self.Bind( wx.EVT_SIZE, self.EventResize )
if media.GetMime() == HC.APPLICATION_FLASH:
self._timer_flash = wx.Timer( self, id = ID_TIMER_FLASH )
self._timer_flash.Start( 100, wx.TIMER_CONTINUOUS )
self._Draw()
def _Draw( self ):
( my_width, my_height ) = self._canvas_bmp.GetSize()
dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
dc.SetPen( wx.TRANSPARENT_PEN )
if self._media.GetMime() in HC.IMAGES:
image_container = self._media_window.GetImageContainer()
num_frames_rendered = image_container.GetNumFramesRendered()
num_frames = image_container.GetNumFrames()
my_rendered_width = int( my_width * ( float( num_frames_rendered ) / num_frames ) )
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) ) )
dc.DrawRectangle( 0, 0, my_rendered_width, ANIMATED_SCANBAR_HEIGHT )
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_SCROLLBAR ) ) )
dc.DrawRectangle( my_rendered_width, 0, my_width - my_rendered_width, ANIMATED_SCANBAR_HEIGHT )
else:
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) ) )
dc.DrawRectangle( 0, 0, my_width, ANIMATED_SCANBAR_HEIGHT )
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_SCROLLBAR ) ) )
dc.DrawRectangle( int( float( my_width - ANIMATED_SCANBAR_CARET_WIDTH ) * float( self._current_frame_index ) / float( self._num_frames - 1 ) ), 0, ANIMATED_SCANBAR_CARET_WIDTH, ANIMATED_SCANBAR_HEIGHT )
def EventKeyDown( self, event ):
self.GetParent().GetParent().ProcessEvent( event )
def EventMouse( self, event ):
( my_width, my_height ) = self.GetClientSize()
if event.Dragging() or event.ButtonDown():
( x, y ) = event.GetPosition()
compensated_x_position = x - ( ANIMATED_SCANBAR_CARET_WIDTH / 2 )
proportion = float( compensated_x_position ) / float( my_width - ANIMATED_SCANBAR_CARET_WIDTH )
if proportion < 0: proportion = 0
if proportion > 1: proportion = 1
self._current_frame_index = int( proportion * ( self._num_frames - 1 ) + 0.5 )
self._Draw()
should_pause = event.Dragging()
self._media_window.GotoFrame( self._current_frame_index )
if not should_pause: self._media_window.Play()
self.GetParent().GetParent().KeepCursorAlive()
else:
screen_position = self.ClientToScreen( event.GetPosition() )
( x, y ) = self.GetParent().ScreenToClient( screen_position )
event.SetX( x )
event.SetY( y )
event.ResumePropagation( 1 )
event.Skip()
def EventPaint( self, event ): wx.BufferedPaintDC( self, self._canvas_bmp )
def EventResize( self, event ):
( my_width, my_height ) = self.GetClientSize()
( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize()
if my_width != current_bmp_width or my_height != current_bmp_height:
if my_width > 0 and my_height > 0:
self._canvas_bmp.Destroy()
self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 )
self._Draw()
def EventTimerFlash( self, event ):
# maybe need to pause this while mouse dragging events are occuring? whatever
if self.IsShown() and self._media.GetMime() == HC.APPLICATION_FLASH:
frame_index = self._media_window.CurrentFrame()
if frame_index != self._current_frame_index:
self._current_frame_index = frame_index
self._Draw()
def GotoFrame( self, frame_index ):
self._current_frame_index = frame_index
self._Draw()
class Canvas():
def __init__( self, file_service_identifier, image_cache ):
@ -938,6 +1097,8 @@ class CanvasFullscreenMediaList( ClientGUIMixins.ListeningMediaList, Canvas, Cli
event.Skip()
def EventFullscreenSwitch( self, event ): self._FullscreenSwitch()
def EventTimerCursorHide( self, event ):
if self._menu_open: self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT )
@ -1943,10 +2104,14 @@ class FullscreenPopoutFilterCustom( FullscreenPopout ):
actions = wx.Button( window, label = 'actions' )
actions.Bind( wx.EVT_BUTTON, parent.EventActions )
fullscreen_switch = wx.Button( window, label = 'switch fullscreen' )
fullscreen_switch.Bind( wx.EVT_BUTTON, parent.EventFullscreenSwitch )
done = wx.Button( window, label = 'done' )
done.Bind( wx.EVT_BUTTON, parent.EventClose )
vbox.AddF( actions, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( fullscreen_switch, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( done, FLAGS_EXPAND_PERPENDICULAR )
window.SetSizer( vbox )
@ -1976,6 +2141,9 @@ class FullscreenPopoutFilterInbox( FullscreenPopout ):
back = wx.Button( window, label = 'back' )
back.Bind( wx.EVT_BUTTON, parent.EventButtonBack )
fullscreen_switch = wx.Button( window, label = 'switch fullscreen' )
fullscreen_switch.Bind( wx.EVT_BUTTON, parent.EventFullscreenSwitch )
done = wx.Button( window, label = 'done' )
done.Bind( wx.EVT_BUTTON, parent.EventButtonDone )
@ -1983,6 +2151,7 @@ class FullscreenPopoutFilterInbox( FullscreenPopout ):
vbox.AddF( delete, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( skip, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( back, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( fullscreen_switch, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( done, FLAGS_EXPAND_PERPENDICULAR )
window.SetSizer( vbox )
@ -2012,6 +2181,9 @@ class FullscreenPopoutFilterLike( FullscreenPopout ):
back = wx.Button( window, label = 'back' )
back.Bind( wx.EVT_BUTTON, parent.EventButtonBack )
fullscreen_switch = wx.Button( window, label = 'switch fullscreen' )
fullscreen_switch.Bind( wx.EVT_BUTTON, parent.EventFullscreenSwitch )
done = wx.Button( window, label = 'done' )
done.Bind( wx.EVT_BUTTON, parent.EventButtonDone )
@ -2019,6 +2191,7 @@ class FullscreenPopoutFilterLike( FullscreenPopout ):
vbox.AddF( dislike, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( skip, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( back, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( fullscreen_switch, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( done, FLAGS_EXPAND_PERPENDICULAR )
window.SetSizer( vbox )
@ -2128,6 +2301,9 @@ class FullscreenPopoutFilterNumerical( FullscreenPopout ):
dont_filter = wx.Button( window, label = 'don\'t filter this file' )
dont_filter.Bind( wx.EVT_BUTTON, self._callable_parent.EventButtonDontFilter )
fullscreen_switch = wx.Button( window, label = 'switch fullscreen' )
fullscreen_switch.Bind( wx.EVT_BUTTON, self._callable_parent.EventFullscreenSwitch )
done = wx.Button( window, label = 'done' )
done.Bind( wx.EVT_BUTTON, self._callable_parent.EventButtonDone )
@ -2143,6 +2319,7 @@ class FullscreenPopoutFilterNumerical( FullscreenPopout ):
vbox.AddF( skip, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( back, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( dont_filter, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( fullscreen_switch, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( done, FLAGS_EXPAND_PERPENDICULAR )
window.SetSizer( vbox )
@ -2807,6 +2984,8 @@ class RatingsFilterFrameNumerical( ClientGUICommon.FrameThatResizes ):
self.Destroy()
def EventFullscreenSwitch( self, event ): self._FullscreenSwitch()
def EventKeyDown( self, event ):
if event.KeyCode in ( wx.WXK_SPACE, wx.WXK_UP, wx.WXK_NUMPAD_UP ): self._Skip()
@ -3333,144 +3512,6 @@ class MediaContainer( wx.Window ):
class AnimationBar( wx.Window ):
def __init__( self, parent, media, media_window ):
( parent_width, parent_height ) = parent.GetClientSize()
wx.Window.__init__( self, parent, size = ( parent_width, ANIMATED_SCANBAR_HEIGHT ), pos = ( 0, parent_height - ANIMATED_SCANBAR_HEIGHT ) )
self._canvas_bmp = wx.EmptyBitmap( parent_width, ANIMATED_SCANBAR_HEIGHT, 24 )
self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
self._media = media
self._media_window = media_window
self._num_frames = self._media.GetNumFrames()
self._current_frame_index = 0
self.Bind( wx.EVT_MOUSE_EVENTS, self.EventMouse )
self.Bind( wx.EVT_TIMER, self.EventTimerFlash, id = ID_TIMER_FLASH )
self.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
self.Bind( wx.EVT_PAINT, self.EventPaint )
self.Bind( wx.EVT_SIZE, self.EventResize )
if media.GetMime() == HC.APPLICATION_FLASH:
self._timer_flash = wx.Timer( self, id = ID_TIMER_FLASH )
self._timer_flash.Start( 100, wx.TIMER_CONTINUOUS )
self._Draw()
def _Draw( self ):
( my_width, my_height ) = self._canvas_bmp.GetSize()
dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
dc.SetPen( wx.TRANSPARENT_PEN )
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) ) )
dc.DrawRectangle( 0, 0, my_width, ANIMATED_SCANBAR_HEIGHT )
dc.SetBrush( wx.Brush( wx.SystemSettings.GetColour( wx.SYS_COLOUR_SCROLLBAR ) ) )
dc.DrawRectangle( int( float( my_width - ANIMATED_SCANBAR_CARET_WIDTH ) * float( self._current_frame_index ) / float( self._num_frames - 1 ) ), 0, ANIMATED_SCANBAR_CARET_WIDTH, ANIMATED_SCANBAR_HEIGHT )
def EventKeyDown( self, event ):
self.GetParent().GetParent().ProcessEvent( event )
def EventMouse( self, event ):
( my_width, my_height ) = self.GetClientSize()
if event.Dragging() or event.ButtonDown():
( x, y ) = event.GetPosition()
compensated_x_position = x - ( ANIMATED_SCANBAR_CARET_WIDTH / 2 )
proportion = float( compensated_x_position ) / float( my_width - ANIMATED_SCANBAR_CARET_WIDTH )
if proportion < 0: proportion = 0
if proportion > 1: proportion = 1
self._current_frame_index = int( proportion * ( self._num_frames - 1 ) + 0.5 )
self._Draw()
should_pause = event.Dragging()
self._media_window.GotoFrame( self._current_frame_index )
if not should_pause: self._media_window.Play()
self.GetParent().GetParent().KeepCursorAlive()
else:
screen_position = self.ClientToScreen( event.GetPosition() )
( x, y ) = self.GetParent().ScreenToClient( screen_position )
event.SetX( x )
event.SetY( y )
event.ResumePropagation( 1 )
event.Skip()
def EventPaint( self, event ): wx.BufferedPaintDC( self, self._canvas_bmp )
def EventResize( self, event ):
( my_width, my_height ) = self.GetClientSize()
( current_bmp_width, current_bmp_height ) = self._canvas_bmp.GetSize()
if my_width != current_bmp_width or my_height != current_bmp_height:
if my_width > 0 and my_height > 0:
self._canvas_bmp.Destroy()
self._canvas_bmp = wx.EmptyBitmap( my_width, my_height, 24 )
self._Draw()
def EventTimerFlash( self, event ):
# maybe need to pause this while mouse dragging events are occuring? whatever
if self.IsShown() and self._media.GetMime() == HC.APPLICATION_FLASH:
frame_index = self._media_window.CurrentFrame()
if frame_index != self._current_frame_index:
self._current_frame_index = frame_index
self._Draw()
def GotoFrame( self, frame_index ):
self._current_frame_index = frame_index
self._Draw()
class EmbedButton( wx.Window ):
def __init__( self, parent, size ):
@ -3670,6 +3711,13 @@ class Image( wx.Window ):
if self._image_container.HasFrame( self._current_frame_index ):
if not self._image_container.IsAnimated():
dc.SetBackground( wx.Brush( wx.WHITE ) )
dc.Clear()
current_frame = self._image_container.GetFrame( self._current_frame_index )
( my_width, my_height ) = self._canvas_bmp.GetSize()
@ -3778,6 +3826,8 @@ class Image( wx.Window ):
def GetImageContainer( self ): return self._image_container
def GotoFrame( self, frame_index ):
self._current_frame_index = frame_index

View File

@ -121,6 +121,22 @@ class AutoCompleteDropdown( wx.TextCtrl ):
tlp.Bind( wx.EVT_MOVE, self.EventMove )
parent = self
while True:
try:
parent = parent.GetParent()
if issubclass( type( parent ), wx.ScrolledWindow ):
parent.Bind( wx.EVT_SCROLLWIN, self.EventMove )
except: break
wx.CallAfter( self._UpdateList )
@ -140,15 +156,18 @@ class AutoCompleteDropdown( wx.TextCtrl ):
def _ShowDropdownIfFocussed( self ):
if not self._dropdown_window.IsShown() and self.GetTopLevelParent().IsActive() and wx.Window.FindFocus() == self:
if self.GetTopLevelParent().IsActive() and wx.Window.FindFocus() == self:
( my_width, my_height ) = self.GetSize()
self._dropdown_window.Fit()
self._dropdown_window.SetSize( ( my_width, -1 ) )
self._dropdown_window.Layout()
if not self._dropdown_window.IsShown():
self._dropdown_window.Fit()
self._dropdown_window.SetSize( ( my_width, -1 ) )
self._dropdown_window.Layout()
self._dropdown_window.SetPosition( self.ClientToScreenXY( -2, my_height - 2 ) )
@ -234,7 +253,7 @@ class AutoCompleteDropdown( wx.TextCtrl ):
try: self._HideDropdown()
except: pass
lag = 100
lag = 250
self._move_hide_timer.Start( lag, wx.TIMER_ONE_SHOT )
@ -1267,6 +1286,8 @@ class ListBook( wx.Panel ):
self.Layout()
self.Refresh()
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, -1 )
self.ProcessEvent( event )
@ -2106,7 +2127,7 @@ class PopupMessageError( PopupMessage ):
self._copy_tb_button.Show()
self.GetParent().EventMove( event )
self.GetParent().MakeSureEverythingFits()
class PopupMessageFiles( PopupMessage ):
@ -2135,6 +2156,143 @@ class PopupMessageFiles( PopupMessage ):
HC.pubsub.pub( 'new_page_query', HC.LOCAL_FILE_SERVICE_IDENTIFIER, initial_media_results = media_results )
class PopupMessageGauge( PopupMessage ):
def __init__( self, parent, job_key, message_string ):
PopupMessage.__init__( self, parent )
self._message_string = message_string
self._job_key = job_key
self._hashes = set()
self._done = False
vbox = wx.BoxSizer( wx.VERTICAL )
self._text = wx.StaticText( self, label = self._message_string , style = wx.ALIGN_CENTER )
self._text.Wrap( 380 )
self._text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
hbox = wx.BoxSizer( wx.HORIZONTAL )
self._gauge = Gauge( self, size = ( 380, -1 ) )
self._gauge.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
self._cancel_button = wx.Button( self, label = 'cancel' )
self._cancel_button.Bind( wx.EVT_BUTTON, self.EventCancelButton )
self._cancel_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
hbox.AddF( self._gauge, FLAGS_EXPAND_BOTH_WAYS )
hbox.AddF( self._cancel_button, FLAGS_MIXED )
self._show_file_button = wx.Button( self, label = self._message_string )
self._show_file_button.Bind( wx.EVT_BUTTON, self.EventShowFileButton )
self._show_file_button.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
self._show_file_button.Hide()
vbox.AddF( self._text, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( hbox, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._show_file_button, FLAGS_EXPAND_PERPENDICULAR )
self.SetSizer( vbox )
HC.pubsub.sub( self, 'Failed', 'message_gauge_failed' )
HC.pubsub.sub( self, 'SetInfo', 'message_gauge_info' )
HC.pubsub.sub( self, 'Importing', 'message_gauge_importing' )
HC.pubsub.sub( self, 'Done', 'message_gauge_done' )
def Done( self, job_key, hashes ):
if job_key == self._job_key:
self._done = True
self._hashes = hashes
self._text.Hide()
self._gauge.Hide()
self._show_file_button.Show()
self.GetParent().MakeSureEverythingFits()
def EventCancelButton( self, event ):
self._job_key.Cancel()
self.GetParent().Dismiss( self )
def EventDismiss( self, event ):
if not self._done:
import ClientGUIDialogs
with ClientGUIDialogs.DialogYesNo( self, 'Do you want to continue the download in the background, or cancel it?', yes_label = 'continue', no_label = 'cancel' ) as dlg:
if dlg.ShowModal() != wx.ID_YES: self._job_key.Cancel()
self.GetParent().Dismiss( self )
def EventShowFileButton( self, event ):
media_results = HC.app.Read( 'media_results', HC.LOCAL_FILE_SERVICE_IDENTIFIER, self._hashes )
HC.pubsub.pub( 'new_page_query', HC.LOCAL_FILE_SERVICE_IDENTIFIER, initial_media_results = media_results )
def Failed( self, job_key ):
if job_key == self._job_key: self.GetParent().Dismiss( self )
def Importing( self, job_key ):
if job_key == self._job_key:
self._text.SetLabel( 'importing ' + self._message_string )
self._text.Show()
self._gauge.Hide()
self._cancel_button.Hide()
self._show_file_button.Hide()
self.GetParent().MakeSureEverythingFits()
def SetInfo( self, job_key, range, value ):
if job_key == self._job_key:
if range is None:
self._gauge.Pulse()
byte_info = HC.ConvertIntToBytes( value )
else:
self._gauge.SetRange( range )
self._gauge.SetValue( value )
byte_info = HC.ConvertIntToBytes( value ) + '/' + HC.ConvertIntToBytes( range )
self._text.SetLabel( self._message_string + ' - ' + byte_info )
self.GetParent().MakeSureEverythingFits()
class PopupMessageText( PopupMessage ):
def __init__( self, parent, message_string ):
@ -2240,6 +2398,12 @@ class PopupMessageManager( wx.Frame ):
window = PopupMessageFiles( self, message_string, hashes )
elif message_type == HC.MESSAGE_TYPE_FILE_DOWNLOAD_GAUGE:
( job_key, message_string ) = info
window = PopupMessageGauge( self, job_key, message_string )
return window
@ -2265,6 +2429,10 @@ class PopupMessageManager( wx.Frame ):
message_string = HC.u( message_string )
elif message_type == HC.MESSAGE_TYPE_FILE_DOWNLOAD_GAUGE:
( job_key, message_string ) = info
try: print( message_string )
except: print( repr( message_string ) )
@ -2343,6 +2511,8 @@ class PopupMessageManager( wx.Frame ):
event.Skip()
def MakeSureEverythingFits( self ): self._SizeAndPositionAndShow()
class RegexButton( wx.Button ):
ID_REGEX_WHITESPACE = 0

View File

@ -1,5 +1,6 @@
import Crypto.PublicKey.RSA
import HydrusConstants as HC
import HydrusDownloading
import HydrusEncryption
import HydrusTags
import ClientConstants as CC
@ -12,6 +13,7 @@ import re
import shutil
import string
import subprocess
import threading
import time
import traceback
import urllib
@ -3860,6 +3862,88 @@ class DialogSelectLocalFiles( Dialog ):
except: wx.MessageBox( traceback.format_exc() )
class DialogSelectYoutubeURL( Dialog ):
def __init__( self, parent, info ):
def InitialiseControls():
self._urls = ClientGUICommon.SaneListCtrl( self, 360, [ ( 'format', 150 ), ( 'resolution', 150 ) ] )
self._urls.SetMinSize( ( 360, 200 ) )
self._ok = wx.Button( self, label = 'ok' )
self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
self._ok.SetForegroundColour( ( 0, 128, 0 ) )
self._cancel = wx.Button( self, id = wx.ID_CANCEL, label='cancel' )
self._cancel.SetForegroundColour( ( 128, 0, 0 ) )
def PopulateControls():
for ( extension, resolution ) in self._info: self._urls.Append( ( extension, resolution ), ( extension, resolution ) )
self._urls.SortListItems( 0 )
def ArrangeControls():
buttons = wx.BoxSizer( wx.HORIZONTAL )
buttons.AddF( self._ok, FLAGS_MIXED )
buttons.AddF( self._cancel, FLAGS_MIXED )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._urls, FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( buttons, FLAGS_BUTTON_SIZERS )
self.SetSizer( vbox )
( x, y ) = self.GetEffectiveMinSize()
self.SetInitialSize( ( x, y ) )
Dialog.__init__( self, parent, 'choose youtube format' )
self._info = info
InitialiseControls()
PopulateControls()
ArrangeControls()
wx.CallAfter( self._ok.SetFocus )
def EventOK( self, event ):
indices = self._urls.GetAllSelected()
if len( indices ) > 0:
for index in indices:
( extension, resolution ) = self._urls.GetClientData( index )
( url, title ) = self._info[ ( extension, resolution ) ]
job_key = HC.JobKey()
threading.Thread( target = HydrusDownloading.DownloadYoutubeURL, args = ( job_key, url ) ).start()
message_string = title + ' ' + resolution + ' ' + extension
HC.pubsub.pub( 'message', HC.Message( HC.MESSAGE_TYPE_FILE_DOWNLOAD_GAUGE, ( job_key, message_string ) ) )
self.EndModal( wx.ID_OK )
class DialogSetupCustomFilterActions( Dialog ):
def __init__( self, parent ):

View File

@ -7,6 +7,7 @@ import ClientConstantsMessages
import ClientGUICommon
import ClientGUIDialogs
import collections
import HydrusNATPunch
import itertools
import os
import random
@ -7134,3 +7135,154 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
class DialogManageUPnP( ClientGUIDialogs.Dialog ):
def __init__( self, parent, service_identifier ):
def InitialiseControls():
self._mappings_list_ctrl = ClientGUICommon.SaneListCtrl( self, 760, [ ( 'description', -1 ), ( 'internal ip', 100 ), ( 'internal port', 80 ), ( 'external ip', 100 ), ( 'external port', 80 ), ( 'protocol', 80 ), ( 'enabled', 80 ) ] )
self._mappings_list_ctrl.SetMinSize( ( 760, 660 ) )
self._add_local = wx.Button( self, label = 'add service mapping' )
self._add_local.Bind( wx.EVT_BUTTON, self.EventAddServiceMapping )
self._add_custom = wx.Button( self, label = 'add custom mapping' )
self._add_custom.Bind( wx.EVT_BUTTON, self.EventAddCustomMapping )
self._edit = wx.Button( self, label = 'edit mapping' )
self._edit.Bind( wx.EVT_BUTTON, self.EventEditMapping )
self._remove = wx.Button( self, label = 'remove mapping' )
self._remove.Bind( wx.EVT_BUTTON, self.EventRemoveMapping )
self._ok = wx.Button( self, label = 'ok' )
self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
self._ok.SetForegroundColour( ( 0, 128, 0 ) )
def PopulateControls():
self._RefreshMappings()
def ArrangeControls():
edit_buttons = wx.BoxSizer( wx.HORIZONTAL )
if self._service_identifier == HC.LOCAL_FILE_SERVICE_IDENTIFIER: self._add_local.Hide()
edit_buttons.AddF( self._add_local, FLAGS_MIXED )
edit_buttons.AddF( self._add_custom, FLAGS_MIXED )
edit_buttons.AddF( self._edit, FLAGS_MIXED )
edit_buttons.AddF( self._remove, FLAGS_MIXED )
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( self._mappings_list_ctrl, FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( edit_buttons, FLAGS_BUTTON_SIZERS )
vbox.AddF( self._ok, FLAGS_LONE_BUTTON )
self.SetSizer( vbox )
( x, y ) = self.GetEffectiveMinSize()
self.SetInitialSize( ( x, y ) )
if service_identifier == HC.LOCAL_FILE_SERVICE_IDENTIFIER: title = 'manage local upnp'
else:
# fetch self._service
title = 'manage upnp for ' + service_identifier.GetName()
ClientGUIDialogs.Dialog.__init__( self, parent, title )
wx.MessageBox( 'This dialog is prototype. None of the buttons do anything yet. If it does not load correctly, please inform the hydrus developer of any error messages you receive.' )
self._service_identifier = service_identifier
InitialiseControls()
PopulateControls()
ArrangeControls()
wx.CallAfter( self._ok.SetFocus )
def _AddMapping( self, description, internal_ip, internal_port, external_ip, external_port, protocol, enabled ):
# tell service to add it
pass
def _RemoveMapping( self, internal_ip, internal_port ):
# tell service to remove it
pass
def _RefreshMappings( self ):
self._mappings_list_ctrl.DeleteAllItems()
if self._service_identifier == HC.LOCAL_FILE_SERVICE_IDENTIFIER: self._mappings = HydrusNATPunch.GetUPnPMappings()
else:
wx.MessageBox( 'get mappings from service' )
for mapping in self._mappings: self._mappings_list_ctrl.Append( mapping, mapping )
self._mappings_list_ctrl.SortListItems( 1 )
def EventAddCustomMapping( self, event ):
# start dialog for custom mapping
# attempt to add mapping via service
# add to listctrl
pass
def EventAddServiceMapping( self, event ):
# start dialog with helpful default values
# attempt to add mapping via service
# add to listctrl
pass
def EventEditMapping( self, event ):
# for each index
# populate dialog for edit
# tell service to remove that mapping
# tell service to add that mapping
# update listctrl
pass
def EventOK( self, event ):
self.EndModal( wx.ID_OK )
def EventRemoveMapping( self, event ):
# for each index
# tell service to remove that mapping
# remove all selected from listctrl
pass

View File

@ -368,7 +368,11 @@ class ManagementPanel( wx.lib.scrolledpanel.ScrolledPanel ):
self._file_service_identifier = file_service_identifier
self._tag_service_identifier = HC.COMBINED_TAG_SERVICE_IDENTIFIER
self._paused = False
HC.pubsub.sub( self, 'SetSearchFocus', 'set_search_focus' )
HC.pubsub.sub( self, 'Pause', 'pause' )
HC.pubsub.sub( self, 'Unpause', 'unpause' )
def _MakeCollect( self, sizer ):
@ -392,10 +396,20 @@ class ManagementPanel( wx.lib.scrolledpanel.ScrolledPanel ):
sizer.AddF( self._sort_by, FLAGS_EXPAND_PERPENDICULAR )
def Pause( self, page_key ):
if page_key == self._page_key: self._paused = True
def SetSearchFocus( self, page_key ): pass
def TryToClose( self ): pass
def Unpause( self, page_key ):
if page_key == self._page_key: self._paused = False
class ManagementPanelDumper( ManagementPanel ):
def __init__( self, parent, page, page_key, imageboard, media_results ):
@ -938,6 +952,8 @@ class ManagementPanelDumper( ManagementPanel ):
try:
if self._paused: return
if self._actually_dumping: return
if self._dumping:
@ -1252,7 +1268,7 @@ class ManagementPanelImport( ManagementPanel ):
self._import_overall_info.SetLabel( ', '.join( status_strings ) )
if self._pause_import: self._import_current_info.SetLabel( 'paused' )
if self._pause_import or self._paused: self._import_current_info.SetLabel( 'paused' )
else:
if self._cancel_import_queue.is_set(): self._import_queue = self._import_queue[ : self._import_queue_position ] # cut excess queue
@ -2609,7 +2625,7 @@ class ManagementPanelQuery( ManagementPanel ):
ManagementPanel.__init__( self, parent, page, page_key, file_service_identifier )
self._query_key = HC.QueryKey()
self._query_key = HC.JobKey()
self._synchronised = True
self._include_current_tags = True
self._include_pending_tags = True
@ -2655,7 +2671,7 @@ class ManagementPanelQuery( ManagementPanel ):
self._query_key.Cancel()
self._query_key = HC.QueryKey()
self._query_key = HC.JobKey()
if self._synchronised:
@ -2826,7 +2842,7 @@ class ManagementPanelMessages( wx.ScrolledWindow ):
self._page_key = page_key
self._identity = identity
self._query_key = HC.QueryKey()
self._query_key = HC.JobKey()
# sort out push-refresh later
#self._refresh_inbox = wx.Button( self, label = 'refresh inbox' )
@ -2883,7 +2899,7 @@ class ManagementPanelMessages( wx.ScrolledWindow ):
self._query_key.Cancel()
self._query_key = HC.QueryKey()
self._query_key = HC.JobKey()
if len( current_predicates ) > 0:

View File

@ -916,6 +916,8 @@ class MediaPanelThumbnails( MediaPanel ):
def _CalculateCurrentIndexBounds( self ):
NUM_ROWS_TO_DRAW_AHEAD = 0 # this is buggy
( xUnit, yUnit ) = self.GetScrollPixelsPerUnit()
y_start = self._GetYStart()
@ -932,7 +934,7 @@ class MediaPanelThumbnails( MediaPanel ):
earliest_row = earliest_y / thumbnail_span_height
earliest_index = earliest_row * self._num_columns
earliest_index = max( 0, ( earliest_row - NUM_ROWS_TO_DRAW_AHEAD ) * self._num_columns )
#
@ -940,7 +942,7 @@ class MediaPanelThumbnails( MediaPanel ):
if last_y % thumbnail_span_height > 0: last_row += 1
virtual_last_index = ( ( last_row + 1 ) * self._num_columns ) - 1
virtual_last_index = ( ( last_row + 1 + NUM_ROWS_TO_DRAW_AHEAD ) * self._num_columns ) - 1
last_index = min( virtual_last_index, len( self._sorted_media ) - 1 )
@ -1300,7 +1302,12 @@ class MediaPanelThumbnails( MediaPanel ):
if last_visible_row > current_canvas_num_rows / 2:
if current_canvas_num_rows == 0: new_canvas_num_rows = min( last_visible_row, virtual_num_rows ) + 1
else: new_canvas_num_rows = min( int( current_canvas_num_rows * 2.5 ), virtual_num_rows ) + 1
else:
how_far_we_want_to_extend_to = max( int( current_canvas_num_rows * 2.5 ), last_visible_row )
new_canvas_num_rows = min( how_far_we_want_to_extend_to, virtual_num_rows ) + 1
# +1 to cover gap
@ -1335,6 +1342,13 @@ class MediaPanelThumbnails( MediaPanel ):
def _RemoveMedia( self, singleton_media, collected_media ):
self._drawn_index_bounds = None
MediaPanel._RemoveMedia( self, singleton_media, collected_media )
def _ScrollToMedia( self, media ):
if media is not None:

View File

@ -54,6 +54,13 @@ class PageBase():
def PageShown( self ): HC.pubsub.pub( 'page_shown', self._page_key )
def Pause( self ):
HC.pubsub.pub( 'pause', self._page_key )
HC.pubsub.pub( 'set_focus', self._page_key, None )
def SetPrettyStatus( self, page_key, status ):
if page_key == self._page_key:
@ -76,6 +83,8 @@ class PageBase():
def TryToClose( self ): pass
def Unpause( self ): HC.pubsub.pub( 'unpause', self._page_key )
class PageLog( PageBase, wx.Panel ):
def __init__( self, parent ):

View File

@ -38,7 +38,7 @@ TEMP_DIR = BASE_DIR + os.path.sep + 'temp'
# Misc
NETWORK_VERSION = 10
SOFTWARE_VERSION = 83
SOFTWARE_VERSION = 84
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -84,6 +84,7 @@ IMPORT_FOLDER_TYPE_SYNCHRONISE = 1
MESSAGE_TYPE_TEXT = 0
MESSAGE_TYPE_ERROR = 1
MESSAGE_TYPE_FILES = 2
MESSAGE_TYPE_FILE_DOWNLOAD_GAUGE = 3
GET_DATA = 0
POST_DATA = 1
@ -572,6 +573,8 @@ def CalculateScoreFromRating( count, rating ):
def CleanTag( tag ):
tag = tag[:1024]
if tag == '': return ''
tag = tag.lower()
@ -1935,6 +1938,27 @@ class JobInternal():
self._result_ready.set()
class JobKey():
def __init__( self ):
self._key = os.urandom( 32 )
self._cancelled = threading.Event()
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __hash__( self ): return self._key.__hash__()
def __ne__( self, other ): return self.__hash__() != other.__hash__()
def Cancel( self ): self._cancelled.set()
def GetKey( self ): return self._key
def IsCancelled( self ): return self._cancelled.is_set()
class JobServer():
yaml_tag = u'!JobServer'
@ -1980,27 +2004,6 @@ class Message():
def GetType( self ): return self._message_type
class QueryKey():
def __init__( self ):
self._key = os.urandom( 32 )
self._cancelled = threading.Event()
def __eq__( self, other ): return self.__hash__() == other.__hash__()
def __hash__( self ): return self._key.__hash__()
def __ne__( self, other ): return self.__hash__() != other.__hash__()
def Cancel( self ): self._cancelled.set()
def GetKey( self ): return self._key
def IsCancelled( self ): return self._cancelled.is_set()
class Predicate():
def __init__( self, predicate_type, value, count ):

View File

@ -1,8 +1,10 @@
import bs4
import collections
import httplib
import HydrusConstants as HC
import json
import lxml
import pafy
import traceback
import urllib
import urlparse
@ -71,6 +73,70 @@ def ConvertTagsToServiceIdentifiersToTags( tags, advanced_tag_options ):
return service_identifiers_to_tags
def DownloadYoutubeURL( job_key, url ):
try:
parse_result = urlparse.urlparse( url )
connection = httplib.HTTPConnection( parse_result.hostname )
connection.request( 'GET', url )
response = connection.getresponse()
try: num_bytes = int( response.getheader( 'Content-Length' ) )
except: num_bytes = None
HC.pubsub.pub( 'message_gauge_info', job_key, num_bytes, 0 )
block_size = 64 * 1024
total_num_bytes = 0
temp_path = HC.GetTempPath()
with open( temp_path, 'wb' ) as f:
while True:
if HC.shutdown or job_key.IsCancelled(): return
block = response.read( block_size )
total_num_bytes += len( block )
HC.pubsub.pub( 'message_gauge_info', job_key, num_bytes, total_num_bytes )
if block == '': break
f.write( block )
HC.pubsub.pub( 'message_gauge_importing', job_key )
( result, hash ) = HC.app.WriteSynchronous( 'import_file', temp_path )
if result in ( 'successful', 'redundant' ): HC.pubsub.pub( 'message_gauge_done', job_key, { hash } )
elif result == 'deleted': HC.pubsub.pub( 'message_gauge_failed', job_key )
except:
HC.pubsub.pub( 'message_gauge_failed', job_key )
raise
def GetYoutubeFormats( youtube_url ):
try: p = pafy.Pafy( youtube_url )
except: raise Exception( 'Could not fetch video info from youtube!' )
info = { ( s.extension, s.resolution ) : ( s.url, s.title ) for s in p.streams if s.extension in ( 'flv', 'mp4' ) }
return info
class Downloader():
def __init__( self ):

View File

@ -60,11 +60,7 @@ def GetFileInfo( path, hash ):
( ( width, height ), duration, num_frames ) = HydrusVideoHandling.GetFLVProperties( path )
elif mime == HC.VIDEO_MP4:
( ( width, height ), duration, num_frames ) = HydrusVideoHandling.GetMP4Properties( path )
elif mime == HC.VIDEO_WMV:
elif mime in ( HC.VIDEO_WMV, HC.VIDEO_MP4 ):
( ( width, height ), duration, num_frames ) = HydrusVideoHandling.GetCVVideoProperties( path )

View File

@ -426,6 +426,8 @@ class RenderedImageContainer():
def GetNumFrames( self ): return self._num_frames
def GetNumFramesRendered( self ): return len( self._frames )
def GetResolution( self ): return self._original_resolution
def GetSize( self ): return self._my_resolution

40
include/HydrusNATPunch.py Normal file
View File

@ -0,0 +1,40 @@
import os
import win32com.client
def GetUPnPMappings():
try:
dispatcher = win32com.client.Dispatch( 'HNetCfg.NATUPnP' )
static_port_mappings = dispatcher.StaticPortMappingCollection
except: raise Exception( 'Could not fetch UPnP Manager!' )
if static_port_mappings is None: raise Exception( 'Could not fetch UPnP info!' + os.linesep + 'Make sure UPnP is enabled for your computer and router, or try restarting your router.' )
mappings = []
for i in range( len( static_port_mappings ) ):
static_port_mapping = static_port_mappings[i]
description = static_port_mapping.Description
internal_client = static_port_mapping.InternalClient
internal_port = static_port_mapping.InternalPort
external_ip_address = static_port_mapping.ExternalIPAddress
external_port = static_port_mapping.ExternalPort
protocol = static_port_mapping.Protocol
enabled = static_port_mapping.Enabled
mappings.append( ( description, internal_client, internal_port, external_ip_address, external_port, protocol, enabled ) )
return mappings
# mappings.Add( external_port,'TCP', internal_port, internal_client, enabled true/false, description )
# socket.gethostbyname( socket.gethostname() )

View File

@ -1,3 +1,4 @@
from include import ClientConstants as CC
from include import HydrusConstants as HC
from include import HydrusTags
from include import TestClientConstants
@ -21,11 +22,13 @@ class App( wx.App ):
self._reads = {}
self._reads[ 'options' ] = {}
self._reads[ 'options' ] = CC.CLIENT_DEFAULT_OPTIONS
self._reads[ 'tag_parents' ] = {}
self._reads[ 'tag_service_precedence' ] = []
self._reads[ 'tag_siblings' ] = {}
HC.options = CC.CLIENT_DEFAULT_OPTIONS
self._writes = collections.defaultdict( list )
self._tag_parents_manager = HydrusTags.TagParentsManager()