Version 134

This commit is contained in:
Hydrus 2014-10-22 17:31:58 -05:00
parent 8585942d95
commit 5662e56649
18 changed files with 376 additions and 128 deletions

View File

@ -1,13 +1,12 @@
## Hydrus Image Tagging Network (Client and Server)
A personal booru-style media tagger that can import files and tags from your hard drive and popular websites. Content can be shared with other users via user-run servers.
The hydrus network client is an application written for anon and other internet-fluent media nerds who have large image/swf collections. It browses with tags instead of folders, a little like a *booru on your desktop. Tags and files can be anonymously shared through custom servers that any user may run. Everything is free, and the source code is included with the release. It is developed for Windows, but fairly functional builds for Linux and OS X are released at the same time.
This is the source-only release, meant for people who want to experiment with the code; if you want an executable for Windows, Linux or OS X, please check out the [compiled releases](http://www.mediafire.com/hydrus). All releases include original source code.
The software is constantly being improved. I put out a new release every Wednesday at 8pm Eastern.
The program can do quite a lot! Please check out the help inside the release or [here](http://hydrusnetwork.github.io/hydrus/help).
* [Homepage](http://hydrusnetwork.github.io/hydrus/)
* [Source Code on Github](https://github.com/hydrusnetwork/hydrus)
## Attribution

View File

@ -62,5 +62,7 @@ except:
import traceback
print( 'Critical error occured! Details written to crash.log!' )
with open( 'crash.log', 'wb' ) as f: f.write( traceback.format_exc() )

View File

@ -8,6 +8,31 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 134</h3></li>
<ul>
<li>updated to wx 3.0.1.1</li>
<li>fixed a critical media scrolling bug due to the wx update</li>
<li>improved some bad media scrolling code, sped things up a bit</li>
<li>fixed 'top' and 'bottom' media scrolling events</li>
<li>fixed a typo that meant the default fullscreen media browsing shortcuts were ctrl+ appended rather than working on their own</li>
<li>overly-verbose errors and other text popups are now cropped to 1KB for gui display, with a notice. the full message will be printed to the log as usual</li>
<li>improved how severe boot crashes are reported</li>
<li>fixed a bit of text-reporting code that wasn't handling non-text very well</li>
<li>improved handling of a weird popup message manager error</li>
<li>fixed an occasional overhasty cleanup error in the checkimportfolders daemon</li>
<li>added options for default values for the thread watcher's number of times to check and check period</li>
<li>fixed the thread watcher complaining about closing when the checking was finished</li>
<li>optimised some id generating code to stop spamming the id cache, which I think was overloading after a while and causing weird PyAssertionErrors</li>
<li>neatened autocomplete dropdown service storage and menu id generation</li>
<li>improved menu id generation for tagbox</li>
<li>fixed opening new petition page from view menu</li>
<li>added a 'this might take ages' warning yes/no dialog when trying to delete a tag service</li>
<li>added a little popup message info to report on progress when deleting a tag service</li>
<li>added some server db testing</li>
<li>fixed an error when double-clicking a tag in a page without search predicates</li>
<li>updated some help links from mediafire to the new github releases page</li>
<li>fixed a typo bug in server db's account flushing code</li>
</ul>
<li><h3>version 133</h3></li>
<ul>
<li>reworked the add file process to correct file repository file counts</li>

View File

@ -8,14 +8,15 @@
<div class="content">
<h3>contact and links</h3>
<p>Please send bug reports straight to my email or forum. Your other ideas and comments are always welcome.</p>
<p>I don't really do chat, and I don't get caught up in the social stuff on twitter/tumblr. If you want to tell me something, please just send me a mail. I like to spend a day or so to think before replying to non-urgent emails, but I do reply to everything. If I said I was going to work on something you care about and seem to have forgotten about it, please do nudge me.</p>
<p>If you have a problem with something on someone's file repository, please, <span class="warning">do not come to me</span>, as I can in no way help with your problem. If your ex-gf's nudes have leaked onto the internet, or you find something terribly offensive, or you just plain hate the free flow of information, I cannot help you at all.</p>
<p>I don't really do chat, and I don't get caught up in the social stuff on twitter/tumblr. I like to spend a day or so to think before replying to non-urgent emails, but I do reply to everything.</p>
<p>I delete all tweets and resolved email conversations after three months. So, if you think you are waiting for a reply, or I said I was going to work on something you care about and seem to have forgotten, please do nudge me.</p>
<p>If you have a problem with something on someone else's server, please, <span class="warning">do not come to me</span>, as I cannot help. If your ex-gf's nudes have leaked onto the internet, or you find something terribly offensive, or you just plain hate the free flow of information, I cannot help you at all.</p>
<p>Anyway:</p>
<ul>
<li><a href="mailto:hydrus.admin@gmail.com">email</a></li>
<li><a href="http://hydrus.x10.mx/forum/">forum</a></li>
<li><a href="http://hydrus.tumblr.com/">tumblr</a> (<a href="http://hydrus.tumblr.com/rss">rss</a>)</li>
<li><a href="http://twitter.com/hydrusnetwork">twitter</a> (<a href="http://api.twitter.com/1/statuses/user_timeline.rss?screen_name=hydrusnetwork">rss</a>)</li>
<li><a href="http://twitter.com/hydrusnetwork">twitter</a></li>
</ul>
<p>If you would like to send me something physical, you can use my PO Box:</p>
<ul>
@ -23,7 +24,6 @@
<li>Rockford, IL, 61126</li>
<li>UNITED STATES</li>
</ul>
<p>I try to make my work <a href="http://en.wikipedia.org/wiki/Project_triangle">good and cheap</a>. As a result, I am not very fast. Whenever I say 'x should be done within a week', please don't believe my optimism.</p>
</div>
</body>
</html>

View File

@ -192,6 +192,7 @@ CLIENT_DEFAULT_OPTIONS[ 'num_autocomplete_chars' ] = 2
CLIENT_DEFAULT_OPTIONS[ 'gui_capitalisation' ] = False
CLIENT_DEFAULT_OPTIONS[ 'default_gui_session' ] = 'just a blank page'
CLIENT_DEFAULT_OPTIONS[ 'ac_timings' ] = ( 3, 500, 250 )
CLIENT_DEFAULT_OPTIONS[ 'thread_checker_timings' ] = ( 3, 1200 )
system_predicates = {}
@ -258,22 +259,22 @@ shortcuts[ wx.ACCEL_CTRL ][ ord( 'I' ) ] = 'synchronised_wait_switch'
shortcuts[ wx.ACCEL_CTRL ][ ord( 'Z' ) ] = 'undo'
shortcuts[ wx.ACCEL_CTRL ][ ord( 'Y' ) ] = 'redo'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_UP ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_LEFT ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_UP ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_LEFT ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_PAGEUP ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_PAGEUP ] = 'previous'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_DOWN ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_RIGHT ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_DOWN ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_RIGHT ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_PAGEDOWN ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_PAGEDOWN ] = 'next'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_HOME ] = 'first'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_HOME ] = 'first'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_END ] = 'last'
shortcuts[ wx.ACCEL_CTRL ][ wx.WXK_NUMPAD_END ] = 'last'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_UP ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_LEFT ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_UP ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_LEFT ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_PAGEUP ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_PAGEUP ] = 'previous'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_DOWN ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_RIGHT ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_DOWN ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_RIGHT ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_PAGEDOWN ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_PAGEDOWN ] = 'next'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_HOME ] = 'first'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_HOME ] = 'first'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_END ] = 'last'
shortcuts[ wx.ACCEL_NORMAL ][ wx.WXK_NUMPAD_END ] = 'last'
shortcuts[ wx.ACCEL_SHIFT ][ wx.WXK_UP ] = 'pan_up'
shortcuts[ wx.ACCEL_SHIFT ][ wx.WXK_DOWN ] = 'pan_down'

View File

@ -277,7 +277,7 @@ The database will be locked while the backup occurs, which may lock up your gui
def OnInit( self ):
self.SetAssertMode(wx.PYAPP_ASSERT_SUPPRESS)
HC.app = self
HC.http = HydrusNetworking.HTTPConnectionManager()

View File

@ -4195,6 +4195,7 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ):
HC.repos_changed = True
recalc_combined_mappings = False
message = None
for ( action, details ) in edit_log:
@ -4212,6 +4213,18 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ):
service = self._GetService( c, service_id )
if service.GetServiceType() == HC.TAG_REPOSITORY:
recalc_combined_mappings = True
if message is None:
message = HC.Message( HC.MESSAGE_TYPE_TEXT, { 'text' : 'updating services: deleting tag data' } )
HC.pubsub.pub( 'message', message )
c.execute( 'DELETE FROM services WHERE service_id = ?;', ( service_id, ) )
service_update = HC.ServiceUpdate( HC.SERVICE_UPDATE_RESET )
@ -4263,7 +4276,14 @@ class ServiceDB( FileDB, MessageDB, TagDB, RatingDB ):
if recalc_combined_mappings: self._RecalcCombinedMappings( c )
if recalc_combined_mappings:
message.SetInfo( 'text', 'updating services: recalculating combined tag data' )
self._RecalcCombinedMappings( c )
message.SetInfo( 'text', 'updating services: done!' )
self.pub_after_commit( 'notify_new_pending' )
@ -5800,12 +5820,13 @@ def DAEMONCheckImportFolders():
details[ 'failed_imported_paths' ].add( path )
HC.ShowText( 'Import folder failed to import ' + path + ':' + os.linesep + traceback.format_exc() )
HC.ShowText( 'Import folder failed to import ' + path + ':' + os.linesep * 2 + traceback.format_exc() )
should_action = False
os.remove( temp_path )
try: os.remove( temp_path )
except: pass # sometimes this fails, I think due to old handles not being cleaned up fast enough. np--it'll be cleaned up later
if should_action:

View File

@ -781,8 +781,8 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
if len( petition_resolve_tag_services ) > 0 or len( petition_resolve_file_services ) > 0:
menu.AppendSeparator()
for service in petition_resolve_tag_services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'petitions', service ), p( service.GetName() + ' Petitions' ), p( 'Open a petition tab for ' + service.GetName() ) )
for service in petition_resolve_file_services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'petitions', service ), p( service.GetName() + ' Petitions' ), p( 'Open a petition tab for ' + service.GetName() ) )
for service in petition_resolve_tag_services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'petitions', service.GetServiceKey() ), p( service.GetName() + ' Petitions' ), p( 'Open a petition tab for ' + service.GetName() ) )
for service in petition_resolve_file_services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'petitions', service.GetServiceKey() ), p( service.GetName() + ' Petitions' ), p( 'Open a petition tab for ' + service.GetName() ) )
menu.AppendSeparator()
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'new_import_url' ), p( '&New URL Download Page' ), p( 'Open a new tab to download files from galleries or threads.' ) )
@ -2072,7 +2072,8 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self.Hide()
self._message_manager.CleanBeforeDestroy()
try: self._message_manager.CleanBeforeDestroy()
except: pass
for page in [ self._notebook.GetPage( i ) for i in range( self._notebook.GetPageCount() ) ]: page.CleanBeforeDestroy()

View File

@ -1982,7 +1982,7 @@ class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ):
if self._current_media.HasInbox(): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'archive' ), '&archive' )
if self._current_media.HasArchive(): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'inbox' ), 'return to &inbox' )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'remove', HC.LOCAL_FILE_SERVICE_KEY ), '&remove' )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'remove' ), '&remove' )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete', HC.LOCAL_FILE_SERVICE_KEY ), '&delete' )
menu.AppendSeparator()

View File

@ -14,6 +14,10 @@ import wx.richtext
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
from wx.lib.mixins.listctrl import ColumnSorterMixin
TEXT_CUTOFF = 1024
#
ID_TIMER_ANIMATED = wx.NewId()
ID_TIMER_SLIDESHOW = wx.NewId()
ID_TIMER_MEDIA_INFO_DISPLAY = wx.NewId()
@ -425,13 +429,16 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
self._current_namespace = ''
self._current_matches = []
self._file_service = HC.app.GetManager( 'services' ).GetService( file_service_key )
self._tag_service = HC.app.GetManager( 'services' ).GetService( tag_service_key )
self._file_service_key = file_service_key
self._tag_service_key = tag_service_key
self._file_repo_button = wx.Button( self._dropdown_window, label = self._file_service.GetName() )
file_service = HC.app.GetManager( 'services' ).GetService( self._file_service_key )
tag_service = HC.app.GetManager( 'services' ).GetService( self._tag_service_key )
self._file_repo_button = wx.Button( self._dropdown_window, label = file_service.GetName() )
self._file_repo_button.Bind( wx.EVT_BUTTON, self.EventFileButton )
self._tag_repo_button = wx.Button( self._dropdown_window, label = self._tag_service.GetName() )
self._tag_repo_button = wx.Button( self._dropdown_window, label = tag_service.GetName() )
self._tag_repo_button.Bind( wx.EVT_BUTTON, self.EventTagButton )
self.Bind( wx.EVT_MENU, self.EventMenu )
@ -465,7 +472,7 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
menu = wx.Menu()
for service in services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_file_repository', service ), service.GetName() )
for service in services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_file_repository', service.GetServiceKey() ), service.GetName() )
self.PopupMenu( menu )
@ -482,27 +489,27 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
if command == 'change_file_repository':
service = data
self._file_service_key = data
self._file_service = service
file_service = HC.app.GetManager( 'services' ).GetService( self._file_service_key )
name = service.GetName()
name = file_service.GetName()
self._file_repo_button.SetLabel( name )
HC.pubsub.pub( 'change_file_repository', self._page_key, service.GetServiceKey() )
HC.pubsub.pub( 'change_file_repository', self._page_key, self._file_service_key )
elif command == 'change_tag_repository':
service = data
self._tag_service_key = data
self._tag_service = service
tag_service = tag_service = HC.app.GetManager( 'services' ).GetService( self._tag_service_key )
name = service.GetName()
name = tag_service.GetName()
self._tag_repo_button.SetLabel( name )
HC.pubsub.pub( 'change_tag_repository', self._page_key, service.GetServiceKey() )
HC.pubsub.pub( 'change_tag_repository', self._page_key, self._tag_service_key )
else:
@ -529,7 +536,7 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
menu = wx.Menu()
for service in services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_tag_repository', service ), service.GetName() )
for service in services: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'change_tag_repository', service.GetServiceKey() ), service.GetName() )
self.PopupMenu( menu )
@ -609,8 +616,8 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
self._first_letters = ''
self._current_namespace = ''
if self._file_service.GetServiceKey() == HC.COMBINED_FILE_SERVICE_KEY: search_service_key = self._tag_service.GetServiceKey()
else: search_service_key = self._file_service.GetServiceKey()
if self._file_service_key == HC.COMBINED_FILE_SERVICE_KEY: search_service_key = self._tag_service_key
else: search_service_key = self._file_service_key
matches = HC.app.Read( 'file_system_predicates', search_service_key )
@ -653,7 +660,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
if len( search_text ) < num_first_letters:
results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service.GetServiceKey(), tag_service_key = self._tag_service.GetServiceKey(), tag = search_text, include_current = self._include_current, include_pending = self._include_pending )
results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, tag = search_text, include_current = self._include_current, include_pending = self._include_pending )
matches = results.GetMatches( half_complete_tag )
@ -663,7 +670,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
self._first_letters = half_complete_tag
self._cached_results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service.GetServiceKey(), tag_service_key = self._tag_service.GetServiceKey(), half_complete_tag = search_text, include_current = self._include_current, include_pending = self._include_pending )
self._cached_results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, half_complete_tag = search_text, include_current = self._include_current, include_pending = self._include_pending )
matches = self._cached_results.GetMatches( half_complete_tag )
@ -682,8 +689,8 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
else: tags_managers.append( m.GetTagsManager() )
lists_of_current_tags = [ list( tags_manager.GetCurrent( self._tag_service.GetServiceKey() ) ) for tags_manager in tags_managers ]
lists_of_pending_tags = [ list( tags_manager.GetPending( self._tag_service.GetServiceKey() ) ) for tags_manager in tags_managers ]
lists_of_current_tags = [ list( tags_manager.GetCurrent( self._tag_service_key ) ) for tags_manager in tags_managers ]
lists_of_pending_tags = [ list( tags_manager.GetPending( self._tag_service_key ) ) for tags_manager in tags_managers ]
current_tags_flat_iterable = itertools.chain.from_iterable( lists_of_current_tags )
pending_tags_flat_iterable = itertools.chain.from_iterable( lists_of_pending_tags )
@ -705,7 +712,7 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
if self._include_current: tags_to_do.update( current_tags_to_count.keys() )
if self._include_pending: tags_to_do.update( pending_tags_to_count.keys() )
results = CC.AutocompleteMatchesPredicates( self._tag_service.GetServiceKey(), [ HC.Predicate( HC.PREDICATE_TYPE_TAG, ( operator, tag ), { HC.CURRENT : current_tags_to_count[ tag ], HC.PENDING : pending_tags_to_count[ tag ] } ) for tag in tags_to_do ] )
results = CC.AutocompleteMatchesPredicates( self._tag_service_key, [ HC.Predicate( HC.PREDICATE_TYPE_TAG, ( operator, tag ), { HC.CURRENT : current_tags_to_count[ tag ], HC.PENDING : pending_tags_to_count[ tag ] } ) for tag in tags_to_do ] )
matches = results.GetMatches( half_complete_tag )
@ -791,15 +798,15 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
tag_censorship_manager = HC.app.GetManager( 'tag_censorship' )
result = tag_censorship_manager.FilterTags( self._tag_service.GetServiceKey(), ( tag, ) )
result = tag_censorship_manager.FilterTags( self._tag_service_key, ( tag, ) )
if len( result ) > 0:
tag_parents_manager = HC.app.GetManager( 'tag_parents' )
parents = tag_parents_manager.GetParents( self._tag_service.GetServiceKey(), tag )
parents = tag_parents_manager.GetParents( self._tag_service_key, tag )
parents = tag_censorship_manager.FilterTags( self._tag_service.GetServiceKey(), parents )
parents = tag_censorship_manager.FilterTags( self._tag_service_key, parents )
self._chosen_tag_callable( tag, parents )
@ -845,7 +852,7 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
if len( search_text ) < num_first_letters:
results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service.GetServiceKey(), tag_service_key = self._tag_service.GetServiceKey(), tag = search_text, collapse = False )
results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, tag = search_text, collapse = False )
matches = results.GetMatches( half_complete_tag )
@ -855,7 +862,7 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
self._first_letters = half_complete_tag
self._cached_results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service.GetServiceKey(), tag_service_key = self._tag_service.GetServiceKey(), half_complete_tag = search_text, collapse = False )
self._cached_results = HC.app.Read( 'autocomplete_tags', file_service_key = self._file_service_key, tag_service_key = self._tag_service_key, half_complete_tag = search_text, collapse = False )
matches = self._cached_results.GetMatches( half_complete_tag )
@ -905,7 +912,7 @@ class AutoCompleteDropdownTagsWrite( AutoCompleteDropdownTags ):
parents_manager = HC.app.GetManager( 'tag_parents' )
raw_parents = parents_manager.GetParents( self._tag_service.GetServiceKey(), tag )
raw_parents = parents_manager.GetParents( self._tag_service_key, tag )
parents = [ HC.Predicate( HC.PREDICATE_TYPE_PARENT, raw_parent ) for raw_parent in raw_parents ]
@ -1859,7 +1866,7 @@ class ListBox( wx.ScrolledWindow ):
self.DoPrepareDC( cdc ) # because this is a scrolled window
return wx.BufferedDC( cdc, self._canvas_bmp )
return wx.BufferedDC( cdc, self._canvas_bmp, wx.BUFFER_VIRTUAL_AREA )
def _GetTextColour( self, text ): return ( 0, 111, 250 )
@ -1968,7 +1975,20 @@ class ListBox( wx.ScrolledWindow ):
( command, data ) = action
if command == 'copy': HC.pubsub.pub( 'clipboard', 'text', data )
if command == 'copy_term':
term = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
HC.pubsub.pub( 'clipboard', 'text', term )
elif command == 'copy_sub_term':
term = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
sub_term = term.split( ':', 1 )[1]
HC.pubsub.pub( 'clipboard', 'text', sub_term )
else:
event.Skip()
@ -1990,13 +2010,13 @@ class ListBox( wx.ScrolledWindow ):
term = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy', term ), 'copy ' + term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy_term' ), 'copy ' + term )
if ':' in term:
sub_term = term.split( ':', 1 )[1]
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy', sub_term ), 'copy ' + sub_term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy_sub_term' ), 'copy ' + sub_term )
self.PopupMenu( menu )
@ -2540,6 +2560,17 @@ class PopupMessageError( PopupMessage ):
if len( HC.u( value ) ) > 0:
if len( HC.u( value ) ) > TEXT_CUTOFF:
new_value = 'An error occured that is too long to display here. Here is the start of it (the rest is printed to the log):'
new_value += os.linesep * 2
new_value += value[:TEXT_CUTOFF]
value = new_value
text = FitResistantStaticText( self, label = HC.u( value ) )
text.Wrap( 380 )
text.Bind( wx.EVT_RIGHT_DOWN, self.EventDismiss )
@ -2593,7 +2624,7 @@ class PopupMessageFiles( PopupMessage ):
PopupMessage.__init__( self, parent, message )
text = message.GetInfo( 'text' )
text = message.GetInfo( 'text' )[:TEXT_CUTOFF]
vbox = wx.BoxSizer( wx.VERTICAL )
@ -2717,7 +2748,7 @@ class PopupMessageGauge( PopupMessage ):
def Update( self ):
mode = self._message.GetInfo( 'mode' )
text = self._message.GetInfo( 'text' )
text = self._message.GetInfo( 'text' )[:TEXT_CUTOFF]
if self._job_key.IsPausable() and self._created - HC.GetNow() > 2: self._pause_button.Show()
else: self._pause_button.Hide()
@ -2771,7 +2802,7 @@ class PopupMessageText( PopupMessage ):
vbox = wx.BoxSizer( wx.VERTICAL )
text = message.GetInfo( 'text' )
text = self._ProcessText( HC.u( message.GetInfo( 'text' ) ) )
self._text = FitResistantStaticText( self, label = text )
self._text.Wrap( 380 )
@ -2782,9 +2813,25 @@ class PopupMessageText( PopupMessage ):
self.SetSizer( vbox )
def _ProcessText( self, text ):
if len( text ) > TEXT_CUTOFF:
new_text = 'A text notice occured that is too long to display here. Here is the start of it (the rest is printed to the log):'
new_text += os.linesep * 2
new_text += text[:TEXT_CUTOFF]
text = new_text
return text
def Update( self ):
text = self._message.GetInfo( 'text' )
text = self._ProcessText( self._message.GetInfo( 'text' ) )
if self._text.GetLabel() != text: self._text.SetLabel( text )
@ -2906,23 +2953,46 @@ class PopupMessageManager( wx.Frame ):
def _SizeAndPositionAndShow( self ):
self.Fit()
parent = self.GetParent()
( parent_width, parent_height ) = parent.GetClientSize()
( my_width, my_height ) = self.GetClientSize()
my_x = ( parent_width - my_width ) - 5
my_y = ( parent_height - my_height ) - 15
self.SetPosition( parent.ClientToScreenXY( my_x, my_y ) )
num_messages_displayed = self._message_vbox.GetItemCount()
if num_messages_displayed > 0: self.Show()
else: self.Hide()
try:
self.Fit()
parent = self.GetParent()
( parent_width, parent_height ) = parent.GetClientSize()
( my_width, my_height ) = self.GetClientSize()
my_x = ( parent_width - my_width ) - 5
my_y = ( parent_height - my_height ) - 15
self.SetPosition( parent.ClientToScreenXY( my_x, my_y ) )
num_messages_displayed = self._message_vbox.GetItemCount()
if num_messages_displayed > 0: self.Show()
else: self.Hide()
except:
# I don't understand the error here.
# It happened for someone in Fit(), causing 'C++ assertion 'm_hDWP failed at blah ... EndRepositioningChildren Shouldn't be called'
# It might be related to an id-cache overflow error I had before, in which case it is fixed
text = 'The popup message manager experienced a fatal error and will now stop working! Please restart the client as soon as possible! If this keeps happening, please email the details and your client.log to the hydrus developer.'
print( text )
print( traceback.format_exc() )
wx.MessageBox( text )
self._timer.Stop()
self.CleanBeforeDestroy()
self.Destroy()
def AddMessage( self, message ):
@ -4010,12 +4080,25 @@ class TagsBox( ListBox ):
( command, data ) = action
if command == 'copy': HC.pubsub.pub( 'clipboard', 'text', data )
if command == 'copy_term':
term = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
HC.pubsub.pub( 'clipboard', 'text', term )
elif command == 'copy_sub_term':
term = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
sub_term = term.split( ':', 1 )[1]
HC.pubsub.pub( 'clipboard', 'text', sub_term )
elif command == 'copy_all_tags': HC.pubsub.pub( 'clipboard', 'text', os.linesep.join( self._GetAllTagsForClipboard() ) )
elif command == 'copy_all_tags_with_counts': HC.pubsub.pub( 'clipboard', 'text', os.linesep.join( self._GetAllTagsForClipboard( with_counts = True ) ) )
elif command in ( 'parent', 'sibling' ):
tag = data
tag = self._strings_to_terms[ self._ordered_strings[ self._current_selected_index ] ]
import ClientGUIDialogsManage
@ -4056,19 +4139,19 @@ class TagsBox( ListBox ):
if type( term ) in ( str, unicode ):
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy', term ), 'copy ' + term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy_term' ), 'copy ' + term )
if ':' in term:
sub_term = term.split( ':', 1 )[1]
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy', sub_term ), 'copy ' + sub_term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'copy_sub_term' ), 'copy ' + sub_term )
menu.AppendSeparator()
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'parent', term ), 'add parent to ' + term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'sibling', term ), 'add sibling to ' + term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'parent' ), 'add parent to ' + term )
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'sibling' ), 'add sibling to ' + term )

View File

@ -3004,6 +3004,19 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
self._listbook.AddPage( self._shortcuts_page, 'shortcuts' )
# thread checker
self._thread_checker_page = wx.Panel( self._listbook )
self._thread_checker_page.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
self._thread_times_to_check = wx.SpinCtrl( self._thread_checker_page, min = 0, max = 100 )
self._thread_times_to_check.SetToolTipString( 'how many times the thread checker will check' )
self._thread_check_period = wx.SpinCtrl( self._thread_checker_page, min = 30, max = 86400 )
self._thread_check_period.SetToolTipString( 'how long the checker will wait between checks' )
self._listbook.AddPage( self._thread_checker_page, 'thread checker' )
#
self._ok = wx.Button( self, id = wx.ID_OK, label = 'Save' )
@ -3193,6 +3206,14 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
self._SortListCtrl()
#
( 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 )
def ArrangeControls():
@ -3473,6 +3494,19 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
#
gridbox = wx.FlexGridSizer( 0, 2 )
gridbox.AddGrowableCol( 1, 1 )
gridbox.AddF( wx.StaticText( self._thread_checker_page, label = 'default number of times to check: ' ), FLAGS_MIXED )
gridbox.AddF( self._thread_times_to_check, FLAGS_MIXED )
gridbox.AddF( wx.StaticText( self._thread_checker_page, label = 'default wait in seconds between checks: ' ), FLAGS_MIXED )
gridbox.AddF( self._thread_check_period, FLAGS_MIXED )
self._thread_checker_page.SetSizer( gridbox )
#
buttons = wx.BoxSizer( wx.HORIZONTAL )
buttons.AddF( self._ok, FLAGS_SMALL_INDENT )
@ -3791,6 +3825,8 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
HC.options[ 'local_port' ] = new_local_port
HC.options[ 'thread_checker_timings' ] = ( self._thread_times_to_check.GetValue(), self._thread_check_period.GetValue() )
try: HC.app.Write( 'save_options' )
except: wx.MessageBox( traceback.format_exc() )
@ -5022,9 +5058,24 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
( service_key, service_type, name, info ) = service_panel.GetInfo()
self._edit_log.append( ( HC.DELETE, service_key ) )
services_listbook.DeleteCurrentPage()
if service_type == HC.TAG_REPOSITORY:
text = 'Deleting a tag service is a potentially very expensive operation.'
text += os.linesep * 2
text += 'If you have millions of tags, it could take twenty minutes or more, during which time your database will be locked.'
text += os.linesep * 2
text += 'Are you sure you want to delete ' + name + '?'
with ClientGUIDialogs.DialogYesNo( self, text ) as dlg:
if dlg.ShowModal() == wx.ID_YES:
self._edit_log.append( ( HC.DELETE, service_key ) )
services_listbook.DeleteCurrentPage()

View File

@ -1665,13 +1665,15 @@ class ManagementPanelImportThreadWatcher( ManagementPanelImport ):
self._thread_info = wx.StaticText( self._thread_panel, label = 'enter a 4chan thread url' )
( times_to_check, check_period ) = HC.options[ 'thread_checker_timings' ]
self._thread_times_to_check = wx.SpinCtrl( self._thread_panel, size = ( 60, -1 ), min = 0, max = 100 )
self._thread_times_to_check.SetValue( 5 )
self._thread_times_to_check.SetValue( times_to_check )
self._thread_times_to_check.Bind( wx.EVT_SPINCTRL, self.EventThreadVariable )
self._thread_time = wx.SpinCtrl( self._thread_panel, size = ( 100, -1 ), min = 30, max = 86400 )
self._thread_time.SetValue( 180 )
self._thread_time.Bind( wx.EVT_SPINCTRL, self.EventThreadVariable )
self._thread_check_period = wx.SpinCtrl( self._thread_panel, size = ( 100, -1 ), min = 30, max = 86400 )
self._thread_check_period.SetValue( check_period )
self._thread_check_period.Bind( wx.EVT_SPINCTRL, self.EventThreadVariable )
self._thread_input = wx.TextCtrl( self._thread_panel, style = wx.TE_PROCESS_ENTER )
self._thread_input.Bind( wx.EVT_KEY_DOWN, self.EventKeyDown )
@ -1684,7 +1686,7 @@ class ManagementPanelImportThreadWatcher( ManagementPanelImport ):
hbox.AddF( wx.StaticText( self._thread_panel, label = 'check ' ), FLAGS_MIXED )
hbox.AddF( self._thread_times_to_check, FLAGS_MIXED )
hbox.AddF( wx.StaticText( self._thread_panel, label = ' more times, every ' ), FLAGS_MIXED )
hbox.AddF( self._thread_time, FLAGS_MIXED )
hbox.AddF( self._thread_check_period, FLAGS_MIXED )
hbox.AddF( wx.StaticText( self._thread_panel, label = ' seconds' ), FLAGS_MIXED )
self._thread_panel.AddF( self._thread_info, FLAGS_EXPAND_PERPENDICULAR )
@ -1705,7 +1707,7 @@ class ManagementPanelImportThreadWatcher( ManagementPanelImport ):
import_queue_job_key = self._import_controller.GetJobKey( 'import_queue' )
thread_time = self._thread_time.GetValue()
thread_time = self._thread_check_period.GetValue()
thread_times_to_check = self._thread_times_to_check.GetValue()
import_queue_job_key.SetVariable( 'thread_time', thread_time )
@ -1858,7 +1860,7 @@ class ManagementPanelImportThreadWatcher( ManagementPanelImport ):
import_queue_position_job_key = self._import_controller.GetJobKey( 'import_queue' )
if import_queue_position_job_key.IsWorking() and not import_queue_position_job_key.IsPaused():
if self._thread_times_to_check.GetValue() > 0 and import_queue_position_job_key.IsWorking() and not import_queue_position_job_key.IsPaused():
with ClientGUIDialogs.DialogYesNo( self, 'This page is still importing. Are you sure you want to close it?' ) as dlg:
@ -2148,7 +2150,7 @@ class ManagementPanelQuery( ManagementPanel ):
def AddPredicate( self, page_key, predicate ):
if page_key == self._page_key:
if self._show_search and page_key == self._page_key:
if predicate is not None:

View File

@ -559,8 +559,6 @@ class MediaPanel( ClientGUIMixins.ListeningMediaList, wx.ScrolledWindow ):
self._shift_focussed_media = None
self._RefitCanvas()
self._RedrawCanvas()
self._PublishSelectionChange()
@ -614,7 +612,7 @@ class MediaPanel( ClientGUIMixins.ListeningMediaList, wx.ScrolledWindow ):
def _Select( self, select_type ):
self._RedrawCanvas()
self._RefitCanvas()
if select_type == 'all': self._DeselectSelect( [], self._sorted_media )
else:
@ -1310,7 +1308,7 @@ class MediaPanelThumbnails( MediaPanel ):
self.DoPrepareDC( cdc ) # because this is a scrolled window
return wx.BufferedDC( cdc, self._canvas_bmp )
return wx.BufferedDC( cdc, self._canvas_bmp, wx.BUFFER_VIRTUAL_AREA )
def _GetThumbnailUnderMouse( self, mouse_event ):
@ -2037,6 +2035,10 @@ class MediaPanelThumbnails( MediaPanel ):
# it seems that some scroll events happen after the viewstart has changed, some happen before
# so I have to keep track of a manual current_y_start
( start_x, start_y ) = self.GetViewStart()
( my_virtual_width, my_virtual_height ) = self.GetVirtualSize()
( my_width, my_height ) = self.GetClientSize()
( xUnit, yUnit ) = self.GetScrollPixelsPerUnit()
@ -2051,6 +2053,8 @@ class MediaPanelThumbnails( MediaPanel ):
elif event_type == wx.wxEVT_SCROLLWIN_THUMBRELEASE: self._current_y_offset = 0
elif event_type == wx.wxEVT_SCROLLWIN_PAGEUP: self._current_y_offset = - page_of_y_units
elif event_type == wx.wxEVT_SCROLLWIN_PAGEDOWN: self._current_y_offset = page_of_y_units
elif event_type == wx.wxEVT_SCROLLWIN_TOP: self._current_y_offset = - start_y
elif event_type == wx.wxEVT_SCROLLWIN_BOTTOM: self._current_y_offset = ( my_virtual_height / yUnit ) - start_y
self._RefitCanvas()
@ -2140,8 +2144,6 @@ class MediaPanelThumbnails( MediaPanel ):
for t in self._sorted_media: t.ReloadFromDBLater()
self._RefitCanvas()
self._RedrawCanvas() # to force redraw

View File

@ -28,16 +28,16 @@ if sys.platform == 'win32': PLATFORM_WINDOWS = True
elif sys.platform == 'darwin': PLATFORM_OSX = True
elif sys.platform == 'linux2': PLATFORM_LINUX = True
if PLATFORM_LINUX:
if not hasattr( sys, 'frozen' ):
import wxversion
if not wxversion.checkInstalled( '2.9' ): raise Exception( 'Need wxPython 2.9 on linux!' )
wxversion.select( '2.9' )
#if PLATFORM_LINUX:
#
# if not hasattr( sys, 'frozen' ):
#
# import wxversion
#
# if not wxversion.checkInstalled( '2.9' ): raise Exception( 'Need wxPython 2.9 on Linux!' )
#
# wxversion.select( '2.9' )
#
import wx
@ -64,7 +64,7 @@ options = {}
# Misc
NETWORK_VERSION = 15
SOFTWARE_VERSION = 133
SOFTWARE_VERSION = 134
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -9,6 +9,7 @@ import HydrusThreading
import lz4
import numpy
import os
from PIL import _imaging
from PIL import Image as PILImage
import shutil
import struct

View File

@ -1098,7 +1098,7 @@ class ServiceDB( FileDB, MessageDB, TagDB ):
requests_dict = HC.BuildKeyToListDict( all_requests )
c.executemany( 'UPDATE account SET used_bytes = used_bytes + ?, used_requests = used_requests + ? WHERE account_key = ?;', [ ( sum( num_bytes_list ), len( num_bytes_list ), sqlite3.Binary( account_key ) ) for ( account_key, num_bytes_list ) in requests_dict.items() ] )
c.executemany( 'UPDATE accounts SET used_bytes = used_bytes + ?, used_requests = used_requests + ? WHERE account_key = ?;', [ ( sum( num_bytes_list ), len( num_bytes_list ), sqlite3.Binary( account_key ) ) for ( account_key, num_bytes_list ) in requests_dict.items() ] )
def _GenerateRegistrationKeys( self, c, service_key, num, title, lifetime = None ):

View File

@ -1110,7 +1110,7 @@ class TestClientDB( unittest.TestCase ):
class TestServerDB( unittest.TestCase ):
def _read( self, action, *args, **kwargs ): return self._db.Read( action, HC.HIGH_PRIORITY, *args, **kwargs )
def _write( self, action, *args, **kwargs ): return self._db.Write( action, HC.HIGH_PRIORITY, True, *args, **kwargs )
def _write( self, action, *args, **kwargs ): return self._db.Write( action, HC.HIGH_PRIORITY, *args, **kwargs )
@classmethod
def setUpClass( self ):
@ -1153,10 +1153,10 @@ class TestServerDB( unittest.TestCase ):
HC.SERVER_THUMBNAILS_DIR = self._old_server_thumbnails_dir
# add read and write funcs like with client db
def _test_account_creation( self ):
# create new account types
# edit the account types
# create rkeys
# create akeys
# test successive rkey fetch gives new akeys
@ -1177,22 +1177,80 @@ class TestServerDB( unittest.TestCase ):
def _test_init_server_admin( self ):
# do it, however it works
result = self._read( 'init' ) # an access key
pass
self.assertEqual( type( result ), str )
self.assertEqual( len( result ), 32 )
self._admin_access_key = result
result = self._read( 'account_key_from_access_key', HC.SERVER_ADMIN_KEY, self._admin_access_key )
self.assertEqual( type( result ), str )
self.assertEqual( len( result ), 32 )
self._admin_account_key = result
def _test_service_creation( self ):
# add tag, add file repo
self._tag_service_key = os.urandom( 32 )
self._file_service_key = os.urandom( 32 )
# fetch service info or whatever to test
edit_log = []
# change the port
t_options = { 'max_monthly_data' : None, 'message' : 'tag repo message', 'port' : 100, 'upnp' : None }
f_options = { 'max_monthly_data' : None, 'message' : 'file repo message', 'port' : 101, 'upnp' : None }
# fetch service info or whatever to test
edit_log.append( ( HC.ADD, ( self._tag_service_key, HC.TAG_REPOSITORY, t_options ) ) )
edit_log.append( ( HC.ADD, ( self._file_service_key, HC.FILE_REPOSITORY, f_options ) ) )
pass
result = self._write( 'services', self._admin_account_key, edit_log )
self.assertIn( self._tag_service_key, result )
self._tag_service_admin_access_key = result[ self._tag_service_key ]
self.assertEqual( type( self._tag_service_admin_access_key ), str )
self.assertEqual( len( self._tag_service_admin_access_key ), 32 )
self.assertIn( self._file_service_key, result )
self._file_service_admin_access_key = result[ self._file_service_key ]
self.assertEqual( type( self._tag_service_admin_access_key ), str )
self.assertEqual( len( self._tag_service_admin_access_key ), 32 )
#
result = self._read( 'service_keys', HC.REPOSITORIES )
self.assertEqual( set( result ), { self._tag_service_key, self._file_service_key } )
#
result = self._read( 'services_info' )
services_info = { service_key : ( service_type, options ) for ( service_key, service_type, options ) in result }
self.assertEqual( services_info[ HC.SERVER_ADMIN_KEY ], ( 99, { 'max_monthly_data' : None, 'message' : 'hydrus server administration service', 'max_storage' : None, 'upnp' : None, 'port' : 45870 } ) )
self.assertEqual( services_info[ self._tag_service_key ], ( HC.TAG_REPOSITORY, t_options ) )
self.assertEqual( services_info[ self._file_service_key ], ( HC.FILE_REPOSITORY, f_options ) )
#
f_options_modified = dict( f_options )
f_options_modified[ 'port' ] = 102
edit_log = [ ( HC.EDIT, ( self._file_service_key, HC.FILE_REPOSITORY, f_options_modified ) ) ]
self._write( 'services', self._admin_account_key, edit_log )
result = self._read( 'services_info' )
services_info = { service_key : ( service_type, options ) for ( service_key, service_type, options ) in result }
self.assertEqual( services_info[ self._file_service_key ], ( HC.FILE_REPOSITORY, f_options_modified ) )
def test_server( self ):

View File

@ -62,5 +62,7 @@ except:
import traceback
print( 'Critical error occured! Details written to crash.log!' )
with open( 'crash.log', 'wb' ) as f: f.write( traceback.format_exc() )