code:
import itertools, os, wx

from lib.GUI.Common import BetterStaticText, RatingLikeDialog, RatingNumericalDialog, \
	WrapInGrid
from lib.GUI.Dialogs import Dialog, DialogInputUPnPMapping
from lib.GUI.ListCtrl import BetterListCtrl, BetterListCtrlPanel
from lib.GUI.Shortcuts import ShortcutsHandler

from lib.Hydrus.Data import ContentUpdate, ShowException, \
	TimeDeltaToPrettyTimeDelta, ToHumanInt
from lib.Hydrus.Ratings import GetLikeStateFromMedia, \
	GetNumericalStateFromMedia

from lib.Networking.NATPunch import AddUPnPMapping, GetLocalIP, GetUPnPMappings, \
	RemoveUPnPMapping

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

from lib.Hydrus.Ratings import DISLIKE, LIKE, NULL, SET

# Option Enums

ID_NULL = wx.NewId()

ID_TIMER_UPDATE = wx.NewId()

### CLASSES ###

class DialogManageRatings( Dialog ):
	def __init__( self, parent, media ):
		self._hashes = set()

		for m in media:
			self._hashes.update( m.GetHashes() )

		( remember, position ) = HC.options[ 'rating_dialog_position' ]

		if remember and position is not None:
			my_position = 'custom'

			wx.CallAfter( self.SetPosition, position )

		else:
			my_position = 'topleft'

		Dialog.__init__( self, parent, 'manage ratings for ' + ToHumanInt( len( self._hashes ) ) + ' files', position = my_position )

		#

		like_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_LIKE, ), randomised = False )
		numerical_services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_RATING_NUMERICAL, ), randomised = False )

		self._panels = []

		if len( like_services ) > 0:
			self._panels.append( self._LikePanel( self, like_services, media ) )

		if len( numerical_services ) > 0:
			self._panels.append( self._NumericalPanel( self, numerical_services, media ) )

		self._apply = wx.Button( self, id = wx.ID_OK, label = 'apply' )
		self._apply.Bind( wx.EVT_BUTTON, self.EventOK )
		self._apply.SetForegroundColour( ( 0, 128, 0 ) )

		self._cancel = wx.Button( self, id = wx.ID_CANCEL, label = 'cancel' )
		self._cancel.SetForegroundColour( ( 128, 0, 0 ) )

		#

		buttonbox = wx.BoxSizer( wx.HORIZONTAL )

		buttonbox.Add( self._apply, CC.FLAGS_VCENTER )
		buttonbox.Add( self._cancel, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		for panel in self._panels:
			vbox.Add( panel, CC.FLAGS_EXPAND_PERPENDICULAR )

		vbox.Add( buttonbox, CC.FLAGS_BUTTON_SIZER )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		self.SetInitialSize( ( x, y ) )

		#

		self._my_shortcut_handler = ShortcutsHandler( self, [ 'media' ] )

	def EventOK( self, event ):
		try:
			service_keys_to_content_updates = {}

			for panel in self._panels:
				sub_service_keys_to_content_updates = panel.GetContentUpdates()

				service_keys_to_content_updates.update( sub_service_keys_to_content_updates )

			if len( service_keys_to_content_updates ) > 0:
				HG.client_controller.Write( 'content_updates', service_keys_to_content_updates )

			( remember, position ) = HC.options[ 'rating_dialog_position' ]

			current_position = self.GetPosition()

			if remember and position != current_position:
				HC.options[ 'rating_dialog_position' ] = ( remember, current_position )

				HG.client_controller.Write( 'save_options', HC.options )

		finally:
			self.EndModal( wx.ID_OK )

	def ProcessApplicationCommand( self, command ):
		command_processed = True

		command_type = command.GetCommandType()
		data = command.GetData()

		if command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
			action = data

			if action == 'manage_file_ratings':
				self.EventOK( None )

			else:
				command_processed = False

		else:
			command_processed = False

		return command_processed

	class _LikePanel( wx.Panel ):
		def __init__( self, parent, services, media ):
			wx.Panel.__init__( self, parent )

			self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_FRAMEBK ) )

			self._services = services

			self._media = media

			self._service_keys_to_controls = {}
			self._service_keys_to_original_ratings_states = {}

			rows = []

			for service in self._services:
				name = service.GetName()

				service_key = service.GetServiceKey()

				rating_state = GetLikeStateFromMedia( self._media, service_key )

				control = RatingLikeDialog( self, service_key )

				control.SetRatingState( rating_state )

				self._service_keys_to_controls[ service_key ] = control
				self._service_keys_to_original_ratings_states[ service_key ] = rating_state

				rows.append( ( name + ': ', control ) )

			gridbox = WrapInGrid( self, rows, expand_text = True )

			self.SetSizer( gridbox )

		def GetContentUpdates( self ):
			service_keys_to_content_updates = {}

			hashes = { hash for hash in itertools.chain.from_iterable( ( media.GetHashes() for media in self._media ) ) }

			for ( service_key, control ) in list( self._service_keys_to_controls.items() ):
				original_rating_state = self._service_keys_to_original_ratings_states[ service_key ]

				rating_state = control.GetRatingState()

				if rating_state != original_rating_state:
					if rating_state == LIKE:
						rating = 1
					elif rating_state == DISLIKE:
						rating = 0
					else:
						rating = None

					content_update = ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) )

					service_keys_to_content_updates[ service_key ] = ( content_update, )

			return service_keys_to_content_updates

	class _NumericalPanel( wx.Panel ):
		def __init__( self, parent, services, media ):
			wx.Panel.__init__( self, parent )

			self._services = services

			self._media = media

			self._service_keys_to_controls = {}
			self._service_keys_to_original_ratings_states = {}

			rows = []

			for service in self._services:
				name = service.GetName()

				service_key = service.GetServiceKey()

				( rating_state, rating ) = GetNumericalStateFromMedia( self._media, service_key )

				control = RatingNumericalDialog( self, service_key )

				if rating_state != SET:
					control.SetRatingState( rating_state )

				else:
					control.SetRating( rating )

				self._service_keys_to_controls[ service_key ] = control
				self._service_keys_to_original_ratings_states[ service_key ] = ( rating_state, rating )

				rows.append( ( name + ': ', control ) )

			gridbox = WrapInGrid( self, rows, expand_text = True )

			self.SetSizer( gridbox )

		def GetContentUpdates( self ):
			service_keys_to_content_updates = {}

			hashes = { hash for hash in itertools.chain.from_iterable( ( media.GetHashes() for media in self._media ) ) }

			for ( service_key, control ) in list( self._service_keys_to_controls.items() ):
				( original_rating_state, original_rating ) = self._service_keys_to_original_ratings_states[ service_key ]

				rating_state = control.GetRatingState()

				if rating_state == NULL:
					rating = None

				else:
					rating = control.GetRating()

				if rating != original_rating:
					content_update = ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) )

					service_keys_to_content_updates[ service_key ] = ( content_update, )

			return service_keys_to_content_updates

class DialogManageUPnP( Dialog ):
	def __init__( self, parent ):
		title = 'manage local upnp'

		Dialog.__init__( self, parent, title )

		self._status_st = BetterStaticText( self )

		listctrl_panel = BetterListCtrlPanel( self )

		columns = [ ( 'description', -1 ), ( 'internal ip', 17 ), ( 'internal port', 7 ), ( 'external ip', 17 ), ( 'external port', 7 ), ( 'prototcol', 5 ), ( 'lease', 12 ) ]

		self._mappings_list_ctrl = BetterListCtrl( listctrl_panel, 'manage_upnp_mappings', 12, 36, columns, self._ConvertDataToListCtrlTuples, delete_key_callback = self._Remove, activation_callback = self._Edit )

		listctrl_panel.SetListCtrl( self._mappings_list_ctrl )

		listctrl_panel.AddButton( 'add custom mapping', self._Add )
		listctrl_panel.AddButton( 'edit mapping', self._Edit, enabled_only_on_selection = True )
		listctrl_panel.AddButton( 'remove mapping', self._Remove, enabled_only_on_selection = True )

		self._ok = wx.Button( self, id = wx.ID_OK, label = 'ok' )
		self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
		self._ok.SetForegroundColour( ( 0, 128, 0 ) )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._status_st, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( listctrl_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( self._ok, CC.FLAGS_LONE_BUTTON )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		x = max( x, 760 )

		self.SetInitialSize( ( x, y ) )

		#

		self._mappings = []

		self._mappings_list_ctrl.Sort( 0 )

		self._RefreshMappings()

	def _Add( self ):
		do_refresh = False

		external_port = HC.DEFAULT_SERVICE_PORT
		protocol = 'TCP'
		internal_port = HC.DEFAULT_SERVICE_PORT
		description = 'hydrus service'
		duration = 0

		with DialogInputUPnPMapping( self, external_port, protocol, internal_port, description, duration ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				( external_port, protocol, internal_port, description, duration ) = dlg.GetInfo()

				for ( existing_description, existing_internal_ip, existing_internal_port, existing_external_ip, existing_external_port, existing_protocol, existing_lease ) in self._mappings:
					if external_port == existing_external_port and protocol == existing_protocol:
						wx.MessageBox( 'That external port already exists!' )

						return

				internal_client = GetLocalIP()

				AddUPnPMapping( internal_client, internal_port, external_port, protocol, description, duration = duration )

				do_refresh = True

		if do_refresh:
			self._RefreshMappings()

	def _ConvertDataToListCtrlTuples( self, mapping ):
		( description, internal_ip, internal_port, external_ip, external_port, protocol, duration ) = mapping

		if duration == 0:
			pretty_duration = 'indefinite'

		else:
			pretty_duration = TimeDeltaToPrettyTimeDelta( duration )

		display_tuple = ( description, internal_ip, str( internal_port ), external_ip, str( external_port ), protocol, pretty_duration )
		sort_tuple = mapping

		return ( display_tuple, sort_tuple )

	def _Edit( self ):
		do_refresh = False

		selected_mappings = self._mappings_list_ctrl.GetData( only_selected = True )

		for selected_mapping in selected_mappings:
			( description, internal_ip, internal_port, external_ip, external_port, protocol, duration ) = selected_mapping

			with DialogInputUPnPMapping( self, external_port, protocol, internal_port, description, duration ) as dlg:
				if dlg.ShowModal() == wx.ID_OK:
					( external_port, protocol, internal_port, description, duration ) = dlg.GetInfo()

					RemoveUPnPMapping( external_port, protocol )

					internal_client = GetLocalIP()

					AddUPnPMapping( internal_client, internal_port, external_port, protocol, description, duration = duration )

					do_refresh = True

				else:
					break

		if do_refresh:
			self._RefreshMappings()

	def _RefreshMappings( self ):
		def wx_code( mappings ):
			if not self:
				return

			self._mappings = mappings

			self._mappings_list_ctrl.SetData( self._mappings )

			self._status_st.SetLabelText( '' )

		def THREADdo_it():
			try:
				mappings = GetUPnPMappings()

			except Exception as e:
				ShowException( e )

				wx.CallAfter( wx.MessageBox, 'Could not load mappings:' + os.linesep * 2 + str( e ) )

				return

			wx.CallAfter( wx_code, mappings )

		self._status_st.SetLabelText( 'Refreshing mappings--please wait...' )

		self._mappings_list_ctrl.SetData( [] )

		HG.client_controller.CallToThread( THREADdo_it )

	def _Remove( self ):
		do_refresh = False

		selected_mappings = self._mappings_list_ctrl.GetData( only_selected = True )

		for selected_mapping in selected_mappings:
			( description, internal_ip, internal_port, external_ip, external_port, protocol, duration ) = selected_mapping

			RemoveUPnPMapping( external_port, protocol )

			do_refresh = True

		if do_refresh:
			self._RefreshMappings()

	def EventOK( self, event ):
		self.EndModal( wx.ID_OK )


code:
import wx

from lib.GUI.ScrolledPanels.Edit import EditDeleteFilesPanel, \
	EditSelectFromListButtonsPanel, EditSelectFromListPanel
from lib.GUI.TopLevelWindows import DialogEdit

from lib.Hydrus.Exceptions import CancelledException

### FUNCTIONS ###

def GetDeleteFilesJobs( win, media, default_reason, suggested_file_service_key = None ):
	title = 'Delete files?'

	with DialogEdit( win, title, frame_key = 'regular_center_dialog' ) as dlg:
		panel = EditDeleteFilesPanel( dlg, media, default_reason, suggested_file_service_key = suggested_file_service_key )

		dlg.SetPanel( panel )

		if panel.QuestionIsAlreadyResolved():
			( involves_physical_delete, jobs ) = panel.GetValue()

			return ( involves_physical_delete, jobs )

		if dlg.ShowModal() == wx.ID_OK:
			( involves_physical_delete, jobs ) = panel.GetValue()

			return ( involves_physical_delete, jobs )

		else:
			raise CancelledException()

def SelectFromList( win, title, choice_tuples, value_to_select = None, sort_tuples = True ):
	with DialogEdit( win, title ) as dlg:
		panel = EditSelectFromListPanel( dlg, choice_tuples, value_to_select = value_to_select, sort_tuples = sort_tuples )

		dlg.SetPanel( panel )

		if dlg.ShowModal() == wx.ID_OK:
			result = panel.GetValue()

			return result

		else:
			raise CancelledException()

def SelectFromListButtons( win, title, choice_tuples ):
	with DialogEdit( win, title, hide_buttons = True ) as dlg:
		panel = EditSelectFromListButtonsPanel( dlg, choice_tuples )

		dlg.SetPanel( panel )

		if dlg.ShowModal() == wx.ID_OK:
			result = panel.GetValue()

			return result

		else:
			raise CancelledException()

license: wtfpl


code:
from .Dialogs import *

__all__ = [
	"Manage",
	"Quick"
]

from . import *


code:
import json, os, wx

from lib.GUI.Common import ClientToScreen, GetXYTopTLP, GetTLP

from lib.Hydrus.Paths import MirrorFile

### CONSTANTS ###

import lib.Hydrus.Globals as HG

### FUNCTIONS ###

def DoFileExportDragDrop( window, page_key, media, alt_down ):
	drop_source = wx.DropSource( window )

	data_object = wx.DataObjectComposite()

	#

	hydrus_media_data_object = wx.CustomDataObject( 'application/hydrus-media' )

	hashes = [ m.GetHash() for m in media ]

	if page_key is None:
		encoded_page_key = None

	else:
		encoded_page_key = page_key.hex()

	data_obj = ( encoded_page_key, [ hash.hex() for hash in hashes ] )

	data_str = json.dumps( data_obj )

	data_bytes = bytes( data_str, 'utf-8' )

	hydrus_media_data_object.SetData( data_bytes )

	data_object.Add( hydrus_media_data_object, True )

	#

	file_data_object = wx.FileDataObject()

	client_files_manager = HG.client_controller.client_files_manager

	original_paths = []

	total_size = 0

	for m in media:
		hash = m.GetHash()
		mime = m.GetMime()

		total_size += m.GetSize()

		original_path = client_files_manager.GetFilePath( hash, mime, check_file_exists = False )

		original_paths.append( original_path )

	#

	new_options = HG.client_controller.new_options

	secret_discord_dnd_fix_possible = new_options.GetBoolean( 'secret_discord_dnd_fix' ) and alt_down

	discord_dnd_fix_possible = new_options.GetBoolean( 'discord_dnd_fix' ) and len( original_paths ) <= 50 and total_size < 200 * 1048576

	temp_dir = HG.client_controller.temp_dir

	if secret_discord_dnd_fix_possible:
		dnd_paths = original_paths

		flags = wx.Drag_AllowMove

	elif discord_dnd_fix_possible and os.path.exists( temp_dir ):
		dnd_paths = []

		for original_path in original_paths:
			filename = os.path.basename( original_path )

			dnd_path = os.path.join( temp_dir, filename )

			if not os.path.exists( dnd_path ):
				MirrorFile( original_path, dnd_path )

			dnd_paths.append( dnd_path )

		flags = wx.Drag_AllowMove

	else:
		dnd_paths = original_paths
		flags = wx.Drag_CopyOnly

	for path in dnd_paths:
		file_data_object.AddFile( path )

	data_object.Add( file_data_object )

	#

	drop_source.SetData( data_object )

	result = drop_source.DoDragDrop( flags )

	return result

class FileDropTarget( wx.DropTarget ):
	def __init__( self, parent, filenames_callable = None, url_callable = None, media_callable = None, page_callable = None ):
		wx.DropTarget.__init__( self )

		self._parent = parent

		self._filenames_callable = filenames_callable
		self._url_callable = url_callable
		self._media_callable = media_callable
		self._page_callable = page_callable

		self._receiving_data_object = wx.DataObjectComposite()

		self._hydrus_media_data_object = wx.CustomDataObject( 'application/hydrus-media' )
		self._hydrus_page_tab_data_object = wx.CustomDataObject( 'application/hydrus-page-tab' )
		self._file_data_object = wx.FileDataObject()
		self._text_data_object = wx.TextDataObject()

		self._receiving_data_object.Add( self._hydrus_media_data_object, True )
		self._receiving_data_object.Add( self._hydrus_page_tab_data_object )
		self._receiving_data_object.Add( self._file_data_object )
		self._receiving_data_object.Add( self._text_data_object )

		self.SetDataObject( self._receiving_data_object )

	def OnData( self, x, y, result ):
		if self.GetData():
			received_format = self._receiving_data_object.GetReceivedFormat()

			received_format_type = received_format.GetType()

			if received_format_type == wx.DF_FILENAME and self._filenames_callable is not None:
				paths = self._file_data_object.GetFilenames()

				wx.CallAfter( self._filenames_callable, paths ) # callafter to terminate dnd event now

				result = wx.DragNone

			elif received_format_type in ( wx.DF_TEXT, wx.DF_UNICODETEXT ) and self._url_callable is not None:
				text = self._text_data_object.GetText()

				wx.CallAfter( self._url_callable, text ) # callafter to terminate dnd event now

				result = wx.DragCopy

			else:
				try:
					format_id = received_format.GetId()

				except Exception:
					format_id = None

				if format_id == 'application/hydrus-media' and self._media_callable is not None:
					mview = self._hydrus_media_data_object.GetData()

					data_bytes = mview.tobytes()

					data_str = str( data_bytes, 'utf-8' )

					( encoded_page_key, encoded_hashes ) = json.loads( data_str )

					if encoded_page_key is not None:
						page_key = bytes.fromhex( encoded_page_key )
						hashes = [ bytes.fromhex( encoded_hash ) for encoded_hash in encoded_hashes ]

						wx.CallAfter( self._media_callable, page_key, hashes ) # callafter so we can terminate dnd event now

					result = wx.DragMove

				if format_id == 'application/hydrus-page-tab' and self._page_callable is not None:
					mview = self._hydrus_page_tab_data_object.GetData()

					page_key = mview.tobytes()

					wx.CallAfter( self._page_callable, page_key ) # callafter so we can terminate dnd event now

					result = wx.DragMove

		return result

	def OnDrop( self, x, y ):
		screen_position = ClientToScreen( self._parent, ( x, y ) )

		drop_tlp = GetXYTopTLP( screen_position )
		my_tlp = GetTLP( self._parent )

		if drop_tlp == my_tlp:
			return True

		else:
			return False

	# setting OnDragOver to return copy gives Linux trouble with page tab drops with shift held down


code:
import os, time, traceback, wx

from lib.Actions.Export import ExportFolder, GenerateExportFilename, GetExportPath, \
	ParseExportPhrase

from lib.GUI.ACDropdown import AutoCompleteDropdownTagsRead
from lib.GUI.Common import BetterChoice, BetterStaticText, ExportPatternButton, \
	StaticBox, StaticBoxSorterForListBoxTags, WrapInGrid
from lib.GUI.Dialogs import DialogYesNo
from lib.GUI.ListBoxes import ListBoxTagsSelection, ListBoxTagsActiveSearchPredicates
from lib.GUI.ListCtrl import BetterListCtrl, BetterListCtrlPanel
from lib.GUI.ScrolledPanels import ReviewPanel, EditPanel
from lib.GUI.ScrolledPanels.Edit import EditChooseMultiple
from lib.GUI.Time import TimeDeltaButton
from lib.GUI.TopLevelWindows import DialogEdit

from lib.Hydrus.Data import BigJobPauser, ContentUpdate, ConvertValueRangeToPrettyString, \
	SplitListIntoChunks, TimeDeltaToPrettyTimeDelta, ToHumanInt
from lib.Hydrus.Exceptions import VetoException
from lib.Hydrus.Paths import LaunchDirectory, MakeFileWritable, MakeSureDirectoryExists, \
	MirrorFile
from lib.Hydrus.Search import FileSearchContext

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

### CLASSES ###

class EditExportFoldersPanel( EditPanel ):
	def __init__( self, parent, export_folders ):
		EditPanel.__init__( self, parent )

		self._export_folders_panel = BetterListCtrlPanel( self )

		columns = export_folders.ToTuple()

		# ( name, path, export_type, delete_from_client_after_export, file_search_context, run_regularly, period, phrase, last_checked, paused, run_now ) = export_folders.ToTuple()
		# PEEVES: Not set?

		self._export_folders = BetterListCtrl( self._export_folders_panel, 'export_folders', 6, 40, columns, self._ConvertExportFolderToListCtrlTuples, use_simple_delete = True, activation_callback = self._Edit )

		self._export_folders_panel.SetListCtrl( self._export_folders )

		self._export_folders_panel.AddButton( 'add', self._AddFolder )
		self._export_folders_panel.AddButton( 'edit', self._Edit, enabled_only_on_selection = True )
		self._export_folders_panel.AddDeleteButton()

		#

		self._export_folders.AddDatas( export_folders )

		vbox = wx.BoxSizer( wx.VERTICAL )

		intro = 'Here you can set the client to regularly export a certain query to a particular location.'

		vbox.Add( BetterStaticText( self, intro ), CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._export_folders_panel, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

	def _AddFolder( self ):
		new_options = HG.client_controller.new_options

		phrase = new_options.GetString( 'export_phrase' )

		name = 'export folder'
		path = ''
		export_type = HC.EXPORT_FOLDER_TYPE_REGULAR
		delete_from_client_after_export = False
		file_search_context = FileSearchContext( file_service_key = CC.LOCAL_FILE_SERVICE_KEY )
		period = 15 * 60

		export_folder = ExportFolder( name, path, export_type = export_type, delete_from_client_after_export = delete_from_client_after_export, file_search_context = file_search_context, period = period, phrase = phrase )

		with DialogEdit( self, 'edit export folder' ) as dlg:
			panel = EditExportFolderPanel( dlg, export_folder )

			dlg.SetPanel( panel )

			if dlg.ShowModal() == wx.ID_OK:
				export_folder = panel.GetValue()

				export_folder.SetNonDupeName( self._GetExistingNames() )

				self._export_folders.AddDatas( ( export_folder, ) )

	def _ConvertExportFolderToListCtrlTuples( self, export_folder ):
		( name, path, export_type, delete_from_client_after_export, file_search_context, run_regularly, period, phrase, last_checked, paused, run_now ) = export_folder.ToTuple()

		if export_type == HC.EXPORT_FOLDER_TYPE_REGULAR:
			pretty_export_type = 'regular'

		elif export_type == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
			pretty_export_type = 'synchronise'

		if delete_from_client_after_export:
			pretty_export_type += ' and deleting from the client!'

		pretty_file_search_context = ', '.join( predicate.ToString( with_count = False ) for predicate in file_search_context.GetPredicates() )

		if run_regularly:
			pretty_period = TimeDeltaToPrettyTimeDelta( period )

		else:
			pretty_period = 'not running regularly'

		if run_now:
			pretty_period += ' (running after dialog ok)'

		if paused:
			pretty_paused = 'yes'

		else:
			pretty_paused = ''

		pretty_phrase = phrase

		display_tuple = ( name, path, pretty_export_type, pretty_file_search_context, pretty_paused, pretty_period, pretty_phrase )

		sort_tuple = ( name, path, pretty_export_type, pretty_file_search_context, paused, period, phrase )

		return ( display_tuple, sort_tuple )

	def _Edit( self ):
		export_folders = self._export_folders.GetData( only_selected = True )

		for export_folder in export_folders:
			with DialogEdit( self, 'edit export folder' ) as dlg:
				panel = EditExportFolderPanel( dlg, export_folder )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					edited_export_folder = panel.GetValue()

					self._export_folders.DeleteDatas( ( export_folder, ) )

					edited_export_folder.SetNonDupeName( self._GetExistingNames() )

					self._export_folders.AddDatas( ( edited_export_folder, ) )

				else:
					return

	def _GetExistingNames( self ):
		existing_names = { export_folder.GetName() for export_folder in self._export_folders.GetData() }

		return existing_names

	def GetValue( self ):
		export_folders = self._export_folders.GetData()

		return export_folders

class EditExportFolderPanel( EditPanel ):
	def __init__( self, parent, export_folder ):
		EditPanel.__init__( self, parent )

		self._export_folder = export_folder

		( name, path, export_type, delete_from_client_after_export, file_search_context, run_regularly, period, phrase, self._last_checked, paused, run_now ) = self._export_folder.ToTuple()

		self._path_box = StaticBox( self, 'name and location' )

		self._name = wx.TextCtrl( self._path_box)

		self._path = wx.DirPickerCtrl( self._path_box, style = wx.DIRP_USE_TEXTCTRL )

		#

		self._type_box = StaticBox( self, 'type of export' )

		self._type = BetterChoice( self._type_box )
		self._type.Append( 'regular', HC.EXPORT_FOLDER_TYPE_REGULAR )
		self._type.Append( 'synchronise', HC.EXPORT_FOLDER_TYPE_SYNCHRONISE )

		self._delete_from_client_after_export = wx.CheckBox( self._type_box )

		#

		self._query_box = StaticBox( self, 'query to export' )

		self._page_key = 'export folders placeholder'

		predicates = file_search_context.GetPredicates()

		self._predicates_box = ListBoxTagsActiveSearchPredicates( self._query_box, self._page_key, predicates )

		self._searchbox = AutoCompleteDropdownTagsRead( self._query_box, self._page_key, file_search_context, allow_all_known_files = False )

		#

		self._period_box = StaticBox( self, 'export period' )

		self._period = TimeDeltaButton( self._period_box, min = 3 * 60, days = True, hours = True, minutes = True )

		self._run_regularly = wx.CheckBox( self._period_box )

		self._paused = wx.CheckBox( self._period_box )

		self._run_now = wx.CheckBox( self._period_box )

		#

		self._phrase_box = StaticBox( self, 'filenames' )

		self._pattern = wx.TextCtrl( self._phrase_box )

		self._examples = ExportPatternButton( self._phrase_box )

		#

		self._name.SetValue( name )

		self._path.SetPath( path )

		self._type.SelectClientData( export_type )

		self._delete_from_client_after_export.SetValue( delete_from_client_after_export )

		self._period.SetValue( period )

		self._run_regularly.SetValue( run_regularly )

		self._paused.SetValue( paused )

		self._run_now.SetValue( run_now )

		self._pattern.SetValue( phrase )

		#

		rows = []

		rows.append( ( 'name: ', self._name ) )
		rows.append( ( 'folder path: ', self._path ) )

		gridbox = WrapInGrid( self._path_box, rows )

		self._path_box.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		#

		text = '''regular - try to export the files to the directory, overwriting if the filesize if different

synchronise - try to export the files to the directory, overwriting if the filesize if different, and delete anything else in the directory

If you select synchronise, be careful!'''

		st = BetterStaticText( self._type_box, label = text )

		st.SetWrapWidth( 440 )

		self._type_box.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
		self._type_box.Add( self._type, CC.FLAGS_EXPAND_PERPENDICULAR )

		rows = []

		rows.append( ( 'delete files from client after export: ', self._delete_from_client_after_export ) )

		gridbox = WrapInGrid( self._type_box, rows )

		self._type_box.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		self._query_box.Add( self._predicates_box, CC.FLAGS_EXPAND_BOTH_WAYS )
		self._query_box.Add( self._searchbox, CC.FLAGS_EXPAND_PERPENDICULAR )

		self._period_box.Add( self._period, CC.FLAGS_EXPAND_PERPENDICULAR )

		rows = []

		rows.append( ( 'run regularly?: ', self._run_regularly ) )
		rows.append( ( 'paused: ', self._paused ) )
		rows.append( ( 'run on dialog ok: ', self._run_now ) )

		gridbox = WrapInGrid( self._period_box, rows )

		self._period_box.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		phrase_hbox = wx.BoxSizer( wx.HORIZONTAL )

		phrase_hbox.Add( self._pattern, CC.FLAGS_EXPAND_BOTH_WAYS )
		phrase_hbox.Add( self._examples, CC.FLAGS_VCENTER )

		self._phrase_box.Add( phrase_hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._path_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._type_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._query_box, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( self._period_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._phrase_box, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		self._UpdateTypeDeleteUI()

		self._type.Bind( wx.EVT_CHOICE, self.EventTypeChoice )
		self._delete_from_client_after_export.Bind( wx.EVT_CHECKBOX, self.EventDeleteFilesAfterExport )

	def _UpdateTypeDeleteUI( self ):
		if self._type.GetChoice() == HC.EXPORT_FOLDER_TYPE_SYNCHRONISE:
			self._delete_from_client_after_export.Disable()

			if self._delete_from_client_after_export.GetValue():
				self._delete_from_client_after_export.SetValue( False )

		else:
			self._delete_from_client_after_export.Enable()

	def CanOK( self ):
		if self._delete_from_client_after_export.GetValue():
			message = 'You have set this export folder to delete the files from the client after export! Are you absolutely sure this is what you want?'

			with DialogYesNo( self, message ) as dlg:
				if dlg.ShowModal() != wx.ID_YES:
					return False

		return True

	def EventDeleteFilesAfterExport( self, event ):
		if self._delete_from_client_after_export.GetValue():
			wx.MessageBox( 'This will delete the exported files from your client after the export! If you do not know what this means, uncheck it!' )

	def EventTypeChoice( self, event ):
		self._UpdateTypeDeleteUI()

	def GetValue( self ):
		name = self._name.GetValue()

		path = self._path.GetPath()

		export_type = self._type.GetChoice()

		delete_from_client_after_export = self._delete_from_client_after_export.GetValue()

		file_search_context = self._searchbox.GetFileSearchContext()

		predicates = self._predicates_box.GetPredicates()

		file_search_context.SetPredicates( predicates )

		run_regularly = self._run_regularly.GetValue()

		period = self._period.GetValue()

		phrase = self._pattern.GetValue()

		if self._path.GetPath() in ( '', None ):
			raise VetoException( 'You must enter a folder path to export to!' )

		phrase = self._pattern.GetValue()

		try:
			ParseExportPhrase( phrase )

		except Exception as e:
			raise VetoException( 'Could not parse that export phrase! ' + str( e ) )

		run_now = self._run_now.GetValue()

		paused = self._paused.GetValue()

		export_folder = ExportFolder( name, path = path, export_type = export_type, delete_from_client_after_export = delete_from_client_after_export, file_search_context = file_search_context, run_regularly = run_regularly, period = period, phrase = phrase, last_checked = self._last_checked, paused = paused, run_now = run_now )

		return export_folder

class ReviewExportFilesPanel( ReviewPanel ):
	def __init__( self, parent, flat_media, do_export_and_then_quit = False ):
		ReviewPanel.__init__( self, parent )

		new_options = HG.client_controller.new_options

		self._media_to_paths = {}
		self._existing_filenames = set()
		self._last_phrase_used = ''
		self._last_dir_used = ''

		self._tags_box = StaticBoxSorterForListBoxTags( self, 'files\' tags' )

		services_manager = HG.client_controller.services_manager

		self._neighbouring_txt_tag_service_keys = services_manager.FilterValidServiceKeys( new_options.GetKeyList( 'default_neighbouring_txt_tag_service_keys' ) )

		t = ListBoxTagsSelection( self._tags_box, include_counts = True, collapse_siblings = True )

		self._tags_box.SetTagsBox( t )

		self._tags_box.SetMinSize( ( 220, 300 ) )

		columns = [ ( 'number', 8 ), ( 'mime', 20 ), ( 'expected path', -1 ) ]

		self._paths = BetterListCtrl( self, 'export_files', 24, 64, columns, self._ConvertDataToListCtrlTuples, use_simple_delete = True )

		self._paths.Sort( 0 )

		self._export_path_box = StaticBox( self, 'export path' )

		self._directory_picker = wx.DirPickerCtrl( self._export_path_box )
		self._directory_picker.Bind( wx.EVT_DIRPICKER_CHANGED, self.EventRecalcPaths )

		self._open_location = wx.Button( self._export_path_box, label = 'open this location' )
		self._open_location.Bind( wx.EVT_BUTTON, self.EventOpenLocation )

		self._filenames_box = StaticBox( self, 'filenames' )

		self._pattern = wx.TextCtrl( self._filenames_box )

		self._update = wx.Button( self._filenames_box, label = 'update' )
		self._update.Bind( wx.EVT_BUTTON, self.EventRecalcPaths )

		self._examples = ExportPatternButton( self._filenames_box )

		self._delete_files_after_export = wx.CheckBox( self, label = 'delete files from client after export?' )
		self._delete_files_after_export.SetForegroundColour( wx.Colour( 127, 0, 0 ) )

		self._export_symlinks = wx.CheckBox( self, label = 'EXPERIMENTAL: export symlinks' )
		self._export_symlinks.SetForegroundColour( wx.Colour( 127, 0, 0 ) )

		text = 'This will export all the files\' tags, newline separated, into .txts beside the files themselves.'

		self._export_tag_txts = wx.CheckBox( self, label = 'export tags to .txt files?' )
		self._export_tag_txts.SetToolTip( text )
		self._export_tag_txts.Bind( wx.EVT_CHECKBOX, self.EventExportTagTxtsChanged )

		self._export = wx.Button( self, label = 'export' )
		self._export.Bind( wx.EVT_BUTTON, self.EventExport )

		#

		export_path = GetExportPath()

		self._directory_picker.SetPath( export_path )

		phrase = new_options.GetString( 'export_phrase' )

		self._pattern.SetValue( phrase )

		if len( self._neighbouring_txt_tag_service_keys ) > 0:
			self._export_tag_txts.SetValue( True )

		self._paths.SetData( list( enumerate( flat_media ) ) )

		self._delete_files_after_export.SetValue( HG.client_controller.new_options.GetBoolean( 'delete_files_after_export' ) )
		self._delete_files_after_export.Bind( wx.EVT_CHECKBOX, self.EventDeleteFilesChanged )

		if not HG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
			self._export_symlinks.Hide()

		#

		top_hbox = wx.BoxSizer( wx.HORIZONTAL )

		top_hbox.Add( self._tags_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		top_hbox.Add( self._paths, CC.FLAGS_EXPAND_BOTH_WAYS )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._directory_picker, CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( self._open_location, CC.FLAGS_VCENTER )

		self._export_path_box.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._pattern, CC.FLAGS_EXPAND_BOTH_WAYS )
		hbox.Add( self._update, CC.FLAGS_VCENTER )
		hbox.Add( self._examples, CC.FLAGS_VCENTER )

		self._filenames_box.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( top_hbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
		vbox.Add( self._export_path_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._filenames_box, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._delete_files_after_export, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._export_symlinks, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._export_tag_txts, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._export, CC.FLAGS_LONE_BUTTON )

		self.SetSizer( vbox )

		self._RefreshTags()

		wx.CallAfter( self._export.SetFocus )

		self._paths.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelectPath )
		self._paths.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelectPath )

		if do_export_and_then_quit:
			wx.CallAfter( self._DoExport, True )

	def _ConvertDataToListCtrlTuples( self, data ):
		directory = self._directory_picker.GetPath()

		( ordering_index, media ) = data

		number = ordering_index
		mime = media.GetMime()

		try:
			path = self._GetPath( media )

		except Exception as e:
			path = str( e )

		pretty_number = ToHumanInt( ordering_index + 1 )
		pretty_mime = HC.mime_string_lookup[ mime ]

		pretty_path = path

		if not path.startswith( directory ):
			pretty_path = 'INVALID, above destination directory: ' + path

		display_tuple = ( pretty_number, pretty_mime, pretty_path )
		sort_tuple = ( number, pretty_mime, path )

		return ( display_tuple, sort_tuple )

	def _DoExport( self, quit_afterwards = False ):
		delete_afterwards = self._delete_files_after_export.GetValue()
		export_symlinks = self._export_symlinks.GetValue() and not delete_afterwards

		if quit_afterwards:
			message = 'Export as shown?'

			if delete_afterwards:
				message += os.linesep * 2
				message += 'THE FILES WILL BE DELETED FROM THE CLIENT AFTERWARDS'

			with DialogYesNo( self, message ) as dlg:
				if dlg.ShowModal() != wx.ID_YES:
					self.GetParent().Close()

					return

		elif delete_afterwards:
			message = 'THE FILES WILL BE DELETED FROM THE CLIENT AFTERWARDS'

			with DialogYesNo( self, message ) as dlg:
				if dlg.ShowModal() != wx.ID_YES:
					return

		self._RefreshPaths()

		export_tag_txts = self._export_tag_txts.GetValue()

		directory = self._directory_picker.GetPath()

		MakeSureDirectoryExists( directory )

		pattern = self._pattern.GetValue()

		new_options = HG.client_controller.new_options

		new_options.SetKeyList( 'default_neighbouring_txt_tag_service_keys', self._neighbouring_txt_tag_service_keys )

		new_options.SetString( 'export_phrase', pattern )

		try:
			ParseExportPhrase( pattern )

		except Exception as e:
			wx.MessageBox( str( e ) )

			return

		client_files_manager = HG.client_controller.client_files_manager

		self._export.Disable()

		to_do = self._paths.GetData()

		num_to_do = len( to_do )

		def wx_update_label( text ):
			if not self or not self._export:
				return

			self._export.SetLabel( text )

		def wx_done( quit_afterwards ):
			if not self or not self._export:
				return

			self._export.Enable()

			if quit_afterwards:
				wx.CallAfter( self.GetParent().Close )

		def do_it( directory, neighbouring_txt_tag_service_keys, delete_afterwards, export_symlinks, quit_afterwards ):
			pauser = BigJobPauser()

			for ( index, ( ordering_index, media ) ) in enumerate( to_do ):
				try:
					wx.CallAfter( wx_update_label, ConvertValueRangeToPrettyString( index + 1, num_to_do ) )

					hash = media.GetHash()
					mime = media.GetMime()

					path = self._GetPath( media )

					path = os.path.normpath( path )

					if not path.startswith( directory ):
						raise Exception( 'It seems a destination path was above the main export directory! The file was "{}" and its destination path was "{}".'.format( hash.hex(), path ) )

					path_dir = os.path.dirname( path )

					MakeSureDirectoryExists( path_dir )

					if export_tag_txts:
						tags_manager = media.GetTagsManager()

						tags = set()

						siblings_manager = HG.controller.tag_siblings_manager

						tag_censorship_manager = HG.client_controller.tag_censorship_manager

						for service_key in neighbouring_txt_tag_service_keys:
							current_tags = tags_manager.GetCurrent( service_key )

							current_tags = siblings_manager.CollapseTags( service_key, current_tags )

							current_tags = tag_censorship_manager.FilterTags( service_key, current_tags )

							tags.update( current_tags )

						tags = list( tags )

						tags.sort()

						txt_path = path + '.txt'

						with open( txt_path, 'w', encoding = 'utf-8' ) as f:
							f.write( os.linesep.join( tags ) )

					source_path = client_files_manager.GetFilePath( hash, mime, check_file_exists = False )

					if export_symlinks:
						os.symlink( source_path, path )

					else:
						MirrorFile( source_path, path )

						MakeFileWritable( path )

				except Exception:
					wx.CallAfter( wx.MessageBox, 'Encountered a problem while attempting to export file with index ' + str( ordering_index + 1 ) + ':' + os.linesep * 2 + traceback.format_exc() )

					break

				pauser.Pause()

			if delete_afterwards:
				wx.CallAfter( wx_update_label, 'deleting' )

				deletee_hashes = { media.GetHash() for ( ordering_index, media ) in to_do }

				chunks_of_hashes = SplitListIntoChunks( deletee_hashes, 64 )

				reason = 'Deleted after manual export to "{}".'.format( directory )

				content_updates = [ ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, chunk_of_hashes, reason = reason ) for chunk_of_hashes in chunks_of_hashes ]

				for content_update in content_updates:
					HG.client_controller.WriteSynchronous( 'content_updates', { CC.LOCAL_FILE_SERVICE_KEY : [ content_update ] } )

			wx.CallAfter( wx_update_label, 'done!' )

			time.sleep( 1 )

			wx.CallAfter( wx_update_label, 'export' )

			wx.CallAfter( wx_done, quit_afterwards )

		HG.client_controller.CallToThread( do_it, directory, self._neighbouring_txt_tag_service_keys, delete_afterwards, export_symlinks, quit_afterwards )

	def _GetPath( self, media ):
		if media in self._media_to_paths:
			return self._media_to_paths[ media ]

		directory = self._directory_picker.GetPath()

		pattern = self._pattern.GetValue()

		terms = ParseExportPhrase( pattern )

		filename = GenerateExportFilename( directory, media, terms )

		i = 1

		while filename in self._existing_filenames:
			filename = GenerateExportFilename( directory, media, terms + [ ( 'string', ' (' + str( i ) + ')' ) ] )

			i += 1

		path = os.path.join( directory, filename )

		path = os.path.normpath( path )

		self._existing_filenames.add( filename )
		self._media_to_paths[ media ] = path

		return path

	def _RefreshPaths( self ):
		pattern = self._pattern.GetValue()
		dir_path = self._directory_picker.GetPath()

		if pattern == self._last_phrase_used and dir_path == self._last_dir_used:
			return

		self._last_phrase_used = pattern
		self._last_dir_used = dir_path

		HG.client_controller.new_options.SetString( 'export_phrase', pattern )

		self._existing_filenames = set()
		self._media_to_paths = {}

		self._paths.UpdateDatas()

	def _RefreshTags( self ):
		data = self._paths.GetData( only_selected = True )

		if len( data ) == 0:
			data = self._paths.GetData()

		all_media = [ media for ( ordering_index, media ) in data ]

		self._tags_box.SetTagsByMedia( all_media )

	def EventExport( self, event ):
		self._DoExport()

	def EventDeleteFilesChanged( self, event ):
		value = self._delete_files_after_export.GetValue()

		HG.client_controller.new_options.SetBoolean( 'delete_files_after_export', value )

		if value:
			self._export_symlinks.SetValue( False )

	def EventExportTagTxtsChanged( self, event ):
		if self._export_tag_txts.GetValue() is True:
			services_manager = HG.client_controller.services_manager

			tag_services = services_manager.GetServices( HC.TAG_SERVICES )

			choice_tuples = [ ( service.GetName(), service.GetServiceKey(), service.GetServiceKey() in self._neighbouring_txt_tag_service_keys ) for service in tag_services ]

			with DialogEdit( self, 'select tag services' ) as dlg:
				panel = EditChooseMultiple( dlg, choice_tuples )

				dlg.SetPanel( panel )

				if dlg.ShowModal() == wx.ID_OK:
					self._neighbouring_txt_tag_service_keys = panel.GetValue()

					if len( self._neighbouring_txt_tag_service_keys ) == 0:
						self._export_tag_txts.SetValue( False )

					else:
						self._export_tag_txts.SetValue( True )

				else:
					self._export_tag_txts.SetValue( False )

		else:
			self._neighbouring_txt_tag_service_keys = []

	def EventOpenLocation( self, event ):
		directory = self._directory_picker.GetPath()

		if directory is not None and directory != '':
			LaunchDirectory( directory )

	def EventRecalcPaths( self, event ):
		self._RefreshPaths()

	def EventSelectPath( self, event ):
		self._RefreshTags()


code:
import os, wx

from lib.Actions.Import.FileSeeds import FileSeed

from lib.GUI.Common import BetterBitmapButton, BetterStaticText, Gauge, GetTLP
from lib.GUI.Dialogs import DialogYesNo
from lib.GUI.ListCtrl import BetterListCtrl, SafeNoneInt
from lib.GUI.Menus import AppendMenu, AppendMenuItem, AppendSeparator
from lib.GUI.ScrolledPanels import EditPanel
from lib.GUI.Serialisable import PngExportPanel
from lib.GUI.TopLevelWindows import DialogEdit, DialogNullipotent, FrameThatTakesScrollablePanel

from lib.Hydrus.Data import Call, ContentUpdate, ConvertValueRangeToPrettyString, \
	TimestampToPrettyTimeDelta, ToHumanInt
from lib.Hydrus.Exceptions import DataMissing
from lib.Hydrus.Paths import OpenFileLocation
from lib.Hydrus.Paths.Local import LaunchURLInWebBrowser
from lib.Hydrus.Serialisable.Local import LoadFromPng
from lib.Hydrus.Text import DeserialiseNewlinedTexts

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

from lib.Actions.Import.FileSeeds import FILE_SEED_TYPE_URL, FILE_SEED_TYPE_HDD

### CLASSES ###

class EditFileSeedCachePanel( EditPanel ):
	def __init__( self, parent, controller, file_seed_cache ):
		EditPanel.__init__( self, parent )

		self._controller = controller
		self._file_seed_cache = file_seed_cache

		self._text = BetterStaticText( self, 'initialising' )

		# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on file_seed add/remove

		columns = [ ( '#', 3 ), ( 'source', -1 ), ( 'status', 12 ), ( 'added', 23 ), ( 'last modified', 23 ), ( 'source time', 23 ), ( 'note', 20 ) ]

		self._list_ctrl = BetterListCtrl( self, 'file_seed_cache', 30, 30, columns, self._ConvertFileSeedToListCtrlTuples, delete_key_callback = self._DeleteSelected )

		#

		self._list_ctrl.AddDatas( self._file_seed_cache.GetFileSeeds() )

		self._list_ctrl.Sort( 0 )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._text, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._list_ctrl, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

		self._list_ctrl.AddMenuCallable( self._GetListCtrlMenu )

		self._controller.sub( self, 'NotifyFileSeedsUpdated', 'file_seed_cache_file_seeds_updated' )

		wx.CallAfter( self._UpdateText )

	def _ConvertFileSeedToListCtrlTuples( self, file_seed ):
		try:
			file_seed_index = self._file_seed_cache.GetFileSeedIndex( file_seed )

			pretty_file_seed_index = ToHumanInt( file_seed_index )

		except Exception:
			file_seed_index = '--'

			pretty_file_seed_index = '--'

		file_seed_data = file_seed.file_seed_data
		status = file_seed.status
		added = file_seed.created
		modified = file_seed.modified
		source_time = file_seed.source_time
		note = file_seed.note

		pretty_file_seed_data = str( file_seed_data )
		pretty_status = CC.status_string_lookup[ status ]
		pretty_added = TimestampToPrettyTimeDelta( added )
		pretty_modified = TimestampToPrettyTimeDelta( modified )

		if source_time is None:
			pretty_source_time = 'unknown'

		else:
			pretty_source_time = TimestampToPrettyTimeDelta( source_time )

		sort_source_time = SafeNoneInt( source_time )

		pretty_note = note.split( os.linesep )[0]

		display_tuple = ( pretty_file_seed_index, pretty_file_seed_data, pretty_status, pretty_added, pretty_modified, pretty_source_time, pretty_note )
		sort_tuple = ( file_seed_index, file_seed_data, status, added, modified, sort_source_time, note )

		return ( display_tuple, sort_tuple )

	def _CopySelectedNotes( self ):
		notes = []

		for file_seed in self._list_ctrl.GetData( only_selected = True ):
			note = file_seed.note

			if note != '':
				notes.append( note )

		if len( notes ) > 0:
			separator = os.linesep * 2

			text = separator.join( notes )

			HG.client_controller.pub( 'clipboard', 'text', text )

	def _CopySelectedFileSeedData( self ):
		file_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( file_seeds ) > 0:
			separator = os.linesep * 2

			text = separator.join( ( file_seed.file_seed_data for file_seed in file_seeds ) )

			HG.client_controller.pub( 'clipboard', 'text', text )

	def _DeleteSelected( self ):
		file_seeds_to_delete = self._list_ctrl.GetData( only_selected = True )

		if len( file_seeds_to_delete ) > 0:
			message = 'Are you sure you want to delete all the selected entries?'

			with DialogYesNo( self, message ) as dlg:
				if dlg.ShowModal() == wx.ID_YES:
					self._file_seed_cache.RemoveFileSeeds( file_seeds_to_delete )

	def _GetListCtrlMenu( self ):
		selected_file_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( selected_file_seeds ) == 0:
			raise DataMissing()

		menu = wx.Menu()

		can_show_files_in_new_page = True in ( file_seed.HasHash() for file_seed in selected_file_seeds )

		if can_show_files_in_new_page:
			AppendMenuItem( self, menu, 'open selected import files in a new page', 'Show all the known selected files in a new thumbnail page. This is complicated, so cannot always be guaranteed, even if the import says \'success\'.', self._ShowSelectionInNewPage )

			AppendSeparator( menu )

		AppendMenuItem( self, menu, 'copy sources', 'Copy all the selected sources to clipboard.', self._CopySelectedFileSeedData )
		AppendMenuItem( self, menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )

		AppendSeparator( menu )

		AppendMenuItem( self, menu, 'open sources', 'Open all the selected sources in your file explorer or web browser.', self._OpenSelectedFileSeedData )

		AppendSeparator( menu )

		AppendMenuItem( self, menu, 'try again', 'Reset the progress of all the selected imports.', Call( self._SetSelected, CC.STATUS_UNKNOWN ) )
		AppendMenuItem( self, menu, 'skip', 'Skip all the selected imports.', Call( self._SetSelected, CC.STATUS_SKIPPED ) )
		AppendMenuItem( self, menu, 'delete from list', 'Remove all the selected imports.', self._DeleteSelected )

		return menu

	def _OpenSelectedFileSeedData( self ):
		file_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( file_seeds ) > 0:
			if len( file_seeds ) > 10:
				message = 'You have many objects selected--are you sure you want to open them all?'

				with DialogYesNo( self, message ) as dlg:
					if dlg.ShowModal() != wx.ID_YES:
						return

			if file_seeds[0].file_seed_data.startswith( 'http' ):
				for file_seed in file_seeds:
					LaunchURLInWebBrowser( file_seed.file_seed_data )

			else:
				try:
					for file_seed in file_seeds:
						OpenFileLocation( file_seed.file_seed_data )

				except Exception as e:
					wx.MessageBox( str( e ) )

	def _SetSelected( self, status_to_set ):
		file_seeds = self._list_ctrl.GetData( only_selected = True )

		if status_to_set == CC.STATUS_UNKNOWN:
			deleted_and_clearable_file_seeds = [ file_seed for file_seed in file_seeds if file_seed.IsDeleted() and file_seed.HasHash() ]

			if len( deleted_and_clearable_file_seeds ) > 0:
				message = 'One or more of these files did not import due to being previously deleted. They will likely fail again unless you erase those deletion records. Would you like to do this now?'

				with DialogYesNo( self, message ) as dlg:
					if dlg.ShowModal() == wx.ID_YES:
						deletee_hashes = { file_seed.GetHash() for file_seed in deleted_and_clearable_file_seeds }

						content_update_erase_record = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ADVANCED, ( 'delete_deleted', deletee_hashes ) )
						content_update_undelete_from_trash = ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, deletee_hashes )

						service_keys_to_content_updates = { CC.COMBINED_LOCAL_FILE_SERVICE_KEY : [ content_update_erase_record, content_update_undelete_from_trash ] }

						HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )

		for file_seed in file_seeds:
			file_seed.SetStatus( status_to_set )

		self._file_seed_cache.NotifyFileSeedsUpdated( file_seeds )

	def _ShowSelectionInNewPage( self ):
		hashes = []

		for file_seed in self._list_ctrl.GetData( only_selected = True ):
			if file_seed.HasHash():
				hashes.append( file_seed.GetHash() )

		if len( hashes ) > 0:
			HG.client_controller.pub( 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY, initial_hashes = hashes )

	def _UpdateListCtrl( self, file_seeds ):
		file_seeds_to_add = []
		file_seeds_to_update = []
		file_seeds_to_delete = []

		for file_seed in file_seeds:
			if self._file_seed_cache.HasFileSeed( file_seed ):
				if self._list_ctrl.HasData( file_seed ):
					file_seeds_to_update.append( file_seed )

				else:
					file_seeds_to_add.append( file_seed )

			else:
				if self._list_ctrl.HasData( file_seed ):
					file_seeds_to_delete.append( file_seed )

		self._list_ctrl.DeleteDatas( file_seeds_to_delete )

		if len( file_seeds_to_add ) > 0:
			self._list_ctrl.AddDatas( file_seeds_to_add )

			# if file_seeds are inserted, then all subsequent indices need to be shuffled up, hence just update all here

			self._list_ctrl.UpdateDatas()

		else:
			self._list_ctrl.UpdateDatas( file_seeds_to_update )

	def _UpdateText( self ):
		( status, simple_status, ( total_processed, total ) ) = self._file_seed_cache.GetStatus()

		self._text.SetLabelText( status )

		self.Layout()

	def GetValue( self ):
		return self._file_seed_cache

	def NotifyFileSeedsUpdated( self, file_seed_cache_key, file_seeds ):
		if file_seed_cache_key == self._file_seed_cache.GetFileSeedCacheKey():
			self._UpdateText()
			self._UpdateListCtrl( file_seeds )

class FileSeedCacheButton( BetterBitmapButton ):
	def __init__( self, parent, controller, file_seed_cache_get_callable, file_seed_cache_set_callable = None ):
		BetterBitmapButton.__init__( self, parent, CC.GlobalBMPs.listctrl, self._ShowFileSeedCacheFrame )

		self._controller = controller
		self._file_seed_cache_get_callable = file_seed_cache_get_callable
		self._file_seed_cache_set_callable = file_seed_cache_set_callable

		self.SetToolTip( 'open detailed file import status--right-click for quick actions, if applicable' )

		self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )

	def _ClearFileSeeds( self, statuses_to_remove ):
		message = 'Are you sure you want to delete all the ' + '/'.join( ( CC.status_string_lookup[ status ] for status in statuses_to_remove ) ) + ' file import items? This is useful for cleaning up and de-laggifying a very large list, but be careful you aren\'t removing something you would want to revisit or what watcher/subscription may be using for future check time calculations.'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				file_seed_cache = self._file_seed_cache_get_callable()

				file_seed_cache.RemoveFileSeedsByStatus( statuses_to_remove )

	def _GetExportableSourcesString( self ):
		file_seed_cache = self._file_seed_cache_get_callable()

		file_seeds = file_seed_cache.GetFileSeeds()

		sources = [ file_seed.file_seed_data for file_seed in file_seeds ]

		return os.linesep.join( sources )

	def _GetSourcesFromSourcesString( self, sources_string ):
		sources = DeserialiseNewlinedTexts( sources_string )

		return sources

	def _ImportFromClipboard( self ):
		raw_text = HG.client_controller.GetClipboardText()

		sources = self._GetSourcesFromSourcesString( raw_text )

		try:
			self._ImportSources( sources )

		except Exception:
			wx.MessageBox( 'Could not import!' )

			raise

	def _ImportFromPng( self ):
		with wx.FileDialog( self, 'select the png with the sources', wildcard = 'PNG (*.png )|*.png' ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				path = dlg.GetPath()

				payload = LoadFromPng( path )

				try:
					sources = self._GetSourcesFromSourcesString( payload )

					self._ImportSources( sources )

				except Exception:
					wx.MessageBox( 'Could not import!' )

					raise

	def _ImportSources( self, sources ):
		file_seed_cache = self._file_seed_cache_get_callable()

		if sources[0].startswith( 'http' ):
			file_seed_type = FILE_SEED_TYPE_URL

		else:
			file_seed_type = FILE_SEED_TYPE_HDD

		file_seeds = [ FileSeed( file_seed_type, source ) for source in sources ]

		file_seed_cache.AddFileSeeds( file_seeds )

	def _ExportToPng( self ):
		payload = self._GetExportableSourcesString()

		with DialogNullipotent( self, 'export to png' ) as dlg:
			panel = PngExportPanel( dlg, payload )

			dlg.SetPanel( panel )

			dlg.ShowModal()

	def _ExportToClipboard( self ):
		payload = self._GetExportableSourcesString()

		HG.client_controller.pub( 'clipboard', 'text', payload )

	def _RetryErrors( self ):
		message = 'Are you sure you want to retry all the files that encountered errors?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				file_seed_cache = self._file_seed_cache_get_callable()

				file_seed_cache.RetryFailures()

	def _RetryIgnored( self ):
		message = 'Are you sure you want to retry all the files that were ignored/vetoed?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				file_seed_cache = self._file_seed_cache_get_callable()

				file_seed_cache.RetryIgnored()

	def _ShowFileSeedCacheFrame( self ):
		file_seed_cache = self._file_seed_cache_get_callable()

		tlp = GetTLP( self )

		if isinstance( tlp, wx.Dialog ):
			if self._file_seed_cache_set_callable is None: # throw up a dialog that edits the file_seed cache in place

				with DialogNullipotent( self, 'file import status' ) as dlg:
					panel = EditFileSeedCachePanel( dlg, self._controller, file_seed_cache )

					dlg.SetPanel( panel )

					dlg.ShowModal()

			else: # throw up a dialog that edits the file_seed cache but can be cancelled

				dupe_file_seed_cache = file_seed_cache.Duplicate()

				with DialogEdit( self, 'file import status' ) as dlg:
					panel = EditFileSeedCachePanel( dlg, self._controller, dupe_file_seed_cache )

					dlg.SetPanel( panel )

					if dlg.ShowModal() == wx.ID_OK:
						self._file_seed_cache_set_callable( dupe_file_seed_cache )

		else: # throw up a frame that edits the file_seed cache in place

			title = 'file import status'
			frame_key = 'file_import_status'

			frame = FrameThatTakesScrollablePanel( self, title, frame_key )

			panel = EditFileSeedCachePanel( frame, self._controller, file_seed_cache )

			frame.SetPanel( panel )

	def EventShowMenu( self, event ):
		menu = wx.Menu()

		file_seed_cache = self._file_seed_cache_get_callable()

		# num_file_seeds = len( file_seed_cache )
		num_successful = file_seed_cache.GetFileSeedCount( CC.STATUS_SUCCESSFUL_AND_NEW ) + file_seed_cache.GetFileSeedCount( CC.STATUS_SUCCESSFUL_BUT_REDUNDANT )
		num_vetoed = file_seed_cache.GetFileSeedCount( CC.STATUS_VETOED )
		num_deleted_and_vetoed = file_seed_cache.GetFileSeedCount( CC.STATUS_DELETED ) + num_vetoed
		num_errors = file_seed_cache.GetFileSeedCount( CC.STATUS_ERROR )
		num_skipped = file_seed_cache.GetFileSeedCount( CC.STATUS_SKIPPED )

		if num_errors > 0:
			AppendMenuItem( self, menu, 'retry ' + ToHumanInt( num_errors ) + ' error failures', 'Tell this cache to reattempt all its error failures.', self._RetryErrors )

		if num_vetoed > 0:
			AppendMenuItem( self, menu, 'retry ' + ToHumanInt( num_vetoed ) + ' ignored', 'Tell this cache to reattempt all its ignored/vetoed results.', self._RetryIgnored )

		AppendSeparator( menu )

		if num_successful > 0:
			num_deletees = num_successful

			AppendMenuItem( self, menu, 'delete ' + ToHumanInt( num_deletees ) + ' successful file import items from the queue', 'Tell this cache to clear out successful files, reducing the size of the queue.', self._ClearFileSeeds, ( CC.STATUS_SUCCESSFUL_AND_NEW, CC.STATUS_SUCCESSFUL_BUT_REDUNDANT ) )

		if num_deleted_and_vetoed > 0:
			num_deletees = num_deleted_and_vetoed

			AppendMenuItem( self, menu, 'delete ' + ToHumanInt( num_deletees ) + ' deleted/ignored file import items from the queue', 'Tell this cache to clear out deleted and ignored files, reducing the size of the queue.', self._ClearFileSeeds, ( CC.STATUS_DELETED, CC.STATUS_VETOED ) )

		if num_errors + num_skipped > 0:
			num_deletees = num_errors + num_skipped

			AppendMenuItem( self, menu, 'delete ' + ToHumanInt( num_deletees ) + ' error/skipped file import items from the queue', 'Tell this cache to clear out errored and skipped files, reducing the size of the queue.', self._ClearFileSeeds, ( CC.STATUS_ERROR, CC.STATUS_SKIPPED ) )

		AppendSeparator( menu )

		if len( file_seed_cache ) > 0:
			submenu = wx.Menu()

			AppendMenuItem( self, submenu, 'to clipboard', 'Copy all the sources in this list to the clipboard.', self._ExportToClipboard )
			AppendMenuItem( self, submenu, 'to png', 'Export all the sources in this list to a png file.', self._ExportToPng )

			AppendMenu( menu, submenu, 'export all sources' )

		submenu = wx.Menu()

		AppendMenuItem( self, submenu, 'from clipboard', 'Import new urls or paths to this list from the clipboard.', self._ImportFromClipboard )
		AppendMenuItem( self, submenu, 'from png', 'Import new urls or paths to this list from a png file.', self._ImportFromPng )

		AppendMenu( menu, submenu, 'import new sources' )

		HG.client_controller.PopupMenu( self, menu )

class FileSeedCacheStatusControl( wx.Panel ):
	def __init__( self, parent, controller, page_key = None ):
		wx.Panel.__init__( self, parent, style = wx.BORDER_DOUBLE )

		self._controller = controller
		self._page_key = page_key

		self._file_seed_cache = None

		self._import_summary_st = BetterStaticText( self, style = wx.ST_ELLIPSIZE_END )
		self._progress_st = BetterStaticText( self, style = wx.ST_ELLIPSIZE_END )

		self._file_seed_cache_button = FileSeedCacheButton( self, self._controller, self._GetFileSeedCache )

		self._progress_gauge = Gauge( self )

		#

		self._Update()

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._progress_st, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
		hbox.Add( self._file_seed_cache_button, CC.FLAGS_VCENTER )

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._import_summary_st, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( hbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
		vbox.Add( self._progress_gauge, CC.FLAGS_EXPAND_PERPENDICULAR )

		self.SetSizer( vbox )

		#

		HG.client_controller.gui.RegisterUIUpdateWindow( self )

	def _GetFileSeedCache( self ):
		return self._file_seed_cache

	def _Update( self ):
		if self._file_seed_cache is None:
			self._import_summary_st.SetLabelText( '' )
			self._progress_st.SetLabelText( '' )
			self._progress_gauge.SetRange( 1 )
			self._progress_gauge.SetValue( 0 )

			if self._file_seed_cache_button.IsEnabled():
				self._file_seed_cache_button.Disable()

		else:
			( import_summary, simple_status, ( num_done, num_to_do ) ) = self._file_seed_cache.GetStatus()

			self._import_summary_st.SetLabelText( import_summary )

			if num_to_do == 0:
				self._progress_st.SetLabelText( '' )

			else:
				self._progress_st.SetLabelText( ConvertValueRangeToPrettyString( num_done, num_to_do ) )

			self._progress_gauge.SetRange( num_to_do )
			self._progress_gauge.SetValue( num_done )

			if not self._file_seed_cache_button.IsEnabled():
				self._file_seed_cache_button.Enable()

	def SetFileSeedCache( self, file_seed_cache ):
		if not self:
			return

		self._file_seed_cache = file_seed_cache

	def TIMERUIUpdate( self ):
		do_it_anyway = False

		if self._file_seed_cache is not None:
			( import_summary, simple_status, ( num_done, num_to_do ) ) = self._file_seed_cache.GetStatus()

			( old_num_done, old_num_to_do ) = self._progress_gauge.GetValueRange()

			if old_num_done != num_done or old_num_to_do != num_to_do:
				if self._page_key is not None:
					do_it_anyway = True # to update the gauge

					HG.client_controller.pub( 'refresh_page_name', self._page_key )

		if self._controller.gui.IShouldRegularlyUpdate( self ) or do_it_anyway:
			self._Update()


code:
import os, wx

from lib.GUI.Common import SaneMultilineTextCtrl
from lib.GUI.TopLevelWindows import Frame

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants.Local as CC

### CLASSES ###

class ShowKeys( Frame ):
	def __init__( self, key_type, keys ):
		if key_type == 'registration':
			title = 'Registration Keys'
		elif key_type == 'access':
			title = 'Access Keys'

		# give it no parent, so this doesn't close when the dialog is closed!
		Frame.__init__( self, None, HG.client_controller.PrepStringForDisplay( title ), float_on_parent = False )

		self._key_type = key_type
		self._keys = keys

		#

		self._text_ctrl = SaneMultilineTextCtrl( self, style = wx.TE_READONLY | wx.TE_DONTWRAP )

		self._save_to_file = wx.Button( self, label = 'save to file' )
		self._save_to_file.Bind( wx.EVT_BUTTON, self.EventSaveToFile )

		self._done = wx.Button( self, label = 'done' )
		self._done.Bind( wx.EVT_BUTTON, self.EventDone )

		#

		if key_type == 'registration':
			prepend = 'r'
		else:
			prepend = ''

		self._text = os.linesep.join( [ prepend + key.hex() for key in self._keys ] )

		self._text_ctrl.SetValue( self._text )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._text_ctrl, CC.FLAGS_EXPAND_BOTH_WAYS )
		vbox.Add( self._save_to_file, CC.FLAGS_LONE_BUTTON )
		vbox.Add( self._done, CC.FLAGS_LONE_BUTTON )

		self.SetSizer( vbox )

		( x, y ) = self.GetEffectiveMinSize()

		if x < 500:
			x = 500
		if y < 200:
			y = 200

		self.SetInitialSize( ( x, y ) )

		self.Show( True )

	def EventDone( self, event ):
		self.Close()

	def EventSaveToFile( self, event ):
		filename = 'keys.txt'

		with wx.FileDialog( self, style=wx.FD_SAVE, defaultFile = filename ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				path = dlg.GetPath()

				with open( path, 'w', encoding = 'utf-8' ) as f:
					f.write( self._text )


code:
import os, wx

from lib.Handling.GallerySeeds import GallerySeed

from lib.GUI.Common import BetterBitmapButton, BetterStaticText, GetTLP
from lib.GUI.Dialogs import DialogYesNo
from lib.GUI.ListCtrl import BetterListCtrl
from lib.GUI.Menus import AppendMenu, AppendMenuItem, AppendSeparator
from lib.GUI.Serialisable import PngExportPanel
from lib.GUI.ScrolledPanels import EditPanel
from lib.GUI.TopLevelWindows import DialogEdit, DialogNullipotent, FrameThatTakesScrollablePanel

from lib.Hydrus.Data import Call, TimestampToPrettyTimeDelta, ToHumanInt
from lib.Hydrus.Exceptions import DataMissing
from lib.Hydrus.Paths.Local import LaunchURLInWebBrowser
from lib.Hydrus.Serialisable.Local import LoadFromPng
from lib.Hydrus.Text import DeserialiseNewlinedTexts

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants.Local as CC

### CLASSES ###

class EditGallerySeedLogPanel( EditPanel ):
	def __init__( self, parent, controller, read_only, can_generate_more_pages, gallery_seed_log ):
		EditPanel.__init__( self, parent )

		self._controller = controller
		self._read_only = read_only
		self._can_generate_more_pages = can_generate_more_pages
		self._gallery_seed_log = gallery_seed_log

		self._text = BetterStaticText( self, 'initialising' )

		# add index control row here, hide it if needed and hook into showing/hiding and postsizechangedevent on gallery_seed add/remove

		columns = [ ( '#', 3 ), ( 'url', -1 ), ( 'status', 12 ), ( 'added', 23 ), ( 'last modified', 23 ), ( 'note', 20 ) ]

		self._list_ctrl = BetterListCtrl( self, 'gallery_seed_log', 30, 30, columns, self._ConvertGallerySeedToListCtrlTuples )

		#

		self._list_ctrl.AddDatas( self._gallery_seed_log.GetGallerySeeds() )

		self._list_ctrl.Sort( 0 )

		#

		vbox = wx.BoxSizer( wx.VERTICAL )

		vbox.Add( self._text, CC.FLAGS_EXPAND_PERPENDICULAR )
		vbox.Add( self._list_ctrl, CC.FLAGS_EXPAND_BOTH_WAYS )

		self.SetSizer( vbox )

		self._list_ctrl.AddMenuCallable( self._GetListCtrlMenu )

		self._controller.sub( self, 'NotifyGallerySeedsUpdated', 'gallery_seed_log_gallery_seeds_updated' )

		wx.CallAfter( self._UpdateText )

	def _ConvertGallerySeedToListCtrlTuples( self, gallery_seed ):
		try:
			gallery_seed_index = self._gallery_seed_log.GetGallerySeedIndex( gallery_seed )

		except Exception:
			gallery_seed_index = '--'

		url = gallery_seed.url
		status = gallery_seed.status
		added = gallery_seed.created
		modified = gallery_seed.modified
		note = gallery_seed.note

		pretty_gallery_seed_index = ToHumanInt( gallery_seed_index )
		pretty_url = url
		pretty_status = CC.status_string_lookup[ status ]
		pretty_added = TimestampToPrettyTimeDelta( added )
		pretty_modified = TimestampToPrettyTimeDelta( modified )
		pretty_note = note.split( os.linesep )[0]

		display_tuple = ( pretty_gallery_seed_index, pretty_url, pretty_status, pretty_added, pretty_modified, pretty_note )
		sort_tuple = ( gallery_seed_index, url, status, added, modified, note )

		return ( display_tuple, sort_tuple )

	def _CopySelectedGalleryURLs( self ):
		gallery_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( gallery_seeds ) > 0:
			separator = os.linesep * 2

			text = separator.join( ( gallery_seed.url for gallery_seed in gallery_seeds ) )

			HG.client_controller.pub( 'clipboard', 'text', text )

	def _CopySelectedNotes( self ):
		notes = []

		for gallery_seed in self._list_ctrl.GetData( only_selected = True ):
			note = gallery_seed.note

			if note != '':
				notes.append( note )

		if len( notes ) > 0:
			separator = os.linesep * 2

			text = separator.join( notes )

			HG.client_controller.pub( 'clipboard', 'text', text )

	def _GetListCtrlMenu( self ):
		selected_gallery_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( selected_gallery_seeds ) == 0:
			raise DataMissing()

		menu = wx.Menu()

		AppendMenuItem( self, menu, 'copy urls', 'Copy all the selected urls to clipboard.', self._CopySelectedGalleryURLs )
		AppendMenuItem( self, menu, 'copy notes', 'Copy all the selected notes to clipboard.', self._CopySelectedNotes )

		AppendSeparator( menu )

		AppendMenuItem( self, menu, 'open urls', 'Open all the selected urls in your web browser.', self._OpenSelectedGalleryURLs )

		AppendSeparator( menu )

		if not self._read_only:
			AppendMenuItem( self, menu, 'try again (just this one page)', 'Schedule this url to occur again.', Call( self._TrySelectedAgain, False ) )

			if self._can_generate_more_pages:
				AppendMenuItem( self, menu, 'try again (and allow search to continue)', 'Schedule this url to occur again and continue.', Call( self._TrySelectedAgain, True ) )

		AppendMenuItem( self, menu, 'skip', 'Skip all the selected urls.', Call( self._SetSelected, CC.STATUS_SKIPPED ) )

		return menu

	def _OpenSelectedGalleryURLs( self ):
		gallery_seeds = self._list_ctrl.GetData( only_selected = True )

		if len( gallery_seeds ) > 0:
			if len( gallery_seeds ) > 10:
				message = 'You have many objects selected--are you sure you want to open them all?'

				with DialogYesNo( self, message ) as dlg:
					if dlg.ShowModal() != wx.ID_YES:
						return

			for gallery_seed in gallery_seeds:
				LaunchURLInWebBrowser( gallery_seed.url )

	def _SetSelected( self, status_to_set ):
		gallery_seeds = self._list_ctrl.GetData( only_selected = True )

		for gallery_seed in gallery_seeds:
			gallery_seed.SetStatus( status_to_set )

		self._gallery_seed_log.NotifyGallerySeedsUpdated( gallery_seeds )

	def _TrySelectedAgain( self, can_generate_more_pages ):
		new_gallery_seeds = []

		gallery_seeds = self._list_ctrl.GetData( only_selected = True )

		for gallery_seed in gallery_seeds:
			new_gallery_seeds.append( gallery_seed.GenerateRestartedDuplicate( can_generate_more_pages ) )

		self._gallery_seed_log.AddGallerySeeds( new_gallery_seeds )
		self._gallery_seed_log.NotifyGallerySeedsUpdated( new_gallery_seeds )

	def _UpdateListCtrl( self, gallery_seeds ):
		gallery_seeds_to_add = []
		gallery_seeds_to_update = []
		gallery_seeds_to_delete = []

		for gallery_seed in gallery_seeds:
			if self._gallery_seed_log.HasGallerySeed( gallery_seed ):
				if self._list_ctrl.HasData( gallery_seed ):
					gallery_seeds_to_update.append( gallery_seed )

				else:
					gallery_seeds_to_add.append( gallery_seed )

			else:
				if self._list_ctrl.HasData( gallery_seed ):
					gallery_seeds_to_delete.append( gallery_seed )

		self._list_ctrl.DeleteDatas( gallery_seeds_to_delete )

		if len( gallery_seeds_to_add ) > 0:
			self._list_ctrl.AddDatas( gallery_seeds_to_add )

			# if gallery_seeds are inserted, then all subsequent indices need to be shuffled up, hence just update all here

			self._list_ctrl.UpdateDatas()

		else:
			self._list_ctrl.UpdateDatas( gallery_seeds_to_update )

	def _UpdateText( self ):
		( status, ( total_processed, total ) ) = self._gallery_seed_log.GetStatus()

		self._text.SetLabelText( status )

		self.Layout()

	def GetValue( self ):
		return self._gallery_seed_log

	def NotifyGallerySeedsUpdated( self, gallery_seed_log_key, gallery_seeds ):
		if gallery_seed_log_key == self._gallery_seed_log.GetGallerySeedLogKey():
			self._UpdateText()
			self._UpdateListCtrl( gallery_seeds )

class GallerySeedLogButton( BetterBitmapButton ):
	def __init__( self, parent, controller, read_only, can_generate_more_pages, gallery_seed_log_get_callable, gallery_seed_log_set_callable = None ):
		BetterBitmapButton.__init__( self, parent, CC.GlobalBMPs.listctrl, self._ShowGallerySeedLogFrame )

		self._controller = controller
		self._read_only = read_only
		self._can_generate_more_pages = can_generate_more_pages
		self._gallery_seed_log_get_callable = gallery_seed_log_get_callable
		self._gallery_seed_log_set_callable = gallery_seed_log_set_callable

		self.SetToolTip( 'open detailed gallery log--right-click for quick actions, if applicable' )

		self.Bind( wx.EVT_RIGHT_DOWN, self.EventShowMenu )

	def _ClearGallerySeeds( self, statuses_to_remove ):
		message = 'Are you sure you want to delete all the ' + '/'.join( ( CC.status_string_lookup[ status ] for status in statuses_to_remove ) ) + ' gallery log entries? This is useful for cleaning up and de-laggifying a very large list, but be careful you aren\'t removing something you would want to revisit.'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				gallery_seed_log = self._gallery_seed_log_get_callable()

				gallery_seed_log.RemoveGallerySeedsByStatus( statuses_to_remove )

	def _GetExportableURLsString( self ):
		gallery_seed_log = self._gallery_seed_log_get_callable()

		gallery_seeds = gallery_seed_log.GetGallerySeeds()

		urls = [ gallery_seed.url for gallery_seed in gallery_seeds ]

		return os.linesep.join( urls )

	def _GetURLsFromURLsString( self, urls_string ):
		urls = DeserialiseNewlinedTexts( urls_string )

		return urls

	def _ImportFromClipboard( self ):
		raw_text = HG.client_controller.GetClipboardText()

		urls = self._GetURLsFromURLsString( raw_text )

		try:
			self._ImportURLs( urls )

		except Exception:
			wx.MessageBox( 'Could not import!' )

			raise

	def _ImportFromPng( self ):
		with wx.FileDialog( self, 'select the png with the urls', wildcard = 'PNG (*.png )|*.png' ) as dlg:
			if dlg.ShowModal() == wx.ID_OK:
				path = dlg.GetPath()

				payload = LoadFromPng( path )

				try:
					urls = self._GetURLsFromURLsString( payload )

					self._ImportURLs( urls )

				except Exception:
					wx.MessageBox( 'Could not import!' )

					raise

	def _ImportURLs( self, urls ):
		gallery_seed_log = self._gallery_seed_log_get_callable()

		filtered_urls = [ url for url in urls if not gallery_seed_log.HasGalleryURL( url ) ]

		urls_to_add = urls

		if len( filtered_urls ) < len( urls ):
			num_urls = len( urls )
			num_removed = num_urls - len( filtered_urls )

			message = 'Of the ' + ToHumanInt( num_urls ) + ' URLs you mean to add, ' + ToHumanInt( num_removed ) + ' are already in the gallery log. Would you like to only add new URLs or add everything (which will force a re-check of the duplicates )?'

			with DialogYesNo( self, message, yes_label = 'only add new urls', no_label = 'add all urls, even duplicates' ) as dlg:
				result = dlg.ShowModal()

				if result == wx.ID_YES:
					urls_to_add = filtered_urls

				elif result == wx.ID_CANCEL:
					return

		can_generate_more_pages = False

		if self._can_generate_more_pages:
			message = 'Would you like these urls to only check for new files, or would you like them to also generate subsequent gallery pages, like a regular search would?'

			with DialogYesNo( self, message, yes_label = 'just check what I am adding', no_label = 'start a potential new search for every url added' ) as dlg:
				result = dlg.ShowModal()

				if result == wx.ID_CANCEL:
					return

				can_generate_more_pages = result == wx.ID_NO

		gallery_seeds = [ GallerySeed( url, can_generate_more_pages = can_generate_more_pages ) for url in urls_to_add ]

		gallery_seed_log.AddGallerySeeds( gallery_seeds )

	def _ExportToPng( self ):
		payload = self._GetExportableURLsString()

		with DialogNullipotent( self, 'export to png' ) as dlg:
			panel = PngExportPanel( dlg, payload )

			dlg.SetPanel( panel )

			dlg.ShowModal()

	def _ExportToClipboard( self ):
		payload = self._GetExportableURLsString()

		HG.client_controller.pub( 'clipboard', 'text', payload )

	def _RetryErrors( self ):
		message = 'Are you sure you want to retry all the files that encountered errors?'

		with DialogYesNo( self, message ) as dlg:
			if dlg.ShowModal() == wx.ID_YES:
				gallery_seed_log = self._gallery_seed_log_get_callable()

				gallery_seed_log.RetryFailures()

	def _ShowGallerySeedLogFrame( self ):
		gallery_seed_log = self._gallery_seed_log_get_callable()

		tlp = GetTLP( self )

		if isinstance( tlp, wx.Dialog ):
			if self._gallery_seed_log_set_callable is None: # throw up a dialog that edits the gallery_seed log in place

				with DialogNullipotent( self, 'gallery import log' ) as dlg:
					panel = EditGallerySeedLogPanel( dlg, self._controller, self._read_only, self._can_generate_more_pages, gallery_seed_log )

					dlg.SetPanel( panel )

					dlg.ShowModal()

			else: # throw up a dialog that edits the gallery_seed log but can be cancelled

				dupe_gallery_seed_log = gallery_seed_log.Duplicate()

				with DialogEdit( self, 'gallery import log' ) as dlg:
					panel = EditGallerySeedLogPanel( dlg, self._controller, self._read_only, self._can_generate_more_pages, dupe_gallery_seed_log )

					dlg.SetPanel( panel )

					if dlg.ShowModal() == wx.ID_OK:
						self._gallery_seed_log_set_callable( dupe_gallery_seed_log )

		else: # throw up a frame that edits the gallery_seed log in place

			title = 'gallery import log'
			frame_key = 'gallery_import_log'

			frame = FrameThatTakesScrollablePanel( self, title, frame_key )

			panel = EditGallerySeedLogPanel( frame, self._controller, self._read_only, self._can_generate_more_pages, gallery_seed_log )

			frame.SetPanel( panel )

	def EventShowMenu( self, event ):
		menu = wx.Menu()

		gallery_seed_log = self._gallery_seed_log_get_callable()

		if len( gallery_seed_log ) > 0:
			if not self._read_only and gallery_seed_log.CanRestartFailedSearch():
				AppendMenuItem( self, menu, 'restart and resume failed search', 'Requeue the last failed attempt and resume search from there.', gallery_seed_log.RestartFailedSearch )

				AppendSeparator( menu )

			submenu = wx.Menu()

			AppendMenuItem( self, submenu, 'to clipboard', 'Copy all the urls in this list to the clipboard.', self._ExportToClipboard )
			AppendMenuItem( self, submenu, 'to png', 'Export all the urls in this list to a png file.', self._ExportToPng )

			AppendMenu( menu, submenu, 'export all urls' )

		if not self._read_only:
			submenu = wx.Menu()

			AppendMenuItem( self, submenu, 'from clipboard', 'Import new urls to this list from the clipboard.', self._ImportFromClipboard )
			AppendMenuItem( self, submenu, 'from png', 'Import new urls to this list from a png file.', self._ImportFromPng )

			AppendMenu( menu, submenu, 'import new urls' )

		HG.client_controller.PopupMenu( self, menu )

class GallerySeedLogStatusControl( wx.Panel ):
	def __init__( self, parent, controller, read_only, can_generate_more_pages, page_key = None ):
		wx.Panel.__init__( self, parent, style = wx.BORDER_DOUBLE )

		self._controller = controller
		self._read_only = read_only
		self._can_generate_more_pages = can_generate_more_pages
		self._page_key = page_key

		self._gallery_seed_log = None

		self._log_summary_st = BetterStaticText( self, style = wx.ST_ELLIPSIZE_END )

		self._gallery_seed_log_button = GallerySeedLogButton( self, self._controller, self._read_only, self._can_generate_more_pages, self._GetGallerySeedLog )

		#

		self._Update()

		#

		hbox = wx.BoxSizer( wx.HORIZONTAL )

		hbox.Add( self._log_summary_st, CC.FLAGS_VCENTER_EXPAND_DEPTH_ONLY )
		hbox.Add( self._gallery_seed_log_button, CC.FLAGS_VCENTER )

		self.SetSizer( hbox )

		#

		HG.client_controller.gui.RegisterUIUpdateWindow( self )

	def _GetGallerySeedLog( self ):
		return self._gallery_seed_log

	def _Update( self ):
		if self._gallery_seed_log is None:
			self._log_summary_st.SetLabelText( '' )

			if self._gallery_seed_log_button.IsEnabled():
				self._gallery_seed_log_button.Disable()

		else:
			( import_summary, ( num_done, num_to_do ) ) = self._gallery_seed_log.GetStatus()

			self._log_summary_st.SetLabelText( import_summary )

			if not self._gallery_seed_log_button.IsEnabled():
				self._gallery_seed_log_button.Enable()

	def SetGallerySeedLog( self, gallery_seed_log ):
		if not self:
			return

		self._gallery_seed_log = gallery_seed_log

	def TIMERUIUpdate( self ):
		if self._controller.gui.IShouldRegularlyUpdate( self ):
			self._Update()