summarylogtreecommitdiffstats
diff options
context:
space:
mode:
authorScore_Under2015-06-11 18:00:28 +0100
committerScore_Under2015-06-11 18:00:28 +0100
commitfbcafe875523c82a7ce6539a3e3048be33502d8d (patch)
treed14eeb50108dc94c48a3672a17674238b39f3714
downloadaur-fbcafe875523c82a7ce6539a3e3048be33502d8d.tar.gz
Initial import from AUR 3
-rw-r--r--.SRCINFO44
-rw-r--r--PKGBUILD120
-rw-r--r--cython-workarounds.patch2483
-rw-r--r--hydrus-client2
-rw-r--r--hydrus-server2
-rw-r--r--hydrus.desktop9
-rw-r--r--hydrus.install11
-rw-r--r--paths-in-opt.patch121
-rw-r--r--running-the-server.patch26
9 files changed, 2818 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO
new file mode 100644
index 000000000000..b4b392f10ddf
--- /dev/null
+++ b/.SRCINFO
@@ -0,0 +1,44 @@
+pkgbase = hydrus
+ pkgdesc = Danbooru-like image tagging and searching system for the desktop
+ pkgver = 160
+ pkgrel = 1
+ url = http://hydrusnetwork.github.io/hydrus/
+ install = hydrus.install
+ arch = any
+ license = WTFPL
+ makedepends = git
+ depends = python2
+ depends = wxpython
+ depends = opencv
+ depends = python2-beautifulsoup4
+ depends = python2-yaml
+ depends = hsaudiotag
+ depends = python2-pypdf2
+ depends = python2-pafy
+ depends = python2-lz4
+ depends = python2-numpy
+ depends = python2-twisted
+ depends = python2-pillow
+ depends = python2-potr
+ depends = python2-flvlib
+ depends = python2-socks
+ optdepends = ffmpeg: show duration and other information on video thumbnails
+ optdepends = miniupnpc: automatic port forwarding
+ options = !strip
+ source = hydrus::git+https://github.com/hydrusnetwork/hydrus.git#commit=54238debc3004889ae5b5777076838bece5a0d57
+ source = paths-in-opt.patch
+ source = running-the-server.patch
+ source = cython-workarounds.patch
+ source = hydrus-client
+ source = hydrus-server
+ source = hydrus.desktop
+ sha256sums = SKIP
+ sha256sums = 039fc987703dd6ea37cbef0831112d03ebf3d1e55fa38271f4d13b6fa686c5b0
+ sha256sums = 9218f8b48ecf91075132914693dbc3afbd8bf0bcd69989be1c044eacb9191da3
+ sha256sums = 5ee65187005e2807c98e385da122ff63c9ac856c35b4875989aad29d0f314d25
+ sha256sums = b2bf66b1068969e9598742d5c128cb04fd609512b0cff0ad5e25ecb6cdd35678
+ sha256sums = ac7254e3cdb359ebae302655b72b9f74b85d9e817c326fa28173791b3fb4f114
+ sha256sums = 9ba3942ac1a37f6b39c98ae6592573402bf08d8376f64554d0696c0fed6fd0e2
+
+pkgname = hydrus
+
diff --git a/PKGBUILD b/PKGBUILD
new file mode 100644
index 000000000000..86b5d9f26a2a
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,120 @@
+# Maintainer: Score_Under <seejay 11@gmail com>
+# Configuration:
+#####
+
+build_pyc=false
+build_pyo=true
+build_cython=false
+remove_py=false
+remove_help=false
+
+#####
+$build_cython || options=(!strip) # Don't strip libs because there aren't any; this wastes time
+DOC_DIRS=(opt/hydrus/help)
+
+pkgname=hydrus
+pkgver=160
+pkgrel=1
+pkgdesc="Danbooru-like image tagging and searching system for the desktop"
+arch=(any)
+license=(WTFPL)
+url=http://hydrusnetwork.github.io/hydrus/
+depends=(python2 wxpython opencv python2-beautifulsoup4 python2-yaml
+ hsaudiotag python2-pypdf2 python2-pafy python2-lz4 python2-numpy
+ python2-twisted python2-pillow python2-potr python2-flvlib python2-socks)
+makedepends=(git)
+$build_cython && makedepends+=(cython2 parallel)
+optdepends=('ffmpeg: show duration and other information on video thumbnails'
+ 'miniupnpc: automatic port forwarding')
+source=("${pkgname}::git+https://github.com/hydrusnetwork/${pkgname}.git#commit=54238debc3004889ae5b5777076838bece5a0d57"
+ paths-in-opt.patch
+ running-the-server.patch
+ cython-workarounds.patch
+ hydrus-client
+ hydrus-server
+ hydrus.desktop)
+sha256sums=('SKIP'
+ '039fc987703dd6ea37cbef0831112d03ebf3d1e55fa38271f4d13b6fa686c5b0'
+ '9218f8b48ecf91075132914693dbc3afbd8bf0bcd69989be1c044eacb9191da3'
+ '5ee65187005e2807c98e385da122ff63c9ac856c35b4875989aad29d0f314d25'
+ 'b2bf66b1068969e9598742d5c128cb04fd609512b0cff0ad5e25ecb6cdd35678'
+ 'ac7254e3cdb359ebae302655b72b9f74b85d9e817c326fa28173791b3fb4f114'
+ '9ba3942ac1a37f6b39c98ae6592573402bf08d8376f64554d0696c0fed6fd0e2')
+install=hydrus.install
+
+prepare() {
+ cd "$pkgname"
+ patch -Np1 -i ../paths-in-opt.patch
+ patch -Np1 -i ../running-the-server.patch
+
+ # Cython patches if applicable
+ $build_cython && patch -Np1 -i ../cython-workarounds.patch
+
+ # Fix permissions
+ chmod a-x include/*.py
+
+ # Remove strange file
+ rm -f "include/pyconfig.h"
+
+ # Remove unit tests
+ rm -f "include/Test"*.py
+ rm -rf "static/testing"
+}
+
+build() {
+ cd "$pkgname"
+
+ # Compile .py files
+ $build_pyc && python2 -m compileall .
+ $build_pyo && python2 -OO -m compileall .
+
+ if $build_cython; then
+ cd include
+ local -a files_to_compile
+ files_to_compile=()
+ for file in *.py; do
+ # ClientGUICommon.py and ClientController.py have problems when built under Cython
+ [ "$file" == ClientGUICommon.py -o "$file" == ClientController.py ] && continue
+ files_to_compile+=("${file%.py}")
+ done
+
+ cython2 -2 --fast-fail -Werror "${files_to_compile[@]/%/.py}"
+ parallel --bar '${CCLD:-gcc} -Os -s -fpic -shared -o {}.so {}.c $(python2-config --libs --includes) $LDFLAGS' ::: "${files_to_compile[@]}"
+ rm -f -- "${files_to_compile/%/.c}"
+ fi
+}
+
+package() {
+ cd "$pkgname"
+
+ # Create /opt/hydrus and copy hydrus sources to there
+ install -m755 -d "${pkgdir}/opt/hydrus"
+ cp -r help include static client.pyw server.pyw "${pkgdir}/opt/hydrus/"
+
+ # Remove .py files
+ $remove_py && find "${pkgdir}/opt/hydrus" -name '*.py' -delete
+
+ # Remove help
+ $remove_help && rm -rf "${pkgdir}/opt/hydrus/help"
+
+ # Fix permissions
+ chown -R root:root "${pkgdir}/opt/hydrus"
+
+ # Create and populate /opt/hydrus/bin
+ install -d -m755 "${pkgdir}/opt/hydrus/bin"
+ ln -s /usr/bin/upnpc "${pkgdir}/opt/hydrus/bin/upnpc_linux"
+ ln -s /usr/bin/ffmpeg "${pkgdir}/opt/hydrus/bin/ffmpeg"
+
+ # Install hydrus-client and hydrus-server executables
+ install -d -m755 "${pkgdir}/usr/bin"
+ install -m755 ../hydrus-{client,server} "${pkgdir}/usr/bin/"
+
+ # Install license files
+ install -d -m755 "${pkgdir}/usr/share/licenses/${pkgname}"
+ install -m644 COPYING "${pkgdir}/usr/share/licenses/${pkgname}/"
+ install -m644 license.txt "${pkgdir}/usr/share/licenses/${pkgname}/"
+
+ # Install .desktop shortcut
+ install -d -m755 "${pkgdir}/usr/share/applications"
+ install -m644 ../hydrus.desktop "${pkgdir}/usr/share/applications/${pkgname}.desktop"
+}
diff --git a/cython-workarounds.patch b/cython-workarounds.patch
new file mode 100644
index 000000000000..5d604946c795
--- /dev/null
+++ b/cython-workarounds.patch
@@ -0,0 +1,2483 @@
+diff --git a/include/ClientDB.py b/include/ClientDB.py
+index 216de51..b0c2b88 100755
+--- a/include/ClientDB.py
++++ b/include/ClientDB.py
+@@ -2279,7 +2279,7 @@ class DB( HydrusDB.HydrusDB ):
+ if tag_service_key == CC.COMBINED_TAG_SERVICE_KEY: service_phrase = ''
+ else: service_phrase = 'service_id = ' + HydrusData.ToString( tag_service_id ) + ' AND '
+
+- query_hash_ids = { id for ( id, count ) in self._c.execute( 'SELECT hash_id, COUNT( DISTINCT tag_id ) FROM mappings WHERE ' + service_phrase + 'hash_id IN ' + HydrusData.SplayListForDB( query_hash_ids ) + ' AND status IN ' + HydrusData.SplayListForDB( statuses ) + ' GROUP BY hash_id;' ) if False not in ( pred( count ) for pred in tag_predicates ) }
++ query_hash_ids = { id for ( id, count ) in self._c.execute( 'SELECT hash_id, COUNT( DISTINCT tag_id ) FROM mappings WHERE ' + service_phrase + 'hash_id IN ' + HydrusData.SplayListForDB( query_hash_ids ) + ' AND status IN ' + HydrusData.SplayListForDB( statuses ) + ' GROUP BY hash_id;' ) if False not in (lambda count:( pred( count ) for pred in tag_predicates ))(count) }
+
+
+ #
+diff --git a/include/ClientGUICanvas.py b/include/ClientGUICanvas.py
+index 732f8e0..e801153 100755
+--- a/include/ClientGUICanvas.py
++++ b/include/ClientGUICanvas.py
+@@ -50,7 +50,8 @@ NON_LARGABLY_ZOOMABLE_MIMES = [ mime for mime in HC.VIDEO if mime != HC.VIDEO_WE
+
+ EMBED_BUTTON_MIMES = [ HC.VIDEO_FLV, HC.APPLICATION_FLASH ]
+
+-def CalculateCanvasZoom( media, ( canvas_width, canvas_height ) ):
++def CalculateCanvasZoom( media, canvas_dims ):
++ ( canvas_width, canvas_height ) = canvas_dims
+
+ ( media_width, media_height ) = media.GetResolution()
+
+diff --git a/include/ClientGUIDialogs.py b/include/ClientGUIDialogs.py
+index 3f5efb5..779be78 100755
+--- a/include/ClientGUIDialogs.py
++++ b/include/ClientGUIDialogs.py
+@@ -244,7 +244,8 @@ class Dialog( wx.Dialog ):
+
+ def EventDialogButton( self, event ): self.EndModal( event.GetId() )
+
+- def SetInitialSize( self, ( width, height ) ):
++ def SetInitialSize( self, dims ):
++ ( width, height ) = dims
+
+ wx.Dialog.SetInitialSize( self, ( width, height ) )
+
+diff --git a/include/ClientGUIMessages.py b/include/ClientGUIMessages.py
+deleted file mode 100755
+index 6a1d8c5..0000000
+--- a/include/ClientGUIMessages.py
++++ /dev/null
+@@ -1,1467 +0,0 @@
+-import HydrusConstants as HC
+-import ClientConstants as CC
+-import ClientCaches
+-import ClientGUICommon
+-import ClientGUIDialogs
+-import ClientGUIMedia
+-import cStringIO
+-import hashlib
+-import os
+-import random
+-import threading
+-import traceback
+-import webbrowser
+-import wx
+-import wx.html
+-import wx.richtext
+-import wx.lib.scrolledpanel
+-import yaml
+-from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
+-from wx.lib.mixins.listctrl import ColumnSorterMixin
+-import HydrusData
+-import HydrusGlobals
+-'''
+-class ConversationsListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
+-
+- def __init__( self, parent, page_key, identity, conversations ):
+-
+- wx.ListCtrl.__init__( self, parent, style = wx.LC_REPORT | wx.LC_SINGLE_SEL )
+- ListCtrlAutoWidthMixin.__init__( self )
+- ColumnSorterMixin.__init__( self, 8 )
+-
+- self._page_key = page_key
+- self._identity = identity
+-
+- image_list = wx.ImageList( 16, 16, True, 2 )
+-
+- image_list.Add( CC.GlobalBMPs.transparent_bmp )
+- image_list.Add( CC.GlobalBMPs.inbox_bmp )
+-
+- self.AssignImageList( image_list, wx.IMAGE_LIST_SMALL )
+-
+- self.InsertColumn( 0, 'inbox', width = 30 )
+- self.InsertColumn( 1, 'subject' )
+- self.InsertColumn( 2, 'creator', width = 90 )
+- self.InsertColumn( 3, 'to', width = 100 )
+- self.InsertColumn( 4, 'messages', width = 60 )
+- self.InsertColumn( 5, 'unread', width = 60 )
+- self.InsertColumn( 6, 'created', width = 130 )
+- self.InsertColumn( 7, 'updated', width = 130 )
+-
+- self.setResizeColumn( 2 ) # subject
+-
+- self._SetConversations( conversations )
+-
+- self.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelected )
+- self.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelected )
+- self.Bind( wx.EVT_LIST_ITEM_RIGHT_CLICK, self.EventShowMenu )
+- self.Bind( wx.EVT_MENU, self.EventMenu )
+-
+- self.RefreshAcceleratorTable()
+-
+- HydrusGlobals.pubsub.sub( self, 'SetConversations', 'set_conversations' )
+- HydrusGlobals.pubsub.sub( self, 'ArchiveConversation', 'archive_conversation_gui' )
+- HydrusGlobals.pubsub.sub( self, 'InboxConversation', 'inbox_conversation_gui' )
+- HydrusGlobals.pubsub.sub( self, 'DeleteConversation', 'delete_conversation_gui' )
+- HydrusGlobals.pubsub.sub( self, 'UpdateMessageStatuses', 'message_statuses_gui' )
+- HydrusGlobals.pubsub.sub( self, 'RefreshAcceleratorTable', 'notify_new_options' )
+-
+-
+- def RefreshAcceleratorTable( self ):
+-
+- entries = [
+- ( wx.ACCEL_NORMAL, wx.WXK_DELETE, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ) ),
+- ( wx.ACCEL_NORMAL, wx.WXK_NUMPAD_DELETE, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ) )
+- ]
+-
+- for ( modifier, key_dict ) in HC.options[ 'shortcuts' ].items(): entries.extend( [ ( modifier, key, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( action ) ) for ( key, action ) in key_dict.items() ] )
+-
+- self.SetAcceleratorTable( wx.AcceleratorTable( entries ) )
+-
+-
+- def _GetIndexFromConversationKey( self, conversation_key ):
+-
+- for i in range( self.GetItemCount() ):
+-
+- data_index = self.GetItemData( i )
+-
+- conversation = self._data_indices_to_conversations[ data_index ]
+-
+- if conversation.GetConversationKey() == conversation_key: return i
+-
+-
+- return None
+-
+-
+- def _GetPrettyStatus( self ):
+-
+- if len( self._conversations ) == 1: return '1 conversation'
+- else: return HydrusData.ToString( len( self._conversations ) ) + ' conversations'
+-
+-
+- def _SetConversations( self, conversations ):
+-
+- self._conversations = list( conversations )
+-
+- self.DeleteAllItems()
+-
+- self.itemDataMap = {}
+- self._data_indices_to_conversations = {}
+-
+- i = 0
+-
+- cmp_conversations = lambda c1, c2: cmp( c1.GetUpdated(), c2.GetUpdated() )
+-
+- self._conversations.sort( cmp = cmp_conversations, reverse = True ) # order by newest change first
+-
+- for conversation in self._conversations:
+-
+- ( conversation_key, inbox, subject, name_from, participants, message_count, unread_count, created, updated ) = conversation.GetListCtrlTuple()
+-
+- if created is None:
+-
+- created_string = ''
+- updated_string = ''
+-
+- else:
+-
+- created_string = HydrusData.ConvertTimestampToHumanPrettyTime( created )
+- updated_string = HydrusData.ConvertTimestampToHumanPrettyTime( updated )
+-
+-
+- self.Append( ( '', subject, name_from, ', '.join( [ contact.GetName() for contact in participants if contact.GetName() != name_from ] ), HydrusData.ToString( message_count ), HydrusData.ToString( unread_count ), created_string, updated_string ) )
+-
+- data_index = i
+-
+- self.SetItemData( i, data_index )
+-
+- if inbox: self.SetItemImage( i, 1 ) # inbox
+- else: self.SetItemImage( i, 0 ) # transparent
+-
+- self.itemDataMap[ data_index ] = ( inbox, subject, name_from, len( participants ), message_count, unread_count, created, updated )
+-
+- self._data_indices_to_conversations[ data_index ] = conversation
+-
+- i += 1
+-
+-
+- HydrusGlobals.pubsub.pub( 'conversation_focus', self._page_key, None )
+- HydrusGlobals.pubsub.pub( 'new_page_status', self._page_key, self._GetPrettyStatus() )
+-
+-
+- def _UpdateConversationItem( self, conversation_key ):
+-
+- selection = self._GetIndexFromConversationKey( conversation_key )
+-
+- if selection is not None:
+-
+- conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
+-
+- ( conversation_key, inbox, subject, name_from, participants, message_count, unread_count, created, updated ) = conversation.GetListCtrlTuple()
+-
+- selection = self._GetIndexFromConversationKey( conversation_key )
+-
+- data_index = self.GetItemData( selection )
+-
+- self.itemDataMap[ data_index ] = ( inbox, subject, name_from, len( participants ), message_count, unread_count, created, updated )
+-
+- if inbox: self.SetItemImage( selection, 1 )
+- else: self.SetItemImage( selection, 0 )
+-
+- self.SetStringItem( selection, 4, HydrusData.ToString( message_count ) )
+- self.SetStringItem( selection, 5, HydrusData.ToString( unread_count ) )
+-
+- if created is None:
+-
+- created_string = ''
+- updated_string = ''
+-
+- else:
+-
+- created_string = HydrusData.ConvertTimestampToHumanPrettyTime( created )
+-
+- updated_string = HydrusData.ConvertTimestampToHumanPrettyTime( updated )
+-
+-
+- self.SetStringItem( selection, 6, created_string )
+- self.SetStringItem( selection, 7, updated_string )
+-
+-
+-
+- def ArchiveConversation( self, conversation_key ): self._UpdateConversationItem( conversation_key )
+-
+- def DeleteConversation( self, conversation_key ):
+-
+- selection = self._GetIndexFromConversationKey( conversation_key )
+-
+- if selection is not None:
+-
+- conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
+-
+- self._conversations.remove( conversation )
+-
+- self.DeleteItem( selection )
+-
+-
+-
+- def EventMenu( self, event ):
+-
+- action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
+-
+- if action is not None:
+-
+- ( command, data ) = action
+-
+- selection = self.GetFirstSelected()
+-
+- conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
+-
+- conversation_key = conversation.GetConversationKey()
+-
+- identity_contact_key = self._identity.GetContactKey()
+-
+- if command == 'archive': wx.GetApp().Write( 'archive_conversation', conversation_key )
+- elif command == 'inbox': wx.GetApp().Write( 'inbox_conversation', conversation_key )
+- elif command == 'read':
+-
+- message_keys = conversation.GetMessageKeysWithDestination( ( self._identity, 'sent' ) )
+-
+- for message_key in message_keys: wx.GetApp().Write( 'message_statuses', message_key, [ ( identity_contact_key, 'read' ) ] )
+-
+- elif command == 'unread':
+-
+- message_keys = conversation.GetMessageKeysWithDestination( ( self._identity, 'read' ) )
+-
+- for message_key in message_keys: wx.GetApp().Write( 'message_statuses', message_key, [ ( identity_contact_key, 'sent' ) ] )
+-
+- elif command == 'delete':
+-
+- with ClientGUIDialogs.DialogYesNo( self, 'Are you sure you want to delete this conversation?' ) as dlg:
+-
+- if dlg.ShowModal() == wx.ID_YES: wx.GetApp().Write( 'delete_conversation', conversation_key )
+-
+-
+- else: event.Skip()
+-
+-
+-
+- def EventSelected( self, event ):
+-
+- selection = self.GetFirstSelected()
+-
+- if selection == wx.NOT_FOUND: HydrusGlobals.pubsub.pub( 'conversation_focus', self._page_key, None )
+- else: HydrusGlobals.pubsub.pub( 'conversation_focus', self._page_key, self._data_indices_to_conversations[ self.GetItemData( selection ) ] )
+-
+-
+- def EventShowMenu( self, event ):
+-
+- conversation = self._data_indices_to_conversations[ self.GetItemData( event.GetIndex() ) ]
+-
+- menu = wx.Menu()
+-
+- if conversation.IsInbox(): menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'archive' ), 'archive' )
+- else: menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'inbox' ), 'return to inbox' )
+-
+- if conversation.HasUnread(): menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'read' ), 'set all as read' )
+- if conversation.HasRead(): menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'unread' ), 'set all as unread' )
+-
+- menu.AppendSeparator()
+- menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ), 'delete' )
+-
+- self.PopupMenu( menu )
+-
+- wx.CallAfter( menu.Destroy )
+-
+-
+- def GetListCtrl( self ): return self
+-
+- def InboxConversation( self, conversation_key ): self._UpdateConversationItem( conversation_key )
+-
+- def SetConversations( self, page_key, conversations ):
+-
+- if page_key == self._page_key:
+-
+- try: self._SetConversations( conversations )
+- except:
+-
+- wx.MessageBox( traceback.format_exc() )
+-
+-
+-
+-
+- def UpdateMessageStatuses( self, message_key, updates ):
+-
+- for conversation in self._data_indices_to_conversations.values():
+-
+- if conversation.HasMessageKey( message_key ):
+-
+- conversation_key = conversation.GetConversationKey()
+-
+- self._UpdateConversationItem( conversation_key )
+-
+-
+-
+-
+-class ConversationPanel( wx.Panel ):
+-
+- def __init__( self, parent, page_key, identity, conversation ):
+-
+- wx.Panel.__init__( self, parent, style = wx.SIMPLE_BORDER )
+-
+- self.SetBackgroundColour( wx.WHITE )
+-
+- self._identity = identity
+- self._page_key = page_key
+- self._conversation = conversation
+-
+- self._vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- self._message_keys_to_message_panels = {}
+- self._draft_keys_to_draft_panels = {}
+-
+- self._scrolling_messages_window = wx.lib.scrolledpanel.ScrolledPanel( self )
+- self._scrolling_messages_window.SetupScrolling()
+- self._scrolling_messages_window.SetScrollRate( 0, 50 )
+-
+- self._window_vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- self._messages_vbox = wx.BoxSizer( wx.VERTICAL )
+- self._drafts_vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- self._window_vbox.AddF( self._messages_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+- self._window_vbox.AddF( self._drafts_vbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+-
+- self._DrawConversation()
+-
+- self.SetSizer( self._vbox )
+-
+- HydrusGlobals.pubsub.sub( self, 'DeleteDraft', 'delete_draft_gui' )
+- HydrusGlobals.pubsub.sub( self, 'NewMessage', 'new_message' )
+-
+-
+- def _DrawConversation( self ):
+-
+- # fix it so this stuff is reusable?
+-
+- self._messages_vbox.DeleteWindows()
+- self._drafts_vbox.DeleteWindows()
+-
+- self._convo_frame = wx.Panel( self )
+-
+- convo_vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- subject_static_text = wx.StaticText( self._convo_frame, label = self._conversation.GetSubject() )
+-
+- f = subject_static_text.GetFont()
+-
+- f.SetWeight( wx.BOLD )
+-
+- subject_static_text.SetFont( f )
+-
+- convo_vbox.AddF( subject_static_text, CC.FLAGS_EXPAND_PERPENDICULAR )
+- convo_vbox.AddF( wx.StaticText( self._convo_frame, label = ', '.join( contact.GetName() for contact in self._conversation.GetParticipants() ) ), CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- self._convo_frame.SetSizer( convo_vbox )
+-
+- # archive_all
+- # set all as read
+- # delete all button, eventually
+-
+- ( messages, drafts ) = self._conversation.GetMessages()
+-
+- for message in messages:
+-
+- message_panel = MessagePanel( self._scrolling_messages_window, message, self._identity )
+-
+- self._message_keys_to_message_panels[ message.GetMessageKey() ] = message_panel
+-
+- self._messages_vbox.AddF( message_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+-
+- for draft in drafts:
+-
+- draft_panel = DraftPanel( self._scrolling_messages_window, draft )
+-
+- self._draft_keys_to_draft_panels[ draft.GetDraftKey() ] = draft_panel
+-
+- self._drafts_vbox.AddF( draft_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+-
+- self._reply_button = wx.Button( self._scrolling_messages_window, label = 'reply' )
+- self._reply_button.Bind( wx.EVT_BUTTON, self.EventReply )
+- self._reply_button.Disable()
+-
+- if len( messages ) > 0 and self._conversation.GetStartedBy().GetName() != 'Anonymous': self._reply_button.Enable()
+-
+- if self._conversation.GetStartedBy() == self._identity: self._reply_button.Enable()
+-
+- self._window_vbox.AddF( self._reply_button, CC.FLAGS_LONE_BUTTON )
+-
+- self._scrolling_messages_window.SetSizer( self._window_vbox )
+-
+- self._vbox.AddF( self._convo_frame, CC.FLAGS_EXPAND_PERPENDICULAR )
+- self._vbox.AddF( self._scrolling_messages_window, CC.FLAGS_EXPAND_BOTH_WAYS )
+-
+- self.SetSizer( self._vbox )
+-
+-
+- def DeleteDraft( self, draft_key ):
+-
+- if draft_key in self._draft_keys_to_draft_panels:
+-
+- draft_panel = self._draft_keys_to_draft_panels[ draft_key ]
+-
+- del self._draft_keys_to_draft_panels[ draft_key ]
+-
+- self._drafts_vbox.Detach( draft_panel )
+-
+- wx.CallAfter( draft_panel.Destroy )
+-
+- self._scrolling_messages_window.FitInside()
+-
+-
+-
+- def EventReply( self, event ):
+-
+- draft_key = os.urandom( 32 )
+- conversation_key = self._conversation.GetConversationKey()
+- subject = self._conversation.GetSubject()
+- contact_from = self._identity
+- participants = self._conversation.GetParticipants()
+- contact_names_to = [ contact.GetName() for contact in participants if contact is not None and contact.GetName() != 'Anonymous' and contact != contact_from ]
+- recipients_visible = True
+- body = ''
+- attachment_hashes = []
+-
+- draft = ClientConstantsMessages.DraftMessage( draft_key, conversation_key, subject, contact_from, contact_names_to, recipients_visible, body, attachment_hashes, is_new = True )
+-
+- self._conversation.AddDraft( draft )
+-
+- draft_panel = DraftPanel( self._scrolling_messages_window, draft )
+-
+- self._draft_keys_to_draft_panels[ draft_key ] = draft_panel
+-
+- self._drafts_vbox.AddF( draft_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- self._scrolling_messages_window.FitInside()
+-
+-
+- def NewMessage( self, conversation_key, message ):
+-
+- if self._conversation is not None and conversation_key == self._conversation.GetConversationKey():
+-
+- message_key = message.GetMessageKey()
+-
+- if message_key not in self._message_keys_to_message_panels: # if not already here!
+-
+- message_panel = MessagePanel( self._scrolling_messages_window, message, self._identity )
+-
+- self._message_keys_to_message_panels[ message_key ] = message_panel
+-
+- self._messages_vbox.AddF( message_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- self._conversation.AddMessage( message )
+-
+- self._scrolling_messages_window.FitInside()
+-
+-
+-
+-
+-class ConversationSplitter( wx.SplitterWindow ):
+-
+- def __init__( self, parent, page_key, identity, conversations = None ):
+-
+- if conversations is None: conversations = []
+-
+- wx.SplitterWindow.__init__( self, parent )
+-
+- self._page_key = page_key
+- self._identity = identity
+- self._conversations = conversations
+-
+- self.SetMinimumPaneSize( 180 )
+- self.SetSashGravity( 0.0 )
+-
+- self._InitConversationsPanel()
+- self._InitConversationPanel()
+-
+- wx.CallAfter( self.SplitHorizontally, self._conversations_panel, self._conversation_panel, 180 )
+- wx.CallAfter( self._conversation_panel.Refresh )
+-
+- HydrusGlobals.pubsub.sub( self, 'SetConversationFocus', 'conversation_focus' )
+-
+-
+- def _InitConversationsPanel( self ): self._conversations_panel = ConversationsListCtrl( self, self._page_key, self._identity, self._conversations )
+-
+- def _InitConversationPanel( self ): self._conversation_panel = wx.Window( self )
+-
+- def SetConversationFocus( self, page_key, conversation ):
+-
+- if page_key == self._page_key:
+-
+- with wx.FrozenWindow( self ):
+-
+- if conversation is None: new_panel = wx.Window( self )
+- else: new_panel = ConversationPanel( self, self._page_key, self._identity, conversation )
+-
+- self.ReplaceWindow( self._conversation_panel, new_panel )
+-
+- self._conversation_panel.Close()
+-
+- self._conversation_panel = new_panel
+-
+-
+-
+-
+-class DestinationPanel( wx.Panel ):
+-
+- def __init__( self, parent, message_key, contact, status, identity ):
+-
+- wx.Panel.__init__( self, parent )
+-
+- self.SetBackgroundColour( CC.COLOUR_MESSAGE )
+-
+- self._message_key = message_key
+- self._contact = contact
+- self._contact_key = contact.GetContactKey()
+- self._identity = identity
+- self._status = status
+-
+- name = contact.GetName()
+-
+- name_static_text = wx.StaticText( self, label = name )
+-
+- if self._contact == self._identity:
+-
+- f = name_static_text.GetFont()
+-
+- f.SetWeight( wx.BOLD )
+-
+- name_static_text.SetFont( f )
+-
+- if self._status == 'sent': self._status = 'unread'
+-
+-
+- self._status_panel = self._CreateStatusPanel()
+-
+- self._hbox = wx.BoxSizer( wx.HORIZONTAL )
+-
+- self._hbox.AddF( name_static_text, CC.FLAGS_MIXED )
+- self._hbox.AddF( self._status_panel, CC.FLAGS_MIXED )
+-
+- self.SetSizer( self._hbox )
+-
+- self.Bind( wx.EVT_MENU, self.EventMenu )
+-
+-
+- def _CreateStatusPanel( self ):
+-
+- if self._status == 'failed':
+-
+- status_text = wx.StaticText( self, label = self._status )
+-
+- status_text.SetForegroundColour( ( 128, 0, 0 ) )
+-
+- status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
+-
+- status_text.Bind( wx.EVT_LEFT_DOWN, self.EventRetryMenu )
+-
+- elif self._status == 'unread':
+-
+- status_text = wx.StaticText( self, label = self._status )
+-
+- status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
+-
+- status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
+-
+- status_text.Bind( wx.EVT_LEFT_DOWN, self.EventReadMenu )
+-
+- elif self._status == 'read':
+-
+- status_text = wx.StaticText( self, label = self._status )
+-
+- status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
+-
+- status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
+-
+- status_text.Bind( wx.EVT_LEFT_DOWN, self.EventUnreadMenu )
+-
+- else:
+-
+- status_text = wx.StaticText( self, label = self._status )
+-
+- status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
+-
+-
+- return status_text
+-
+-
+- def EventMenu( self, event ):
+-
+- action = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
+-
+- if action is not None:
+-
+- ( command, data ) = action
+-
+- if command in ( 'retry', 'read', 'unread' ):
+-
+- if command == 'retry': status = 'pending'
+- elif command == 'read': status = 'read'
+- elif command == 'unread': status = 'sent'
+-
+- my_message_depot = wx.GetApp().GetManager( 'services' ).GetService( self._identity.GetServiceKey() )
+-
+- connection = my_message_depot.GetConnection()
+-
+- my_public_key = self._identity.GetPublicKey()
+- my_contact_key = self._identity.GetContactKey()
+-
+- contacts_contact_key = self._contact.GetContactKey()
+-
+- status_updates = []
+-
+- status_key = hashlib.sha256( contacts_contact_key + self._message_key ).digest()
+-
+- packaged_status = HydrusMessageHandling.PackageStatusForDelivery( ( self._message_key, contacts_contact_key, status ), my_public_key )
+-
+- status_updates = ( ( status_key, packaged_status ), )
+-
+- connection.Post( 'message_statuses', contact_key = my_contact_key, statuses = status_updates )
+-
+- wx.GetApp().Write( 'message_statuses', self._message_key, [ ( self._contact_key, status ) ] )
+-
+- else: event.Skip()
+-
+-
+-
+- def EventReadMenu( self, event ):
+-
+- menu = wx.Menu()
+-
+- menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'read' ), 'read' )
+-
+- self.PopupMenu( menu )
+-
+- wx.CallAfter( menu.Destroy )
+-
+-
+- def EventRetryMenu( self, event ):
+-
+- menu = wx.Menu()
+-
+- menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'retry' ), 'retry' )
+-
+- self.PopupMenu( menu )
+-
+- wx.CallAfter( menu.Destroy )
+-
+-
+- def EventUnreadMenu( self, event ):
+-
+- menu = wx.Menu()
+-
+- menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'unread' ), 'unread' )
+-
+- self.PopupMenu( menu )
+-
+- wx.CallAfter( menu.Destroy )
+-
+-
+- def SetStatus( self, status ):
+-
+- if self._contact == self._identity and status == 'sent': status = 'unread'
+-
+- self._status = status
+-
+- new_status_panel = self._CreateStatusPanel()
+-
+- self._hbox.Replace( self._status_panel, new_status_panel )
+-
+- self._status_panel.Close()
+-
+- self._status_panel = new_status_panel
+-
+-
+-class DestinationsPanel( wx.Panel ):
+-
+- def __init__( self, parent, message_key, destinations, identity ):
+-
+- wx.Panel.__init__( self, parent )
+-
+- self.SetBackgroundColour( CC.COLOUR_MESSAGE )
+-
+- self._message_key = message_key
+- self._my_panels = {}
+-
+- vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- for ( contact, status ) in destinations:
+-
+- destination_panel = DestinationPanel( self, message_key, contact, status, identity )
+-
+- vbox.AddF( destination_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- self._my_panels[ contact.GetContactKey() ] = destination_panel
+-
+-
+- self.SetSizer( vbox )
+-
+- HydrusGlobals.pubsub.sub( self, 'UpdateMessageStatuses', 'message_statuses_gui' )
+-
+-
+- def UpdateMessageStatuses( self, message_key, updates ):
+-
+- if message_key == self._message_key:
+-
+- with wx.FrozenWindow( self ):
+-
+- for ( contact_key, status ) in updates:
+-
+- if contact_key in self._my_panels: self._my_panels[ contact_key ].SetStatus( status )
+-
+-
+- # doing replace on the destpanels' tricky sizer is a huge pain, hence the size event
+- # has to be postevent, not processevent
+- wx.PostEvent( self.GetParent(), wx.SizeEvent() )
+-
+-
+-
+-
+-# A whole bunch of this is cribbed from/inspired by the excellent rtc example in the wxPython Demo
+-class DraftBodyPanel( wx.Panel ):
+-
+- ID_BOLD = 0
+- ID_ITALIC = 1
+- ID_UNDERLINE = 2
+-
+- ID_ALIGN_LEFT = 3
+- ID_ALIGN_CENTER = 4
+- ID_ALIGN_RIGHT = 5
+- ID_ALIGN_JUSTIFY = 6 # rtc doesn't yet support this, sadly
+-
+- ID_INDENT_LESS = 7
+- ID_INDENT_MORE = 8
+-
+- ID_FONT = 9
+- ID_FONT_COLOUR = 10
+-
+- ID_LINK = 11
+- ID_LINK_BREAK = 12
+-
+- def __init__( self, parent, xml ):
+-
+- wx.Panel.__init__( self, parent )
+-
+- self._CreateToolBar()
+-
+- self._CreateRTC( xml )
+-
+- vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- vbox.AddF( self._toolbar, CC.FLAGS_EXPAND_PERPENDICULAR )
+- vbox.AddF( self._rtc, CC.FLAGS_EXPAND_BOTH_WAYS )
+-
+- self.SetSizer( vbox )
+-
+- self.SetAcceleratorTable( wx.AcceleratorTable( [
+- ( wx.ACCEL_CTRL, ord( 'b' ), self.ID_BOLD ),
+- ( wx.ACCEL_CTRL, ord( 'i' ), self.ID_ITALIC ),
+- ( wx.ACCEL_CTRL, ord( 'u' ), self.ID_UNDERLINE )
+- ] ) )
+-
+- self.Bind( wx.EVT_TOOL, self.EventToolBar )
+-
+- self.Bind( wx.EVT_UPDATE_UI, self.EventUpdateUI )
+-
+-
+- def _CreateToolBar( self ):
+-
+- self._toolbar = wx.ToolBar( self )
+-
+- self._toolbar.SetToolBitmapSize( ( 16, 16 ) )
+-
+- self._toolbar.AddCheckTool( self.ID_BOLD, CC.GlobalBMPs.bold_bmp )
+- self._toolbar.AddCheckTool( self.ID_ITALIC, CC.GlobalBMPs.italic_bmp )
+- self._toolbar.AddCheckTool( self.ID_UNDERLINE, CC.GlobalBMPs.underline_bmp )
+-
+- self._toolbar.AddSeparator()
+-
+- self._toolbar.AddRadioTool( self.ID_ALIGN_LEFT, CC.GlobalBMPs.align_left_bmp )
+- self._toolbar.AddRadioTool( self.ID_ALIGN_CENTER, CC.GlobalBMPs.align_center_bmp )
+- self._toolbar.AddRadioTool( self.ID_ALIGN_RIGHT, CC.GlobalBMPs.align_right_bmp )
+-
+- self._toolbar.AddSeparator()
+-
+- self._toolbar.AddLabelTool( self.ID_INDENT_LESS, 'indent less', CC.GlobalBMPs.indent_less_bmp )
+- self._toolbar.AddLabelTool( self.ID_INDENT_MORE, 'indent more', CC.GlobalBMPs.indent_more_bmp )
+-
+- self._toolbar.AddSeparator()
+-
+- self._toolbar.AddLabelTool( self.ID_FONT, 'font', CC.GlobalBMPs.font_bmp )
+- self._toolbar.AddLabelTool( self.ID_FONT_COLOUR, 'font colour', CC.GlobalBMPs.colour_bmp, shortHelp = 'font colour' )
+-
+- # font background
+- # message background?
+-
+- self._toolbar.AddSeparator()
+-
+- self._toolbar.AddLabelTool( self.ID_LINK, 'link', CC.GlobalBMPs.link_bmp )
+- self._toolbar.AddLabelTool( self.ID_LINK_BREAK, 'break link', CC.GlobalBMPs.link_break_bmp )
+-
+- self._toolbar.Realize()
+-
+-
+- def _CreateRTC( self, xml ):
+-
+- self._rtc = wx.richtext.RichTextCtrl( self, size = ( -1, 300 ), style = wx.WANTS_CHARS | wx.richtext.RE_MULTILINE )
+-
+- if len( xml ) > 0:
+-
+- xml_handler = wx.richtext.RichTextXMLHandler()
+-
+- stream = cStringIO.StringIO( xml )
+-
+- xml_handler.LoadStream( self._rtc.GetBuffer(), stream )
+-
+-
+-
+- def EventUpdateUI( self, event ):
+-
+- self._toolbar.ToggleTool( self.ID_BOLD, self._rtc.IsSelectionBold() )
+- self._toolbar.ToggleTool( self.ID_ITALIC, self._rtc.IsSelectionItalics() )
+- self._toolbar.ToggleTool( self.ID_UNDERLINE, self._rtc.IsSelectionUnderlined() )
+-
+- if self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_LEFT ): self._toolbar.ToggleTool( self.ID_ALIGN_LEFT, True )
+- elif self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_CENTER ): self._toolbar.ToggleTool( self.ID_ALIGN_CENTER, True )
+- elif self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_RIGHT ): self._toolbar.ToggleTool( self.ID_ALIGN_RIGHT, True )
+-
+- event.Skip()
+-
+-
+- def EventToolBar( self, event ):
+-
+- id = event.GetId()
+-
+- if id == self.ID_BOLD: self._rtc.ApplyBoldToSelection()
+- elif id == self.ID_ITALIC: self._rtc.ApplyItalicToSelection()
+- elif id == self.ID_UNDERLINE: self._rtc.ApplyUnderlineToSelection()
+- elif id == self.ID_ALIGN_LEFT: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_LEFT )
+- elif id == self.ID_ALIGN_CENTER: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_CENTRE )
+- elif id == self.ID_ALIGN_RIGHT: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_RIGHT )
+- elif id == self.ID_INDENT_LESS:
+-
+- text_attribute = wx.TEXTAttrEx()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
+-
+- ip = self._rtc.GetInsertionPoint()
+-
+- if self._rtc.GetStyle( ip, text_attribute ): # this copies the current style into text_attribute, returning true if successful
+-
+- if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
+- else: selection_range = wx.richtext.RichTextRange( ip, ip )
+-
+- if text_attribute.GetLeftIndent() >= 100:
+-
+- text_attribute.SetLeftIndent( text_attribute.GetLeftIndent() - 100 )
+- text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+-
+-
+- elif id == self.ID_INDENT_MORE:
+-
+- text_attribute = wx.richtext.TextAttrEx()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
+-
+- ip = self._rtc.GetInsertionPoint()
+-
+- if self._rtc.GetStyle( ip, text_attribute ): # this copies the current style into text_attribute, returning true if successful
+-
+- if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
+- else: selection_range = wx.richtext.RichTextRange( ip, ip )
+-
+- text_attribute.SetLeftIndent( text_attribute.GetLeftIndent() + 100 )
+- text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+-
+- elif id == self.ID_FONT:
+-
+- font_data = wx.FontData()
+- font_data.EnableEffects( False )
+-
+- text_attribute = wx.richtext.TextAttrEx()
+- text_attribute.SetFlags( wx.TEXT_ATTR_FONT )
+-
+- if self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute ): font_data.SetInitialFont( text_attribute.GetFont() )
+-
+- with wx.FontDialog( self, font_data ) as dlg:
+-
+- if dlg.ShowModal() == wx.ID_OK:
+-
+- font_data = dlg.GetFontData()
+-
+- font = font_data.GetChosenFont()
+-
+- if not self._rtc.HasSelection(): self._rtc.BeginFont( font )
+- else:
+-
+- selection_range = self._rtc.GetSelectionRange()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_FONT )
+- text_attribute.SetFont( font )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+-
+-
+-
+- elif id == self.ID_FONT_COLOUR:
+-
+- colour_data = wx.ColourData()
+-
+- text_attribute = wx.richtext.TextAttrEx()
+- text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
+-
+- if self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute ): colour_data.SetColour( text_attribute.GetTextColour() )
+-
+- with wx.ColourDialog( self, colour_data ) as dlg:
+-
+- if dlg.ShowModal() == wx.ID_OK:
+-
+- colour_data = dlg.GetColourData()
+- colour = colour_data.GetColour()
+-
+- if colour:
+-
+- if not self._rtc.HasSelection(): self._rtc.BeginTextColour( colour )
+- else:
+-
+- selection_range = self._rtc.GetSelectionRange()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
+- text_attribute.SetTextColour( colour )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+-
+-
+-
+-
+- elif id == self.ID_LINK:
+-
+- text_attribute = wx.richtext.TextAttrEx()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_URL )
+-
+- ip = self._rtc.GetInsertionPoint()
+-
+- self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute )
+-
+- if text_attribute.HasURL(): initial_url = text_attribute.GetURL()
+- else: initial_url = 'http://'
+-
+- with ClientGUIDialogs.DialogTextEntry( self, 'Enter url.', default = initial_url ) as dlg:
+-
+- if dlg.ShowModal() == wx.ID_OK:
+-
+- url = dlg.GetValue()
+-
+- if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
+- else: selection_range = wx.richtext.RichTextRange( ip, ip )
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
+- text_attribute.SetTextColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
+-
+- text_attribute.SetFontUnderlined( True )
+-
+- text_attribute.SetURL( url )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+-
+-
+- elif id == self.ID_LINK_BREAK:
+-
+- if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
+- else: selection_range = wx.richtext.RichTextRange( ip, ip )
+-
+- text_attribute = wx.richtext.TextAttrEx()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
+- text_attribute.SetTextColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOWTEXT ) )
+-
+- text_attribute.SetFontUnderlined( False )
+-
+- self._rtc.SetStyle( selection_range, text_attribute )
+-
+- text_attribute = wx.richtext.TextAttrEx()
+-
+- text_attribute.SetFlags( wx.TEXT_ATTR_URL )
+-
+- self._rtc.SetStyleEx( selection_range, text_attribute, wx.richtext.RICHTEXT_SETSTYLE_REMOVE )
+-
+-
+-
+- def GetXMLHTML( self ):
+-
+- xml_handler = wx.richtext.RichTextXMLHandler()
+-
+- stream = cStringIO.StringIO()
+-
+- xml_handler.SaveStream( self._rtc.GetBuffer(), stream )
+-
+- stream.seek( 0 )
+-
+- xml = stream.read()
+-
+- html_handler = wx.richtext.RichTextHTMLHandler()
+- html_handler.SetFlags( wx.richtext.RICHTEXT_HANDLER_SAVE_IMAGES_TO_MEMORY )
+- html_handler.SetFontSizeMapping( [7,9,11,12,14,22,100] )
+-
+- stream = cStringIO.StringIO()
+-
+- html_handler.SaveStream( self._rtc.GetBuffer(), stream )
+-
+- stream.seek( 0 )
+-
+- html = stream.read()
+-
+- return yaml.safe_dump( ( xml, html ) )
+-
+-
+-
+-class DraftPanel( wx.Panel ):
+-
+- def __init__( self, parent, draft_message ):
+-
+- wx.Panel.__init__( self, parent )
+-
+- self.SetBackgroundColour( CC.COLOUR_MESSAGE )
+-
+- self._compose_key = os.urandom( 32 )
+-
+- self._draft_message = draft_message
+-
+- ( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes ) = self._draft_message.GetInfo()
+-
+- is_new = self._draft_message.IsNew()
+-
+- self._from = wx.StaticText( self, label = self._contact_from.GetName() )
+-
+- if not self._draft_message.IsReply():
+-
+- self._to_panel = ClientGUICommon.StaticBox( self, 'to' )
+-
+- self._recipients_list = wx.ListCtrl( self._to_panel, style = wx.LC_LIST | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL )
+- self._recipients_list.InsertColumn( 0, 'contacts' )
+- for name in contacts_to: self._recipients_list.Append( ( name, ) )
+- self._recipients_list.Bind( wx.EVT_LIST_ITEM_ACTIVATED, self.EventRemove )
+-
+- self._new_recipient = ClientGUICommon.AutoCompleteDropdownContacts( self._to_panel, self._compose_key, self._contact_from )
+-
+- self._recipients_visible = wx.CheckBox( self._to_panel )
+- self._recipients_visible.SetValue( recipients_visible )
+- self._recipients_visible.Bind( wx.EVT_CHECKBOX, self.EventChanged )
+-
+- self._subject_panel = ClientGUICommon.StaticBox( self, 'subject' )
+-
+- self._subject = wx.TextCtrl( self._subject_panel, value = subject )
+- self._subject.Bind( wx.EVT_KEY_DOWN, self.EventChanged )
+-
+-
+- if body == '': xml = ''
+- else: ( xml, html ) = yaml.safe_load( body )
+-
+- self._body = DraftBodyPanel( self, xml )
+- self.Bind( wx.richtext.EVT_RICHTEXT_STYLE_CHANGED, self.EventChanged )
+- self.Bind( wx.richtext.EVT_RICHTEXT_CHARACTER, self.EventChanged )
+- self.Bind( wx.richtext.EVT_RICHTEXT_RETURN, self.EventChanged )
+- self.Bind( wx.richtext.EVT_RICHTEXT_DELETE, self.EventChanged )
+-
+- self._attachments = wx.TextCtrl( self, value = os.linesep.join( [ hash.encode( 'hex' ) for hash in attachment_hashes ] ), style = wx.TE_MULTILINE )
+- self._attachments.Bind( wx.EVT_KEY_DOWN, self.EventChanged )
+- # do thumbnails later! for now, do a listbox or whatever
+-
+- self._send = wx.Button( self, label = 'send' )
+- self._send.Bind( wx.EVT_BUTTON, self.EventSend )
+- self._send.SetForegroundColour( ( 0, 128, 0 ) )
+- if len( contacts_to ) == 0: self._send.Disable()
+-
+- self._delete_draft = wx.Button( self, label = 'delete' )
+- self._delete_draft.Bind( wx.EVT_BUTTON, self.EventDeleteDraft )
+- self._delete_draft.SetForegroundColour( ( 128, 0, 0 ) )
+-
+- self._save_draft = wx.Button( self, label = 'save' )
+- self._save_draft.Bind( wx.EVT_BUTTON, self.EventSaveDraft )
+-
+- if is_new:
+-
+- self._draft_changed = True
+- self._delete_draft.SetLabel( 'discard' )
+-
+- else:
+-
+- self._draft_changed = False
+- self._save_draft.SetLabel( 'saved' )
+- self._save_draft.Disable()
+-
+-
+- vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- vbox.AddF( self._from, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- if not self._draft_message.IsReply():
+-
+- recipients_hbox = wx.BoxSizer( wx.HORIZONTAL )
+-
+- recipients_hbox.AddF( wx.StaticText( self._to_panel, label = 'recipients can see each other' ), CC.FLAGS_MIXED )
+- recipients_hbox.AddF( self._recipients_visible, CC.FLAGS_MIXED )
+-
+- self._to_panel.AddF( self._recipients_list, CC.FLAGS_EXPAND_PERPENDICULAR )
+- self._to_panel.AddF( self._new_recipient, CC.FLAGS_LONE_BUTTON )
+- self._to_panel.AddF( recipients_hbox, CC.FLAGS_BUTTON_SIZER )
+-
+- self._subject_panel.AddF( self._subject, CC.FLAGS_EXPAND_BOTH_WAYS )
+-
+- vbox.AddF( self._to_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+- vbox.AddF( self._subject_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+-
+- vbox.AddF( self._body, CC.FLAGS_EXPAND_BOTH_WAYS )
+- #vbox.AddF( wx.StaticText( self, label = 'attachment hashes:' ), CC.FLAGS_MIXED )
+- #vbox.AddF( self._attachments, CC.FLAGS_EXPAND_PERPENDICULAR )
+- self._attachments.Hide()
+- button_hbox = wx.BoxSizer( wx.HORIZONTAL )
+- button_hbox.AddF( self._send, CC.FLAGS_MIXED )
+- button_hbox.AddF( self._delete_draft, CC.FLAGS_MIXED )
+- button_hbox.AddF( self._save_draft, CC.FLAGS_MIXED )
+-
+- vbox.AddF( button_hbox, CC.FLAGS_BUTTON_SIZER )
+-
+- self.SetSizer( vbox )
+-
+- HydrusGlobals.pubsub.sub( self, 'AddContact', 'add_contact' )
+- HydrusGlobals.pubsub.sub( self, 'DraftSaved', 'draft_saved' )
+-
+- if not self._draft_message.IsReply(): wx.CallAfter( self._new_recipient.SetFocus )
+-
+-
+- def _GetDraftMessage( self ):
+-
+- ( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes ) = self._draft_message.GetInfo()
+-
+- if not self._draft_message.IsReply():
+-
+- subject = self._subject.GetValue()
+- contacts_to = [ self._recipients_list.GetItemText( i ) for i in range( self._recipients_list.GetItemCount() ) ]
+- recipients_visible = self._recipients_visible.GetValue()
+-
+-
+- body = self._body.GetXMLHTML()
+-
+- try:
+-
+- raw_attachments = self._attachments.GetValue()
+-
+- attachment_hashes = [ hash.decode( 'hex' ) for hash in raw_attachments.split( os.linesep ) if hash != '' ]
+-
+- except:
+-
+- attachment_hashes = []
+-
+- wx.MessageBox( 'Could not parse attachments!' )
+-
+-
+- return ClientConstantsMessages.DraftMessage( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes )
+-
+-
+- def AddContact( self, compose_key, name ):
+-
+- if compose_key == self._compose_key:
+-
+- index = self._recipients_list.FindItem( -1, name )
+-
+- if index == -1: self._recipients_list.Append( ( name, ) )
+- else: self._recipients_list.DeleteItem( index )
+-
+- self.EventChanged( None )
+-
+-
+-
+- def DraftSaved( self, draft_key, draft_message ):
+-
+- if draft_key == self._draft_key:
+-
+- self._draft_changed = False
+-
+- self._save_draft.SetLabel( 'saved' )
+- self._save_draft.Disable()
+-
+- self._delete_draft.SetLabel( 'delete' )
+-
+- self._draft_message.Saved()
+-
+-
+-
+- def EventChanged( self, event ):
+-
+- if not self._draft_changed:
+-
+- self._draft_changed = True
+-
+- self._send.Enable()
+- self._save_draft.Enable()
+-
+-
+- if event is not None: event.Skip()
+-
+-
+- def EventDeleteDraft( self, event ): wx.GetApp().Write( 'delete_draft', self._draft_key )
+-
+- def EventSend( self, event ):
+-
+- draft_message = self._GetDraftMessage()
+-
+- transport_messages = wx.GetApp().Read( 'transport_messages_from_draft', draft_message )
+-
+- if self._contact_from.GetName() != 'Anonymous':
+-
+- try:
+-
+- my_message_depot = wx.GetApp().GetManager( 'services' ).GetService( self._contact_from.GetServiceKey() )
+-
+- connection = my_message_depot.GetConnection()
+-
+- my_public_key = self._contact_from.GetPublicKey()
+- my_contact_key = self._contact_from.GetContactKey()
+-
+- for transport_message in transport_messages:
+-
+- packaged_message = HydrusMessageHandling.PackageMessageForDelivery( transport_message, my_public_key )
+-
+- connection.Post( 'message', contact_key = my_contact_key, message = packaged_message )
+-
+- message_key = transport_message.GetMessageKey()
+-
+- status_updates = []
+-
+- for contact_to in transport_message.GetContactsTo():
+-
+- contact_to_key = contact_to.GetContactKey()
+-
+- status_key = hashlib.sha256( contact_to_key + message_key ).digest()
+-
+- status = HydrusMessageHandling.PackageStatusForDelivery( ( message_key, contact_to_key, 'pending' ), my_public_key )
+-
+- status_updates.append( ( status_key, status ) )
+-
+-
+- connection.Post( 'message_statuses', contact_key = my_contact_key, statuses = status_updates )
+-
+-
+- except:
+-
+- HydrusData.ShowText( 'The hydrus client could not connect to your message depot, so the message could not be sent!' )
+-
+- return
+-
+-
+-
+- for transport_message in transport_messages: wx.GetApp().Write( 'message', transport_message, forced_status = 'pending' )
+-
+- draft_key = draft_message.GetDraftKey()
+-
+- wx.GetApp().Write( 'delete_draft', draft_key )
+-
+-
+- def EventSaveDraft( self, event ):
+-
+- draft_message = self._GetDraftMessage()
+-
+- wx.GetApp().Write( 'draft_message', draft_message )
+-
+-
+- def EventRemove( self, event ):
+-
+- selection = self._recipients_list.GetFirstSelected()
+-
+- if selection != wx.NOT_FOUND:
+-
+- self._recipients_list.DeleteItem( selection )
+-
+- self.EventChanged( None )
+-
+-
+-
+- def GetConversationKey( self ): return self._conversation_key
+-
+- def GetDraftKey( self ): return self._draft_key
+-
+-class MessageHTML( wx.html.HtmlWindow ):
+-
+- def __init__( self, *args, **kwargs ):
+-
+- kwargs[ 'style' ] = wx.html.HW_SCROLLBAR_NEVER
+-
+- wx.html.HtmlWindow.__init__( self, *args, **kwargs )
+-
+- self.Bind( wx.EVT_MOUSEWHEEL, self.EventScroll )
+-
+- self.SetRelatedFrame( wx.GetTopLevelParent( self ), '%s' )
+- self.SetRelatedStatusBar( 0 )
+-
+-
+- def EventScroll( self, event ):
+-
+- sw = self.GetParent().GetParent()
+-
+- sw.GetEventHandler().ProcessEvent( event )
+-
+-
+- def GetClientSize( self ): return self.GetSize()
+-
+- def OnLinkClicked( self, link ): webbrowser.open( link.GetHref() )
+-
+- def OnOpeningURL( self, url_type, url, redirect ): return wx.html.HTML_BLOCK
+-
+-class MessagePanel( wx.Panel ):
+-
+- def __init__( self, parent, message, identity ):
+-
+- wx.Panel.__init__( self, parent )
+-
+- self.SetBackgroundColour( CC.COLOUR_MESSAGE )
+-
+- self._message = message
+- self._identity = identity
+-
+- vbox = wx.BoxSizer( wx.VERTICAL )
+-
+- contact_from = self._message.GetContactFrom()
+-
+- if contact_from is None: name = 'Anonymous'
+- else: name = self._message.GetContactFrom().GetName()
+-
+- #vbox.AddF( wx.StaticText( self, label = name + ', ' + HC.ConvertTimestampToPrettyAgo( self._message.GetTimestamp() ) ), CC.FLAGS_EXPAND_PERPENDICULAR )
+- vbox.AddF( ClientGUICommon.AnimatedStaticTextTimestamp( self, name + ', ', HydrusData.ConvertTimestampToPrettyAgo, self._message.GetTimestamp(), '' ), CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- body = self._message.GetBody()
+-
+- display_body = not ( body is None or body == '' )
+-
+- if display_body:
+-
+- self._body_panel = wx.Panel( self )
+-
+- wx.CallAfter( self.SetBody, body )
+-
+- else: self._body_panel = wx.StaticText( self, label = 'no body' )
+-
+- self._message_key = self._message.GetMessageKey()
+- destinations = self._message.GetDestinations()
+-
+- self._destinations_panel = DestinationsPanel( self, self._message_key, destinations, identity )
+-
+- self._hbox = wx.BoxSizer( wx.HORIZONTAL )
+-
+- self._hbox.AddF( self._body_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
+- self._hbox.AddF( self._destinations_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- vbox.AddF( self._hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
+-
+- # vbox.AddF( some kind of attachment window! )
+-
+- self.SetSizer( vbox )
+-
+-
+- def SetBody( self, body ):
+-
+- with wx.FrozenWindow( self ):
+-
+- ( width, height ) = self._body_panel.GetClientSize()
+-
+- body_panel = MessageHTML( self, size = ( width, -1 ) )
+- body_panel.SetPage( body )
+-
+- internal = body_panel.GetInternalRepresentation()
+-
+- body_panel.SetSize( ( -1, internal.GetHeight() ) )
+-
+- self._hbox.Replace( self._body_panel, body_panel )
+-
+- self._body_panel.Close()
+-
+- self._body_panel = body_panel
+-
+-
+- self.Layout()
+- self.GetParent().FitInside()
+-
+-
+-
+-# here starts the message reboot code
+-
+-class IMFrame( ClientGUICommon.Frame ):
+-
+- def __init__( self, parent, me_account, them_account, context ):
+-
+- def InitialiseControls():
+-
+- self._me_label = MeLabel( self, me_account ) # maybe these two should be the same, and infer me/them status itself
+- self._them_label = ThemLabel( self, them_account )
+- self._convo_box = ConvoBox( self, context_key ) # something like this
+- self._text_input = ConvoTextInput( self, callable ) # callable should be private method of this, or similar!
+-
+-
+- def PopulateControls():
+-
+- # could introduce last convo here, or whatever.
+-
+- pass
+-
+-
+- def ArrangeControls():
+-
+- hbox = wx.BoxSizer( wx.HORIZONTAL )
+-
+- hbox.AddF( self._me_label, CC.FLAGS_MIXED )
+- hbox.AddF( wx.StaticText( self, label = ' talking to ' ), CC.FLAGS_MIXED )
+- hbox.AddF( self._them_label, CC.FLAGS_MIXED )
+-
+- vbox = wx.BoxSizer( wx.VERTICAL )
+- vbox.AddF( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
+- vbox.AddF( self._convo_box, CC.FLAGS_EXPAND_BOTH_WAYS )
+- vbox.AddF( self._text_input, CC.FLAGS_EXPAND_PERPENDICULAR )
+-
+- self.SetSizer( vbox )
+-
+- self.SetInitialSize( ( 400, 600 ) ) # this should be remembered, stuck in options
+-
+-
+- me_name = me_account.GetNameBlah()
+- them_name = them_account.GetNameBlah()
+-
+- ClientGUICommon.Frame.__init__( self, parent, title = me_name + ' talking to ' + them_name )
+-
+- InitialiseControls()
+-
+- PopulateControls()
+-
+- ArrangeControls()
+-
+- self.Show( True )
+-
+-
+- def TextInputCallable( self, text ):
+-
+- pass
+-
+- # send it to the context, which will report it
+-
+- '''
+\ No newline at end of file
+diff --git a/include/ClientGUIPages.py b/include/ClientGUIPages.py
+index 8aae01e..8e8b36e 100755
+--- a/include/ClientGUIPages.py
++++ b/include/ClientGUIPages.py
+@@ -90,59 +90,6 @@ class PageBase( object ):
+
+ HydrusGlobals.pubsub.pub( 'resume', self._page_key )
+
+- '''
+-class PageMessages( PageBase, wx.SplitterWindow ):
+-
+- def __init__( self, parent, identity, starting_from_session = False ):
+-
+- wx.SplitterWindow.__init__( self, parent )
+- PageBase.__init__( self, starting_from_session = starting_from_session )
+-
+- self.SetMinimumPaneSize( 120 )
+- self.SetSashGravity( 0.0 )
+-
+- self._identity = identity
+-
+- self._search_preview_split = wx.SplitterWindow( self, style=wx.SP_NOBORDER )
+-
+- self._search_preview_split.SetMinimumPaneSize( 180 )
+- self._search_preview_split.SetSashGravity( 0.5 )
+-
+- self._search_preview_split.Bind( wx.EVT_SPLITTER_DCLICK, self.EventPreviewUnsplit )
+-
+- self._InitManagementPanel()
+- self._preview_panel = ClientGUICanvas.CanvasPanel( self._search_preview_split, self._page_key, CC.LOCAL_FILE_SERVICE_KEY )
+- self._InitMessagesPanel()
+-
+- self.SplitVertically( self._search_preview_split, self._messages_panel, HC.options[ 'hpos' ] )
+- wx.CallAfter( self._search_preview_split.SplitHorizontally, self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )
+-
+-
+- def _InitManagementPanel( self ): self._management_panel = ClientGUIManagement.ManagementPanelMessages( self._search_preview_split, self._page_key, self._identity, starting_from_session = self._starting_from_session )
+-
+- def _InitMessagesPanel( self ): self._messages_panel = ClientGUIMessages.ConversationSplitter( self, self._page_key, self._identity )
+-
+- def EventPreviewUnsplit( self, event ): self._search_preview_split.Unsplit( self._preview_panel )
+-
+- def GetSashPositions( self ):
+-
+- if self.IsSplit(): x = self.GetSashPosition()
+- else: x = HC.options[ 'hpos' ]
+-
+- if self._search_preview_split.IsSplit(): y = -1 * self._preview_panel.GetSize()[1]
+- else: y = HC.options[ 'vpos' ]
+-
+- return ( x, y )
+-
+-
+- def ShowHideSplit( self ):
+-
+- if self._search_preview_split.IsSplit(): self._search_preview_split.Unsplit( self._preview_panel )
+- else: self._search_preview_split.SplitHorizontally( self._management_panel, self._preview_panel, HC.options[ 'vpos' ] )
+-
+-
+- def TestAbleToClose( self ): self._management_panel.TestAbleToClose()
+- '''
+ class PageWithMedia( PageBase, wx.SplitterWindow ):
+
+ def __init__( self, parent, file_service_key = CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = None, initial_media_results = None, starting_from_session = False ):
+diff --git a/include/ClientImporting.py b/include/ClientImporting.py
+deleted file mode 100644
+index c205af2..0000000
+--- a/include/ClientImporting.py
++++ /dev/null
+@@ -1,553 +0,0 @@
+-import ClientConstants as CC
+-import HydrusConstants as HC
+-import HydrusData
+-import HydrusSerialisable
+-import threading
+-import traceback
+-
+-class ImportController( HydrusSerialisable.SerialisableBase ):
+-
+- def __init__( self ):
+-
+- HydrusSerialisable.SerialisableBase.__init__( self )
+-
+- # queues of stuff, where every kind of queue can be serialised
+- # hence don't have __init__ for subclasses! nothing beyond temp vars
+- # everything must fit inside what I declare here
+- # subclasses should mostly fill in _ProcessQueue kind of stuff.
+- # don't forget THREAD stuff, which should probably be explicitly started by the managementpanel?
+- # also, what about page_key? how are we reporting new imports and so on? maybe that can be temp var in the daemonspawner
+-
+- # hence maybe make a 'queue' object representing a list of urls or whatever -- maybe a urlcache can do that job.
+-
+- # a number of queues
+- # a thing that extends a queue using a search
+- # a list of searches that will be built into queues
+-
+- # maybe some class variables saying what parts to engage, like HDD doesn't accept new queues and so on
+-
+- self._file_status_counts = {}
+-
+- # if I decide to link search_seeds to the import_seed_queues, then why not bundle the import_seed_queue into the search_seed_info?
+- # yes, this is a good idea.
+-
+- self._import_seed_queues = []
+- self._importer_status = ( '', 0, 1 )
+-
+- self._search_seeds = SeedQueue()
+- self._searcher_status = ( '', 0, 1 )
+-
+- self._options = {}
+-
+- self._lock = threading.Lock()
+- self._import_status = ''
+-
+-
+- def _GetSerialisableInfo( self ):
+-
+- # collapse file status counts into a list because of stupid int dict json thing
+-
+- serialisable_url_cache = HydrusSerialisable.DumpToTuple( self._url_cache )
+-
+- serialisable_options = { name : HydrusSerialisable.DumpToTuple( options ) for ( name, options ) in self._options.items() }
+-
+- return ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, serialisable_url_cache, serialisable_options )
+-
+-
+- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+-
+- ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, serialisable_url_cache_tuple, serialisable_options_tuple ) = serialisable_info
+-
+- self._url_cache = HydrusSerialisable.CreateFromTuple( serialisable_url_cache_tuple )
+-
+- self._options = { name : HydrusSerialisable.CreateFromTuple( serialisable_suboptions_tuple ) for ( name, serialisable_suboptions_tuple ) in serialisable_options_tuple.items() }
+-
+-
+- def _ProcessImportSeed( self, seed, seed_info ):
+-
+- raise NotImplementedError()
+-
+-
+- def _ProcessSearchSeed( self, seed, seed_info ):
+-
+- raise NotImplementedError()
+-
+-
+- def _DAEMONProcessImportSeeds( self ):
+-
+- while True:
+-
+- # if importer paused
+-
+- with self._lock:
+-
+- result = None
+-
+- # determine paused/cancelled status via searchseedqueue or whatever
+-
+- for import_seed_queue in self._import_seed_queues:
+-
+- result = import_seed_queue.GetNextUnknownSeed()
+-
+- if result is not None:
+-
+- # remember current import_seed_queue so we can set seed status later
+-
+- break
+-
+-
+-
+-
+- if result is not None:
+-
+- ( seed, seed_info ) = result
+-
+- self._ProcessImportSeed( import_seed, seed_info )
+-
+-
+-
+-
+- def _DAEMONProcessSearchSeeds( self ):
+-
+- while True:
+-
+- # if searcher paused
+-
+- with self._lock:
+-
+- result = import_seed_queue.GetNextUnknownSeed()
+-
+-
+-
+- if result is not None:
+-
+- ( seed, seed_info ) = result
+-
+- self._ProcessSearchSeed( seed, seed_info )
+-
+-
+-
+-
+- def GetOptions( self, name ):
+-
+- with self._lock:
+-
+- return self._options[ name ]
+-
+-
+-
+- def GetStatuses( self ):
+-
+- with self._lock:
+-
+- return ( dict( self._file_status_counts ), self._import_status, self._current_queue_status, self._searcher_status )
+-
+-
+-
+- def PauseSearcher( self ):
+-
+- with self._lock:
+-
+- self._searcher_paused = True
+-
+-
+-
+- def ResumeCurrentQueue( self ):
+-
+- with self._lock:
+-
+- self._current_queue_paused = False
+-
+-
+-
+- def ResumeSearcher( self ):
+-
+- with self._lock:
+-
+- self._searcher_paused = False
+-
+-
+-
+- def SetOptions( self, name, options ):
+-
+- with self._lock:
+-
+- self._options[ name ] = options
+-
+-
+-
+-class ImportControllerHDD( HydrusSerialisable.SerialisableBase ):
+-
+- SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_HDD_IMPORT
+- SERIALISABLE_VERSION = 1
+-
+- def __init__( self ):
+-
+- HydrusSerialisable.SerialisableBase.__init__( self )
+-
+- # this stuff is all moved to the search seed
+- self._paths_info = None
+- self._paths_to_tags = None
+- self._delete_file_after_import = None
+- self._import_file_options = None
+-
+- self._lock = threading.Lock()
+-
+-
+- def _GetSerialisableInfo( self ):
+-
+- serialisable_url_cache = HydrusSerialisable.DumpToTuple( self._url_cache )
+-
+- serialisable_options = { name : HydrusSerialisable.DumpToTuple( options ) for ( name, options ) in self._options.items() }
+-
+- return ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, serialisable_url_cache, serialisable_options )
+-
+-
+- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+-
+- ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, serialisable_url_cache_tuple, serialisable_options_tuple ) = serialisable_info
+-
+- self._url_cache = HydrusSerialisable.CreateFromTuple( serialisable_url_cache_tuple )
+-
+- self._options = { name : HydrusSerialisable.CreateFromTuple( serialisable_suboptions_tuple ) for ( name, serialisable_suboptions_tuple ) in serialisable_options_tuple.items() }
+-
+-
+- def GetImportStatus( self ):
+-
+- with self._lock:
+-
+- return self._import_status
+-
+-
+-
+- def GetQueueStatus( self ):
+-
+- with self._lock:
+-
+- gauge_value = self._current_position
+- gauge_range = len( self._paths_info )
+-
+- # return progress string
+- # also return string for num_successful and so on
+-
+- pass
+-
+-
+-
+- def MainLoop( self ):
+-
+- # use the lock sparingly, remember
+- # obey pause and hc.shutdown
+- # maybe also an internal shutdown, on managementpanel cleanupbeforedestroy
+- # update file_status_counts
+- # increment current_position
+-
+- pass
+-
+-
+- def Pause( self ):
+-
+- with self._lock:
+-
+- self._paused = True
+-
+-
+-
+- def Resume( self ):
+-
+- with self._lock:
+-
+- self._paused = False
+-
+-
+-
+- def SetTuple( self, paths_info, paths_to_tags, delete_file_after_import, import_file_options ):
+-
+- self._paths_info = paths_info
+- self._paths_to_tags = paths_to_tags
+- self._delete_file_after_import = delete_file_after_import
+- self._import_file_options = import_file_options
+-
+-
+- def Start( self ):
+-
+- # init a daemon to work through the list
+-
+- pass
+-
+-
+-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_HDD_IMPORT ] = HDDImport
+-
+-class GalleryQuery( HydrusSerialisable.SerialisableBase ):
+-
+- SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_GALLERY_QUERY
+- SERIALISABLE_VERSION = 1
+-
+- def __init__( self, name ):
+-
+- HydrusSerialisable.SerialisableBase.__init__( self )
+-
+- self._site_type = None
+- self._query_type = None
+- self._query = None
+- self._get_tags_if_redundant = False
+- self._file_limit = 500
+- self._paused = False
+- self._page_index = 0
+- self._url_cache = None
+- self._options = {}
+-
+-
+- def _GetSerialisableInfo( self ):
+-
+- serialisable_url_cache = HydrusSerialisable.DumpToTuple( self._url_cache )
+-
+- serialisable_options = { name : HydrusSerialisable.DumpToTuple( options ) for ( name, options ) in self._options.items() }
+-
+- return ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, self._file_limit, serialisable_url_cache, serialisable_options )
+-
+-
+- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+-
+- ( self._site_type, self._query_type, self._query, self._get_tags_if_redundant, serialisable_url_cache_tuple, serialisable_options_tuple ) = serialisable_info
+-
+- self._url_cache = HydrusSerialisable.CreateFromTuple( serialisable_url_cache_tuple )
+-
+- self._options = { name : HydrusSerialisable.CreateFromTuple( serialisable_suboptions_tuple ) for ( name, serialisable_suboptions_tuple ) in serialisable_options_tuple.items() }
+-
+-
+- def GetQuery( self ):
+-
+- return self._query
+-
+-
+- def SetTuple( self, site_type, query_type, query, get_tags_if_redundant, file_limit, options ):
+-
+- self._site_type = site_type
+- self._query_type = query_type
+- self._query = query
+- self._get_tags_if_redundant = get_tags_if_redundant
+- self._file_limit = file_limit
+- self._url_cache = URLCache()
+- self._options = options
+-
+-
+-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_GALLERY_QUERY ] = GalleryQuery
+-
+-class SubscriptionController( HydrusSerialisable.SerialisableBaseNamed ):
+-
+- SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION
+- SERIALISABLE_VERSION = 1
+-
+- def __init__( self, name ):
+-
+- HydrusSerialisable.SerialisableBaseNamed.__init__( self, name )
+-
+- self._site_type = None
+- self._query_type = None
+- self._query = None
+- self._get_tags_if_redundant = False
+- self._file_limit = 500
+- self._periodic = None
+- self._page_index = 0
+- self._url_cache = None
+- self._options = {}
+-
+-
+- def _GetSerialisableInfo( self ):
+-
+- return ( HydrusSerialisable.DumpToTuple( self._gallery_query ), HydrusSerialisable.DumpToTuple( self._periodic ) )
+-
+-
+- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+-
+- ( serialised_gallery_query_tuple, serialised_periodic_tuple ) = serialisable_info
+-
+- self._gallery_query = HydrusSerialisable.CreateFromTuple( serialised_gallery_query_tuple )
+-
+- self._periodic = HydrusSerialisable.CreateFromTuple( serialised_periodic_tuple )
+-
+-
+-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SUBSCRIPTION ] = Subscription
+-
+-class SeedQueue( HydrusSerialisable.SerialisableBase ):
+-
+- SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_SEED_QUEUE
+- SERIALISABLE_VERSION = 1
+-
+- def __init__( self ):
+-
+- HydrusSerialisable.SerialisableBase.__init__( self )
+-
+- self._seeds_ordered = []
+- self._seeds_to_info = {}
+-
+- self._lock = threading.Lock()
+-
+-
+- def _GetSerialisableInfo( self ):
+-
+- with self._lock:
+-
+- serialisable_info = []
+-
+- for seed in self._seeds_ordered:
+-
+- seed_info = self._seeds_to_info[ seed ]
+-
+- serialisable_info.append( ( seed, seed_info ) )
+-
+-
+- return serialisable_info
+-
+-
+-
+- def _InitialiseFromSerialisableInfo( self, serialisable_info ):
+-
+- with self._lock:
+-
+- for ( seed, seed_info ) in serialisable_info:
+-
+- self._seeds_ordered.append( seed )
+-
+- self._seeds_to_info[ seed ] = seed_info
+-
+-
+-
+-
+- def AddSeed( self, seed, additional_info = None ):
+-
+- with self._lock:
+-
+- if seed in self._seeds_to_info:
+-
+- self._seeds_ordered.remove( seed )
+-
+-
+- self._seeds_ordered.append( seed )
+-
+- seed_info = {}
+-
+- seed_info[ 'status' ] = CC.STATUS_UNKNOWN
+- seed_info[ 'timestamp' ] = HydrusData.GetNow()
+- seed_info[ 'note' ] = ''
+-
+- if additional_info is not None:
+-
+- seed_info.update( additional_info )
+-
+-
+- self._seeds_to_info[ seed ] = seed_info
+-
+-
+-
+-
+- def AdvanceSeed( self, seed ):
+-
+- with self._lock:
+-
+- if seed in self._seeds_to_info:
+-
+- index = self._seeds_ordered.index( seed )
+-
+- if index > 0:
+-
+- self._seeds_ordered.remove( seed )
+-
+- self._seeds_ordered.insert( index - 1, seed )
+-
+-
+-
+-
+-
+- def DelaySeed( self, seed ):
+-
+- with self._lock:
+-
+- if seed in self._seeds_to_info:
+-
+- index = self._seeds_ordered.index( seed )
+-
+- if index < len( self._seeds_ordered ) - 1:
+-
+- self._seeds_ordered.remove( seed )
+-
+- self._seeds_ordered.insert( index + 1, seed )
+-
+-
+-
+-
+-
+- def GetNextUnknownSeed( self ):
+-
+- with self._lock:
+-
+- for seed in self._seeds_ordered:
+-
+- seed_info = self._seeds_to_info[ seed ]
+-
+- if seed_info[ 'status' ] == CC.STATUS_UNKNOWN:
+-
+- return ( seed, seed_info )
+-
+-
+-
+-
+- return None
+-
+-
+- def GetSeeds( self ):
+-
+- with self._lock:
+-
+- return list( self._seeds_ordered )
+-
+-
+-
+- def GetSeedsDisplayInfo( self ):
+-
+- with self._lock:
+-
+- all_info = []
+-
+- for seed in self._seeds_ordered:
+-
+- seed_info = self._seeds_to_info[ seed ]
+-
+- timestamp = seed_info[ 'timestamp' ]
+- status = seed_info[ 'status' ]
+- note = seed_info[ 'note' ]
+-
+- all_info.append( ( seed, status, timestamp, note ) )
+-
+-
+- return all_info
+-
+-
+-
+- def RemoveSeed( self, seed ):
+-
+- with self._lock:
+-
+- if seed in self._seeds_to_info:
+-
+- del self._seeds_to_info[ seed ]
+-
+- self._seeds_ordered.remove( seed )
+-
+-
+-
+-
+- def SetSeedStatus( self, seed, status, note = '' ):
+-
+- with self._lock:
+-
+- seed_info = self._seeds_to_info[ seed ]
+-
+- seed_info[ 'status' ] = status
+- seed_info[ 'timestamp' ] = HydrusData.GetNow()
+- seed_info[ 'note' ] = note
+-
+-
+-
+-HydrusSerialisable.SERIALISABLE_TYPES_TO_OBJECT_TYPES[ HydrusSerialisable.SERIALISABLE_TYPE_SEED_QUEUE ] = SeedQueue
+diff --git a/include/HydrusData.py b/include/HydrusData.py
+index 4ab9f67..259f478 100644
+--- a/include/HydrusData.py
++++ b/include/HydrusData.py
+@@ -963,15 +963,15 @@ class ClientToServerContentUpdatePackage( HydrusYAMLBase ):
+
+ if data_type in ( HC.CONTENT_DATA_TYPE_TAG_SIBLINGS, HC.CONTENT_DATA_TYPE_TAG_PARENTS ) and action in ( HC.CONTENT_UPDATE_PENDING, HC.CONTENT_UPDATE_PETITION ):
+
+- munge_row = lambda ( pair, reason ): pair
++ munge_row = lambda tup: tup[0]
+
+ elif data_type == HC.CONTENT_DATA_TYPE_FILES and action == HC.CONTENT_UPDATE_PETITION:
+
+- munge_row = lambda ( hashes, reason ): hashes
++ munge_row = lambda tup: tup[0]
+
+ elif data_type == HC.CONTENT_DATA_TYPE_MAPPINGS and action == HC.CONTENT_UPDATE_PETITION:
+
+- munge_row = lambda ( tag, hashes, reason ): ( tag, hashes )
++ munge_row = lambda tup: tup[:-1]
+
+
+ else: new_action = action
+diff --git a/include/HydrusImageHandling.py b/include/HydrusImageHandling.py
+index 97c2fca..a69fe4e 100755
+--- a/include/HydrusImageHandling.py
++++ b/include/HydrusImageHandling.py
+@@ -50,7 +50,8 @@ def ConvertToPngIfBmp( path ):
+
+
+
+-def EfficientlyResizeNumpyImage( numpy_image, ( target_x, target_y ) ):
++def EfficientlyResizeNumpyImage( numpy_image, target_dims ):
++ ( target_x, target_y ) = target_dims
+
+ ( im_y, im_x, depth ) = numpy_image.shape
+
+@@ -63,7 +64,8 @@ def EfficientlyResizeNumpyImage( numpy_image, ( target_x, target_y ) ):
+
+ return cv2.resize( result, ( target_x, target_y ), interpolation = cv2.INTER_LINEAR )
+
+-def EfficientlyResizePILImage( pil_image, ( target_x, target_y ) ):
++def EfficientlyResizePILImage( pil_image, target_dims ):
++ ( target_x, target_y ) = target_dims
+
+ ( im_x, im_y ) = pil_image.size
+
+@@ -76,7 +78,8 @@ def EfficientlyResizePILImage( pil_image, ( target_x, target_y ) ):
+
+ return pil_image.resize( ( target_x, target_y ), PILImage.ANTIALIAS )
+
+-def EfficientlyThumbnailNumpyImage( numpy_image, ( target_x, target_y ) ):
++def EfficientlyThumbnailNumpyImage( numpy_image, target_dims ):
++ ( target_x, target_y ) = target_dims
+
+ ( im_y, im_x, depth ) = numpy_image.shape
+
+@@ -86,7 +89,8 @@ def EfficientlyThumbnailNumpyImage( numpy_image, ( target_x, target_y ) ):
+
+ return cv2.resize( numpy_image, ( target_x, target_y ), interpolation = cv2.INTER_AREA )
+
+-def EfficientlyThumbnailPILImage( pil_image, ( target_x, target_y ) ):
++def EfficientlyThumbnailPILImage( pil_image, target_dims ):
++ ( target_x, target_y ) = target_dims
+
+ ( im_x, im_y ) = pil_image.size
+
+@@ -348,7 +352,9 @@ def GetResolutionAndNumFrames( path ):
+
+ return ( ( x, y ), num_frames )
+
+-def GetThumbnailResolution( ( im_x, im_y ), ( target_x, target_y ) ):
++def GetThumbnailResolution( im_dims, target_dims ):
++ ( im_x, im_y ) = im_dims
++ ( target_x, target_y ) = target_dims
+
+ im_x = float( im_x )
+ im_y = float( im_y )
+diff --git a/include/HydrusMessageHandling.py b/include/HydrusMessageHandling.py
+index 1b29013..2ce6c3b 100755
+--- a/include/HydrusMessageHandling.py
++++ b/include/HydrusMessageHandling.py
+@@ -162,237 +162,3 @@ class Message( HydrusData.HydrusYAMLBase ):
+ verifier = Crypto.Signature.PKCS1_v1_5.new( public_key )
+
+ return verifier.verify( hash_object, self._signature )
+-
+-
+-# here begins the new stuff, I'm pretty sure
+-
+-class Identity( object ): # should be a yamlable object
+-
+- def __init__( self ):
+-
+- # no name, right? we associate names and addresses with the identity, but the id only has keys
+-
+- # store key_type -> key
+- # hence need a key_type enum
+-
+- pass
+-
+-
+-class IMManager( object ):
+-
+- def __init__( self ):
+-
+- self._accounts = {}
+- self._contexts = {}
+- self._persistent_connections = {}
+- self._temporary_connections = {}
+-
+- # go fetch all accounts from the db
+-
+- # set up many pubsubs
+-
+- # start up some sort of daemon to keep our accounts logged in
+-
+- pass
+-
+-
+- def _GetContext( self, identifier_local, name_local, identifier_remote, name_remote ):
+-
+- if ( identifier_remote, name_remote ) not in self._contexts[ identifier_local ]:
+-
+- account = self._accounts[ ( identifier_local, name_local ) ]
+-
+- context = HydrusEncryption.HydrusOTRContext( account, identifier_remote, name_remote )
+-
+- self._contexts[ ( identifier_local, name_local, identifier_remote, name_remote ) ] = context
+-
+-
+- context = self._contexts[ identifier_local ][ ( identifier_remote, name_remote ) ]
+-
+- return context
+-
+-
+- def LoginPersistentConnections( self ):
+-
+- # this is on a daemon thread, so move to twisted
+-
+- for ( identifier, name ) in self._accounts.keys():
+-
+- if ( identifier, name ) not in self._persistent_connections:
+-
+- # get host, port for that identity
+-
+- creator = ClientCreator( reactor, HydrusServerAMP.MessagingClientProtocol )
+-
+- deferred = creator.connectTCP( host, port )
+-
+- # deferred is called with the connection, or an error
+- # callRemote to register with session key and whatnot
+-
+- self._persistent_connections[ ( identifier, name ) ] = connection
+-
+-
+-
+-
+- def ReceiveMessage( self, identifier_from, name_from, identifier_to, name_to, message ):
+-
+- # currently on wx loop
+- # move it to the twisted loop
+-
+- if ( identifier_from, name_from, identifier_to, name_to ) not in self._temporary_connections:
+-
+- self._temporary_connections[ ( identifier_from, name_from, identifier_to, name_to ) ] = self._persistent_connections[ ( identifier_to, name_to ) ]
+- # this should have a better error, if the _to doesn't exist
+- # we should really just disregard it, and any other weirdness
+-
+-
+- context = self._GetContext( identifier_to, name_to, identifier_from, name_from )
+-
+- response = context.receiveMessage( message )
+-
+- if response is not None:
+-
+- ( decrypted_message, gumpf ) = response
+-
+- message_object = yaml.safe_load( decrypted_message )
+-
+- # do the pubsub
+-
+-
+-
+- def RemovePersistentConnection( self, identifier, name ):
+-
+- # if it is still alive, loseConnection or whatever.
+- # remove it
+- # pubsub the login daemon
+-
+- pass
+-
+-
+- def RemoveTemporaryConnection( self, identifier_from, name_from, identifier_to, name_to ):
+-
+- # if it is still alive, loseConnection or whatever.
+- # remove it
+-
+- pass
+-
+-
+- def SendMessage( self, identifier_from, name_from, identifier_to, name_to, message ):
+-
+- context = self._GetContext( identifier_from, name_from, identifier_to, name_to )
+-
+- context.sendMessage( potr.context.FRAGMENT_SEND_ALL, message )
+-
+-
+- def SendEncryptedMessage( self, identifier_from, name_from, identifier_to, name_to, message ):
+-
+- # currently on wx loop
+- # move it to the twisted loop
+-
+- connection = self._temporary_connections[ ( identifier_from, name_from, identifier_to, name_to ) ]
+-
+- connection.callRemote( HydrusServerAMP.IMMessageServer, identifier_to = identifier_to, name_to = name_to, message = message )
+-
+- # if it breaks, we should pubsub that it broke
+-
+-
+- def StartTalking( self, identifier_from, name_from, identifier_to, name_to ):
+-
+- # currently on wx loop
+- # move it to the twisted loop
+-
+- # fetch host and port for that id
+-
+- creator = ClientCreator( reactor, HydrusServerAMP.MessagingClientProtocol )
+-
+- deferred = creator.connectTCP( host, port )
+-
+- # deferred is called with the connection, or an error
+- # callRemote to register identifier_from and name_from as temp login
+- # then add to temp_connections
+-
+- self._temporary_connections[ ( identifier_from, name_from, identifier_to, name_to ) ] = connection
+-
+- message = '' # this is just to get the OTR handshake going; it'll never be sent
+-
+- connection.callRemote( HydrusServerAMP.IMMessageServer, identifier_to = identifier_to, name_to = name_to, message = message )
+-
+- # how do I detect when we are ready to do encrypted comms?
+- # I can check periodically context.status, but that is a _little_ bleh
+- # I can write a pubsub in the setStatus thing in context
+- # check that article again, or the code, on the exact name
+-
+- # do a pubsub to say we are ready to do encrypted comms
+-
+- # if it fails, we should pubsub that it broke
+-
+-
+- def StopTalking( self, identifier, name ):
+-
+- # close temp connection
+- #
+-
+- pass
+-
+-
+-class IMMessage( HydrusData.HydrusYAMLBase ):
+-
+- yaml_tag = u'!IMMessage'
+-
+-class IMMessageQuestion( IMMessage ):
+-
+- yaml_tag = u'!IMMessageQuestion'
+-
+- def __init__( self, job_key = None ):
+-
+- if job_key is None: job_key = os.urandom( 32 )
+-
+- self._job_key = job_key
+-
+-
+- def GenerateAnswer( self, answer ):
+-
+- return IMMessageQuestionAnswer( self._job_key, answer )
+-
+-
+- def GetJobKey( self ): return self._job_key
+-
+-class IMMessageQuestionAnswer( IMMessageQuestion ):
+-
+- yaml_tag = u'!IMMessageQuestionAnswer'
+-
+- def __init__( self, job_key, answer ):
+-
+- IMMessageQuestion.__init__( self, job_key )
+-
+- self._answer = answer
+-
+-
+- def GetAnswer( self ): return self._answer
+-
+-class IMMessageQuestionFiles( IMMessageQuestion ):
+-
+- yaml_tag = u'!IMMessageFiles'
+-
+- def __init__( self, media_results ):
+-
+- IMMessageQuestion.__init__( self )
+-
+- self._text = text
+-
+-
+-IM_MESSAGE_TYPE_CONVO = 0
+-IM_MESSAGE_TYPE_STATUS = 1
+-
+-class IMMessageText( IMMessage ):
+-
+- yaml_tag = u'!IMMessageText'
+-
+- def __init__( self, message_type, text ):
+-
+- self._type = message_type
+- self._text = text
+-
+-
+- def ToTuple( self ): return ( self._type, self._text )
+-
+\ No newline at end of file
+diff --git a/include/HydrusSerialisable.py b/include/HydrusSerialisable.py
+index 473cfe9..533f55c 100644
+--- a/include/HydrusSerialisable.py
++++ b/include/HydrusSerialisable.py
+@@ -130,4 +130,4 @@ class SerialisableBaseNamed( SerialisableBase ):
+ def GetName( self ): return self._name
+
+ def SetName( self, name ): self._name = name
+-
+\ No newline at end of file
++
+diff --git a/include/HydrusTags.py b/include/HydrusTags.py
+index dee1b78..413c6f0 100644
+--- a/include/HydrusTags.py
++++ b/include/HydrusTags.py
+@@ -352,7 +352,7 @@ class TagsManagerSimple( object ):
+ combined_current = combined_statuses_to_tags[ HC.CURRENT ]
+ combined_pending = combined_statuses_to_tags[ HC.PENDING ]
+
+- slice = { tag for tag in combined_current.union( combined_pending ) if True in ( tag.startswith( namespace + ':' ) for namespace in namespaces ) }
++ slice = { tag for tag in combined_current.union( combined_pending ) if True in (lambda tag:( tag.startswith( namespace + ':' ) for namespace in namespaces ))(tag) }
+
+ if collapse_siblings:
+
diff --git a/hydrus-client b/hydrus-client
new file mode 100644
index 000000000000..003a2adc0b53
--- /dev/null
+++ b/hydrus-client
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec python2 -OO /opt/hydrus/client.pyw "$@"
diff --git a/hydrus-server b/hydrus-server
new file mode 100644
index 000000000000..66f4bbc158f3
--- /dev/null
+++ b/hydrus-server
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec python2 -OO /opt/hydrus/server.pyw "$@"
diff --git a/hydrus.desktop b/hydrus.desktop
new file mode 100644
index 000000000000..518a6037019c
--- /dev/null
+++ b/hydrus.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=148
+Name=Hydrus Client
+Comment=A booru-like media organizer for the desktop
+Exec=hydrus-client
+Icon=/opt/hydrus/static/hydrus_non-transparent.png
+Terminal=false
+Type=Application
+Categories=Application;FileTools;Graphics;Network;
diff --git a/hydrus.install b/hydrus.install
new file mode 100644
index 000000000000..e111ef946053
--- /dev/null
+++ b/hydrus.install
@@ -0,0 +1,11 @@
+post_install() {
+ update-desktop-database -q
+}
+
+post_upgrade() {
+ post_install
+}
+
+post_remove() {
+ post_install
+}
diff --git a/paths-in-opt.patch b/paths-in-opt.patch
new file mode 100644
index 000000000000..5e50d3b4f954
--- /dev/null
+++ b/paths-in-opt.patch
@@ -0,0 +1,121 @@
+diff --git a/include/ClientCaches.py b/include/ClientCaches.py
+index 4dd2292..9e0e3ee 100644
+--- a/include/ClientCaches.py
++++ b/include/ClientCaches.py
+@@ -486,4 +486,4 @@ class ThumbnailCache( object ):
+
+
+
+-
+\ No newline at end of file
++
+diff --git a/include/ClientGUI.py b/include/ClientGUI.py
+index 96fef72..ea9f71f 100755
+--- a/include/ClientGUI.py
++++ b/include/ClientGUI.py
+@@ -131,7 +131,7 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
+ aboutinfo.SetVersion( HydrusData.ToString( HC.SOFTWARE_VERSION ) )
+ aboutinfo.SetDescription( CC.CLIENT_DESCRIPTION )
+
+- with open( HC.BASE_DIR + os.path.sep + 'license.txt', 'rb' ) as f: license = f.read()
++ with open( '/usr/share/licenses/hydrus/license.txt', 'rb' ) as f: license = f.read()
+
+ aboutinfo.SetLicense( license )
+
+@@ -1884,7 +1884,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
+
+ 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': webbrowser.open( 'file:///opt/hydrus/help/index.html' )
+ elif command == 'help_about': self._AboutWindow()
+ elif command == 'help_shortcuts': wx.MessageBox( CC.SHORTCUT_HELP )
+ elif command == 'import_files': self._ImportFiles()
+@@ -3100,4 +3100,4 @@ class FrameSplash( ClientGUICommon.Frame ):
+
+ self.Refresh()
+
+-
+\ No newline at end of file
++
+diff --git a/include/ClientGUIDialogs.py b/include/ClientGUIDialogs.py
+index 3b964a7..3f5efb5 100755
+--- a/include/ClientGUIDialogs.py
++++ b/include/ClientGUIDialogs.py
+@@ -664,7 +664,7 @@ class DialogFirstStart( Dialog ):
+ self._ok.SetForegroundColour( ( 0, 128, 0 ) )
+
+ message1 = 'Hi, this looks like the first time you have started the hydrus client. Don\'t forget to check out the'
+- link = wx.HyperlinkCtrl( self, id = -1, label = 'help', url = 'file://' + HC.BASE_DIR + '/help/index.html' )
++ link = wx.HyperlinkCtrl( self, id = -1, label = 'help', url = 'file:///opt/hydrus/help/index.html' )
+ message2 = 'if you haven\'t already.'
+ message3 = 'When you close this dialog, the client will start its local http server. You will probably get a firewall warning.'
+ message4 = 'You can block it if you like, or you can allow it. It doesn\'t phone home, or expose your files to your network; it just provides another way to locally export your files.'
+@@ -5133,4 +5133,4 @@ class DialogYesNo( Dialog ):
+ if event.KeyCode == wx.WXK_ESCAPE: self.EndModal( wx.ID_NO )
+ else: event.Skip()
+
+-
+\ No newline at end of file
++
+diff --git a/include/HydrusConstants.py b/include/HydrusConstants.py
+index 79580fd..327989c 100755
+--- a/include/HydrusConstants.py
++++ b/include/HydrusConstants.py
+@@ -3,9 +3,9 @@ import sys
+
+ # dirs
+
+-BASE_DIR = sys.path[0]
++BASE_DIR = os.path.expanduser("~/.local/share/hydrus")
+
+-BIN_DIR = BASE_DIR + os.path.sep + 'bin'
++BIN_DIR = "/opt/hydrus/bin"
+ DB_DIR = BASE_DIR + os.path.sep + 'db'
+ CLIENT_ARCHIVES_DIR = DB_DIR + os.path.sep + 'client_archives'
+ CLIENT_FILES_DIR = DB_DIR + os.path.sep + 'client_files'
+@@ -16,8 +16,14 @@ SERVER_MESSAGES_DIR = DB_DIR + os.path.sep + 'server_messages'
+ CLIENT_UPDATES_DIR = DB_DIR + os.path.sep + 'client_updates'
+ SERVER_UPDATES_DIR = DB_DIR + os.path.sep + 'server_updates'
+ LOGS_DIR = BASE_DIR + os.path.sep + 'logs'
+-STATIC_DIR = BASE_DIR + os.path.sep + 'static'
+-
++STATIC_DIR = '/opt/hydrus/static'
++TEMP_DIR = BASE_DIR + os.path.sep + 'temp'
++
++for dirs in [LOGS_DIR, TEMP_DIR, DB_DIR]:
++ try:
++ os.makedirs(dirs)
++ except os.error:
++ pass
+ #
+
+ PLATFORM_WINDOWS = False
+@@ -556,4 +562,4 @@ sqlite3.register_adapter( bool, int )
+
+ sqlite3.register_converter( 'BLOB_BYTES', str )
+ sqlite3.register_converter( 'INTEGER_BOOLEAN', integer_boolean_to_bool )
+-sqlite3.register_converter( 'TEXT_YAML', yaml.safe_load )
+\ No newline at end of file
++sqlite3.register_converter( 'TEXT_YAML', yaml.safe_load )
+diff --git a/include/HydrusServerResources.py b/include/HydrusServerResources.py
+index 8c3779c..3037df8 100644
+--- a/include/HydrusServerResources.py
++++ b/include/HydrusServerResources.py
+@@ -784,11 +784,11 @@ class HydrusResourceCommandBooruThumbnail( HydrusResourceCommandBooru ):
+ mime = media_result.GetMime()
+
+ if mime in HC.MIMES_WITH_THUMBNAILS: path = ClientFiles.GetThumbnailPath( hash, full_size = False )
+- elif mime in HC.AUDIO: path = HC.STATIC_DIR + os.path.sep + 'audio_resized.png'
+- elif mime in HC.VIDEO: path = HC.STATIC_DIR + os.path.sep + 'video_resized.png'
+- elif mime == HC.APPLICATION_FLASH: path = HC.STATIC_DIR + os.path.sep + 'flash_resized.png'
+- elif mime == HC.APPLICATION_PDF: path = HC.STATIC_DIR + os.path.sep + 'pdf_resized.png'
+- else: path = HC.STATIC_DIR + os.path.sep + 'hydrus_resized.png'
++ elif mime in HC.AUDIO: path = HC.TEMP_DIR + os.path.sep + 'audio_resized.png'
++ elif mime in HC.VIDEO: path = HC.TEMP_DIR + os.path.sep + 'video_resized.png'
++ elif mime == HC.APPLICATION_FLASH: path = HC.TEMP_DIR + os.path.sep + 'flash_resized.png'
++ elif mime == HC.APPLICATION_PDF: path = HC.TEMP_DIR + os.path.sep + 'pdf_resized.png'
++ else: path = HC.TEMP_DIR + os.path.sep + 'hydrus_resized.png'
+
+ response_context = ResponseContext( 200, path = path )
+
diff --git a/running-the-server.patch b/running-the-server.patch
new file mode 100644
index 000000000000..28dfd8b41f13
--- /dev/null
+++ b/running-the-server.patch
@@ -0,0 +1,26 @@
+diff --git a/include/ClientGUI.py b/include/ClientGUI.py
+index ea9f71f..3edbf21 100755
+--- a/include/ClientGUI.py
++++ b/include/ClientGUI.py
+@@ -234,20 +234,7 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
+
+ HydrusData.ShowText( u'Starting server\u2026' )
+
+- my_scriptname = sys.argv[0]
+-
+- if my_scriptname.endswith( 'pyw' ):
+-
+- if HC.PLATFORM_WINDOWS or HC.PLATFORM_OSX: python_bin = 'pythonw'
+- else: python_bin = 'python'
+-
+- subprocess.Popen( python_bin + ' "' + HC.BASE_DIR + os.path.sep + 'server.pyw"', shell = True )
+-
+- else:
+-
+- if HC.PLATFORM_WINDOWS: subprocess.Popen( '"' + HC.BASE_DIR + os.path.sep + 'server.exe"', shell = True )
+- else: subprocess.Popen( '"./' + HC.BASE_DIR + os.path.sep + 'server"', shell = True )
+-
++ subprocess.Popen( ["hydrus-server"] )
+
+ time_waited = 0
+