Version 145

This commit is contained in:
Hydrus 2015-01-21 16:49:58 -06:00
parent d13afc0285
commit 2f75a7f777
14 changed files with 667 additions and 490 deletions

View File

@ -8,6 +8,21 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 145</h3></li>
<ul>
<li>added custom gui colours for thumbnail backgrounds and borders, the autocomplete background, and media background and text</li>
<li>added <- and -> arrows to manage tags dialog launched from navigable media viewer</li>
<li>on empty input in the manage tags dialog, page up and page down work as shortcuts for the new <- and -> buttons</li>
<li>fixed the media height calculation for animations, so when they are vertically scaled, the total height including scanbar won't overflow off screen</li>
<li>allowed non-integer page, chapter, volume tags in display and sort calculations</li>
<li>semi-integer tags will sort along with integer tags and string tags like so: 0 < 0a < 0b < 1 < 2 < 22</li>
<li>improved the old tag/media sorting code</li>
<li>removed loli and shota from hentai foundry filter options</li>
<li>patched old db-stored predicates to attempt to convert to the new format when queried for _inclusive</li>
<li>this _should_ have fixed the recent export folder problems</li>
<li>created an 8chan board, and updated my various links, including in the client, to migrate from my old forum to this</li>
<li>misc code improvements</li>
</ul>
<li><h3>version 144</h3></li>
<ul>
<li>files named 'Thumbs.db' will now be skipped in the import files dialog</li>

View File

@ -7,16 +7,20 @@
<body>
<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. I like to spend a day or so to think before replying to non-urgent emails, but I do reply to everything.</p>
<p>Please send bug reports straight to my email or the board. Your other ideas and comments are always welcome.</p>
<p>I don't really do chat, and I don't get caught up in social networking stuff. I often 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://hydrusnetwork.github.io/hydrus/">homepage</a></li>
<li><a href="https://github.com/hydrusnetwork/hydrus/releases">new downloads</a></li>
<li><a href="http://www.mediafire.com/hydrus">old downloads</a></li>
<li><a href="https://github.com/hydrusnetwork/hydrus">github</a></li>
<li><a href="http://8ch.net/hydrus/index.html">8chan board</a></li>
<li><a href="http://twitter.com/hydrusnetwork">twitter</a></li>
<li><a href="http://hydrus.tumblr.com">tumblr</a> (<a href="http://hydrus.tumblr.com/rss">rss</a>)</li>
<li><a href="mailto:hydrus.admin@gmail.com">email</a></li>
</ul>
<p>If you would like to send me something physical, you can use my PO Box:</p>
<ul>

View File

@ -197,6 +197,7 @@ CLIENT_DEFAULT_OPTIONS[ 'thread_checker_timings' ] = ( 3, 1200 )
CLIENT_DEFAULT_OPTIONS[ 'idle_period' ] = 60 * 30
CLIENT_DEFAULT_OPTIONS[ 'maintenance_delete_orphans_period' ] = 86400 * 3
CLIENT_DEFAULT_OPTIONS[ 'maintenance_vacuum_period' ] = 86400 * 5
CLIENT_DEFAULT_OPTIONS[ 'fit_to_canvas' ] = False
system_predicates = {}
@ -226,6 +227,23 @@ default_namespace_colours[ '' ] = ( 0, 111, 250 )
CLIENT_DEFAULT_OPTIONS[ 'namespace_colours' ] = default_namespace_colours
default_gui_colours = {}
default_gui_colours[ 'thumb_background' ] = ( 255, 255, 255 )
default_gui_colours[ 'thumb_background_selected' ] = ( 217, 242, 255 ) # light blue
default_gui_colours[ 'thumb_background_remote' ] = ( 32, 32, 36 ) # 50% Payne's Gray
default_gui_colours[ 'thumb_background_remote_selected' ] = ( 64, 64, 72 ) # Payne's Gray
default_gui_colours[ 'thumb_border' ] = ( 223, 227, 230 ) # light grey
default_gui_colours[ 'thumb_border_selected' ] = ( 1, 17, 26 ) # dark grey
default_gui_colours[ 'thumb_border_remote' ] = ( 248, 208, 204 ) # 25% Vermillion, 75% White
default_gui_colours[ 'thumb_border_remote_selected' ] = ( 227, 66, 52 ) # Vermillion, lol
default_gui_colours[ 'thumbgrid_background' ] = ( 255, 255, 255 )
default_gui_colours[ 'autocomplete_background' ] = ( 235, 248, 255 ) # very light blue
default_gui_colours[ 'media_background' ] = ( 255, 255, 255 )
default_gui_colours[ 'media_text' ] = ( 0, 0, 0 )
CLIENT_DEFAULT_OPTIONS[ 'gui_colours' ] = default_gui_colours
default_sort_by_choices = []
default_sort_by_choices.append( ( 'namespaces', [ 'series', 'creator', 'title', 'volume', 'chapter', 'page' ] ) )

View File

@ -5345,107 +5345,6 @@ class DB( ServiceDB ):
def _UpdateDB( self, version ):
if version == 95:
self._c.execute( 'COMMIT' )
self._c.execute( 'PRAGMA foreign_keys = OFF;' )
self._c.execute( 'BEGIN IMMEDIATE' )
service_basic_info = self._c.execute( 'SELECT service_id, service_key, type, name FROM services;' ).fetchall()
service_address_info = self._c.execute( 'SELECT service_id, host, port, last_error FROM addresses;' ).fetchall()
service_account_info = self._c.execute( 'SELECT service_id, access_key, account FROM accounts;' ).fetchall()
service_repository_info = self._c.execute( 'SELECT service_id, first_begin, next_begin FROM repositories;' ).fetchall()
service_ratings_like_info = self._c.execute( 'SELECT service_id, like, dislike FROM ratings_like;' ).fetchall()
service_ratings_numerical_info = self._c.execute( 'SELECT service_id, lower, upper FROM ratings_numerical;' ).fetchall()
service_address_info = { service_id : ( host, port, last_error ) for ( service_id, host, port, last_error ) in service_address_info }
service_account_info = { service_id : ( access_key, account ) for ( service_id, access_key, account ) in service_account_info }
service_repository_info = { service_id : ( first_begin, next_begin ) for ( service_id, first_begin, next_begin ) in service_repository_info }
service_ratings_like_info = { service_id : ( like, dislike ) for ( service_id, like, dislike ) in service_ratings_like_info }
service_ratings_numerical_info = { service_id : ( lower, upper ) for ( service_id, lower, upper ) in service_ratings_numerical_info }
self._c.execute( 'DROP TABLE services;' )
self._c.execute( 'DROP TABLE addresses;' )
self._c.execute( 'DROP TABLE accounts;' )
self._c.execute( 'DROP TABLE repositories;' )
self._c.execute( 'DROP TABLE ratings_like;' )
self._c.execute( 'DROP TABLE ratings_numerical;' )
self._c.execute( 'CREATE TABLE services ( service_id INTEGER PRIMARY KEY, service_key BLOB_BYTES, service_type INTEGER, name TEXT, info TEXT_YAML );' )
self._c.execute( 'CREATE UNIQUE INDEX services_service_key_index ON services ( service_key );' )
services = []
for ( service_id, service_key, service_type, name ) in service_basic_info:
info = {}
if service_id in service_address_info:
( host, port, last_error ) = service_address_info[ service_id ]
info[ 'host' ] = host
info[ 'port' ] = port
info[ 'last_error' ] = last_error
if service_id in service_account_info:
( access_key, account ) = service_account_info[ service_id ]
info[ 'access_key' ] = access_key
info[ 'account' ] = account
if service_id in service_repository_info:
( first_begin, next_begin ) = service_repository_info[ service_id ]
info[ 'first_begin' ] = first_begin
info[ 'next_begin' ] = next_begin
if service_id in service_ratings_like_info:
( like, dislike ) = service_ratings_like_info[ service_id ]
info[ 'like' ] = like
info[ 'dislike' ] = dislike
if service_id in service_ratings_numerical_info:
( lower, upper ) = service_ratings_numerical_info[ service_id ]
info[ 'lower' ] = lower
info[ 'upper' ] = upper
self._c.execute( 'INSERT INTO services ( service_id, service_key, service_type, name, info ) VALUES ( ?, ?, ?, ?, ? );', ( service_id, sqlite3.Binary( service_key ), service_type, name, info ) )
self._c.execute( 'COMMIT' )
self._c.execute( 'PRAGMA foreign_keys = ON;' )
self._c.execute( 'BEGIN IMMEDIATE' )
if version == 95:
for ( service_id, info ) in self._c.execute( 'SELECT service_id, info FROM services;' ).fetchall():
if 'account' in info:
info[ 'account' ].MakeStale()
self._c.execute( 'UPDATE services SET info = ? WHERE service_id = ?;', ( info, service_id ) )
if version == 101:
self._c.execute( 'CREATE TABLE yaml_dumps ( dump_type INTEGER, dump_name TEXT, dump TEXT_YAML, PRIMARY KEY ( dump_type, dump_name ) );' )
@ -7296,8 +7195,6 @@ def DAEMONSynchroniseSubscriptions():
info[ 'rating_yaoi' ] = 1
info[ 'rating_yuri' ] = 1
info[ 'rating_loli' ] = 1
info[ 'rating_shota' ] = 1
info[ 'rating_teen' ] = 1
info[ 'rating_guro' ] = 1
info[ 'rating_furry' ] = 1

View File

@ -998,18 +998,18 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
dont_know.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'auto_server_setup' ), p( 'Just set up the server on this computer, please' ) )
menu.AppendMenu( wx.ID_NONE, p( 'I don\'t know what I am doing' ), dont_know )
links = wx.Menu()
tumblr = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'tumblr' ), p( 'Tumblr' ) )
tumblr.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'tumblr.png' ) )
twitter = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'twitter' ), p( 'Twitter' ) )
twitter.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'twitter.png' ) )
site = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'site' ), p( 'Site' ) )
site.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'file_repository_small.png' ) )
forum = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'forum' ), p( 'Forum' ) )
forum.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'file_repository_small.png' ) )
links.AppendItem( tumblr )
links.AppendItem( twitter )
board = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( '8chan_board' ), p( '8chan Board' ) )
board.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + '8chan.png' ) )
twitter = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'twitter' ), p( 'Twitter' ) )
twitter.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'twitter.png' ) )
tumblr = wx.MenuItem( links, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'tumblr' ), p( 'Tumblr' ) )
tumblr.SetBitmap( wx.Bitmap( HC.STATIC_DIR + os.path.sep + 'tumblr.png' ) )
links.AppendItem( site )
links.AppendItem( forum )
links.AppendItem( board )
links.AppendItem( twitter )
links.AppendItem( tumblr )
menu.AppendMenu( wx.ID_NONE, p( 'Links' ), links )
debug = wx.Menu()
debug.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'debug_garbage' ), p( 'Garbage' ) )
@ -1828,7 +1828,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
elif command == 'delete_service_info': self._DeleteServiceInfo()
elif command == 'exit': self.EventExit( event )
elif command == 'fetch_ip': self._FetchIP( data )
elif command == 'forum': webbrowser.open( 'http://hydrus.x10.mx/forum' )
elif command == '8chan_board': webbrowser.open( 'http://8ch.net/hydrus/index.html' )
elif command == 'file_integrity': self._CheckFileIntegrity()
elif command == 'help': webbrowser.open( 'file://' + HC.BASE_DIR + '/help/index.html' )
elif command == 'help_about': self._AboutWindow()

View File

@ -7,6 +7,7 @@ import ClientGUIMixins
import collections
import gc
import HydrusImageHandling
import HydrusTags
import HydrusVideoHandling
import os
import Queue
@ -156,7 +157,7 @@ class Animation( wx.Window ):
dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
dc.SetBackground( wx.Brush( wx.WHITE ) )
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) ) )
dc.Clear()
@ -441,6 +442,8 @@ class Canvas( object ):
self._file_service = HC.app.GetManager( 'services' ).GetService( self._file_service_key )
self._canvas_key = os.urandom( 32 )
self._closing = False
self._service_keys_to_services = {}
@ -457,7 +460,7 @@ class Canvas( object ):
self._last_drag_coordinates = None
self._total_drag_delta = ( 0, 0 )
self.SetBackgroundColour( wx.WHITE )
self.SetBackgroundColour( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) )
self._canvas_bmp = wx.EmptyBitmap( 0, 0, 24 )
@ -481,7 +484,7 @@ class Canvas( object ):
dc = wx.BufferedDC( cdc, self._canvas_bmp )
dc.SetBackground( wx.Brush( wx.WHITE ) )
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) ) )
dc.Clear()
@ -529,7 +532,7 @@ class Canvas( object ):
current_y += y
dc.SetTextForeground( wx.BLACK )
dc.SetTextForeground( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_text' ] ) )
# icons
@ -697,9 +700,7 @@ class Canvas( object ):
if self._current_media is not None:
try:
with ClientGUIDialogsManage.DialogManageTags( self, self._file_service_key, ( self._current_media, ) ) as dlg: dlg.ShowModal()
except: wx.MessageBox( 'Had a problem displaying the manage tags dialog from fullscreen.' )
with ClientGUIDialogsManage.DialogManageTags( self, self._file_service_key, ( self._current_media, ) ) as dlg: dlg.ShowModal()
@ -727,14 +728,14 @@ class Canvas( object ):
( media_width, media_height ) = self._current_display_media.GetResolution()
if ShouldHaveAnimationBar( self._current_display_media ):
media_height += ANIMATED_SCANBAR_HEIGHT
if ShouldHaveAnimationBar( self._current_display_media ): my_height -= ANIMATED_SCANBAR_HEIGHT
if self._current_display_media.GetMime() in NON_LARGABLY_ZOOMABLE_MIMES: my_width -= 1
if media_width > my_width or media_height > my_height:
media_needs_to_be_scaled_down = media_width > my_width or media_height > my_height
media_needs_to_be_scaled_up = media_width < my_width and media_height < my_height and HC.options[ 'fit_to_canvas' ]
if media_needs_to_be_scaled_down or media_needs_to_be_scaled_up:
width_zoom = my_width / float( media_width )
@ -854,6 +855,7 @@ class Canvas( object ):
class CanvasPanel( Canvas, wx.Window ):
def __init__( self, parent, page_key, file_service_key ):
@ -1000,7 +1002,12 @@ class CanvasFullscreenMediaList( ClientGUIMixins.ListeningMediaList, Canvas, Cli
collections_string_append = 'volume ' + HC.u( volume )
else: collections_string_append = 'volumes ' + HC.u( min( volumes ) ) + '-' + HC.u( max( volumes ) )
else:
volumes_sorted = HydrusTags.SortTags( volumes )
collections_string_append = 'volumes ' + HC.u( volumes_sorted[0] ) + '-' + HC.u( volumes_sorted[-1] )
if len( collections_string ) > 0: collections_string += ' - ' + collections_string_append
else: collections_string = collections_string_append
@ -1014,7 +1021,12 @@ class CanvasFullscreenMediaList( ClientGUIMixins.ListeningMediaList, Canvas, Cli
collections_string_append = 'chapter ' + HC.u( chapter )
else: collections_string_append = 'chapters ' + HC.u( min( chapters ) ) + '-' + HC.u( max( chapters ) )
else:
chapters_sorted = HydrusTags.SortTags( chapters )
collections_string_append = 'chapters ' + HC.u( chapters_sorted[0] ) + '-' + HC.u( chapters_sorted[-1] )
if len( collections_string ) > 0: collections_string += ' - ' + collections_string_append
else: collections_string = collections_string_append
@ -1028,7 +1040,12 @@ class CanvasFullscreenMediaList( ClientGUIMixins.ListeningMediaList, Canvas, Cli
collections_string_append = 'page ' + HC.u( page )
else: collections_string_append = 'pages ' + HC.u( min( pages ) ) + '-' + HC.u( max( pages ) )
else:
pages_sorted = HydrusTags.SortTags( pages )
collections_string_append = 'pages ' + HC.u( pages_sorted[0] ) + '-' + HC.u( pages_sorted[-1] )
if len( collections_string ) > 0: collections_string += ' - ' + collections_string_append
else: collections_string = collections_string_append
@ -1400,11 +1417,281 @@ class CanvasFullscreenMediaList( ClientGUIMixins.ListeningMediaList, Canvas, Cli
else: self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) )
class CanvasFullscreenMediaListBrowser( CanvasFullscreenMediaList ):
class CanvasFullscreenMediaListFilter( CanvasFullscreenMediaList ):
def __init__( self, my_parent, page_key, file_service_key, media_results ):
CanvasFullscreenMediaList.__init__( self, my_parent, page_key, file_service_key, media_results )
self._kept = set()
self._deleted = set()
self.Bind( wx.EVT_LEFT_DOWN, self.EventMouseKeep )
self.Bind( wx.EVT_LEFT_DCLICK, self.EventMouseKeep )
self.Bind( wx.EVT_MIDDLE_DOWN, self.EventBack )
self.Bind( wx.EVT_MIDDLE_DCLICK, self.EventBack )
self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
self.Bind( wx.EVT_RIGHT_DOWN, self.EventDelete )
self.Bind( wx.EVT_RIGHT_DCLICK, self.EventDelete )
self.Bind( wx.EVT_MENU, self.EventMenu )
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
self.SetMedia( self._GetFirst() )
def _Delete( self ):
self._deleted.add( self._current_media )
if self._current_media == self._GetLast(): self.EventClose( None )
else: self._ShowNext()
def _Keep( self ):
self._kept.add( self._current_media )
if self._current_media == self._GetLast(): self.EventClose( None )
else: self._ShowNext()
def EventBack( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if self._current_media == self._GetFirst(): return
else:
self._ShowPrevious()
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
def EventButtonBack( self, event ): self.EventBack( event )
def EventButtonDelete( self, event ): self._Delete()
def EventButtonDone( self, event ): self.EventClose( event )
def EventButtonKeep( self, event ): self._Keep()
def EventButtonSkip( self, event ):
if self._current_media == self._GetLast(): self.EventClose( event )
else: self._ShowNext()
def EventClose( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if len( self._kept ) > 0 or len( self._deleted ) > 0:
with ClientGUIDialogs.DialogFinishFiltering( self, len( self._kept ), len( self._deleted ) ) as dlg:
modal = dlg.ShowModal()
if modal == wx.ID_CANCEL:
if self._current_media in self._kept: self._kept.remove( self._current_media )
if self._current_media in self._deleted: self._deleted.remove( self._current_media )
else:
if modal == wx.ID_YES:
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
self._kept_hashes = [ media.GetHash() for media in self._kept ]
content_updates = []
content_updates.append( HC.ContentUpdate( HC.CONTENT_DATA_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, self._deleted_hashes ) )
content_updates.append( HC.ContentUpdate( HC.CONTENT_DATA_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) )
HC.app.Write( 'content_updates', { HC.LOCAL_FILE_SERVICE_KEY : content_updates } )
self._kept = set()
self._deleted = set()
self._current_media = self._GetFirst() # so the pubsub on close is better
CanvasFullscreenMediaList.EventClose( self, event )
else: CanvasFullscreenMediaList.EventClose( self, event )
def EventCharHook( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
( modifier, key ) = HC.GetShortcutFromEvent( event )
if modifier == wx.ACCEL_NORMAL and key == wx.WXK_SPACE: self._Keep()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '+' ), wx.WXK_ADD, wx.WXK_NUMPAD_ADD ): self._ZoomIn()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '-' ), wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT ): self._ZoomOut()
elif modifier == wx.ACCEL_NORMAL and key == ord( 'Z' ): self._ZoomSwitch()
elif modifier == wx.ACCEL_NORMAL and key == wx.WXK_BACK: self.EventBack( event )
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self.EventClose( event )
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE ): self.EventDelete( event )
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ):
with wx.BusyCursor(): HC.app.Write( 'copy_files', ( self._current_media.GetHash(), ) )
elif not event.ShiftDown() and key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ): self.EventSkip( event )
else:
key_dict = HC.options[ 'shortcuts' ][ modifier ]
if key in key_dict:
action = key_dict[ key ]
self.ProcessEvent( wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( action ) ) )
else: event.Skip()
def EventDelete( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else: self._Delete()
def EventMouseKeep( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if event.ShiftDown(): self.EventDragBegin( event )
else: self._Keep()
def EventMenu( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
if action is not None:
( command, data ) = action
if command == 'archive': self._Keep()
elif command == 'back': self.EventBack( event )
elif command == 'close': self.EventClose( event )
elif command == 'delete': self.EventDelete( event )
elif command == 'fullscreen_switch': self._FullscreenSwitch()
elif command == 'filter': self.EventClose( event )
elif command == 'frame_back': self._media_container.GotoPreviousOrNextFrame( -1 )
elif command == 'frame_next': self._media_container.GotoPreviousOrNextFrame( 1 )
elif command == 'manage_ratings': self._ManageRatings()
elif command == 'manage_tags': self._ManageTags()
elif command in ( 'pan_up', 'pan_down', 'pan_left', 'pan_right' ):
distance = 20
if command == 'pan_up': self._DoManualPan( 0, -distance )
elif command == 'pan_down': self._DoManualPan( 0, distance )
elif command == 'pan_left': self._DoManualPan( -distance, 0 )
elif command == 'pan_right': self._DoManualPan( distance, 0 )
elif command == 'zoom_in': self._ZoomIn()
elif command == 'zoom_out': self._ZoomOut()
else: event.Skip()
def EventMouseWheel( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if event.CmdDown():
if event.GetWheelRotation() > 0: self._ZoomIn()
else: self._ZoomOut()
def EventSkip( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if self._current_media == self._GetLast(): self.EventClose( event )
else: self._ShowNext()
class CanvasFullscreenMediaListFilterInbox( CanvasFullscreenMediaListFilter ):
def __init__( self, my_parent, page_key, file_service_key, media_results ):
CanvasFullscreenMediaListFilter.__init__( self, my_parent, page_key, file_service_key, media_results )
FullscreenPopoutFilterInbox( self )
class CanvasFullscreenMediaListNavigable( CanvasFullscreenMediaList ):
def __init__( self, my_parent, page_key, file_service_key, media_results ):
CanvasFullscreenMediaList.__init__( self, my_parent, page_key, file_service_key, media_results )
HC.pubsub.sub( self, 'ShowNext', 'canvas_show_next' )
HC.pubsub.sub( self, 'ShowPrevious', 'canvas_show_previous' )
def _BroadcastCurrentDisplayMedia( self ):
HC.pubsub.pub( 'canvas_broadcast_current_display_media', self._canvas_key, self._current_display_media )
def _ManageTags( self ):
if self._current_media is not None:
with ClientGUIDialogsManage.DialogManageTags( self, self._file_service_key, ( self._current_media, ), canvas_key = self._canvas_key ) as dlg: dlg.ShowModal()
def ShowNext( self, canvas_key ):
if canvas_key == self._canvas_key:
self._ShowNext()
self._BroadcastCurrentDisplayMedia()
def ShowPrevious( self, canvas_key ):
if canvas_key == self._canvas_key:
self._ShowPrevious()
self._BroadcastCurrentDisplayMedia()
class CanvasFullscreenMediaListBrowser( CanvasFullscreenMediaListNavigable ):
def __init__( self, my_parent, page_key, file_service_key, media_results, first_hash ):
CanvasFullscreenMediaList.__init__( self, my_parent, page_key, file_service_key, media_results )
CanvasFullscreenMediaListNavigable.__init__( self, my_parent, page_key, file_service_key, media_results )
self._timer_slideshow = wx.Timer( self, id = ID_TIMER_SLIDESHOW )
@ -1681,11 +1968,11 @@ class CanvasFullscreenMediaListBrowser( CanvasFullscreenMediaList ):
def TIMEREventSlideshow( self, event ): self._ShowNext()
class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ):
class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaListNavigable ):
def __init__( self, my_parent, page_key, file_service_key, media_results, actions ):
CanvasFullscreenMediaList.__init__( self, my_parent, page_key, file_service_key, media_results )
CanvasFullscreenMediaListNavigable.__init__( self, my_parent, page_key, file_service_key, media_results )
self._actions = actions
@ -2046,233 +2333,6 @@ class CanvasFullscreenMediaListCustomFilter( CanvasFullscreenMediaList ):
event.Skip()
class CanvasFullscreenMediaListFilter( CanvasFullscreenMediaList ):
def __init__( self, my_parent, page_key, file_service_key, media_results ):
CanvasFullscreenMediaList.__init__( self, my_parent, page_key, file_service_key, media_results )
self._kept = set()
self._deleted = set()
self.Bind( wx.EVT_LEFT_DOWN, self.EventMouseKeep )
self.Bind( wx.EVT_LEFT_DCLICK, self.EventMouseKeep )
self.Bind( wx.EVT_MIDDLE_DOWN, self.EventBack )
self.Bind( wx.EVT_MIDDLE_DCLICK, self.EventBack )
self.Bind( wx.EVT_MOUSEWHEEL, self.EventMouseWheel )
self.Bind( wx.EVT_RIGHT_DOWN, self.EventDelete )
self.Bind( wx.EVT_RIGHT_DCLICK, self.EventDelete )
self.Bind( wx.EVT_MENU, self.EventMenu )
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
self.SetMedia( self._GetFirst() )
def _Delete( self ):
self._deleted.add( self._current_media )
if self._current_media == self._GetLast(): self.EventClose( None )
else: self._ShowNext()
def _Keep( self ):
self._kept.add( self._current_media )
if self._current_media == self._GetLast(): self.EventClose( None )
else: self._ShowNext()
def EventBack( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if self._current_media == self._GetFirst(): return
else:
self._ShowPrevious()
self._kept.discard( self._current_media )
self._deleted.discard( self._current_media )
def EventButtonBack( self, event ): self.EventBack( event )
def EventButtonDelete( self, event ): self._Delete()
def EventButtonDone( self, event ): self.EventClose( event )
def EventButtonKeep( self, event ): self._Keep()
def EventButtonSkip( self, event ):
if self._current_media == self._GetLast(): self.EventClose( event )
else: self._ShowNext()
def EventClose( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if len( self._kept ) > 0 or len( self._deleted ) > 0:
with ClientGUIDialogs.DialogFinishFiltering( self, len( self._kept ), len( self._deleted ) ) as dlg:
modal = dlg.ShowModal()
if modal == wx.ID_CANCEL:
if self._current_media in self._kept: self._kept.remove( self._current_media )
if self._current_media in self._deleted: self._deleted.remove( self._current_media )
else:
if modal == wx.ID_YES:
self._deleted_hashes = [ media.GetHash() for media in self._deleted ]
self._kept_hashes = [ media.GetHash() for media in self._kept ]
content_updates = []
content_updates.append( HC.ContentUpdate( HC.CONTENT_DATA_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, self._deleted_hashes ) )
content_updates.append( HC.ContentUpdate( HC.CONTENT_DATA_TYPE_FILES, HC.CONTENT_UPDATE_ARCHIVE, self._kept_hashes ) )
HC.app.Write( 'content_updates', { HC.LOCAL_FILE_SERVICE_KEY : content_updates } )
self._kept = set()
self._deleted = set()
self._current_media = self._GetFirst() # so the pubsub on close is better
CanvasFullscreenMediaList.EventClose( self, event )
else: CanvasFullscreenMediaList.EventClose( self, event )
def EventCharHook( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
( modifier, key ) = HC.GetShortcutFromEvent( event )
if modifier == wx.ACCEL_NORMAL and key == wx.WXK_SPACE: self._Keep()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '+' ), wx.WXK_ADD, wx.WXK_NUMPAD_ADD ): self._ZoomIn()
elif modifier == wx.ACCEL_NORMAL and key in ( ord( '-' ), wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT ): self._ZoomOut()
elif modifier == wx.ACCEL_NORMAL and key == ord( 'Z' ): self._ZoomSwitch()
elif modifier == wx.ACCEL_NORMAL and key == wx.WXK_BACK: self.EventBack( event )
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE ): self.EventClose( event )
elif modifier == wx.ACCEL_NORMAL and key in ( wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE ): self.EventDelete( event )
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ):
with wx.BusyCursor(): HC.app.Write( 'copy_files', ( self._current_media.GetHash(), ) )
elif not event.ShiftDown() and key in ( wx.WXK_UP, wx.WXK_NUMPAD_UP ): self.EventSkip( event )
else:
key_dict = HC.options[ 'shortcuts' ][ modifier ]
if key in key_dict:
action = key_dict[ key ]
self.ProcessEvent( wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( action ) ) )
else: event.Skip()
def EventDelete( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else: self._Delete()
def EventMouseKeep( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if event.ShiftDown(): self.EventDragBegin( event )
else: self._Keep()
def EventMenu( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
if action is not None:
( command, data ) = action
if command == 'archive': self._Keep()
elif command == 'back': self.EventBack( event )
elif command == 'close': self.EventClose( event )
elif command == 'delete': self.EventDelete( event )
elif command == 'fullscreen_switch': self._FullscreenSwitch()
elif command == 'filter': self.EventClose( event )
elif command == 'frame_back': self._media_container.GotoPreviousOrNextFrame( -1 )
elif command == 'frame_next': self._media_container.GotoPreviousOrNextFrame( 1 )
elif command == 'manage_ratings': self._ManageRatings()
elif command == 'manage_tags': self._ManageTags()
elif command in ( 'pan_up', 'pan_down', 'pan_left', 'pan_right' ):
distance = 20
if command == 'pan_up': self._DoManualPan( 0, -distance )
elif command == 'pan_down': self._DoManualPan( 0, distance )
elif command == 'pan_left': self._DoManualPan( -distance, 0 )
elif command == 'pan_right': self._DoManualPan( distance, 0 )
elif command == 'zoom_in': self._ZoomIn()
elif command == 'zoom_out': self._ZoomOut()
else: event.Skip()
def EventMouseWheel( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if event.CmdDown():
if event.GetWheelRotation() > 0: self._ZoomIn()
else: self._ZoomOut()
def EventSkip( self, event ):
if self._ShouldSkipInputDueToFlash(): event.Skip()
else:
if self._current_media == self._GetLast(): self.EventClose( event )
else: self._ShowNext()
class CanvasFullscreenMediaListFilterInbox( CanvasFullscreenMediaListFilter ):
def __init__( self, my_parent, page_key, file_service_key, media_results ):
CanvasFullscreenMediaListFilter.__init__( self, my_parent, page_key, file_service_key, media_results )
FullscreenPopoutFilterInbox( self )
class FullscreenPopout( wx.Frame ):
def __init__( self, parent ):
@ -3884,7 +3944,7 @@ class EmbedButton( wx.Window ):
dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
dc.SetBackground( wx.WHITE_BRUSH )
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) ) )
dc.Clear() # gcdc doesn't support clear
@ -3900,7 +3960,7 @@ class EmbedButton( wx.Window ):
dc.DrawCircle( center_x, center_y, radius )
dc.SetBrush( wx.WHITE_BRUSH )
dc.SetBrush( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) ) )
m = ( 2 ** 0.5 ) / 2 # 45 degree angle
@ -4077,7 +4137,7 @@ class StaticImage( wx.Window ):
dc = wx.BufferedDC( wx.ClientDC( self ), self._canvas_bmp )
dc.SetBackground( wx.Brush( wx.WHITE ) )
dc.SetBackground( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'media_background' ] ) ) )
dc.Clear()

View File

@ -95,7 +95,7 @@ class AutoCompleteDropdown( wx.TextCtrl ):
wx.TextCtrl.__init__( self, parent, style=wx.TE_PROCESS_ENTER )
self.SetBackgroundColour( CC.COLOUR_LIGHT_SELECTED )
self.SetBackgroundColour( wx.Colour( *HC.options[ 'gui_colours' ][ 'autocomplete_background' ] ) )
#self._dropdown_window = wx.PopupWindow( self, flags = wx.BORDER_RAISED )
#self._dropdown_window = wx.PopupTransientWindow( self, style = wx.BORDER_RAISED )
@ -215,6 +215,15 @@ class AutoCompleteDropdown( wx.TextCtrl ):
self.ProcessEvent( new_event )
elif event.KeyCode in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ) and self.GetValue() == '' and len( self._dropdown_list ) == 0:
if event.KeyCode in ( wx.WXK_PAGEUP, wx.WXK_NUMPAD_PAGEUP ): id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'canvas_show_previous' )
elif event.KeyCode in ( wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN ): id = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'canvas_show_next' )
new_event = wx.CommandEvent( commandType = wx.wxEVT_COMMAND_MENU_SELECTED, winid = id )
self.ProcessEvent( new_event )
else: self._dropdown_list.ProcessEvent( event )
@ -3444,8 +3453,6 @@ class AdvancedHentaiFoundryOptions( AdvancedOptions ):
self._rating_yaoi = wx.CheckBox( panel )
self._rating_yuri = wx.CheckBox( panel )
self._rating_loli = wx.CheckBox( panel )
self._rating_shota = wx.CheckBox( panel )
self._rating_teen = wx.CheckBox( panel )
self._rating_guro = wx.CheckBox( panel )
self._rating_furry = wx.CheckBox( panel )
@ -3457,8 +3464,6 @@ class AdvancedHentaiFoundryOptions( AdvancedOptions ):
self._rating_yaoi.SetValue( True )
self._rating_yuri.SetValue( True )
self._rating_loli.SetValue( True )
self._rating_shota.SetValue( True )
self._rating_teen.SetValue( True )
self._rating_guro.SetValue( True )
self._rating_furry.SetValue( True )
@ -3507,12 +3512,6 @@ class AdvancedHentaiFoundryOptions( AdvancedOptions ):
gridbox.AddF( wx.StaticText( panel, label = 'yuri' ), FLAGS_MIXED )
gridbox.AddF( self._rating_yuri, FLAGS_EXPAND_BOTH_WAYS )
gridbox.AddF( wx.StaticText( panel, label = 'loli' ), FLAGS_MIXED )
gridbox.AddF( self._rating_loli, FLAGS_EXPAND_BOTH_WAYS )
gridbox.AddF( wx.StaticText( panel, label = 'shota' ), FLAGS_MIXED )
gridbox.AddF( self._rating_shota, FLAGS_EXPAND_BOTH_WAYS )
gridbox.AddF( wx.StaticText( panel, label = 'teen' ), FLAGS_MIXED )
gridbox.AddF( self._rating_teen, FLAGS_EXPAND_BOTH_WAYS )
@ -3558,8 +3557,6 @@ class AdvancedHentaiFoundryOptions( AdvancedOptions ):
info[ 'rating_yaoi' ] = int( self._rating_yaoi.GetValue() )
info[ 'rating_yuri' ] = int( self._rating_yuri.GetValue() )
info[ 'rating_loli' ] = int( self._rating_loli.GetValue() )
info[ 'rating_shota' ] = int( self._rating_shota.GetValue() )
info[ 'rating_teen' ] = int( self._rating_teen.GetValue() )
info[ 'rating_guro' ] = int( self._rating_guro.GetValue() )
info[ 'rating_furry' ] = int( self._rating_furry.GetValue() )
@ -3587,8 +3584,6 @@ class AdvancedHentaiFoundryOptions( AdvancedOptions ):
self._rating_yaoi.SetValue( bool( info[ 'rating_yaoi' ] ) )
self._rating_yuri.SetValue( bool( info[ 'rating_yuri' ] ) )
self._rating_loli.SetValue( bool( info[ 'rating_loli' ] ) )
self._rating_shota.SetValue( bool( info[ 'rating_shota' ] ) )
self._rating_teen.SetValue( bool( info[ 'rating_teen' ] ) )
self._rating_guro.SetValue( bool( info[ 'rating_guro' ] ) )
self._rating_furry.SetValue( bool( info[ 'rating_furry' ] ) )
@ -4305,7 +4300,7 @@ class TagsBoxCounts( TagsBox ):
self._sort = HC.options[ 'default_tag_sort' ]
self._last_media = None
self._last_media = set()
self._tag_service_key = HC.COMBINED_TAG_SERVICE_KEY
@ -4384,7 +4379,7 @@ class TagsBoxCounts( TagsBox ):
self._tag_service_key = service_key
if self._last_media is not None: self.SetTagsByMedia( self._last_media, force_reload = True )
self.SetTagsByMedia( self._last_media, force_reload = True )
def SetSort( self, sort ):
@ -4430,12 +4425,8 @@ class TagsBoxCounts( TagsBox ):
else:
if self._last_media is None: ( removees, adds ) = ( set(), media )
else:
removees = self._last_media.difference( media )
adds = media.difference( self._last_media )
removees = self._last_media.difference( media )
adds = media.difference( self._last_media )
siblings_manager = HC.app.GetManager( 'tag_siblings' )

View File

@ -2844,6 +2844,15 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
self._listbook.AddPage( self._maintenance_page, 'maintenance and memory' )
# media
self._media_page = wx.Panel( self._listbook )
self._media_page.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
self._fit_to_canvas = wx.CheckBox( self._media_page, label = '' )
self._listbook.AddPage( self._media_page, 'media' )
# gui
self._gui_page = wx.Panel( self._listbook )
@ -2963,6 +2972,17 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
self._colour_page = wx.Panel( self._listbook )
self._colour_page.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
self._gui_colours = {}
for ( name, rgb ) in HC.options[ 'gui_colours' ].items():
ctrl = wx.ColourPickerCtrl( self._colour_page )
ctrl.SetMaxSize( ( 20, -1 ) )
self._gui_colours[ name ] = ctrl
self._namespace_colours = ClientGUICommon.TagsBoxColourOptions( self._colour_page, HC.options[ 'namespace_colours' ] )
self._edit_namespace_colour = wx.Button( self._colour_page, label = 'edit selected' )
@ -3081,6 +3101,10 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
#
self._fit_to_canvas.SetValue( HC.options[ 'fit_to_canvas' ] )
#
gui_sessions = HC.app.Read( 'gui_sessions' )
gui_session_names = gui_sessions.keys()
@ -3189,6 +3213,10 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
#
for ( name, rgb ) in HC.options[ 'gui_colours' ].items(): self._gui_colours[ name ].SetColour( wx.Colour( *rgb ) )
#
for ( name, ato ) in HC.options[ 'default_advanced_tag_options' ].items():
if name == 'default': pretty_name = 'default'
@ -3313,6 +3341,17 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
gridbox.AddGrowableCol( 1, 1 )
gridbox.AddF( wx.StaticText( self._media_page, label = 'Zoom smaller images to fit media canvas: ' ), FLAGS_MIXED )
gridbox.AddF( self._fit_to_canvas, FLAGS_MIXED )
self._media_page.SetSizer( gridbox )
#
gridbox = wx.FlexGridSizer( 0, 2 )
gridbox.AddGrowableCol( 1, 1 )
gridbox.AddF( wx.StaticText( self._gui_page, label = 'Default session on startup:' ), FLAGS_MIXED )
gridbox.AddF( self._default_gui_session, FLAGS_MIXED )
@ -3486,6 +3525,43 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
vbox = wx.BoxSizer( wx.VERTICAL )
gridbox = wx.FlexGridSizer( 0, 2 )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'thumbnail background (local: normal/selected, remote: normal/selected): ' ), FLAGS_MIXED )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._gui_colours[ 'thumb_background' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_background_selected' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_background_remote' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_background_remote_selected' ], FLAGS_MIXED )
gridbox.AddF( hbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'thumbnail border (local: normal/selected, remote: normal/selected): ' ), FLAGS_MIXED )
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._gui_colours[ 'thumb_border' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_border_selected' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_border_remote' ], FLAGS_MIXED )
hbox.AddF( self._gui_colours[ 'thumb_border_remote_selected' ], FLAGS_MIXED )
gridbox.AddF( hbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'thumbnail grid background: '), FLAGS_MIXED )
gridbox.AddF( self._gui_colours[ 'thumbgrid_background' ], FLAGS_MIXED )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'autocomplete background: '), FLAGS_MIXED )
gridbox.AddF( self._gui_colours[ 'autocomplete_background' ], FLAGS_MIXED )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'media viewer background: '), FLAGS_MIXED )
gridbox.AddF( self._gui_colours[ 'media_background' ], FLAGS_MIXED )
gridbox.AddF( wx.StaticText( self._colour_page, label = 'media viewer text: '), FLAGS_MIXED )
gridbox.AddF( self._gui_colours[ 'media_text' ], FLAGS_MIXED )
vbox.AddF( gridbox, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._namespace_colours, FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( self._new_namespace_colour, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._edit_namespace_colour, FLAGS_EXPAND_PERPENDICULAR )
@ -3823,6 +3899,17 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
HC.options[ 'ac_timings' ] = ( char_limit, long_wait, short_wait )
HC.options[ 'fit_to_canvas' ] = self._fit_to_canvas.GetValue()
for ( name, ctrl ) in self._gui_colours.items():
colour = ctrl.GetColour()
rgb = ( colour.Red(), colour.Green(), colour.Blue() )
HC.options[ 'gui_colours' ][ name ] = rgb
HC.options[ 'namespace_colours' ] = self._namespace_colours.GetNamespaceColours()
sort_by_choices = []
@ -7527,10 +7614,19 @@ class DialogManageTagSiblings( ClientGUIDialogs.Dialog ):
class DialogManageTags( ClientGUIDialogs.Dialog ):
def __init__( self, parent, file_service_key, media ):
def __init__( self, parent, file_service_key, media, canvas_key = None ):
def InitialiseControls():
if canvas_key is not None:
self._next = wx.Button( self, label = '->' )
self._next.Bind( wx.EVT_BUTTON, self.EventNext )
self._previous = wx.Button( self, label = '<-' )
self._previous.Bind( wx.EVT_BUTTON, self.EventPrevious )
self._tag_repositories = ClientGUICommon.ListBook( self )
self._tag_repositories.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.EventServiceChanged )
@ -7573,6 +7669,17 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
vbox = wx.BoxSizer( wx.VERTICAL )
if canvas_key is not None:
hbox = wx.BoxSizer( wx.HORIZONTAL )
hbox.AddF( self._previous, FLAGS_MIXED )
hbox.AddF( ( 20, 20 ), FLAGS_EXPAND_BOTH_WAYS )
hbox.AddF( self._next, FLAGS_MIXED )
vbox.AddF( hbox, FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._tag_repositories, FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( buttonbox, FLAGS_BUTTON_SIZER )
@ -7587,6 +7694,8 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
self._hashes = set()
self._canvas_key = canvas_key
for m in media: self._hashes.update( m.GetHashes() )
ClientGUIDialogs.Dialog.__init__( self, parent, 'manage tags for ' + HC.ConvertIntToPrettyString( len( self._hashes ) ) + ' files' )
@ -7601,6 +7710,33 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
self.RefreshAcceleratorTable()
if self._canvas_key is not None: HC.pubsub.sub( self, 'CanvasHasNewMedia', 'canvas_broadcast_current_display_media' )
def _ClearPanels( self ):
for page in self._tag_repositories.GetNameToPageDict().values(): page.SetMedia( set() )
def _CommitCurrentChanges( self, sync = False ):
service_keys_to_content_updates = {}
for page in self._tag_repositories.GetNameToPageDict().values():
( service_key, content_updates ) = page.GetContentUpdates()
if len( content_updates ) > 0: service_keys_to_content_updates[ service_key ] = content_updates
if len( service_keys_to_content_updates ) > 0:
if sync: m = HC.app.WriteSynchronous
else: m = HC.app.Write
m( 'content_updates', service_keys_to_content_updates )
def _SetSearchFocus( self ):
@ -7609,6 +7745,14 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
page.SetTagBoxFocus()
def CanvasHasNewMedia( self, canvas_key, new_media ):
if canvas_key == self._canvas_key:
for page in self._tag_repositories.GetNameToPageDict().values(): page.SetMedia( ( new_media, ) )
def EventMenu( self, event ):
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
@ -7617,31 +7761,39 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
( command, data ) = action
if command == 'manage_tags': self.EndModal( wx.ID_CANCEL )
if command == 'canvas_show_next': self.EventNext( event )
elif command == 'canvas_show_previous': self.EventPrevious( event )
elif command == 'manage_tags': self.EndModal( wx.ID_CANCEL )
elif command == 'set_search_focus': self._SetSearchFocus()
elif command == 'ok': self.EventOK( event )
else: event.Skip()
def EventNext( self, event ):
self._CommitCurrentChanges( sync = True )
self._ClearPanels()
HC.pubsub.pub( 'canvas_show_next', self._canvas_key )
def EventOK( self, event ):
try:
service_keys_to_content_updates = {}
for page in self._tag_repositories.GetNameToPageDict().values():
( service_key, content_updates ) = page.GetContentUpdates()
service_keys_to_content_updates[ service_key ] = content_updates
if len( service_keys_to_content_updates ) > 0: HC.app.Write( 'content_updates', service_keys_to_content_updates )
try: self._CommitCurrentChanges()
finally: self.EndModal( wx.ID_OK )
def EventPrevious( self, event ):
self._CommitCurrentChanges( sync = True )
self._ClearPanels()
HC.pubsub.pub( 'canvas_show_previous', self._canvas_key )
def EventServiceChanged( self, event ):
page = self._tag_repositories.GetCurrentPage()
@ -7693,7 +7845,7 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
self._tags_box.ChangeTagRepository( self._tag_service_key )
self._tags_box.SetTagsByMedia( self._media )
self.SetMedia( media )
def ArrangeControls():
@ -7728,10 +7880,6 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
self._i_am_local_tag_service = self._tag_service_key == HC.LOCAL_TAG_SERVICE_KEY
self._hashes = { hash for hash in itertools.chain.from_iterable( ( m.GetHashes() for m in media ) ) }
self._content_updates = []
if not self._i_am_local_tag_service:
service = HC.app.GetManager( 'services' ).GetService( tag_service_key )
@ -7740,15 +7888,6 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
except: self._account = HC.GetUnknownAccount()
hashes = set( itertools.chain.from_iterable( ( m.GetHashes() for m in media ) ) )
media_results = HC.app.Read( 'media_results', self._file_service_key, hashes )
# this should now be a nice clean copy of the original media
self._media = [ ClientGUIMixins.MediaSingleton( media_result ) for media_result in media_results ]
tags_managers = [ m.GetTagsManager() for m in self._media ]
InitialiseControls()
PopulateControls()
@ -7840,6 +7979,22 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
def SetMedia( self, media ):
self._hashes = { hash for hash in itertools.chain.from_iterable( ( m.GetHashes() for m in media ) ) }
self._content_updates = []
hashes = set( itertools.chain.from_iterable( ( m.GetHashes() for m in media ) ) )
media_results = HC.app.Read( 'media_results', self._file_service_key, hashes )
# this should now be a nice clean copy of the original media
self._media = [ ClientGUIMixins.MediaSingleton( media_result ) for media_result in media_results ]
self._tags_box.SetTagsByMedia( self._media )
def EventCopyTags( self, event ):
( current_tags_to_count, deleted_tags_to_count, pending_tags_to_count, petitioned_tags_to_count ) = CC.GetMediasTagCount( self._media, self._tag_service_key )

View File

@ -6,6 +6,7 @@ import ClientGUIDialogsManage
import ClientGUICanvas
import ClientGUIMixins
import collections
import HydrusTags
import HydrusThreading
import itertools
import os
@ -1083,7 +1084,7 @@ class MediaPanelThumbnails( MediaPanel ):
dc = self._GetScrolledDC()
dc.SetBrush( wx.WHITE_BRUSH )
dc.SetBrush( wx.Brush( wx.Colour( *HC.options[ 'gui_colours' ][ 'thumbgrid_background' ] ) ) )
dc.SetPen( wx.TRANSPARENT_PEN )
@ -2331,15 +2332,17 @@ class Thumbnail( Selectable ):
if not local:
if self._selected: dc.SetBackground( wx.Brush( wx.Colour( 64, 64, 72 ) ) ) # Payne's Gray
else: dc.SetBackground( wx.Brush( wx.Colour( 32, 32, 36 ) ) ) # 50% Payne's Gray
if self._selected: rgb = HC.options[ 'gui_colours' ][ 'thumb_background_remote_selected' ]
else: rgb = HC.options[ 'gui_colours' ][ 'thumb_background_remote' ]
else:
if self._selected: dc.SetBackground( wx.Brush( CC.COLOUR_SELECTED ) )
else: dc.SetBackground( wx.Brush( wx.WHITE ) )
if self._selected: rgb = HC.options[ 'gui_colours' ][ 'thumb_background_selected' ]
else: rgb = HC.options[ 'gui_colours' ][ 'thumb_background' ]
dc.SetBackground( wx.Brush( wx.Colour( *rgb ) ) )
dc.Clear()
( thumb_width, thumb_height ) = self._hydrus_bmp.GetSize()
@ -2364,7 +2367,12 @@ class Thumbnail( Selectable ):
collections_string = 'v' + HC.u( volume )
else: collections_string = 'v' + HC.u( min( volumes ) ) + '-' + HC.u( max( volumes ) )
else:
volumes_sorted = HydrusTags.SortTags( volumes )
collections_string_append = 'v' + HC.u( volumes_sorted[0] ) + '-' + HC.u( volumes_sorted[-1] )
if len( chapters ) > 0:
@ -2375,7 +2383,12 @@ class Thumbnail( Selectable ):
collections_string_append = 'c' + HC.u( chapter )
else: collections_string_append = 'c' + HC.u( min( chapters ) ) + '-' + HC.u( max( chapters ) )
else:
chapters_sorted = HydrusTags.SortTags( chapters )
collections_string_append = 'c' + HC.u( chapters_sorted[0] ) + '-' + HC.u( chapters_sorted[-1] )
if len( collections_string ) > 0: collections_string += '-' + collections_string_append
else: collections_string = collections_string_append
@ -2389,7 +2402,12 @@ class Thumbnail( Selectable ):
collections_string_append = 'p' + HC.u( page )
else: collections_string_append = 'p' + HC.u( min( pages ) ) + '-' + HC.u( max( pages ) )
else:
pages_sorted = HydrusTags.SortTags( pages )
collections_string_append = 'p' + HC.u( pages_sorted[0] ) + '-' + HC.u( pages_sorted[-1] )
if len( collections_string ) > 0: collections_string += '-' + collections_string_append
else: collections_string = collections_string_append
@ -2465,16 +2483,16 @@ class Thumbnail( Selectable ):
if not local:
if self._selected: colour = wx.Colour( 227, 66, 52 ) # Vermillion, lol
else: colour = wx.Colour( 248, 208, 204 ) # 25% Vermillion, 75% White
if self._selected: rgb = HC.options[ 'gui_colours' ][ 'thumb_border_remote_selected' ]
else: rgb = HC.options[ 'gui_colours' ][ 'thumb_border_remote' ]
else:
if self._selected: colour = CC.COLOUR_SELECTED_DARK
else: colour = CC.COLOUR_UNSELECTED
if self._selected: rgb = HC.options[ 'gui_colours' ][ 'thumb_border_selected' ]
else: rgb = HC.options[ 'gui_colours' ][ 'thumb_border' ]
dc.SetPen( wx.Pen( colour, style=wx.SOLID ) )
dc.SetPen( wx.Pen( wx.Colour( *rgb ), style=wx.SOLID ) )
dc.DrawRectangle( 0, 0, width, height )

View File

@ -66,7 +66,7 @@ options = {}
# Misc
NETWORK_VERSION = 15
SOFTWARE_VERSION = 144
SOFTWARE_VERSION = 145
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -2188,7 +2188,24 @@ class Predicate( HydrusYAMLBase ):
else: return self._counts[ current_or_pending ]
def GetInclusive( self ): return self._inclusive
def GetInclusive( self ):
# patch from an upgrade mess-up ~v144
if not hasattr( self, '_inclusive' ):
if self._predicate_type != PREDICATE_TYPE_SYSTEM:
( operator, value ) = self._value
self._value = value
self._inclusive = operator == '+'
else: self._inclusive = True
return self._inclusive
def GetInfo( self ): return ( self._predicate_type, self._value, self._inclusive )
@ -2701,37 +2718,33 @@ class SortedList( object ):
if sort_function is None: sort_function = lambda x: x
self._sorted_list = [ ( sort_function( item ), item ) for item in initial_items ]
self._sort_function = sort_function
self._sorted_list = list( initial_items )
self._items_to_indices = None
self._sort_function = sort_function
if do_sort: self.sort()
def __contains__( self, item ): return self._items_to_indices.__contains__( item )
def __getitem__( self, value ):
if type( value ) == int: return self._sorted_list.__getitem__( value )[1]
elif type( value ) == slice: return [ item for ( sort_item, item ) in self._sorted_list.__getitem__( value ) ]
def __getitem__( self, value ): return self._sorted_list.__getitem__( value )
def __iter__( self ):
for ( sorting_value, item ) in self._sorted_list: yield item
for item in self._sorted_list: yield item
def __len__( self ): return self._sorted_list.__len__()
def _DirtyIndices( self ): self._items_to_indices = None
def _RecalcIndices( self ): self._items_to_indices = { item : index for ( index, ( sort_item, item ) ) in enumerate( self._sorted_list ) }
def _RecalcIndices( self ): self._items_to_indices = { item : index for ( index, item ) in enumerate( self._sorted_list ) }
def append_items( self, items ):
self._sorted_list.extend( [ ( self._sort_function( item ), item ) for item in items ] )
self._sorted_list.extend( items )
self._DirtyIndices()
@ -2745,9 +2758,9 @@ class SortedList( object ):
def insert_items( self, items ):
for item in items: bisect.insort( self._sorted_list, ( self._sort_function( item ), item ) )
self.append_items( items )
self._DirtyIndices()
self.sort()
def remove_items( self, items ):
@ -2758,7 +2771,7 @@ class SortedList( object ):
deletee_indices.reverse()
for index in deletee_indices: self._sorted_list.pop( index )
for index in deletee_indices: del self._sorted_list[ index ]
self._DirtyIndices()
@ -2767,9 +2780,7 @@ class SortedList( object ):
if f is not None: self._sort_function = f
self._sorted_list = [ ( self._sort_function( item ), item ) for ( old_value, item ) in self._sorted_list ]
self._sorted_list.sort()
self._sorted_list.sort( key = f )
self._DirtyIndices()

View File

@ -241,6 +241,43 @@ def MergeTagsManagers( tags_managers ):
return TagsManagerSimple( merged_service_keys_to_statuses_to_tags )
def SortTags( tags ):
def tagkey( t ):
if t[0].isdigit():
# We want to maintain that:
# 0 < 0a < 0b < 1 ( lexicographic comparison )
# -and-
# 2 < 22 ( value comparison )
# So, if the first bit can be turned into an int, split it into ( int, extra )
int_component = ''
i = 0
for character in t:
if character.isdigit(): int_component += character
else: break
i += 1
str_component = t[i:]
return ( int( int_component ), str_component )
else: return t
tags = list( tags )
tags.sort( key = tagkey )
return tags
class TagsManagerSimple( object ):
def __init__( self, service_keys_to_statuses_to_tags ):
@ -265,25 +302,6 @@ class TagsManagerSimple( object ):
self._combined_namespaces_cache = HC.BuildKeyToSetDict( tag.split( ':', 1 ) for tag in combined_current.union( combined_pending ) if ':' in tag )
only_int_allowed = ( 'volume', 'chapter', 'page' )
for namespace in only_int_allowed:
tags = self._combined_namespaces_cache[ namespace ]
int_tags = set()
for tag in tags:
try: tag = int( tag )
except: continue
int_tags.add( tag )
self._combined_namespaces_cache[ namespace ] = int_tags
result = { namespace : self._combined_namespaces_cache[ namespace ] for namespace in namespaces }
@ -314,15 +332,7 @@ class TagsManagerSimple( object ):
tags = [ tag.split( ':', 1 )[1] for tag in tags ]
def process_tag( t ):
try: return int( t )
except: return t
tags = [ process_tag( tag ) for tag in tags ]
tags.sort()
tags = SortTags( tags )
tags = tuple( tags )

View File

@ -260,8 +260,6 @@ class TestDownloaders( unittest.TestCase ):
info[ 'rating_yaoi' ] = 1
info[ 'rating_yuri' ] = 1
info[ 'rating_loli' ] = 1
info[ 'rating_shota' ] = 1
info[ 'rating_teen' ] = 1
info[ 'rating_guro' ] = 1
info[ 'rating_furry' ] = 1

View File

@ -66,7 +66,7 @@ class TestMergeTagsManagers( unittest.TestCase ):
#
result = { 'creator' : { 'tsutomu nihei' }, 'series' : { 'blame!' }, 'title' : { 'double page spread' }, 'volume' : { 3 }, 'chapter' : { 1, 2 }, 'page' : { 4, 5 } }
result = { 'creator' : { 'tsutomu nihei' }, 'series' : { 'blame!' }, 'title' : { 'double page spread' }, 'volume' : { '3' }, 'chapter' : { '1', '2' }, 'page' : { '4', '5' } }
self.assertEqual( tags_manager.GetCombinedNamespaces( ( 'creator', 'series', 'title', 'volume', 'chapter', 'page' ) ), result )
@ -92,7 +92,7 @@ class TestTagsManager( unittest.TestCase ):
service_keys_to_statuses_to_tags[ self._second_key ][ HC.PENDING ] = { 'pending' }
service_keys_to_statuses_to_tags[ self._second_key ][ HC.PETITIONED ] = { 'petitioned' }
service_keys_to_statuses_to_tags[ self._third_key ][ HC.CURRENT ] = { 'petitioned', 'volume:broken_volume', 'chapter:broken_chapter', 'page:broken_page' }
service_keys_to_statuses_to_tags[ self._third_key ][ HC.CURRENT ] = { 'petitioned' }
service_keys_to_statuses_to_tags[ self._third_key ][ HC.DELETED ] = { 'pending' }
self._tags_manager = HydrusTags.TagsManager( service_keys_to_statuses_to_tags )
@ -126,7 +126,7 @@ class TestTagsManager( unittest.TestCase ):
def test_get_cstvcp( self ):
result = { 'creator' : { 'tsutomu nihei' }, 'series' : { 'blame!' }, 'title' : { 'test title' }, 'volume' : { 3 }, 'chapter' : { 2 }, 'page' : { 1 } }
result = { 'creator' : { 'tsutomu nihei' }, 'series' : { 'blame!' }, 'title' : { 'test title' }, 'volume' : { '3' }, 'chapter' : { '2' }, 'page' : { '1' } }
self.assertEqual( self._tags_manager.GetCombinedNamespaces( ( 'creator', 'series', 'title', 'volume', 'chapter', 'page' ) ), result )
@ -146,9 +146,9 @@ class TestTagsManager( unittest.TestCase ):
self.assertEqual( self._tags_manager.GetCurrent( self._first_key ), { 'current', u'\u2835', 'creator:tsutomu nihei', 'series:blame!', 'title:test title', 'volume:3', 'chapter:2', 'page:1' } )
self.assertEqual( self._tags_manager.GetCurrent( self._second_key ), { 'deleted', u'\u2835' } )
self.assertEqual( self._tags_manager.GetCurrent( self._third_key ), { 'petitioned', 'volume:broken_volume', 'chapter:broken_chapter', 'page:broken_page' } )
self.assertEqual( self._tags_manager.GetCurrent( self._third_key ), { 'petitioned' } )
self.assertEqual( self._tags_manager.GetCurrent(), { 'current', 'deleted', u'\u2835', 'creator:tsutomu nihei', 'series:blame!', 'title:test title', 'volume:3', 'chapter:2', 'page:1', 'petitioned', 'volume:broken_volume', 'chapter:broken_chapter', 'page:broken_page' } )
self.assertEqual( self._tags_manager.GetCurrent(), { 'current', 'deleted', u'\u2835', 'creator:tsutomu nihei', 'series:blame!', 'title:test title', 'volume:3', 'chapter:2', 'page:1', 'petitioned' } )
def test_get_deleted( self ):
@ -182,14 +182,14 @@ class TestTagsManager( unittest.TestCase ):
self.assertEqual( self._tags_manager.GetNumTags( self._second_key, include_current_tags = True, include_pending_tags = True ), 3 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = False, include_pending_tags = False ), 0 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = True, include_pending_tags = False ), 4 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = True, include_pending_tags = False ), 1 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = False, include_pending_tags = True ), 0 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = True, include_pending_tags = True ), 4 )
self.assertEqual( self._tags_manager.GetNumTags( self._third_key, include_current_tags = True, include_pending_tags = True ), 1 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = False, include_pending_tags = False ), 0 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = True, include_pending_tags = False ), 13 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = True, include_pending_tags = False ), 10 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = False, include_pending_tags = True ), 1 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = True, include_pending_tags = True ), 14 )
self.assertEqual( self._tags_manager.GetNumTags( HC.COMBINED_TAG_SERVICE_KEY, include_current_tags = True, include_pending_tags = True ), 11 )
def test_get_pending( self ):

BIN
static/8chan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B