Version 402

closes #276, closes #244, closes #264, closes #277, closes #235, closes #256
This commit is contained in:
Hydrus Network Developer 2020-06-24 16:25:24 -05:00
parent 3126b5d1a1
commit f0c9969b27
66 changed files with 1690 additions and 998 deletions

View File

@ -1,6 +1,6 @@
If running the client executable does nothing or gives you an odd error before dumping out, here are some common fixes to try:
1. Look for a 'crash.log' in your 'db' directory. Failing that, is there a 'client - [date].log' file? Does it have an error in it? If there is something, please send it in to me, hydrus_dev (see contact.html in the help directory for my contact details).
1. Look for a 'crash.log' in your 'db' directory or user desktop. Failing that, is there a 'client - [date].log' file? Does it have an error in it? If there is something, please send it in to me, hydrus_dev (see contact.html in the help directory for my contact details).
2. Some anti-virus program updates falsely detect that one of the dlls or other files in the client is bad and quietly quarantine them. Please check your anti-virus logs or compare your install directory with the 'extract only' release archive to see if there are missing files. Avast has done this several times. Instances of this are useful to know about as several users usually get hit by the same thing at the same time. Please feel free to also start a conversation if you just want to double-check it is a false-positive after all.

View File

@ -8,6 +8,64 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 402</h3></li>
<ul>
<li>in many situations--such as a search result that gives no results, or a search cancel, or a downloader page cleared of a highlight--pages will now report a special status text rather than '0 files', such as 'no results for this search' or 'search cancelled!' (issue #277)</li>
<li>new pages, and the first page of a loaded session, should now correctly publish their status text to the status bar immediately after initialisation, (previously blank until first change)</li>
<li>clicking the 'searching immediately' button while a search is ongoing now correctly cancels a search, cleaning up status and page and buttons, rather than just stopping current work immediately</li>
<li>added 'copy_xxx_hash' shortcuts to the media shortcut set for 'md5', 'sha1', and 'sha512'</li>
<li>when copying file hashes to clipboard, a popup appears for two seconds to verify what happened</li>
<li>when copying file hashes to clipboard, recovery from missing hashes is more graceful, with multiple error report states</li>
<li>the way the client shuts down is untangled. the order in which the gui, managers, threads, database are shut down is smoothed out, with better error handling and fewer potential logical holes</li>
<li>the 'should I do shutdown work?' dialog is now only presented in the clean shutdown pipeline</li>
<li>menu labels now elide at 128 characters, extended from 64 previously. hopefully this strikes a better balance between fixed texts we do want to read while still not letting long dynamic texts go nuts (issue #276)</li>
<li>gallery and watcher pages now have 'show file/gallery log' on their menus, which directly zoom in to the edit dialogs for the top-most selected query or watcher (issue #256)</li>
<li>when file maintenance is forced to run from the thumbnail menu or file maintenance job panel, it now provides x/y progress text and gauge based on total jobs, e.g. 1,234/10,000, rather than out of the 256-job batches (issue #264)</li>
<li>the simple downloader page now updates its pending jobs list more efficiently, and supports multiple selection, and presents a yes/no confirmation on delete</li>
<li>most lists with clipboard/png import/export buttons can now also do .json files. they also accept json files in a drag and drop. you can mix json and png files in a multi-file drag and drop</li>
<li>when selecting a parser for a url class in 'manage url class links', those parsers with example urls that match the url class are now separately listed at the top of the choice dialog</li>
<li>in the recent autocomplete rewrite, the hidden repository update file domain was accidentally exposed in the file domain button. after some testing, it actually works(!), but as this is an advanced topic, it is now hidden behind advanced mode</li>
<li>the way services are deleted or completely reset is now changed to what should be a significantly faster and smaller operation</li>
<li>the latest user-made nitter/twitter downloader is rolled in to the update. some little fixes and adds support for mobile.twitter.com url imports</li>
<li>fixed an issue where uninitialised repositories thought they were caught up</li>
<li>to reflect that it does nothing in this case, the mouse shortcut edit panel now disables the press/release choice on double-click or scroll</li>
<li>fixed file save dialogs not filling in the default filename properly</li>
<li>removed an old wx safety hack where new pages would silently not create while the client was minimised. this fixes issues with large session loading and subscriptions publishing files to page names that do not yet exist while the client is minimised</li>
<li>removed an old wx safety hack where some tag lists would not regen their current tag display while the client was minimised</li>
<li>in lieu of a future better bit of html subscription help that I link to from the subscription panel, the 'file limits' help button has temporarily briefer text so it doesn't make such a giant popup</li>
<li>moving back to pyinstaller 3.5 (from 3.6) for the windows build, which appears to fix some dll loading for some users (issue #244)</li>
<li>the windows and linux builds are updated to Qt 5.15 (from 5.13.2). it does not seem to have the odd problems 5.14 gave us. let me know if you have any trouble or if any weird graphical issues magically fix themselves</li>
<li>.</li>
<li>client api:</li>
<li>the /get_files/file_metadata call has a new true/false parameter, 'detailed_url_information', default false, that adds 'detailed_known_urls' structure to list the known urls results as in /add_urls/get_url_info. it has a help example and a unit test and everything (issue #235)</li>
<li>the client api version is now 13</li>
<li>.</li>
<li>boring cleanup details:</li>
<li>reshuffled the shutdown code. now the controller takes the lead, booting splash as appropriate and commanding gui to save and close, and then proceeds to other shutdown</li>
<li>fast and normal shutdown code is unified, just run differently</li>
<li>shutdown calls should now always be idempotent</li>
<li>a catch for some OS-level shutdown commands, normally user log-off, also hooks into the newer UI-free fast shutdown</li>
<li>SIGINT and SIGTERM also hook better into the new shutdown, and are thread safe</li>
<li>performing multiple SIGINTS on shutdown should no longer throw an error after the gui is deleted</li>
<li>more potential startup/shutdown errors are now caught and presented to the user and saved to log, with subsequent shutdown urgency accelerated afterwards</li>
<li>critical errors on a fast shutdown no longer present to the user--they just save to log</li>
<li>updated how an emergency shutdown state is tested</li>
<li>updated how a 'clean exit complete' state is set and tested</li>
<li>various unusual shutdown states now skip human interaction and jump straight to guaranteed fast shutdown</li>
<li>refactored splash window to its own file</li>
<li>wrote a new qlistwidget subclass to do some common data storage/retrieval/selection. it will eventually replace most lists across the program</li>
<li>the 'queue' list widget that has up/delete/down and add/edit buttons beside a list has nicer backend code and now initialises with its buttons correctly disabled due to no selection</li>
<li>the similar 'add/edit/delete' list widget is updated to use the nicer backend</li>
<li>some wx->Qt list hacks, which were themselves using borked old display-string-based indexing, are deleted</li>
<li>the repository download/process daemon has been moved to the newer job scheduler. it should start up and close out on program exit a bit more neatly</li>
<li>untangled some messy value-change radio button code in the shortcut edit panel</li>
<li>updated the way page status text propagates up from the thumbnail grid to the main gui to Qt signals instead of the old inefficient pubsub</li>
<li>all UI file hash clipboard copying code is now unified and improved</li>
<li>added a new subscription file publish debug test to help->debug->gui</li>
<li>refactored some client specific time delta rendering code out of core to client</li>
<li>misc event cleanup code</li>
<li>misc code style cleanup</li>
</ul>
<li><h3>version 401</h3></li>
<ul>
<li>subscriptions:</li>

View File

@ -852,6 +852,7 @@
<li>file_ids : (a list of numerical file ids)</li>
<li>hashes : (a list of hexadecimal SHA256 hashes)</li>
<li>only_return_identifiers : true or false (optional, defaulting to false)</li>
<li>detailed_url_information : true or false (optional, defaulting to false)</li>
</ul>
</li>
<p>You need one of file_ids or hashes. If your access key is restricted by tag, you cannot search by hashes, and <b>the file_ids you search for must have been in the most recent search result</b>.</p>
@ -961,6 +962,29 @@
<li>3 - petitioned</li>
</ul>
<p>Note that since JSON Object keys must be strings, these status numbers are strings, not ints.</p>
<p>If you add detailed_url_information=true, a new entry, 'detailed_known_urls', will be added for each file, with a list of the same structure as /add_urls/get_url_info. This may be an expensive request if you are querying thousands of files at once.</p>
<p>For example:</p>
<ul>
<li>
<pre>"detailed_known_urls" : [
{
"normalised_url": "https://gelbooru.com/index.php?id=4841557&page=post&s=view",
"url_type": 0,
"url_type_string": "post url",
"match_name": "gelbooru file page",
"can_parse": True
},
{
"normalised_url": "https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg",
"url_type": 5,
"url_type_string": "unknown url",
"match_name": "unknown url",
"can_parse": False
}
]</pre>
</li>
</ul>
</ul>
</div>
<div class="apiborder" id="get_files_file">

View File

@ -28,6 +28,7 @@ from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUI
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIScrolledPanelsManagement
from hydrus.client.gui import ClientGUISplash
from hydrus.client.gui import ClientGUIStyle
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
@ -111,7 +112,7 @@ class App( QW.QApplication ):
QC.qInstallMessageHandler( MessageHandler )
self.setQuitOnLastWindowClosed( True )
self.setQuitOnLastWindowClosed( False )
self.call_after_catcher = QP.CallAfterEventCatcher( self )
@ -122,19 +123,16 @@ class App( QW.QApplication ):
def EventEndSession( self ):
# Since aboutToQuit gets called not only on external shutdown events (like user logging off), but even if we explicitely call QApplication.exit(),
# this check will make sure that we only do an emergency exit if it's really necessary (i.e. QApplication.exit() wasn't called by us).
if not QW.QApplication.instance().property( 'normal_exit' ):
# If a user log-off causes the OS to call the Qt Application's quit/exit func, we still want to save and close nicely
if HG.client_controller is not None:
HG.emergency_exit = True
if HG.client_controller.gui is not None and QP.isValid( HG.client_controller.gui ):
if not HG.client_controller.ProgramIsShuttingDown():
HG.client_controller.gui.SaveAndClose()
HG.client_controller.SetDoingFastExit( True )
HG.client_controller.Exit()
HG.view_shutdown = True
HG.model_shutdown = True
@ -144,10 +142,9 @@ class Controller( HydrusController.HydrusController ):
def __init__( self, db_dir ):
self._last_shutdown_was_bad = False
self._qt_app_running = False
self._is_booted = False
self._program_is_shutting_down = False
self._splash = None
@ -244,8 +241,13 @@ class Controller( HydrusController.HydrusController ):
HydrusData.DebugPrint( traceback.format_exc() )
self.SafeShowCriticalMessage( 'shutdown error', text )
self.SafeShowCriticalMessage( 'shutdown error', traceback.format_exc() )
if not self._doing_fast_exit:
self.SafeShowCriticalMessage( 'shutdown error', text )
self.SafeShowCriticalMessage( 'shutdown error', traceback.format_exc() )
self._doing_fast_exit = True
def _ShutdownSubscriptionsManager( self ):
@ -403,19 +405,27 @@ class Controller( HydrusController.HydrusController ):
def CatchSignal( self, sig, frame ):
if sig in ( signal.SIGINT, signal.SIGTERM ):
if self._program_is_shutting_down:
if sig == signal.SIGTERM:
HG.emergency_exit = True
return
if hasattr( self, 'gui' ):
if sig == signal.SIGINT:
if self.gui is not None and QP.isValid( self.gui ):
event = QG.QCloseEvent()
QW.QApplication.postEvent( self.gui, event )
QW.QApplication.instance().postEvent( self.gui, event )
else:
QP.CallAfter( QW.QApplication.instance().quit )
elif sig == signal.SIGTERM:
QP.CallAfter( QW.QApplication.instance().quit )
@ -437,8 +447,6 @@ class Controller( HydrusController.HydrusController ):
if result != QW.QDialog.Accepted:
HG.shutting_down_due_to_already_running = True
raise HydrusExceptions.ShutdownException()
@ -496,9 +504,14 @@ class Controller( HydrusController.HydrusController ):
def CreateSplash( self, title ):
if self._splash is not None:
self._DestroySplash()
try:
self._splash = ClientGUI.FrameSplash( self, title )
self._splash = ClientGUISplash.FrameSplash( self, title )
except:
@ -512,7 +525,7 @@ class Controller( HydrusController.HydrusController ):
def CurrentlyIdle( self ):
if HG.program_is_shutting_down:
if self._program_is_shutting_down:
return False
@ -579,7 +592,7 @@ class Controller( HydrusController.HydrusController ):
def CurrentlyVeryIdle( self ):
if HG.program_is_shutting_down:
if self._program_is_shutting_down:
return False
@ -622,97 +635,52 @@ class Controller( HydrusController.HydrusController ):
def Exit( self ):
# this is idempotent and does not stop. we are shutting down now or in the very near future
if self._program_is_shutting_down:
return
self._program_is_shutting_down = True
if not self._is_booted:
HG.emergency_exit = True
self._doing_fast_exit = True
HG.program_is_shutting_down = True
if HG.emergency_exit:
try:
HydrusData.DebugPrint( 'doing fast shutdown\u2026' )
self.ShutdownView()
self.ShutdownModel()
HydrusData.CleanRunningFile( self.db_dir, 'client' )
else:
try:
if not self._doing_fast_exit:
last_shutdown_work_time = self.Read( 'last_shutdown_work_time' )
idle_shutdown_action = self.options[ 'idle_shutdown' ]
auto_shutdown_work_ok_by_user = idle_shutdown_action in ( CC.IDLE_ON_SHUTDOWN, CC.IDLE_ON_SHUTDOWN_ASK_FIRST )
shutdown_work_period = self.new_options.GetInteger( 'shutdown_work_period' )
auto_shutdown_work_due = HydrusData.TimeHasPassed( last_shutdown_work_time + shutdown_work_period )
manual_shutdown_work_not_already_set = not HG.do_idle_shutdown_work
we_can_turn_on_auto_shutdown_work = auto_shutdown_work_ok_by_user and auto_shutdown_work_due and manual_shutdown_work_not_already_set
if we_can_turn_on_auto_shutdown_work:
idle_shutdown_max_minutes = self.options[ 'idle_shutdown_max_minutes' ]
time_to_stop = HydrusData.GetNow() + ( idle_shutdown_max_minutes * 60 )
work_to_do = self.GetIdleShutdownWorkDue( time_to_stop )
if len( work_to_do ) > 0:
if idle_shutdown_action == CC.IDLE_ON_SHUTDOWN_ASK_FIRST:
from hydrus.client.gui import ClientGUIDialogsQuick
text = 'Is now a good time for the client to do up to ' + HydrusData.ToHumanInt( idle_shutdown_max_minutes ) + ' minutes\' maintenance work? (Will auto-no in 15 seconds)'
text += os.linesep * 2
text += 'The outstanding jobs appear to be:'
text += os.linesep * 2
text += os.linesep.join( work_to_do )
result = ClientGUIDialogsQuick.GetYesNo( self._splash, text, title = 'Maintenance is due', auto_no_time = 15 )
if result == QW.QDialog.Accepted:
HG.do_idle_shutdown_work = True
else:
# if they said no, don't keep asking
self.Write( 'last_shutdown_work_time', HydrusData.GetNow() )
else:
HG.do_idle_shutdown_work = True
self.CreateSplash( 'hydrus client exiting' )
if HG.do_idle_shutdown_work:
self._splash.MakeCancelShutdownButton()
self._splash.ShowCancelShutdownButton()
self.CallToThreadLongRunning( self.THREADExitEverything )
if self.gui is not None and QP.isValid( self.gui ):
except:
self._DestroySplash()
HydrusData.DebugPrint( traceback.format_exc() )
HG.emergency_exit = True
self.Exit()
self.gui.SaveAndClose()
except Exception as e:
self._ReportShutdownException()
if self._doing_fast_exit:
HydrusData.DebugPrint( 'doing fast shutdown\u2026' )
self.THREADExitEverything()
else:
self.CallToThreadLongRunning( self.THREADExitEverything )
def GetClipboardText( self ):
@ -1079,7 +1047,6 @@ class Controller( HydrusController.HydrusController ):
if not HG.no_daemons:
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'MaintainTrash', ClientDaemons.DAEMONMaintainTrash, init_wait = 120 ) )
self._daemons.append( HydrusThreading.DAEMONForegroundWorker( self, 'SynchroniseRepositories', ClientDaemons.DAEMONSynchroniseRepositories, ( 'notify_restart_repo_sync_daemon', 'notify_new_permissions', 'wake_idle_workers' ), period = 4 * 3600, pre_call_wait = 1 ) )
self.files_maintenance_manager.Start()
@ -1093,6 +1060,13 @@ class Controller( HydrusController.HydrusController ):
job.WakeOnPubSub( 'notify_unknown_accounts' )
self._daemon_jobs[ 'synchronise_accounts' ] = job
job = self.CallRepeating( 5.0, 3600.0 * 4, self.SynchroniseRepositories )
job.ShouldDelayOnWakeup( True )
job.WakeOnPubSub( 'notify_restart_repo_sync' )
job.WakeOnPubSub( 'notify_new_permissions' )
job.WakeOnPubSub( 'wake_idle_workers' )
self._daemon_jobs[ 'synchronise_repositories' ] = job
job = self.CallRepeatingQtSafe( self, 10.0, 10.0, self.CheckMouseIdle )
self._daemon_jobs[ 'check_mouse_idle' ] = job
@ -1123,11 +1097,6 @@ class Controller( HydrusController.HydrusController ):
return self._is_booted
def LastShutdownWasBad( self ):
return self._last_shutdown_was_bad
def MaintainDB( self, maintenance_mode = HC.MAINTENANCE_IDLE, stop_time = None ):
if maintenance_mode == HC.MAINTENANCE_IDLE and not self.GoodTimeToStartBackgroundWork():
@ -1261,6 +1230,11 @@ class Controller( HydrusController.HydrusController ):
#QW.QApplication.instance().postEvent( QW.QApplication.instance().pubsub_catcher, PubSubEvent( self._pubsub ) )
def ProgramIsShuttingDown( self ):
return self._program_is_shutting_down
def RefreshServices( self ):
self.services_manager.RefreshServices()
@ -1352,7 +1326,7 @@ class Controller( HydrusController.HydrusController ):
self.CallToThreadLongRunning( THREADRestart )
QP.CallAfter( self.gui.SaveAndClose )
self.Exit()
@ -1676,7 +1650,7 @@ class Controller( HydrusController.HydrusController ):
def ShutdownView( self ):
if not HG.emergency_exit:
if not self._doing_fast_exit:
self.pub( 'splash_set_status_text', 'waiting for subscriptions to exit' )
@ -1744,6 +1718,38 @@ class Controller( HydrusController.HydrusController ):
def SynchroniseRepositories( self ):
if not self.options[ 'pause_repo_sync' ]:
services = self.services_manager.GetServices( HC.REPOSITORIES, randomised = True )
for service in services:
if HydrusThreading.IsThreadShuttingDown():
return
if self.options[ 'pause_repo_sync' ]:
return
service.SyncRemote()
service.SyncProcessUpdates( maintenance_mode = HC.MAINTENANCE_IDLE )
if HydrusThreading.IsThreadShuttingDown():
return
time.sleep( 1 )
def SystemBusy( self ):
if HG.force_idle_mode:
@ -1794,9 +1800,7 @@ class Controller( HydrusController.HydrusController ):
try:
self._last_shutdown_was_bad = HydrusData.LastShutdownWasBad( self.db_dir, 'client' )
HydrusData.RecordRunningStart( self.db_dir, 'client' )
self.RecordRunningStart()
self.InitModel()
@ -1808,9 +1812,9 @@ class Controller( HydrusController.HydrusController ):
HydrusData.Print( e )
HydrusData.CleanRunningFile( self.db_dir, 'client' )
self.CleanRunningFile()
QP.CallAfter( QW.QApplication.exit, 0 )
QP.CallAfter( QW.QApplication.quit )
except Exception as e:
@ -1825,7 +1829,7 @@ class Controller( HydrusController.HydrusController ):
self.SafeShowCriticalMessage( 'boot error', traceback.format_exc() )
QP.CallAfter( QW.QApplication.exit, 0 )
QP.CallAfter( QW.QApplication.exit, 1 )
finally:
@ -1849,7 +1853,7 @@ class Controller( HydrusController.HydrusController ):
self.pub( 'splash_set_title_text', 'cleaning up\u2026' )
HydrusData.CleanRunningFile( self.db_dir, 'client' )
self.CleanRunningFile()
except ( HydrusExceptions.DBCredentialsException, HydrusExceptions.ShutdownException ):
@ -1861,11 +1865,11 @@ class Controller( HydrusController.HydrusController ):
finally:
QW.QApplication.instance().setProperty( 'normal_exit', True )
QW.QApplication.instance().setProperty( 'exit_complete', True )
self._DestroySplash()
QP.CallAfter( QW.QApplication.exit )
QP.CallAfter( QW.QApplication.quit )

View File

@ -1655,10 +1655,10 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE IF NOT EXISTS ideal_client_files_locations ( location TEXT, weight INTEGER );' )
self._c.execute( 'CREATE TABLE IF NOT EXISTS ideal_thumbnail_override_location ( location TEXT );' )
self._c.execute( 'CREATE TABLE current_files ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, timestamp INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE current_files ( service_id INTEGER, hash_id INTEGER, timestamp INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._CreateIndex( 'current_files', [ 'timestamp' ] )
self._c.execute( 'CREATE TABLE deleted_files ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE deleted_files ( service_id INTEGER, hash_id INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE IF NOT EXISTS duplicate_files ( media_id INTEGER PRIMARY KEY, king_hash_id INTEGER UNIQUE );' )
@ -1688,10 +1688,10 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE file_notes ( hash_id INTEGER, name_id INTEGER, note_id INTEGER, PRIMARY KEY ( hash_id, name_id ) );' )
self._CreateIndex( 'file_notes', [ 'note_id' ] )
self._c.execute( 'CREATE TABLE file_transfers ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE file_transfers ( service_id INTEGER, hash_id INTEGER, PRIMARY KEY ( service_id, hash_id ) );' )
self._CreateIndex( 'file_transfers', [ 'hash_id' ] )
self._c.execute( 'CREATE TABLE file_petitions ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, hash_id, reason_id ) );' )
self._c.execute( 'CREATE TABLE file_petitions ( service_id INTEGER, hash_id INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, hash_id, reason_id ) );' )
self._CreateIndex( 'file_petitions', [ 'hash_id' ] )
self._c.execute( 'CREATE TABLE json_dict ( name TEXT PRIMARY KEY, dump BLOB_BYTES );' )
@ -1700,7 +1700,7 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE last_shutdown_work_time ( last_shutdown_work_time INTEGER );' )
self._c.execute( 'CREATE TABLE local_ratings ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, rating REAL, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE local_ratings ( service_id INTEGER, hash_id INTEGER, rating REAL, PRIMARY KEY ( service_id, hash_id ) );' )
self._CreateIndex( 'local_ratings', [ 'hash_id' ] )
self._CreateIndex( 'local_ratings', [ 'rating' ] )
@ -1709,30 +1709,25 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE options ( options TEXT_YAML );', )
self._c.execute( 'CREATE TABLE recent_tags ( service_id INTEGER REFERENCES services ON DELETE CASCADE, tag_id INTEGER, timestamp INTEGER, PRIMARY KEY ( service_id, tag_id ) );' )
self._c.execute( 'CREATE TABLE remote_ratings ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, count INTEGER, rating REAL, score REAL, PRIMARY KEY ( service_id, hash_id ) );' )
self._CreateIndex( 'remote_ratings', [ 'hash_id' ] )
self._CreateIndex( 'remote_ratings', [ 'rating' ] )
self._CreateIndex( 'remote_ratings', [ 'score' ] )
self._c.execute( 'CREATE TABLE recent_tags ( service_id INTEGER, tag_id INTEGER, timestamp INTEGER, PRIMARY KEY ( service_id, tag_id ) );' )
self._c.execute( 'CREATE TABLE remote_thumbnails ( service_id INTEGER, hash_id INTEGER, PRIMARY KEY( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_filenames ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, filename TEXT, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_directories ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, num_files INTEGER, total_size INTEGER, note TEXT, PRIMARY KEY ( service_id, directory_id ) );' )
self._c.execute( 'CREATE TABLE service_directory_file_map ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, hash_id INTEGER, PRIMARY KEY ( service_id, directory_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_filenames ( service_id INTEGER, hash_id INTEGER, filename TEXT, PRIMARY KEY ( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_directories ( service_id INTEGER, directory_id INTEGER, num_files INTEGER, total_size INTEGER, note TEXT, PRIMARY KEY ( service_id, directory_id ) );' )
self._c.execute( 'CREATE TABLE service_directory_file_map ( service_id INTEGER, directory_id INTEGER, hash_id INTEGER, PRIMARY KEY ( service_id, directory_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_info ( service_id INTEGER REFERENCES services ON DELETE CASCADE, info_type INTEGER, info INTEGER, PRIMARY KEY ( service_id, info_type ) );' )
self._c.execute( 'CREATE TABLE service_info ( service_id INTEGER, info_type INTEGER, info INTEGER, PRIMARY KEY ( service_id, info_type ) );' )
self._c.execute( 'CREATE TABLE statuses ( status_id INTEGER PRIMARY KEY, status TEXT UNIQUE );' )
self._c.execute( 'CREATE TABLE tag_parents ( service_id INTEGER REFERENCES services ON DELETE CASCADE, child_tag_id INTEGER, parent_tag_id INTEGER, status INTEGER, PRIMARY KEY ( service_id, child_tag_id, parent_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_parents ( service_id INTEGER, child_tag_id INTEGER, parent_tag_id INTEGER, status INTEGER, PRIMARY KEY ( service_id, child_tag_id, parent_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_parent_petitions ( service_id INTEGER REFERENCES services ON DELETE CASCADE, child_tag_id INTEGER, parent_tag_id INTEGER, status INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, child_tag_id, parent_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_parent_petitions ( service_id INTEGER, child_tag_id INTEGER, parent_tag_id INTEGER, status INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, child_tag_id, parent_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_siblings ( service_id INTEGER REFERENCES services ON DELETE CASCADE, bad_tag_id INTEGER, good_tag_id INTEGER, status INTEGER, PRIMARY KEY ( service_id, bad_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_siblings ( service_id INTEGER, bad_tag_id INTEGER, good_tag_id INTEGER, status INTEGER, PRIMARY KEY ( service_id, bad_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_sibling_petitions ( service_id INTEGER REFERENCES services ON DELETE CASCADE, bad_tag_id INTEGER, good_tag_id INTEGER, status INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, bad_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE tag_sibling_petitions ( service_id INTEGER, bad_tag_id INTEGER, good_tag_id INTEGER, status INTEGER, reason_id INTEGER, PRIMARY KEY ( service_id, bad_tag_id, status ) );' )
self._c.execute( 'CREATE TABLE url_map ( hash_id INTEGER, url_id INTEGER, PRIMARY KEY ( hash_id, url_id ) );' )
self._CreateIndex( 'url_map', [ 'url_id' ] )
@ -2195,9 +2190,27 @@ class DB( HydrusDB.HydrusDB ):
service_key = service.GetServiceKey()
service_type = service.GetServiceType()
# for a long time, much of this was done with foreign keys, which had to be turned on especially for this operation
# however, this seemed to cause some immense temp drive space bloat when dropping the mapping tables, as there seems to be a trigger/foreign reference check for every row to be deleted
# so now we just blat all tables and trust in the Lord that we don't forget to add any new ones in future
self._c.execute( 'DELETE FROM services WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM current_files WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM deleted_files WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM file_transfers WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM file_petitions WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM local_ratings WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM recent_tags WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM remote_thumbnails WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM service_filenames WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM service_directories WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM service_directory_file_map WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM service_info WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM tag_parents WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM tag_parent_petitions WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM tag_siblings WHERE service_id = ?;', ( service_id, ) )
self._c.execute( 'DELETE FROM tag_sibling_petitions WHERE service_id = ?;', ( service_id, ) )
if service_type in HC.REPOSITORIES:
@ -6916,7 +6929,7 @@ class DB( HydrusDB.HydrusDB ):
( timestamp, ) = result
note = 'Currently in trash ({}). Sent there at {}, which was {} before this check.'.format( file_deletion_reason, HydrusData.ConvertTimestampToPrettyTime( timestamp ), HydrusData.TimestampToPrettyTimeDelta( timestamp, just_now_threshold = 0 ) )
note = 'Currently in trash ({}). Sent there at {}, which was {} before this check.'.format( file_deletion_reason, HydrusData.ConvertTimestampToPrettyTime( timestamp ), ClientData.TimestampToPrettyTimeDelta( timestamp, just_now_threshold = 0 ) )
return ( CC.STATUS_DELETED, hash, prefix + note )
@ -6945,7 +6958,7 @@ class DB( HydrusDB.HydrusDB ):
note = 'Imported at {}, which was {} before this check.'.format( HydrusData.ConvertTimestampToPrettyTime( timestamp ), HydrusData.TimestampToPrettyTimeDelta( timestamp, just_now_threshold = 0 ) )
note = 'Imported at {}, which was {} before this check.'.format( HydrusData.ConvertTimestampToPrettyTime( timestamp ), ClientData.TimestampToPrettyTimeDelta( timestamp, just_now_threshold = 0 ) )
return ( CC.STATUS_SUCCESSFUL_BUT_REDUNDANT, hash, prefix + note )
@ -8675,7 +8688,7 @@ class DB( HydrusDB.HydrusDB ):
if minimum_age is not None:
message += ' with minimum age ' + HydrusData.TimestampToPrettyTimeDelta( timestamp_cutoff, just_now_threshold = 0 ) + ','
message += ' with minimum age ' + ClientData.TimestampToPrettyTimeDelta( timestamp_cutoff, just_now_threshold = 0 ) + ','
message += ' I found ' + HydrusData.ToHumanInt( len( hash_ids ) ) + '.'
@ -9179,7 +9192,7 @@ class DB( HydrusDB.HydrusDB ):
return False
HG.client_controller.pub( 'splash_set_status_subtext', 'cached ' + HydrusData.TimestampToPrettyTimeDelta( stop_time, just_now_string = 'ok', just_now_threshold = 1 ) )
HG.client_controller.pub( 'splash_set_status_subtext', 'cached ' + ClientData.TimestampToPrettyTimeDelta( stop_time, just_now_string = 'ok', just_now_threshold = 1 ) )
next_stop_time_presentation = HydrusData.GetNow() + 1
@ -12082,12 +12095,6 @@ class DB( HydrusDB.HydrusDB ):
def _ResetRepository( self, service ):
self._Commit()
self._c.execute( 'PRAGMA foreign_keys = ON;' )
self._BeginImmediate()
( service_key, service_type, name, dictionary ) = service.ToTuple()
service_id = self._GetServiceId( service_key )
@ -14813,6 +14820,55 @@ class DB( HydrusDB.HydrusDB ):
if version == 401:
result = self._c.execute( 'SELECT 1 FROM sqlite_master WHERE name = ?;', ( 'remote_ratings', ) ).fetchone()
if result is not None:
try:
# never used
self._c.execute( 'DROP TABLE remote_ratings;' )
except Exception as e:
HydrusData.PrintException( e )
try:
domain_manager = self._GetJSONDump( HydrusSerialisable.SERIALISABLE_TYPE_NETWORK_DOMAIN_MANAGER )
domain_manager.Initialise()
#
domain_manager.OverwriteDefaultURLClasses( [ 'nitter tweet', 'twitter tweet' ] )
#
domain_manager.OverwriteDefaultParsers( [ 'nitter media parser', 'nitter retweet parser', 'nitter tweet parser (video from koto.reisen)', 'nitter tweet parser' ] )
#
domain_manager.TryToLinkURLClassesAndParsers()
#
self._SetJSONDump( domain_manager )
except Exception as e:
HydrusData.PrintException( e )
message = 'Trying to update some downloaders failed! Please let hydrus dev know!'
self.pub_initial_message( message )
self._controller.pub( 'splash_set_title_text', 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
@ -15159,12 +15215,6 @@ class DB( HydrusDB.HydrusDB ):
def _UpdateServices( self, services ):
self._Commit()
self._c.execute( 'PRAGMA foreign_keys = ON;' )
self._BeginImmediate()
current_service_keys = { service_key for ( service_key, ) in self._c.execute( 'SELECT service_key FROM services;' ) }
future_service_keys = { service.GetServiceKey() for service in services }

View File

@ -127,34 +127,3 @@ def DAEMONMaintainTrash( controller ):
def DAEMONSynchroniseRepositories( controller ):
if not controller.options[ 'pause_repo_sync' ]:
services = controller.services_manager.GetServices( HC.REPOSITORIES, randomised = True )
for service in services:
if HydrusThreading.IsThreadShuttingDown():
return
if controller.options[ 'pause_repo_sync' ]:
return
service.SyncRemote()
service.SyncProcessUpdates( maintenance_mode = HC.MAINTENANCE_IDLE )
if HydrusThreading.IsThreadShuttingDown():
return
time.sleep( 1 )

View File

@ -357,6 +357,17 @@ def ShowTextClient( text ):
HG.client_controller.pub( 'message', job_key )
def TimestampToPrettyTimeDelta( timestamp, just_now_string = 'now', just_now_threshold = 3, show_seconds = True ):
if HG.client_controller.new_options.GetBoolean( 'always_show_iso_time' ):
return HydrusData.ConvertTimestampToPrettyTime( timestamp )
else:
return HydrusData.TimestampToPrettyTimeDelta( timestamp, just_now_string = just_now_string, just_now_threshold = just_now_threshold, show_seconds = show_seconds )
class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
SERIALISABLE_TYPE = HydrusSerialisable.SERIALISABLE_TYPE_APPLICATION_COMMAND

View File

@ -499,7 +499,7 @@ def GetDefaultObjectsFromPNGs( dir_path, allowed_object_types ):
try:
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
obj = HydrusSerialisable.CreateFromNetworkBytes( payload )

View File

@ -1749,7 +1749,7 @@ class FilesMaintenanceManager( object ):
self._active_work_rules.AddRule( HC.BANDWIDTH_TYPE_REQUESTS, file_maintenance_active_throttle_time_delta, file_maintenance_active_throttle_files * NORMALISED_BIG_JOB_WEIGHT )
def _RunJob( self, media_results, job_type, job_key ):
def _RunJob( self, media_results, job_type, job_key, job_done_hook = None ):
num_bad_files = 0
num_thumb_refits = 0
@ -1776,10 +1776,10 @@ class FilesMaintenanceManager( object ):
return
status_text = '{}: {}'.format( regen_file_enum_to_str_lookup[ job_type ], HydrusData.ConvertValueRangeToPrettyString( i + 1, num_to_do ) )
job_key.SetVariable( 'popup_text_1', status_text )
job_key.SetVariable( 'popup_gauge_1', ( i + 1, num_to_do ) )
if job_done_hook is not None:
job_done_hook( job_type )
additional_data = None
@ -1905,10 +1905,31 @@ class FilesMaintenanceManager( object ):
def ForceMaintenance( self, mandated_job_types = None ):
self._reset_background_event.set()
job_key = ClientThreading.JobKey( cancellable = True )
job_types_to_counts = HG.client_controller.Read( 'file_maintenance_get_job_counts' )
# in a dict so the hook has scope to alter it
vr_status = {}
vr_status[ 'num_jobs_done' ] = 0
total_num_jobs_to_do = sum( ( value for ( key, value ) in job_types_to_counts.items() if mandated_job_types is None or key in mandated_job_types ) )
def job_done_hook( job_type ):
vr_status[ 'num_jobs_done' ] += 1
num_jobs_done = vr_status[ 'num_jobs_done' ]
status_text = '{} - {}'.format( HydrusData.ConvertValueRangeToPrettyString( num_jobs_done, total_num_jobs_to_do ), regen_file_enum_to_str_lookup[ job_type ] )
job_key.SetVariable( 'popup_text_1', status_text )
job_key.SetVariable( 'popup_gauge_1', ( num_jobs_done, total_num_jobs_to_do ) )
self._reset_background_event.set()
job_key.SetVariable( 'popup_title', 'regenerating file data' )
message_pubbed = False
@ -1953,7 +1974,7 @@ class FilesMaintenanceManager( object ):
with self._lock:
self._RunJob( media_results, job_type, job_key )
self._RunJob( media_results, job_type, job_key, job_done_hook = job_done_hook )
time.sleep( 0.0001 )
@ -2118,6 +2139,26 @@ class FilesMaintenanceManager( object ):
job_key = ClientThreading.JobKey( cancellable = True )
total_num_jobs_to_do = len( media_results )
# in a dict so the hook has scope to alter it
vr_status = {}
vr_status[ 'num_jobs_done' ] = 0
def job_done_hook( job_type ):
vr_status[ 'num_jobs_done' ] += 1
num_jobs_done = vr_status[ 'num_jobs_done' ]
status_text = '{} - {}'.format( HydrusData.ConvertValueRangeToPrettyString( num_jobs_done, total_num_jobs_to_do ), regen_file_enum_to_str_lookup[ job_type ] )
job_key.SetVariable( 'popup_text_1', status_text )
job_key.SetVariable( 'popup_gauge_1', ( num_jobs_done, total_num_jobs_to_do ) )
job_key.SetVariable( 'popup_title', 'regenerating file data' )
if pub_job_key:
@ -2131,7 +2172,7 @@ class FilesMaintenanceManager( object ):
try:
self._RunJob( media_results, job_type, job_key )
self._RunJob( media_results, job_type, job_key, job_done_hook = job_done_hook )
finally:

View File

@ -34,7 +34,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'simple' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'detailed_url_information', 'simple' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
def ParseLocalBooruGETArgs( requests_args ):
@ -1210,13 +1210,13 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLInfo( HydrusResourceClientAP
normalised_url = HG.client_controller.network_engine.domain_manager.NormaliseURL( url )
( url_type, match_name, can_parse ) = HG.client_controller.network_engine.domain_manager.GetURLParseCapability( normalised_url )
except HydrusExceptions.URLClassException as e:
raise HydrusExceptions.BadRequestException( e )
( url_type, match_name, can_parse ) = HG.client_controller.network_engine.domain_manager.GetURLParseCapability( normalised_url )
body_dict = { 'normalised_url' : normalised_url, 'url_type' : url_type, 'url_type_string' : HC.url_type_string_lookup[ url_type ], 'match_name' : match_name, 'can_parse' : can_parse }
body = json.dumps( body_dict )
@ -1396,6 +1396,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
def _threadDoGETJob( self, request ):
only_return_identifiers = request.parsed_request_args.GetValue( 'only_return_identifiers', bool, default_value = False )
detailed_url_information = request.parsed_request_args.GetValue( 'detailed_url_information', bool, default_value = False )
try:
@ -1489,6 +1490,31 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
metadata_row[ 'known_urls' ] = known_urls
if detailed_url_information:
detailed_known_urls = []
for known_url in known_urls:
try:
normalised_url = HG.client_controller.network_engine.domain_manager.NormaliseURL( known_url )
( url_type, match_name, can_parse ) = HG.client_controller.network_engine.domain_manager.GetURLParseCapability( normalised_url )
except HydrusExceptions.URLClassException as e:
continue
detailed_dict = { 'normalised_url' : normalised_url, 'url_type' : url_type, 'url_type_string' : HC.url_type_string_lookup[ url_type ], 'match_name' : match_name, 'can_parse' : can_parse }
detailed_known_urls.append( detailed_dict )
metadata_row[ 'detailed_known_urls' ] = detailed_known_urls
tags_manager = media_result.GetTagsManager()
service_names_to_statuses_to_tags = {}

View File

@ -14,7 +14,6 @@ from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTags
# now let's fill out grandparents
def BuildServiceKeysToChildrenToParents( service_keys_to_simple_children_to_parents ):
@ -258,8 +257,6 @@ class BitmapManager( object ):
height = 20
key = ( width, height )
return QG.QPixmap( width, height )
@ -517,8 +514,7 @@ class ServicesManager( object ):
key = lambda s: s.GetName()
self._services_sorted = list( services )
self._services_sorted.sort( key = key )
self._services_sorted = sorted( services, key = key )
def Filter( self, service_keys: typing.Iterable[ bytes ], desired_types: typing.Iterable[ int ] ):

View File

@ -138,7 +138,7 @@ def CreateTopImage( width, title, payload_description, text ):
return top_image
def DumpToPng( width, payload_bytes, title, payload_description, text, path ):
def DumpToPNG( width, payload_bytes, title, payload_description, text, path ):
payload_bytes_length = len( payload_bytes )
@ -238,7 +238,7 @@ def GetPayloadDescriptionAndBytes( payload_obj ):
return ( payload_description, payload_bytes )
def LoadFromPng( path ):
def LoadFromPNG( path ):
# this is to deal with unicode paths, which cv2 can't handle
( os_file_handle, temp_path ) = HydrusPaths.GetTempPath()

View File

@ -8,6 +8,7 @@ import traceback
from qtpy import QtWidgets as QW
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientDownloading
from hydrus.client import ClientFiles
from hydrus.client import ClientRatings
@ -626,7 +627,7 @@ class ServiceRemote( Service ):
if not HydrusData.TimeHasPassed( self._no_requests_until ):
raise HydrusExceptions.InsufficientCredentialsException( self._no_requests_reason + ' - next request ' + HydrusData.TimestampToPrettyTimeDelta( self._no_requests_until ) )
raise HydrusExceptions.InsufficientCredentialsException( self._no_requests_reason + ' - next request ' + ClientData.TimestampToPrettyTimeDelta( self._no_requests_until ) )
if including_bandwidth:
@ -839,7 +840,7 @@ class ServiceRestricted( ServiceRemote ):
else:
s = HydrusData.TimestampToPrettyTimeDelta( self._next_account_sync )
s = ClientData.TimestampToPrettyTimeDelta( self._next_account_sync )
return 'next account sync ' + s
@ -1894,15 +1895,24 @@ class ServiceRepository( ServiceRestricted ):
if self._is_mostly_caught_up is None:
next_begin = self._metadata.GetNextUpdateBegin()
# haven't synced new metadata, so def not caught up
if next_begin < two_weeks_ago:
if not self._metadata.HasDoneInitialSync():
self._is_mostly_caught_up = False
return self._is_mostly_caught_up
else:
next_begin = self._metadata.GetNextUpdateBegin()
# haven't synced new metadata, so def not caught up
if next_begin < two_weeks_ago:
self._is_mostly_caught_up = False
return self._is_mostly_caught_up
else:

View File

@ -65,6 +65,7 @@ from hydrus.client.gui import ClientGUIScrolledPanelsManagement
from hydrus.client.gui import ClientGUIScrolledPanelsReview
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import ClientGUIShortcutControls
from hydrus.client.gui import ClientGUISplash
from hydrus.client.gui import ClientGUIStyle
from hydrus.client.gui import ClientGUISubscriptions
from hydrus.client.gui import ClientGUISystemTray
@ -492,8 +493,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self._currently_minimised_to_system_tray = True
QW.QApplication.instance().setQuitOnLastWindowClosed( False )
self.hide()
self._system_tray_hidden_tlws.append( ( self.isMaximized(), self ) )
@ -1181,7 +1180,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if value == 'file':
with QP.FileDialog( self, 'select where to save content', defaultFile = 'result.html', acceptMode = QW.QFileDialog.AcceptSave ) as f_dlg:
with QP.FileDialog( self, 'select where to save content', default_filename = 'result.html', acceptMode = QW.QFileDialog.AcceptSave ) as f_dlg:
if f_dlg.exec() == QW.QDialog.Accepted:
@ -1686,8 +1685,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
if not self._currently_minimised_to_system_tray:
QW.QApplication.instance().setQuitOnLastWindowClosed( False )
visible_tlws = [ tlw for tlw in QW.QApplication.topLevelWidgets() if tlw.isVisible() or tlw.isMinimized() ]
visible_dialogs = [ tlw for tlw in visible_tlws if isinstance( tlw, QW.QDialog ) ]
@ -1740,8 +1737,6 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
self.RestoreOrActivateWindow()
QW.QApplication.instance().setQuitOnLastWindowClosed( True )
self._currently_minimised_to_system_tray = not self._currently_minimised_to_system_tray
@ -2980,7 +2975,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
HC.options[ 'pause_repo_sync' ] = not HC.options[ 'pause_repo_sync' ]
self._controller.pub( 'notify_restart_repo_sync_daemon' )
self._controller.pub( 'notify_restart_repo_sync' )
elif sync_type == 'export_folders':
@ -3824,7 +3819,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._system_tray_icon.highlight.connect( self.RestoreOrActivateWindow )
self._system_tray_icon.flip_show_ui.connect( self._FlipShowHideWholeUI )
self._system_tray_icon.exit_client.connect( self.TryToSaveAndClose )
self._system_tray_icon.exit_client.connect( self.TryToExit )
self._system_tray_icon.flip_pause_network_jobs.connect( self.FlipNetworkTrafficPaused )
self._system_tray_icon.flip_pause_subscription_jobs.connect( self.FlipSubscriptionsPaused )
@ -4006,9 +4001,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
return
exit_allowed = self.TryToSaveAndClose()
self.TryToExit()
event.ignore()
event.ignore() # we always ignore, as we'll close through the window through other means
def DeleteAllClosedPages( self ):
@ -4177,7 +4172,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
window.TIMERAnimationUpdate()
except Exception as e:
except Exception:
self._animation_update_windows.discard( window )
@ -4740,6 +4735,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a non-cancellable modal popup in five seconds', 'Throw up a delayed modal popup to test with. It will stay alive for five seconds.', self._DebugMakeDelayedModalPopup, False )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a new page in five seconds', 'Throw a delayed page at the main notebook, giving you time to minimise or otherwise alter the client before it arrives.', self._controller.CallLater, 5, self._controller.pub, 'new_page_query', CC.LOCAL_FILE_SERVICE_KEY )
ClientGUIMenus.AppendMenuItem( gui_actions, 'refresh pages menu in five seconds', 'Delayed refresh the pages menu, giving you time to minimise or otherwise alter the client before it arrives.', self._controller.CallLater, 5, self._menu_updater_pages.update )
ClientGUIMenus.AppendMenuItem( gui_actions, 'publish some sub files in five seconds', 'Publish some files like a subscription would.', self._controller.CallLater, 5, lambda: HG.client_controller.pub( 'imported_files_to_page', [ HydrusData.GenerateKey() for i in range( 5 ) ], 'example sub files' ) )
ClientGUIMenus.AppendMenuItem( gui_actions, 'make a parentless text ctrl dialog', 'Make a parentless text control in a dialog to test some character event catching.', self._DebugMakeParentlessTextCtrl )
ClientGUIMenus.AppendMenuItem( gui_actions, 'force a main gui layout now', 'Tell the gui to relayout--useful to test some gui bootup layout issues.', self.adjustSize )
ClientGUIMenus.AppendMenuItem( gui_actions, 'save \'last session\' gui session', 'Make an immediate save of the \'last session\' gui session. Mostly for testing crashes, where last session is not saved correctly.', self.ProposeSaveGUISession, 'last session' )
@ -4905,12 +4901,12 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if not we_borked_linux_pyinstaller:
ClientGUIMenus.AppendMenuItem( menu, 'restart', 'Shut the client down and then start it up again.', self.TryToSaveAndClose, restart = True )
ClientGUIMenus.AppendMenuItem( menu, 'restart', 'Shut the client down and then start it up again.', self.TryToExit, restart = True )
ClientGUIMenus.AppendMenuItem( menu, 'exit and force shutdown maintenance', 'Shut the client down and force any outstanding shutdown maintenance to run.', self.TryToSaveAndClose, force_shutdown_maintenance = True )
ClientGUIMenus.AppendMenuItem( menu, 'exit and force shutdown maintenance', 'Shut the client down and force any outstanding shutdown maintenance to run.', self.TryToExit, force_shutdown_maintenance = True )
ClientGUIMenus.AppendMenuItem( menu, 'exit', 'Shut the client down.', self.TryToSaveAndClose )
ClientGUIMenus.AppendMenuItem( menu, 'exit', 'Shut the client down.', self.TryToExit )
return ( menu, '&file' )
@ -5502,15 +5498,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if action == 'exit_application':
self.TryToSaveAndClose()
self.TryToExit()
elif action == 'exit_application_force_maintenance':
self.TryToSaveAndClose( force_shutdown_maintenance = True )
self.TryToExit( force_shutdown_maintenance = True )
elif action == 'restart_application':
self.TryToSaveAndClose( restart = True )
self.TryToExit( restart = True )
elif action == 'hide_to_system_tray':
@ -6077,11 +6073,6 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
return
if not HG.emergency_exit:
self._controller.CreateSplash( 'hydrus client exiting' )
HG.client_controller.pub( 'pause_all_media' )
try:
@ -6107,7 +6098,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
for tlw in QW.QApplication.topLevelWidgets():
if not isinstance( tlw, FrameSplash ):
if not isinstance( tlw, ClientGUISplash.FrameSplash ):
tlw.hide()
@ -6147,18 +6138,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HydrusData.PrintException( e )
if HG.emergency_exit:
self.deleteLater()
self._controller.Exit()
else:
QP.CallAfter( self._controller.Exit )
self.deleteLater()
self.deleteLater()
def SetMediaFocus( self ):
@ -6183,11 +6163,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._notebook.ShowPage( page )
def TryToSaveAndClose( self, restart = False, force_shutdown_maintenance = False ):
def TryToExit( self, restart = False, force_shutdown_maintenance = False ):
# the return value here is 'exit allowed'
if not HG.emergency_exit:
if not self._controller.DoingFastExit():
able_to_close_statement = self._notebook.GetTestAbleToCloseStatement()
@ -6212,7 +6190,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
if result == QW.QDialog.Rejected:
return False
return
@ -6222,14 +6200,72 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HG.restart = True
if force_shutdown_maintenance:
if force_shutdown_maintenance or HG.do_idle_shutdown_work:
HG.do_idle_shutdown_work = True
else:
try:
idle_shutdown_action = self._controller.options[ 'idle_shutdown' ]
shutdown_work_ok_by_options = idle_shutdown_action in ( CC.IDLE_ON_SHUTDOWN, CC.IDLE_ON_SHUTDOWN_ASK_FIRST )
last_shutdown_work_time = self._controller.Read( 'last_shutdown_work_time' )
shutdown_work_period = self._controller.new_options.GetInteger( 'shutdown_work_period' )
shutdown_work_due = HydrusData.TimeHasPassed( last_shutdown_work_time + shutdown_work_period )
if shutdown_work_due:
if idle_shutdown_action == CC.IDLE_ON_SHUTDOWN:
HG.do_idle_shutdown_work = True
elif idle_shutdown_action == CC.IDLE_ON_SHUTDOWN_ASK_FIRST:
idle_shutdown_max_minutes = self._controller.options[ 'idle_shutdown_max_minutes' ]
time_to_stop = HydrusData.GetNow() + ( idle_shutdown_max_minutes * 60 )
work_to_do = self._controller.GetIdleShutdownWorkDue( time_to_stop )
if len( work_to_do ) > 0:
text = 'Is now a good time for the client to do up to ' + HydrusData.ToHumanInt( idle_shutdown_max_minutes ) + ' minutes\' maintenance work? (Will auto-no in 15 seconds)'
text += os.linesep * 2
text += 'The outstanding jobs appear to be:'
text += os.linesep * 2
text += os.linesep.join( work_to_do )
result = ClientGUIDialogsQuick.GetYesNo( self, text, title = 'Maintenance is due', auto_no_time = 15 )
if result == QW.QDialog.Accepted:
HG.do_idle_shutdown_work = True
else:
# if they said no, don't keep asking
self._controller.Write( 'last_shutdown_work_time', HydrusData.GetNow() )
except Exception as e:
self._controller.SafeShowCriticalMessage( 'shutdown error', 'There was a problem trying to review pending shutdown maintenance work. No shutdown maintenance work will be done, and info has been written to the log. Please let hydev know.' )
HydrusData.PrintException( e )
HG.do_idle_shutdown_work = False
self.SaveAndClose()
return True
QP.CallAfter( self._controller.Exit )
def UnregisterAnimationUpdateWindow( self, window ):
@ -6242,246 +6278,3 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._ui_update_windows.discard( window )
class FrameSplashPanel( QW.QWidget ):
def __init__( self, parent, controller ):
QW.QWidget.__init__( self, parent )
self._controller = controller
self._my_status = FrameSplashStatus( self._controller, self )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self, 64 )
self.setMinimumWidth( width )
self.setMaximumWidth( width * 2 )
self._drag_last_pos = None
self._initial_position = self.parentWidget().pos()
# this is 124 x 166
self._hydrus_pixmap = QG.QPixmap( os.path.join( HC.STATIC_DIR, 'hydrus_splash.png' ) )
self._image_label = QW.QLabel( self )
self._image_label.setPixmap( self._hydrus_pixmap )
self._image_label.setAlignment( QC.Qt.AlignCenter )
self._title_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._status_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._status_sub_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._title_label.setAlignment( QC.Qt.AlignCenter )
self._status_label.setAlignment( QC.Qt.AlignCenter )
self._status_sub_label.setAlignment( QC.Qt.AlignCenter )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._image_label, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, self._title_label, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._status_label, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._status_sub_label, CC.FLAGS_EXPAND_PERPENDICULAR )
margin = ClientGUIFunctions.ConvertTextToPixelWidth( self, 3 )
self._image_label.setMargin( margin )
self.setLayout( vbox )
def mouseMoveEvent( self, event ):
if ( event.buttons() & QC.Qt.LeftButton ) and self._drag_last_pos is not None:
mouse_pos = QG.QCursor.pos()
delta = mouse_pos - self._drag_last_pos
win = self.window()
win.move( win.pos() + delta )
self._drag_last_pos = QC.QPoint( mouse_pos )
event.accept()
return
QW.QWidget.mouseMoveEvent( self, event )
def mousePressEvent( self, event ):
if event.button() == QC.Qt.LeftButton:
self._drag_last_pos = QG.QCursor.pos()
event.accept()
return
QW.QWidget.mousePressEvent( self, event )
def mouseReleaseEvent( self, event ):
if event.button() == QC.Qt.LeftButton:
self._drag_last_pos = None
event.accept()
return
QW.QWidget.mouseReleaseEvent( self, event )
def SetDirty( self ):
( title_text, status_text, status_subtext ) = self._my_status.GetTexts()
self._title_label.setText( title_text )
self._status_label.setText( status_text )
self._status_sub_label.setText( status_subtext )
# We have this to be an off-Qt-thread-happy container for this info, as the framesplash has to deal with messages in the fuzzy time of shutdown
# all of a sudden, pubsubs are processed in non Qt-thread time, so this handles that safely and lets the gui know if the Qt controller is still running
class FrameSplashStatus( object ):
def __init__( self, controller, ui ):
self._controller = controller
self._lock = threading.Lock()
self._updater = ClientGUIAsync.FastThreadToGUIUpdater( ui, ui.SetDirty )
self._title_text = ''
self._status_text = ''
self._status_subtext = ''
self._controller.sub( self, 'SetTitleText', 'splash_set_title_text' )
self._controller.sub( self, 'SetText', 'splash_set_status_text' )
self._controller.sub( self, 'SetSubtext', 'splash_set_status_subtext' )
def _NotifyUI( self ):
self._updater.Update()
def GetTexts( self ):
with self._lock:
return ( self._title_text, self._status_text, self._status_subtext )
def SetText( self, text, print_to_log = True ):
if print_to_log and len( text ) > 0:
HydrusData.Print( text )
with self._lock:
self._status_text = text
self._status_subtext = ''
self._NotifyUI()
def SetSubtext( self, text ):
with self._lock:
self._status_subtext = text
self._NotifyUI()
def SetTitleText( self, text, clear_undertexts = True, print_to_log = True ):
if print_to_log:
HydrusData.DebugPrint( text )
with self._lock:
self._title_text = text
if clear_undertexts:
self._status_text = ''
self._status_subtext = ''
self._NotifyUI()
class FrameSplash( QW.QWidget ):
def __init__( self, controller, title ):
self._controller = controller
QW.QWidget.__init__( self, None )
self.setWindowFlag( QC.Qt.CustomizeWindowHint )
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
self.setWindowFlag( QC.Qt.WindowCloseButtonHint, on = False )
self.setWindowFlag( QC.Qt.WindowMaximizeButtonHint, on = False )
self.setAttribute( QC.Qt.WA_DeleteOnClose )
self.setWindowTitle( title )
self.setWindowIcon( QG.QIcon( self._controller.frame_icon_pixmap ) )
self._my_panel = FrameSplashPanel( self, self._controller )
self._vbox = QP.VBoxLayout()
QP.AddToLayout( self._vbox, self._my_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.setLayout( self._vbox )
screen = ClientGUIFunctions.GetMouseScreen()
if screen is not None:
self.move( screen.availableGeometry().center() - self.rect().center() )
self.show()
self.raise_()
def CancelShutdownMaintenance( self ):
self._cancel_shutdown_maintenance.setText( 'stopping\u2026' )
self._cancel_shutdown_maintenance.setEnabled( False )
HG.do_idle_shutdown_work = False
def MakeCancelShutdownButton( self ):
self._cancel_shutdown_maintenance = ClientGUICommon.BetterButton( self, 'stop shutdown maintenance', self.CancelShutdownMaintenance )
self._vbox.insertWidget( 0, self._cancel_shutdown_maintenance )

View File

@ -1378,6 +1378,11 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
for service in services:
if service.GetServiceKey() == CC.LOCAL_UPDATE_SERVICE_KEY and not advanced_mode:
continue
ClientGUIMenus.AppendMenuItem( menu, service.GetName(), 'Change the current file domain to ' + service.GetName() + '.', self._ChangeFileService, service.GetServiceKey() )

View File

@ -459,29 +459,12 @@ class Canvas( QW.QWidget ):
def _CopyHashToClipboard( self, hash_type ):
sha256_hash = self._current_media.GetHash()
if hash_type == 'sha256':
if self._current_media is None:
hex_hash = sha256_hash.hex()
else:
if self._current_media.GetLocationsManager().IsLocal():
( other_hash, ) = HG.client_controller.Read( 'file_hashes', ( sha256_hash, ), 'sha256', hash_type )
hex_hash = other_hash.hex()
else:
QW.QMessageBox.warning( self, 'Warning', 'Unfortunately, you do not have that file in your database, so its non-sha256 hashes are unknown.' )
return
return
HG.client_controller.pub( 'clipboard', 'text', hex_hash )
ClientGUIMedia.CopyHashesToClipboard( self, hash_type, [ self._current_media ] )
def _CopyFileToClipboard( self ):
@ -1482,6 +1465,18 @@ class Canvas( QW.QWidget ):
self._CopyHashToClipboard( 'sha256' )
elif action == 'copy_md5_hash':
self._CopyHashToClipboard( 'md5' )
elif action == 'copy_sha1_hash':
self._CopyHashToClipboard( 'sha1' )
elif action == 'copy_sha512_hash':
self._CopyHashToClipboard( 'sha512' )
elif action == 'delete_file':
self._Delete()

View File

@ -87,15 +87,15 @@ class CanvasFrame( ClientGUITopLevelWindows.FrameThatResizesWithHovers ):
if action == 'exit_application':
HG.client_controller.gui.TryToSaveAndClose()
HG.client_controller.gui.TryToExit()
elif action == 'exit_application_force_maintenance':
HG.client_controller.gui.TryToSaveAndClose( force_shutdown_maintenance = True )
HG.client_controller.gui.TryToExit( force_shutdown_maintenance = True )
elif action == 'restart_application':
HG.client_controller.gui.TryToSaveAndClose( restart = True )
HG.client_controller.gui.TryToExit( restart = True )
elif action == 'hide_to_system_tray':

View File

@ -11,6 +11,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientPaths
from hydrus.client import ClientSerialisable
from hydrus.client.gui import ClientGUICommon
@ -88,8 +89,8 @@ class EditFileSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
pretty_file_seed_data = str( file_seed_data )
pretty_status = CC.status_string_lookup[ status ]
pretty_added = HydrusData.TimestampToPrettyTimeDelta( added )
pretty_modified = HydrusData.TimestampToPrettyTimeDelta( modified )
pretty_added = ClientData.TimestampToPrettyTimeDelta( added )
pretty_modified = ClientData.TimestampToPrettyTimeDelta( modified )
if source_time is None:
@ -97,7 +98,7 @@ class EditFileSeedCachePanel( ClientGUIScrolledPanels.EditPanel ):
else:
pretty_source_time = HydrusData.TimestampToPrettyTimeDelta( source_time )
pretty_source_time = ClientData.TimestampToPrettyTimeDelta( source_time )
sort_source_time = ClientGUIListCtrl.SafeNoneInt( source_time )
@ -435,7 +436,7 @@ class FileSeedCacheButton( ClientGUICommon.BetterBitmapButton ):
def _ImportFromPng( self ):
def _ImportFromPNG( self ):
with QP.FileDialog( self, 'select the png with the sources', wildcard = 'PNG (*.png)' ) as dlg:
@ -443,7 +444,7 @@ class FileSeedCacheButton( ClientGUICommon.BetterBitmapButton ):
path = dlg.GetPath()
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
try:
@ -479,13 +480,13 @@ class FileSeedCacheButton( ClientGUICommon.BetterBitmapButton ):
file_seed_cache.AddFileSeeds( file_seeds )
def _ExportToPng( self ):
def _ExportToPNG( self ):
payload = self._GetExportableSourcesString()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, payload )
panel = ClientGUISerialisable.PNGExportPanel( dlg, payload )
dlg.SetPanel( panel )
@ -678,7 +679,7 @@ class FileSeedCacheButton( ClientGUICommon.BetterBitmapButton ):
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'to clipboard', 'Copy all the sources in this list to the clipboard.', self._ExportToClipboard )
ClientGUIMenus.AppendMenuItem( submenu, 'to png', 'Export all the sources in this list to a png file.', self._ExportToPng )
ClientGUIMenus.AppendMenuItem( submenu, 'to png', 'Export all the sources in this list to a png file.', self._ExportToPNG )
ClientGUIMenus.AppendMenu( menu, submenu, 'export all sources' )
@ -686,7 +687,7 @@ class FileSeedCacheButton( ClientGUICommon.BetterBitmapButton ):
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'from clipboard', 'Import new urls or paths to this list from the clipboard.', self._ImportFromClipboard )
ClientGUIMenus.AppendMenuItem( submenu, 'from png', 'Import new urls or paths to this list from a png file.', self._ImportFromPng )
ClientGUIMenus.AppendMenuItem( submenu, 'from png', 'Import new urls or paths to this list from a png file.', self._ImportFromPNG )
ClientGUIMenus.AppendMenu( menu, submenu, 'import new sources' )

View File

@ -65,7 +65,7 @@ class ShowKeys( ClientGUITopLevelWindows.Frame ):
filename = 'keys.txt'
with QP.FileDialog( self, acceptMode = QW.QFileDialog.AcceptSave, defaultFile = filename ) as dlg:
with QP.FileDialog( self, acceptMode = QW.QFileDialog.AcceptSave, default_filename = filename ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:

View File

@ -9,6 +9,7 @@ from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientPaths
from hydrus.client import ClientSerialisable
from hydrus.client.gui import ClientGUICommon
@ -83,8 +84,8 @@ class EditGallerySeedLogPanel( ClientGUIScrolledPanels.EditPanel ):
pretty_gallery_seed_index = HydrusData.ToHumanInt( gallery_seed_index )
pretty_url = url
pretty_status = CC.status_string_lookup[ status ]
pretty_added = HydrusData.TimestampToPrettyTimeDelta( added )
pretty_modified = HydrusData.TimestampToPrettyTimeDelta( modified )
pretty_added = ClientData.TimestampToPrettyTimeDelta( added )
pretty_modified = ClientData.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 )
@ -356,7 +357,7 @@ class GallerySeedLogButton( ClientGUICommon.BetterBitmapButton ):
def _ImportFromPng( self ):
def _ImportFromPNG( self ):
with QP.FileDialog( self, 'select the png with the urls', wildcard = 'PNG (*.png)' ) as dlg:
@ -364,7 +365,7 @@ class GallerySeedLogButton( ClientGUICommon.BetterBitmapButton ):
path = dlg.GetPath()
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
try:
@ -435,13 +436,13 @@ class GallerySeedLogButton( ClientGUICommon.BetterBitmapButton ):
gallery_seed_log.AddGallerySeeds( gallery_seeds )
def _ExportToPng( self ):
def _ExportToPNG( self ):
payload = self._GetExportableURLsString()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, payload )
panel = ClientGUISerialisable.PNGExportPanel( dlg, payload )
dlg.SetPanel( panel )
@ -557,7 +558,7 @@ class GallerySeedLogButton( ClientGUICommon.BetterBitmapButton ):
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'to clipboard', 'Copy all the urls in this list to the clipboard.', self._ExportToClipboard )
ClientGUIMenus.AppendMenuItem( submenu, 'to png', 'Export all the urls in this list to a png file.', self._ExportToPng )
ClientGUIMenus.AppendMenuItem( submenu, 'to png', 'Export all the urls in this list to a png file.', self._ExportToPNG )
ClientGUIMenus.AppendMenu( menu, submenu, 'export all urls' )
@ -567,7 +568,7 @@ class GallerySeedLogButton( ClientGUICommon.BetterBitmapButton ):
submenu = QW.QMenu( menu )
ClientGUIMenus.AppendMenuItem( submenu, 'from clipboard', 'Import new urls to this list from the clipboard.', self._ImportFromClipboard )
ClientGUIMenus.AppendMenuItem( submenu, 'from png', 'Import new urls to this list from a png file.', self._ImportFromPng )
ClientGUIMenus.AppendMenuItem( submenu, 'from png', 'Import new urls to this list from a png file.', self._ImportFromPNG )
ClientGUIMenus.AppendMenu( menu, submenu, 'import new urls' )

View File

@ -14,6 +14,7 @@ from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTags
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientTags
from hydrus.client.gui import ClientGUIACDropdown
from hydrus.client.gui import ClientGUICommon
@ -2478,7 +2479,7 @@ class WatcherReviewPanel( ClientGUICommon.StaticBox ):
else:
watcher_status = 'next check ' + HydrusData.TimestampToPrettyTimeDelta( next_check_time, just_now_threshold = 0 )
watcher_status = 'next check ' + ClientData.TimestampToPrettyTimeDelta( next_check_time, just_now_threshold = 0 )

View File

@ -25,6 +25,184 @@ from hydrus.client.gui import ClientGUISearch
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import QtPorting as QP
class BetterQListWidget( QW.QListWidget ):
def _DeleteIndices( self, indices: typing.Iterable[ int ] ):
indices = sorted( indices, reverse = True )
for index in indices:
item = self.takeItem( index )
del item
def _GetDataIndices( self, datas: typing.Iterable[ object ] ) -> typing.List[ int ]:
indices = []
for index in range( self.count() ):
list_widget_item = self.item( index )
data = self._GetRowData( list_widget_item )
if data in datas:
indices.append( index )
return indices
def _GetListWidgetItems( self, only_selected = False ):
# not sure if selectedItems is always sorted, so just do it manually
list_widget_items = []
for index in range( self.count() ):
list_widget_item = self.item( index )
if only_selected and not list_widget_item.isSelected():
continue
list_widget_items.append( list_widget_item )
return list_widget_items
def _GetRowData( self, list_widget_item: QW.QListWidgetItem ):
return list_widget_item.data( QC.Qt.UserRole )
def _GetSelectedIndices( self ):
return [ model_index.row() for model_index in self.selectedIndexes() ]
def _MoveRow( self, index: int, distance: int ):
new_index = index + distance
new_index = max( 0, new_index )
new_index = min( new_index, self.count() - 1 )
if index == new_index:
return
was_selected = self.item( index ).isSelected()
list_widget_item = self.takeItem( index )
self.insertItem( new_index, list_widget_item )
list_widget_item.setSelected( was_selected )
def Append( self, text: str, data: object ):
item = QW.QListWidgetItem()
item.setText( text )
item.setData( QC.Qt.UserRole, data )
self.addItem( item )
def DeleteData( self, datas: typing.Iterable[ object ] ):
indices = self._GetDataIndices( datas )
self._DeleteIndices( indices )
def DeleteSelected( self ):
indices = self._GetSelectedIndices()
self._DeleteIndices( indices )
def GetData( self, only_selected: bool = False ) -> typing.List[ object ]:
datas = []
list_widget_items = self._GetListWidgetItems( only_selected = only_selected )
for list_widget_item in list_widget_items:
data = self._GetRowData( list_widget_item )
datas.append( data )
return datas
def GetNumSelected( self ) -> int:
indices = self._GetSelectedIndices()
return len( indices )
def MoveSelected( self, distance: int ):
if distance == 0:
return
# if going up, -1, then do them in ascending order
# if going down, +1, then do them in descending order
indices = sorted( self._GetSelectedIndices(), reverse = distance > 0 )
for index in indices:
self._MoveRow( index, distance )
def PopData( self, index: int ):
if index < 0 or index > self.count() - 1:
return None
list_widget_item = self.item( index )
data = self._GetRowData( list_widget_item )
self._DeleteIndices( [ index ] )
return data
def SelectData( self, datas: typing.Iterable[ object ] ):
list_widget_items = self._GetListWidgetItems()
for list_widget_item in list_widget_items:
data = self._GetRowData( list_widget_item )
list_widget_item.setSelected( data in datas )
class AddEditDeleteListBox( QW.QWidget ):
listBoxChanged = QC.Signal()
@ -37,7 +215,7 @@ class AddEditDeleteListBox( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._listbox = QW.QListWidget( self )
self._listbox = BetterQListWidget( self )
self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection )
self._add_button = ClientGUICommon.BetterButton( self, 'add', self._Add )
@ -112,10 +290,7 @@ class AddEditDeleteListBox( QW.QWidget ):
pretty_data = self._data_to_pretty_callable( data )
item = QW.QListWidgetItem()
item.setText( pretty_data )
item.setData( QC.Qt.UserRole, data )
self._listbox.addItem( item )
self._listbox.Append( pretty_data, data )
def _AddSomeDefaults( self, defaults_callable ):
@ -151,40 +326,32 @@ class AddEditDeleteListBox( QW.QWidget ):
def _Delete( self ):
indices = list( map( lambda idx: idx.row(), self._listbox.selectedIndexes() ) )
num_selected = self._listbox.GetNumSelected()
if len( indices ) == 0:
if num_selected == 0:
return
indices.sort( reverse = True )
from hydrus.client.gui import ClientGUIDialogsQuick
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove {} selected?'.format( HydrusData.ToHumanInt( num_selected ) ) )
if result == QW.QDialog.Accepted:
if result != QW.QDialog.Accepted:
for i in indices:
QP.ListWidgetDelete( self._listbox, i )
return
self._listbox.DeleteSelected()
self.listBoxChanged.emit()
def _Edit( self ):
for i in range( self._listbox.count() ):
for list_widget_item in self._listbox.selectedItems():
if not QP.ListWidgetIsSelected( self._listbox, i ):
continue
data = QP.GetClientData( self._listbox, i )
data = list_widget_item.data( QC.Qt.UserRole )
try:
@ -195,17 +362,12 @@ class AddEditDeleteListBox( QW.QWidget ):
break
QP.ListWidgetDelete( self._listbox, i )
self._SetNoneDupeName( new_data )
pretty_new_data = self._data_to_pretty_callable( new_data )
item = QW.QListWidgetItem()
item.setText( pretty_new_data )
item.setData( QC.Qt.UserRole, new_data )
self._listbox.addItem( item )
self._listbox.insertItem( i, item )
list_widget_item.setText( pretty_new_data )
list_widget_item.setData( QC.Qt.UserRole, new_data )
self.listBoxChanged.emit()
@ -235,7 +397,7 @@ class AddEditDeleteListBox( QW.QWidget ):
def _ExportToPng( self ):
def _ExportToPNG( self ):
export_object = self._GetExportObject()
@ -246,7 +408,7 @@ class AddEditDeleteListBox( QW.QWidget ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object )
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
dlg.SetPanel( panel )
@ -255,7 +417,7 @@ class AddEditDeleteListBox( QW.QWidget ):
def _ExportToPngs( self ):
def _ExportToPNGs( self ):
export_object = self._GetExportObject()
@ -266,7 +428,7 @@ class AddEditDeleteListBox( QW.QWidget ):
if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
self._ExportToPng()
self._ExportToPNG()
return
@ -276,7 +438,7 @@ class AddEditDeleteListBox( QW.QWidget ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to pngs' ) as dlg:
panel = ClientGUISerialisable.PngsExportPanel( dlg, export_object )
panel = ClientGUISerialisable.PNGsExportPanel( dlg, export_object )
dlg.SetPanel( panel )
@ -333,7 +495,7 @@ class AddEditDeleteListBox( QW.QWidget ):
def _ImportFromPng( self ):
def _ImportFromPNG( self ):
with QP.FileDialog( self, 'select the png or pngs with the encoded data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'PNG (*.png)|*.png' ) as dlg:
@ -343,7 +505,7 @@ class AddEditDeleteListBox( QW.QWidget ):
try:
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
except Exception as e:
@ -415,7 +577,7 @@ class AddEditDeleteListBox( QW.QWidget ):
def _ShowHideButtons( self ):
if len( self._listbox.selectedItems() ) == 0:
if self._listbox.GetNumSelected() == 0:
self._edit_button.setEnabled( False )
self._delete_button.setEnabled( False )
@ -469,19 +631,19 @@ class AddEditDeleteListBox( QW.QWidget ):
export_menu_items = []
export_menu_items.append( ( 'normal', 'to clipboard', 'Serialise the selected data and put it on your clipboard.', self._ExportToClipboard ) )
export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPng ) )
export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPNG ) )
all_objs_are_named = False not in ( issubclass( o, HydrusSerialisable.SerialisableBaseNamed ) for o in self._permitted_object_types )
if all_objs_are_named:
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPngs ) )
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPNGs ) )
import_menu_items = []
import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
import_menu_items.append( ( 'normal', 'from pngs', 'Load a data from an encoded png.', self._ImportFromPng ) )
import_menu_items.append( ( 'normal', 'from pngs', 'Load a data from an encoded png.', self._ImportFromPNG ) )
button = ClientGUICommon.MenuButton( self, 'export', export_menu_items )
QP.AddToLayout( self._buttons_hbox, button, CC.FLAGS_VCENTER )
@ -509,21 +671,7 @@ class AddEditDeleteListBox( QW.QWidget ):
def GetData( self, only_selected = False ):
datas = []
for i in range( self._listbox.count() ):
if only_selected and not QP.ListWidgetIsSelected( self._listbox, i ):
continue
data = QP.GetClientData( self._listbox, i )
datas.append( data )
return datas
return self._listbox.GetData( only_selected = only_selected )
def GetValue( self ):
@ -552,7 +700,7 @@ class QueueListBox( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._listbox = QW.QListWidget( self )
self._listbox = BetterQListWidget( self )
self._listbox.setSelectionMode( QW.QListWidget.ExtendedSelection )
self._up_button = ClientGUICommon.BetterButton( self, '\u2191', self._Up )
@ -608,9 +756,11 @@ class QueueListBox( QW.QWidget ):
#
self._listbox.itemSelectionChanged.connect( self.EventSelection )
self._listbox.itemSelectionChanged.connect( self._UpdateButtons )
self._listbox.itemDoubleClicked.connect( self._Edit )
self._UpdateButtons()
def _Add( self ):
@ -632,33 +782,25 @@ class QueueListBox( QW.QWidget ):
pretty_data = self._data_to_pretty_callable( data )
item = QW.QListWidgetItem()
item.setText( pretty_data )
item.setData( QC.Qt.UserRole, data )
self._listbox.addItem( item )
self._listbox.Append( pretty_data, data )
def _Delete( self ):
indices = list( self._listbox.selectedIndexes() )
num_selected = self._listbox.GetNumSelected()
if len( indices ) == 0:
if num_selected == 0:
return
indices.sort( reverse = True )
from hydrus.client.gui import ClientGUIDialogsQuick
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove all selected?' )
result = ClientGUIDialogsQuick.GetYesNo( self, 'Remove {} selected?'.format( HydrusData.ToHumanInt( num_selected ) ) )
if result == QW.QDialog.Accepted:
for i in indices:
QP.ListWidgetDelete( self._listbox, i )
self._listbox.DeleteSelected()
self.listBoxChanged.emit()
@ -666,34 +808,16 @@ class QueueListBox( QW.QWidget ):
def _Down( self ):
indices = list( map( lambda idx: idx.row(), self._listbox.selectedIndexes() ) )
indices.sort( reverse = True )
for i in indices:
if i < self._listbox.count() - 1:
if not QP.ListWidgetIsSelected( self._listbox, i+1 ): # is the one below not selected?
self._SwapRows( i, i + 1 )
self._listbox.MoveSelected( 1 )
self.listBoxChanged.emit()
def _Edit( self ):
for i in range( self._listbox.count() ):
for list_widget_item in self._listbox.selectedItems():
if not QP.ListWidgetIsSelected( self._listbox, i ):
continue
data = QP.GetClientData( self._listbox, i )
data = list_widget_item.data( QC.Qt.UserRole )
try:
@ -704,87 +828,25 @@ class QueueListBox( QW.QWidget ):
break
QP.ListWidgetDelete( self._listbox, i )
pretty_new_data = self._data_to_pretty_callable( new_data )
new_item = QW.QListWidgetItem()
new_item.setText( pretty_new_data )
new_item.setData( QC.Qt.UserRole, new_data )
self._listbox.insertItem( i, new_item )
list_widget_item.setText( pretty_new_data )
list_widget_item.setData( QC.Qt.UserRole, new_data )
self.listBoxChanged.emit()
def _SwapRows( self, index_a, index_b ):
a_was_selected = QP.ListWidgetIsSelected( self._listbox, index_a )
b_was_selected = QP.ListWidgetIsSelected( self._listbox, index_b )
data_a = QP.GetClientData( self._listbox, index_a )
data_b = QP.GetClientData( self._listbox, index_b )
pretty_data_a = self._data_to_pretty_callable( data_a )
pretty_data_b = self._data_to_pretty_callable( data_b )
QP.ListWidgetDelete( self._listbox, index_a )
item_b = QW.QListWidgetItem()
item_b.setText( pretty_data_b )
item_b.setData( QC.Qt.UserRole, data_b )
self._listbox.insertItem( index_a, item_b )
QP.ListWidgetDelete( self._listbox, index_b )
item_a = QW.QListWidgetItem()
item_a.setText( pretty_data_a )
item_a.setData( QC.Qt.UserRole, data_a )
self._listbox.insertItem( index_b, item_a )
if b_was_selected:
QP.ListWidgetSetSelection( self._listbox, index_a )
if a_was_selected:
QP.ListWidgetSetSelection( self._listbox, index_b )
def _Up( self ):
indices = map( lambda idx: idx.row(), self._listbox.selectedIndexes() )
for i in indices:
if i > 0:
if not QP.ListWidgetIsSelected( self._listbox, i-1 ): # is the one above not selected?
self._SwapRows( i, i - 1 )
self._listbox.MoveSelected( -1 )
self.listBoxChanged.emit()
def AddDatas( self, datas ):
def _UpdateButtons( self ):
for data in datas:
self._AddData( data )
self.listBoxChanged.emit()
def EventSelection( self ):
if len( self._listbox.selectedIndexes() ) == 0:
if self._listbox.GetNumSelected() == 0:
self._up_button.setEnabled( False )
self._delete_button.setEnabled( False )
@ -802,6 +864,16 @@ class QueueListBox( QW.QWidget ):
def AddDatas( self, datas ):
for data in datas:
self._AddData( data )
self.listBoxChanged.emit()
def GetCount( self ):
return self._listbox.count()
@ -809,16 +881,7 @@ class QueueListBox( QW.QWidget ):
def GetData( self, only_selected = False ):
datas = []
for i in range( self._listbox.count() ):
data = QP.GetClientData( self._listbox, i )
datas.append( data )
return datas
return self._listbox.GetData( only_selected = only_selected )
def Pop( self ):
@ -828,11 +891,7 @@ class QueueListBox( QW.QWidget ):
return None
data = QP.GetClientData( self._listbox, 0 )
QP.ListWidgetDelete( self._listbox, 0 )
return data
return self._listbox.PopData( 0 )
class ListBox( QW.QScrollArea ):
@ -2898,11 +2957,6 @@ class ListBoxTagsStrings( ListBoxTags ):
def ForceTagRecalc( self ):
if self.window().isMinimized():
return
self._RecalcTags()
@ -3300,11 +3354,6 @@ class ListBoxTagsMedia( ListBoxTags ):
def ForceTagRecalc( self ):
if self.window().isMinimized():
return
self.SetTagsByMedia( self._last_media )

View File

@ -703,7 +703,44 @@ class BetterListCtrlPanel( QW.QWidget ):
def _ExportToPng( self ):
def _ExportToJSON( self ):
export_object = self._GetExportObject()
if export_object is not None:
json = export_object.DumpToString()
with QP.FileDialog( self, 'select where to save the json file', default_filename = 'export.json', wildcard = 'JSON (*.json)', acceptMode = QW.QFileDialog.AcceptSave ) as f_dlg:
if f_dlg.exec() == QW.QDialog.Accepted:
path = f_dlg.GetPath()
if os.path.exists( path ):
from hydrus.client.gui import ClientGUIDialogsQuick
message = 'The path "{}" already exists! Ok to overwrite?'.format( path )
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result != QW.QDialog.Accepted:
return
with open( path, 'w', encoding = 'utf-8' ) as f:
f.write( json )
def _ExportToPNG( self ):
export_object = self._GetExportObject()
@ -714,7 +751,7 @@ class BetterListCtrlPanel( QW.QWidget ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object )
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
dlg.SetPanel( panel )
@ -723,7 +760,7 @@ class BetterListCtrlPanel( QW.QWidget ):
def _ExportToPngs( self ):
def _ExportToPNGs( self ):
export_object = self._GetExportObject()
@ -734,7 +771,7 @@ class BetterListCtrlPanel( QW.QWidget ):
if not isinstance( export_object, HydrusSerialisable.SerialisableList ):
self._ExportToPng()
self._ExportToPNG()
return
@ -744,7 +781,7 @@ class BetterListCtrlPanel( QW.QWidget ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to pngs' ) as dlg:
panel = ClientGUISerialisable.PngsExportPanel( dlg, export_object )
panel = ClientGUISerialisable.PNGsExportPanel( dlg, export_object )
dlg.SetPanel( panel )
@ -819,7 +856,22 @@ class BetterListCtrlPanel( QW.QWidget ):
self._listctrl.Sort()
def _ImportFromPng( self ):
def _ImportFromJSON( self ):
with QP.FileDialog( self, 'select the json or jsons with the serialised data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'JSON (*.json)|*.json' ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
paths = dlg.GetPaths()
self._ImportJSONs( paths )
self._listctrl.Sort()
def _ImportFromPNG( self ):
with QP.FileDialog( self, 'select the png or pngs with the encoded data', acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFiles, wildcard = 'PNG (*.png)|*.png' ) as dlg:
@ -827,7 +879,7 @@ class BetterListCtrlPanel( QW.QWidget ):
paths = dlg.GetPaths()
self._ImportPngs( paths )
self._ImportPNGs( paths )
@ -871,13 +923,46 @@ class BetterListCtrlPanel( QW.QWidget ):
def _ImportPngs( self, paths ):
def _ImportJSONs( self, paths ):
for path in paths:
try:
payload = ClientSerialisable.LoadFromPng( path )
with open( path, 'r', encoding = 'utf-8' ) as f:
payload = f.read()
except Exception as e:
QW.QMessageBox.critical( self, 'Error', str(e) )
return
try:
obj = HydrusSerialisable.CreateFromString( payload )
self._ImportObject( obj )
except:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in "{}"!'.format( path ) )
return
def _ImportPNGs( self, paths ):
for path in paths:
try:
payload = ClientSerialisable.LoadFromPNG( path )
except Exception as e:
@ -894,7 +979,7 @@ class BetterListCtrlPanel( QW.QWidget ):
except:
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in the file!' )
QW.QMessageBox.critical( self, 'Error', 'I could not understand what was encoded in "{}"!'.format( path ) )
return
@ -975,7 +1060,8 @@ class BetterListCtrlPanel( QW.QWidget ):
export_menu_items = []
export_menu_items.append( ( 'normal', 'to clipboard', 'Serialise the selected data and put it on your clipboard.', self._ExportToClipboard ) )
export_menu_items.append( ( 'normal', 'to png', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPng ) )
export_menu_items.append( ( 'normal', 'to json file', 'Serialise the selected data and export to a json file.', self._ExportToJSON ) )
export_menu_items.append( ( 'normal', 'to png file', 'Serialise the selected data and encode it to an image file you can easily share with other hydrus users.', self._ExportToPNG ) )
if self._custom_get_callable is None:
@ -983,14 +1069,15 @@ class BetterListCtrlPanel( QW.QWidget ):
if all_objs_are_named:
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPngs ) )
export_menu_items.append( ( 'normal', 'to pngs', 'Serialise the selected data and encode it to multiple image files you can easily share with other hydrus users.', self._ExportToPNGs ) )
import_menu_items = []
import_menu_items.append( ( 'normal', 'from clipboard', 'Load a data from text in your clipboard.', self._ImportFromClipboard ) )
import_menu_items.append( ( 'normal', 'from pngs (note you can also drag and drop pngs onto this list)', 'Load a data from an encoded png.', self._ImportFromPng ) )
import_menu_items.append( ( 'normal', 'from json files', 'Load a data from .json files.', self._ImportFromJSON ) )
import_menu_items.append( ( 'normal', 'from png files (you can also drag and drop pngs onto this list)', 'Load a data from an encoded png.', self._ImportFromPNG ) )
self.AddMenuButton( 'export', export_menu_items, enabled_only_on_selection = True )
self.AddMenuButton( 'import', import_menu_items )
@ -1043,13 +1130,16 @@ class BetterListCtrlPanel( QW.QWidget ):
from hydrus.client.gui import ClientGUIDialogsQuick
message = 'Try to import the ' + HydrusData.ToHumanInt( len( paths ) ) + ' dropped files to this list? I am expecting png files.'
message = 'Try to import the ' + HydrusData.ToHumanInt( len( paths ) ) + ' dropped files to this list? I am expecting json or png files.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result == QW.QDialog.Accepted:
self._ImportPngs( paths )
( jsons, pngs ) = HydrusData.PartitionIteratorIntoLists( lambda path: path.endswith( '.png' ), paths )
self._ImportPNGs( pngs )
self._ImportJSONs( jsons )
self._listctrl.Sort()

View File

@ -14,8 +14,8 @@ from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTags
from hydrus.core import HydrusThreading
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientDefaults
from hydrus.client.media import ClientMedia
from hydrus.client import ClientParsing
from hydrus.client import ClientPaths
from hydrus.client import ClientSearch
@ -48,6 +48,7 @@ from hydrus.client.importing import ClientImportLocal
from hydrus.client.importing import ClientImportOptions
from hydrus.client.importing import ClientImportSimpleURLs
from hydrus.client.importing import ClientImportWatchers
from hydrus.client.media import ClientMedia
MANAGEMENT_TYPE_DUMPER = 0
MANAGEMENT_TYPE_IMPORT_MULTIPLE_GALLERY = 1
@ -769,6 +770,11 @@ class ManagementPanel( QW.QScrollArea ):
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
return 'empty page'
def ConnectMediaPanelSignals( self, media_panel: ClientGUIResults.MediaPanel ):
if self._current_selection_tags_list is not None:
@ -824,6 +830,19 @@ class ManagementPanel( QW.QScrollArea ):
pass
def GetDefaultEmptyMediaPanel( self ) -> ClientGUIResults.MediaPanel:
file_service_key = self._management_controller.GetKey( 'file_service' )
media_panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, file_service_key, [] )
status = self._GetDefaultEmptyPageStatusOverride()
media_panel.SetEmptyPageStatusOverride( status )
return media_panel
def PageHidden( self ):
pass
@ -1309,6 +1328,8 @@ class ManagementPanelDuplicateFilter( ManagementPanel ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, file_service_key, media_results )
panel.SetEmptyPageStatusOverride( 'no dupes found' )
self._page.SwapMediaPanel( panel )
@ -1800,6 +1821,8 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, CC.LOCAL_FILE_SERVICE_KEY, media_results )
panel.SetEmptyPageStatusOverride( 'no highlighted query' )
self._page.SwapMediaPanel( panel )
self._gallery_importers_listctrl.UpdateDatas()
@ -1866,7 +1889,7 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
added = gallery_import.GetCreationTime()
pretty_added = HydrusData.TimestampToPrettyTimeDelta( added, show_seconds = False )
pretty_added = ClientData.TimestampToPrettyTimeDelta( added, show_seconds = False )
display_tuple = ( pretty_query_text, pretty_source, pretty_files_paused, pretty_gallery_paused, pretty_status, pretty_progress, pretty_added )
sort_tuple = ( query_text, pretty_source, files_paused, gallery_paused, status, progress, added )
@ -1886,6 +1909,11 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
return 'no highlighted query'
def _GetListCtrlMenu( self ):
selected_watchers = self._gallery_importers_listctrl.GetData( only_selected = True )
@ -1906,6 +1934,11 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
ClientGUIMenus.AppendMenuItem( menu, 'show all importers\' files', 'Gather the presented files for the selected importers and show them in a new page.', self._ShowSelectedImportersFiles, show='all' )
ClientGUIMenus.AppendMenuItem( menu, 'show all importers\' files (including trash)', 'Gather the presented files (including trash) for the selected importers and show them in a new page.', self._ShowSelectedImportersFiles, show='all_and_trash' )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'show file import status', 'Show the file import status windows for the selected query.', self._ShowSelectedImportersFileSeedCaches )
ClientGUIMenus.AppendMenuItem( menu, 'show gallery log', 'Show the gallery log windows for the selected query.', self._ShowSelectedImportersGallerySeedLogs )
if self._CanRetryFailed() or self._CanRetryIgnored():
ClientGUIMenus.AppendSeparator( menu )
@ -1959,6 +1992,8 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, CC.LOCAL_FILE_SERVICE_KEY, sorted_media_results )
panel.SetEmptyPageStatusOverride( 'no files for this query and its publishing settings' )
self._page.SwapMediaPanel( panel )
self._gallery_importers_listctrl_panel.UpdateButtons()
@ -2149,6 +2184,29 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
CGC.core().PopupMenu( self._cog_button, menu )
def _ShowSelectedImportersFileSeedCaches( self ):
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
if len( gallery_imports ) == 0:
return
gallery_import = gallery_imports[0]
file_seed_cache = gallery_import.GetFileSeedCache()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'file import status' ) as dlg:
panel = ClientGUIFileSeedCache.EditFileSeedCachePanel( dlg, HG.client_controller, file_seed_cache )
dlg.SetPanel( panel )
dlg.exec()
def _ShowSelectedImportersFiles( self, show = 'presented' ):
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
@ -2205,7 +2263,33 @@ class ManagementPanelImporterMultipleGallery( ManagementPanelImporter ):
else:
QW.QMessageBox.critical( self, 'Error', 'No presented hashes for that selection!' )
QW.QMessageBox.warning( self, 'Warning', 'No presented hashes for that selection!' )
def _ShowSelectedImportersGallerySeedLogs( self ):
gallery_imports = self._gallery_importers_listctrl.GetData( only_selected = True )
if len( gallery_imports ) == 0:
return
gallery_import = gallery_imports[0]
gallery_seed_log = gallery_import.GetGallerySeedLog()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'gallery import log' ) as dlg:
read_only = False
can_generate_more_pages = True
panel = ClientGUIGallerySeedLog.EditGallerySeedLogPanel( dlg, HG.client_controller, read_only, can_generate_more_pages, gallery_seed_log )
dlg.SetPanel( panel )
dlg.exec()
@ -2522,6 +2606,8 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, CC.LOCAL_FILE_SERVICE_KEY, media_results )
panel.SetEmptyPageStatusOverride( 'no highlighted watcher' )
self._page.SwapMediaPanel( panel )
self._watchers_listctrl.UpdateDatas()
@ -2575,7 +2661,7 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
added = watcher.GetCreationTime()
pretty_added = HydrusData.TimestampToPrettyTimeDelta( added, show_seconds = False )
pretty_added = ClientData.TimestampToPrettyTimeDelta( added, show_seconds = False )
watcher_status = self._multiple_watcher_import.GetWatcherSimpleStatus( watcher )
@ -2604,6 +2690,11 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
return 'no highlighted watcher'
def _GetListCtrlMenu( self ):
selected_watchers = self._watchers_listctrl.GetData( only_selected = True )
@ -2625,6 +2716,11 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
ClientGUIMenus.AppendMenuItem( menu, 'show all watchers\' files', 'Gather the presented files for the selected watchers and show them in a new page.', self._ShowSelectedImportersFiles, show='all' )
ClientGUIMenus.AppendMenuItem( menu, 'show all watchers\' files (including trash)', 'Gather the presented files (including trash) for the selected watchers and show them in a new page.', self._ShowSelectedImportersFiles, show='all_and_trash' )
ClientGUIMenus.AppendSeparator( menu )
ClientGUIMenus.AppendMenuItem( menu, 'show file import status', 'Show the file import status windows for the selected watcher.', self._ShowSelectedImportersFileSeedCaches )
ClientGUIMenus.AppendMenuItem( menu, 'show checker log', 'Show the checker log windows for the selected watcher.', self._ShowSelectedImportersGallerySeedLogs )
if self._CanRetryFailed() or self._CanRetryIgnored():
ClientGUIMenus.AppendSeparator( menu )
@ -2676,6 +2772,8 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, CC.LOCAL_FILE_SERVICE_KEY, sorted_media_results )
panel.SetEmptyPageStatusOverride( 'no files for this watcher and its publishing settings' )
self._page.SwapMediaPanel( panel )
self._multiple_watcher_import.SetHighlightedWatcher( self._highlighted_watcher )
@ -2864,6 +2962,29 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
def _ShowSelectedImportersFileSeedCaches( self ):
watchers = self._watchers_listctrl.GetData( only_selected = True )
if len( watchers ) == 0:
return
watcher = watchers[0]
file_seed_cache = watcher.GetFileSeedCache()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'file import status' ) as dlg:
panel = ClientGUIFileSeedCache.EditFileSeedCachePanel( dlg, HG.client_controller, file_seed_cache )
dlg.SetPanel( panel )
dlg.exec()
def _ShowSelectedImportersFiles( self, show = 'presented' ):
watchers = self._watchers_listctrl.GetData( only_selected = True )
@ -2920,7 +3041,33 @@ class ManagementPanelImporterMultipleWatcher( ManagementPanelImporter ):
else:
QW.QMessageBox.critical( self, 'Error', 'No presented hashes for that selection!' )
QW.QMessageBox.warning( self, 'Warning', 'No presented hashes for that selection!' )
def _ShowSelectedImportersGallerySeedLogs( self ):
watchers = self._watchers_listctrl.GetData( only_selected = True )
if len( watchers ) == 0:
return
watcher = watchers[0]
gallery_seed_log = watcher.GetGallerySeedLog()
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'checker log' ) as dlg:
read_only = True
can_generate_more_pages = False
panel = ClientGUIGallerySeedLog.EditGallerySeedLogPanel( dlg, HG.client_controller, read_only, can_generate_more_pages, gallery_seed_log )
dlg.SetPanel( panel )
dlg.exec()
@ -3083,7 +3230,9 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
self._page_download_control = ClientGUIControls.NetworkJobControl( self._simple_parsing_jobs_panel )
self._pending_jobs_listbox = QW.QListWidget( self._simple_parsing_jobs_panel )
self._pending_jobs_listbox = ClientGUIListBoxes.BetterQListWidget( self._simple_parsing_jobs_panel )
self._pending_jobs_listbox.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
self._advance_button = QW.QPushButton( '\u2191', self._simple_parsing_jobs_panel )
self._advance_button.clicked.connect( self.EventAdvance )
@ -3339,11 +3488,11 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
( pending_jobs, parser_status, current_action, queue_paused, files_paused ) = self._simple_downloader_import.GetStatus()
current_pending_jobs = [ QP.GetClientData( self._pending_jobs_listbox, i ) for i in range( self._pending_jobs_listbox.count() ) ]
current_pending_jobs = self._pending_jobs_listbox.GetData()
if current_pending_jobs != pending_jobs:
selected_string = QP.ListWidgetGetStringSelection( self._pending_jobs_listbox )
selected_jobs = set( self._pending_jobs_listbox.GetData( only_selected = True ) )
self._pending_jobs_listbox.clear()
@ -3353,18 +3502,10 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
pretty_job = simple_downloader_formula.GetName() + ': ' + url
item = QW.QListWidgetItem()
item.setText( pretty_job )
item.setData( QC.Qt.UserRole, job )
self._pending_jobs_listbox.addItem( item )
self._pending_jobs_listbox.Append( pretty_job, job )
selection_index = QP.ListWidgetIndexForString( self._pending_jobs_listbox, selected_string )
if selection_index != -1:
QP.ListWidgetSetSelection( self._pending_jobs_listbox, selection_index )
self._pending_jobs_listbox.SelectData( selected_jobs )
if queue_paused:
@ -3416,42 +3557,56 @@ class ManagementPanelImporterSimpleDownloader( ManagementPanelImporter ):
def EventAdvance( self ):
selection = QP.ListWidgetGetSelection( self._pending_jobs_listbox )
selected_jobs = self._pending_jobs_listbox.GetData( only_selected = True )
if selection != -1:
job = QP.GetClientData( self._pending_jobs_listbox, selection )
for job in selected_jobs:
self._simple_downloader_import.AdvanceJob( job )
if len( selected_jobs ) > 0:
self._UpdateImportStatus()
def EventDelay( self ):
selection = QP.ListWidgetGetSelection( self._pending_jobs_listbox )
selected_jobs = list( self._pending_jobs_listbox.GetData( only_selected = True ) )
if selection != -1:
job = QP.GetClientData( self._pending_jobs_listbox, selection )
selected_jobs.reverse()
for job in selected_jobs:
self._simple_downloader_import.DelayJob( job )
if len( selected_jobs ) > 0:
self._UpdateImportStatus()
def EventDelete( self ):
selection = QP.ListWidgetGetSelection( self._pending_jobs_listbox )
selected_jobs = self._pending_jobs_listbox.GetData( only_selected = True )
if selection != -1:
message = 'Delete {} jobs?'.format( HydrusData.ToHumanInt( len( selected_jobs ) ) )
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result != QW.QDialog.Accepted:
job = QP.GetClientData( self._pending_jobs_listbox, selection )
return
for job in selected_jobs:
self._simple_downloader_import.DeleteJob( job )
if len( selected_jobs ) > 0:
self._UpdateImportStatus()
@ -4469,12 +4624,19 @@ class ManagementPanelQuery( ManagementPanel ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, file_service_key, [] )
panel.SetEmptyPageStatusOverride( 'search cancelled!' )
self._page.SwapMediaPanel( panel )
self._UpdateCancelButton()
def _GetDefaultEmptyPageStatusOverride( self ) -> str:
return 'no search done yet'
def _MakeCurrentSelectionTagsBox( self, sizer ):
tags_box = ClientGUIListBoxes.StaticBoxSorterForListBoxTags( self, 'selection tags' )
@ -4505,6 +4667,8 @@ class ManagementPanelQuery( ManagementPanel ):
self._controller.ResetIdleTimer()
interrupting_current_search = not self._query_job_key.IsDone()
self._query_job_key.Cancel()
if self._search_enabled:
@ -4536,9 +4700,15 @@ class ManagementPanelQuery( ManagementPanel ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, file_service_key, [] )
panel.SetEmptyPageStatusOverride( 'no search' )
self._page.SwapMediaPanel( panel )
elif interrupting_current_search:
self._CancelSearch()
else:
@ -4654,6 +4824,8 @@ class ManagementPanelQuery( ManagementPanel ):
panel = ClientGUIResults.MediaPanelThumbnails( self._page, self._page_key, file_service_key, media_results )
panel.SetEmptyPageStatusOverride( 'no files found for this search' )
panel.Collect( self._page_key, self._media_collect.GetValue() )
panel.Sort( self._media_sort.GetSort() )

View File

@ -1,3 +1,4 @@
import itertools
import os
import random
import time
@ -17,6 +18,63 @@ from hydrus.client import ClientThreading
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIMenus
def CopyHashesToClipboard( win: QW.QWidget, hash_type: str, medias: typing.List[ ClientMedia.Media ] ):
sha256_hashes = list( itertools.chain.from_iterable( ( media.GetHashes( ordered = True ) for media in medias ) ) )
if hash_type == 'sha256':
desired_hashes = sha256_hashes
else:
num_hashes = len( sha256_hashes )
num_remote_sha256_hashes = len( [ itertools.chain.from_iterable( ( media.GetHashes( discriminant = CC.DISCRIMINANT_NOT_LOCAL, ordered = True ) for media in medias ) ) ] )
desired_hashes = HG.client_controller.Read( 'file_hashes', sha256_hashes, 'sha256', hash_type )
num_missing = num_hashes - len( desired_hashes )
if num_missing > 0:
if num_missing == num_hashes:
message = 'Unfortunately, none of the {} hashes could be found.'.format( hash_type )
else:
message = 'Unfortunately, {} of the {} hashes could not be found.'.format( HydrusData.ToHumanInt( num_missing ), hash_type )
if num_remote_sha256_hashes > 0:
message += ' {} of the files you wanted are not currently in this client. If they have never visited this client, the lookup is impossible.'.format( HydrusData.ToHumanInt( num_remote_sha256_hashes ) )
if num_remote_sha256_hashes < num_hashes:
message += ' It could be that some of the local files are currently missing this information in the hydrus database. A file maintenance job (under the database menu) can repopulate this data.'
QW.QMessageBox.warning( win, 'Warning', message )
if len( desired_hashes ) > 0:
hex_hashes = os.linesep.join( [ desired_hash.hex() for desired_hash in desired_hashes ] )
HG.client_controller.pub( 'clipboard', 'text', hex_hashes )
job_key = ClientThreading.JobKey()
job_key.SetVariable( 'popup_text_1', '{} {} hashes copied'.format( HydrusData.ToHumanInt( len( desired_hashes ) ), hash_type ) )
HG.client_controller.pub( 'message', job_key )
job_key.Delete( 2 )
def CopyMediaURLs( medias ):
urls = set()

View File

@ -26,7 +26,7 @@ def AppendMenuBitmapItem( menu, label, description, bitmap, callable, *args, **k
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( HydrusText.ElideText( label, 64, elide_center = True ) )
menu_item.setText( HydrusText.ElideText( label, 128, elide_center = True ) )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
@ -51,7 +51,7 @@ def AppendMenuCheckItem( menu, label, description, initial_value, callable, *arg
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( HydrusText.ElideText( label, 64, elide_center = True ) )
menu_item.setText( HydrusText.ElideText( label, 128, elide_center = True ) )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )
@ -77,7 +77,7 @@ def AppendMenuItem( menu, label, description, callable, *args, **kwargs ):
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
elided_label = HydrusText.ElideText( label, 64, elide_center = True )
elided_label = HydrusText.ElideText( label, 128, elide_center = True )
menu_item.setText( elided_label )
@ -113,7 +113,7 @@ def AppendMenuLabel( menu, label, description = '' ):
menu_item.setMenuRole( QW.QAction.ApplicationSpecificRole )
menu_item.setText( HydrusText.ElideText( label, 64, elide_center = True ) )
menu_item.setText( HydrusText.ElideText( label, 128, elide_center = True ) )
menu_item.setStatusTip( description )
menu_item.setToolTip( description )

View File

@ -428,15 +428,13 @@ class Page( QW.QSplitter ):
self._management_panel = ClientGUIManagement.CreateManagementPanel( self._search_preview_split, self, self._controller, self._management_controller )
file_service_key = self._management_controller.GetKey( 'file_service' )
self._preview_panel = QW.QFrame( self._search_preview_split )
self._preview_panel.setFrameStyle( QW.QFrame.Panel | QW.QFrame.Sunken )
self._preview_panel.setLineWidth( 2 )
self._preview_canvas = ClientGUICanvas.CanvasPanel( self._preview_panel, self._page_key )
self._media_panel = ClientGUIResults.MediaPanelThumbnails( self, self._page_key, file_service_key, [] )
self._media_panel = self._management_panel.GetDefaultEmptyMediaPanel()
vbox = QP.VBoxLayout( margin = 0 )
@ -460,7 +458,6 @@ class Page( QW.QSplitter ):
self._search_preview_split._handle_event_filter = QP.WidgetEventFilter( self._search_preview_split.handle( 1 ) )
self._search_preview_split._handle_event_filter.EVT_LEFT_DCLICK( self.EventPreviewUnsplit )
self._controller.sub( self, 'SetPrettyStatus', 'new_page_status' )
self._controller.sub( self, 'SetSplitterPositions', 'set_splitter_positions' )
self._ConnectMediaPanelSignals()
@ -471,11 +468,12 @@ class Page( QW.QSplitter ):
self._media_panel.refreshQuery.connect( self.RefreshQuery )
self._media_panel.focusMediaChanged.connect( self._preview_canvas.SetMedia )
self._media_panel.focusMediaCleared.connect( self._preview_canvas.ClearMedia )
self._media_panel.statusTextChanged.connect( self._SetPrettyStatus )
self._management_panel.ConnectMediaPanelSignals( self._media_panel )
def _SetPrettyStatus( self, status ):
def _SetPrettyStatus( self, status: str ):
self._pretty_status = status
@ -2473,11 +2471,6 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
def NewPage( self, management_controller, initial_hashes = None, forced_insertion_index = None, on_deepest_notebook = False, select_page = True ):
if self.window().isMinimized():
return None
current_page = self.currentWidget()
if on_deepest_notebook and isinstance( current_page, PagesNotebook ):
@ -2802,7 +2795,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
def qt_finish( page, media_results ):
if not page:
if not QP.isValid( page ):
return

View File

@ -325,7 +325,7 @@ class DownloaderExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
description = ', '.join( gug_names )
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object, title = title, description = description )
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object, title = title, description = description )
dlg.SetPanel( panel )
@ -3738,14 +3738,14 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
menu_items = []
menu_items.append( ( 'normal', 'to clipboard', 'Serialise the script and put it on your clipboard.', self.ExportToClipboard ) )
menu_items.append( ( 'normal', 'to png', 'Serialise the script and encode it to an image file you can easily share with other hydrus users.', self.ExportToPng ) )
menu_items.append( ( 'normal', 'to png', 'Serialise the script and encode it to an image file you can easily share with other hydrus users.', self.ExportToPNG ) )
self._export_button = ClientGUICommon.MenuButton( self, 'export', menu_items )
menu_items = []
menu_items.append( ( 'normal', 'from clipboard', 'Load a script from text in your clipboard.', self.ImportFromClipboard ) )
menu_items.append( ( 'normal', 'from png', 'Load a script from an encoded png.', self.ImportFromPng ) )
menu_items.append( ( 'normal', 'from png', 'Load a script from an encoded png.', self.ImportFromPNG ) )
self._import_button = ClientGUICommon.MenuButton( self, 'import', menu_items )
@ -3963,7 +3963,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
def ExportToPng( self ):
def ExportToPNG( self ):
export_object = self._GetExportObject()
@ -3971,7 +3971,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
with ClientGUITopLevelWindowsPanels.DialogNullipotent( self, 'export to png' ) as dlg:
panel = ClientGUISerialisable.PngExportPanel( dlg, export_object )
panel = ClientGUISerialisable.PNGExportPanel( dlg, export_object )
dlg.SetPanel( panel )
@ -4005,7 +4005,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
def ImportFromPng( self ):
def ImportFromPNG( self ):
with QP.FileDialog( self, 'select the png with the encoded script', wildcard = 'PNG (*.png)' ) as dlg:
@ -4015,7 +4015,7 @@ class ManageParsingScriptsPanel( ClientGUIScrolledPanels.ManagePanel ):
try:
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
except Exception as e:

View File

@ -48,6 +48,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
selectedMediaTagPresentationChanged = QC.Signal( list, bool )
selectedMediaTagPresentationIncremented = QC.Signal( list )
statusTextChanged = QC.Signal( str )
focusMediaChanged = QC.Signal( ClientMedia.Media )
focusMediaCleared = QC.Signal()
@ -76,6 +77,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._next_best_media_after_focused_media_removed = None
self._shift_focused_media = None
self._empty_page_status_override = None
HG.client_controller.sub( self, 'AddMediaResults', 'add_media_results' )
HG.client_controller.sub( self, 'Collect', 'collect_media' )
HG.client_controller.sub( self, 'FileDumped', 'file_dumped' )
@ -196,57 +199,15 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
media = self._GetFocusSingleton()
sha256_hash = media.GetHash()
if hash_type == 'sha256':
hex_hash = sha256_hash.hex()
else:
if media.GetLocationsManager().IsLocal():
( other_hash, ) = HG.client_controller.Read( 'file_hashes', ( sha256_hash, ), 'sha256', hash_type )
hex_hash = other_hash.hex()
else:
QW.QMessageBox.critical( self, 'Error', 'Unfortunately, you do not have that file in your database, so its non-sha256 hashes are unknown.' )
return
HG.client_controller.pub( 'clipboard', 'text', hex_hash )
ClientGUIMedia.CopyHashesToClipboard( self, hash_type, [ media ] )
def _CopyHashesToClipboard( self, hash_type ):
if hash_type == 'sha256':
hex_hashes = os.linesep.join( [ hash.hex() for hash in self._GetSelectedHashes( ordered = True ) ] )
else:
sha256_hashes = self._GetSelectedHashes( discriminant = CC.DISCRIMINANT_LOCAL, ordered = True )
if len( sha256_hashes ) > 0:
other_hashes = HG.client_controller.Read( 'file_hashes', sha256_hashes, 'sha256', hash_type )
hex_hashes = os.linesep.join( [ other_hash.hex() for other_hash in other_hashes ] )
else:
QW.QMessageBox.critical( self, 'Error', 'Unfortunately, none of those files are in your database, so their non-sha256 hashes are unknown.' )
return
medias = self._GetSelectedMediaOrdered()
HG.client_controller.pub( 'clipboard', 'text', hex_hashes )
ClientGUIMedia.CopyHashesToClipboard( self, hash_type, medias )
def _CopyPathToClipboard( self ):
@ -550,10 +511,24 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
return sum( [ media.GetNumFiles() for media in self._selected_media ] )
def _GetPrettyStatus( self ):
def _GetPrettyStatus( self ) -> str:
num_files = len( self._hashes )
if self._empty_page_status_override is not None:
if num_files == 0:
return self._empty_page_status_override
else:
# user has dragged files onto this page or similar
self._empty_page_status_override = None
num_selected = self._GetNumSelected()
( num_files_descriptor, selected_files_descriptor ) = self._GetSortedSelectedMimeDescriptors()
@ -668,12 +643,9 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
result = []
for media in self._sorted_media:
for media in self._GetSelectedMediaOrdered():
if media in self._selected_media:
result.extend( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )
result.extend( media.GetHashes( has_location, discriminant, not_uploaded_to, ordered ) )
else:
@ -709,6 +681,21 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
return flat_media
def _GetSelectedMediaOrdered( self ):
medias = []
for media in self._sorted_media:
if media in self._selected_media:
medias.append( media )
return medias
def _GetSimilarTo( self, max_hamming ):
hashes = set()
@ -1145,7 +1132,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self.selectedMediaTagPresentationChanged.emit( tags_media, tags_changed )
HG.client_controller.pub( 'new_page_status', self._page_key, self._GetPrettyStatus() )
self.statusTextChanged.emit( self._GetPrettyStatus() )
if tags_changed:
@ -1166,7 +1153,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self.selectedMediaTagPresentationIncremented.emit( medias )
HG.client_controller.pub( 'new_page_status', self._page_key, self._GetPrettyStatus() )
self.statusTextChanged.emit( self._GetPrettyStatus() )
else:
@ -1875,6 +1862,18 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
self._CopyHashesToClipboard( 'sha256' )
elif action == 'copy_md5_hash':
self._CopyHashesToClipboard( 'md5' )
elif action == 'copy_sha1_hash':
self._CopyHashesToClipboard( 'sha1' )
elif action == 'copy_sha512_hash':
self._CopyHashesToClipboard( 'sha512' )
elif action == 'duplicate_media_clear_focused_false_positives':
if self._HasFocusSingleton():
@ -2198,6 +2197,11 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
return self._SetDuplicates( duplicate_type, media_group = media_group )
def SetEmptyPageStatusOverride( self, value: str ):
self._empty_page_status_override = value
def SetFocusedMedia( self, media ):
pass

View File

@ -5789,17 +5789,30 @@ class EditURLClassLinksPanel( ClientGUIScrolledPanels.EditPanel ):
url_class = self._url_class_keys_to_url_classes[ url_class_key ]
choice_tuples = [ ( parser.GetName(), parser ) for parser in self._parsers ]
matching_parsers = [ parser for parser in self._parsers if True in ( url_class.Matches( url ) for url in parser.GetExampleURLs() ) ]
unmatching_parsers = [ parser for parser in self._parsers if parser not in matching_parsers ]
matching_parsers.sort( key = lambda p: p.GetName() )
unmatching_parsers.sort( key = lambda p: p.GetName() )
choice_tuples = [ ( parser.GetName(), parser ) for parser in matching_parsers ]
choice_tuples.append( ( '------', None ) )
choice_tuples.extend( [ ( parser.GetName(), parser ) for parser in unmatching_parsers ] )
try:
parser = ClientGUIDialogsQuick.SelectFromList( self, 'select parser for ' + url_class.GetName(), choice_tuples )
parser = ClientGUIDialogsQuick.SelectFromList( self, 'select parser for ' + url_class.GetName(), choice_tuples, sort_tuples = False )
except HydrusExceptions.CancelledException:
break
if parser is None:
break
self._parser_list_ctrl.DeleteDatas( ( data, ) )
new_data = ( url_class_key, parser.GetParserKey() )

View File

@ -888,7 +888,7 @@ def THREADMigrateDatabase( controller, source, portable_locations, dest ):
def qt_code( job_key ):
HG.client_controller.CallLaterQtSafe( controller.gui, 3.0, controller.gui.SaveAndClose )
HG.client_controller.CallLaterQtSafe( controller.gui, 3.0, controller.Exit )
# no parent because this has to outlive the gui, obvs
@ -1376,7 +1376,7 @@ class MigrateTagsPanel( ClientGUIScrolledPanels.ReviewPanel ):
message = 'Select the destination location for the Archive. Existing Archives are also ok, and will be appended to.'
with QP.FileDialog( self, message = message, acceptMode = QW.QFileDialog.AcceptSave, defaultFile = 'archive.db' ) as dlg:
with QP.FileDialog( self, message = message, acceptMode = QW.QFileDialog.AcceptSave, default_filename = 'archive.db' ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
@ -2090,7 +2090,7 @@ class ReviewDownloaderImport( ClientGUIScrolledPanels.ReviewPanel ):
try:
payload = ClientSerialisable.LoadFromPng( path )
payload = ClientSerialisable.LoadFromPNG( path )
except Exception as e:

View File

@ -12,7 +12,7 @@ from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import QtPorting as QP
class PngExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
class PNGExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
def __init__( self, parent, payload_obj, title = None, description = None, payload_description = None ):
@ -168,14 +168,14 @@ class PngExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
path += '.png'
ClientSerialisable.DumpToPng( width, payload_bytes, title, payload_description, text, path )
ClientSerialisable.DumpToPNG( width, payload_bytes, title, payload_description, text, path )
self._export.setText( 'done!' )
HG.client_controller.CallLaterQtSafe(self._export, 2.0, self._export.setText, 'export')
class PngsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
class PNGsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
def __init__( self, parent, payload_objs ):
@ -272,7 +272,7 @@ class PngsExportPanel( ClientGUIScrolledPanels.ReviewPanel ):
path += '.png'
ClientSerialisable.DumpToPng( width, payload_bytes, title, payload_description, text, path )
ClientSerialisable.DumpToPNG( width, payload_bytes, title, payload_description, text, path )
self._export.setText( 'done!' )

View File

@ -1097,10 +1097,10 @@ class ShortcutWidget( QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._mouse_radio = QW.QRadioButton( 'mouse', self )
self._mouse_shortcut = MouseShortcutWidget( self, self._mouse_radio )
self._mouse_shortcut = MouseShortcutWidget( self )
self._keyboard_radio = QW.QRadioButton( 'keyboard', self )
self._keyboard_shortcut = KeyboardShortcutWidget( self, self._keyboard_radio )
self._keyboard_shortcut = KeyboardShortcutWidget( self )
#
@ -1121,6 +1121,9 @@ class ShortcutWidget( QW.QWidget ):
self.setLayout( vbox )
self._mouse_shortcut.valueChanged.connect( self._mouse_radio.click )
self._keyboard_shortcut.valueChanged.connect( self._keyboard_radio.click )
def GetValue( self ):
@ -1150,12 +1153,12 @@ class ShortcutWidget( QW.QWidget ):
class KeyboardShortcutWidget( QW.QLineEdit ):
def __init__( self, parent, related_radio = None ):
valueChanged = QC.Signal()
def __init__( self, parent ):
self._shortcut = ClientGUIShortcuts.Shortcut()
self._related_radio = related_radio
QW.QLineEdit.__init__( self, parent )
self._SetShortcutString()
@ -1176,13 +1179,10 @@ class KeyboardShortcutWidget( QW.QLineEdit ):
self._shortcut = shortcut
if self._related_radio is not None:
self._related_radio.setChecked( True )
self._SetShortcutString()
self.valueChanged.emit()
def GetValue( self ):
@ -1199,13 +1199,13 @@ class KeyboardShortcutWidget( QW.QLineEdit ):
class MouseShortcutWidget( QW.QWidget ):
def __init__( self, parent, related_radio = None ):
valueChanged = QC.Signal()
def __init__( self, parent ):
QW.QWidget.__init__( self, parent )
self._related_radio = related_radio
self._button = MouseShortcutButton( self, related_radio = related_radio )
self._button = MouseShortcutButton( self )
self._press_or_release = ClientGUICommon.BetterChoice( self )
@ -1220,6 +1220,14 @@ class MouseShortcutWidget( QW.QWidget ):
self.setLayout( layout )
self._press_or_release.currentIndexChanged.connect( self._NewChoice )
self._button.valueChanged.connect( self._ButtonValueChanged )
def _ButtonValueChanged( self ):
self._press_or_release.setEnabled( self._button.GetValue().IsAppropriateForPressRelease() )
self.valueChanged.emit()
def _NewChoice( self ):
@ -1230,6 +1238,8 @@ class MouseShortcutWidget( QW.QWidget ):
self._button.SetPressInsteadOfRelease( press_instead_of_release )
self.valueChanged.emit()
def GetValue( self ):
@ -1238,21 +1248,25 @@ class MouseShortcutWidget( QW.QWidget ):
def SetValue( self, shortcut ):
self.blockSignals( True )
self._button.SetValue( shortcut )
self._press_or_release.SetValue( shortcut.shortcut_press_type )
self.blockSignals( False )
class MouseShortcutButton( QW.QPushButton ):
def __init__( self, parent, related_radio = None ):
valueChanged = QC.Signal()
def __init__( self, parent ):
self._shortcut = ClientGUIShortcuts.Shortcut( ClientGUIShortcuts.SHORTCUT_TYPE_MOUSE, ClientGUIShortcuts.SHORTCUT_MOUSE_LEFT, ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_PRESS, [] )
self._press_instead_of_release = True
self._related_radio = related_radio
QW.QPushButton.__init__( self, parent )
self._SetShortcutString()
@ -1268,13 +1282,10 @@ class MouseShortcutButton( QW.QPushButton ):
self._shortcut = shortcut
if self._related_radio is not None:
self._related_radio.setChecked( True )
self._SetShortcutString()
self.valueChanged.emit()
def _SetShortcutString( self ):
@ -1310,7 +1321,7 @@ class MouseShortcutButton( QW.QPushButton ):
self._ProcessMouseEvent( event )
def GetValue( self ):
def GetValue( self ) -> ClientGUIShortcuts.Shortcut:
return self._shortcut
@ -1319,7 +1330,7 @@ class MouseShortcutButton( QW.QPushButton ):
self._press_instead_of_release = press_instead_of_release
if self._shortcut.shortcut_press_type != ClientGUIShortcuts.SHORTCUT_PRESS_TYPE_DOUBLE_CLICK and self._shortcut.shortcut_key in ClientGUIShortcuts.SHORTCUT_MOUSE_CLICKS:
if self._shortcut.IsAppropriateForPressRelease():
self._shortcut = self._shortcut.Duplicate()
@ -1334,12 +1345,16 @@ class MouseShortcutButton( QW.QPushButton ):
self._SetShortcutString()
self.valueChanged.emit()
def SetValue( self, shortcut ):
def SetValue( self, shortcut: ClientGUIShortcuts.Shortcut ):
self._shortcut = shortcut
self._shortcut = shortcut.Duplicate()
self._SetShortcutString()
self.valueChanged.emit()

View File

@ -214,7 +214,7 @@ shortcut_names_to_descriptions[ 'preview_media_window' ] = 'Actions for any vide
SHORTCUTS_RESERVED_NAMES = [ 'global', 'archive_delete_filter', 'duplicate_filter', 'media', 'main_gui', 'media_viewer_browser', 'media_viewer', 'media_viewer_media_window', 'preview_media_window' ]
SHORTCUTS_GLOBAL_ACTIONS = [ 'global_audio_mute', 'global_audio_unmute', 'global_audio_mute_flip', 'exit_application', 'exit_application_force_maintenance', 'restart_application', 'hide_to_system_tray' ]
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'manage_file_urls', 'manage_file_notes', 'archive_file', 'inbox_file', 'delete_file', 'undelete_file', 'export_files', 'export_files_quick_auto_export', 'remove_file_from_view', 'open_file_in_external_program', 'open_selection_in_new_page', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_bmp_or_file_if_not_bmpable', 'copy_file', 'copy_path', 'copy_sha256_hash', 'get_similar_to_exact', 'get_similar_to_very_similar', 'get_similar_to_similar', 'get_similar_to_speculative', 'duplicate_media_set_alternate', 'duplicate_media_set_alternate_collections', 'duplicate_media_set_custom', 'duplicate_media_set_focused_better', 'duplicate_media_set_focused_king', 'duplicate_media_set_same_quality', 'open_known_url' ]
SHORTCUTS_MEDIA_ACTIONS = [ 'manage_file_tags', 'manage_file_ratings', 'manage_file_urls', 'manage_file_notes', 'archive_file', 'inbox_file', 'delete_file', 'undelete_file', 'export_files', 'export_files_quick_auto_export', 'remove_file_from_view', 'open_file_in_external_program', 'open_selection_in_new_page', 'launch_the_archive_delete_filter', 'copy_bmp', 'copy_bmp_or_file_if_not_bmpable', 'copy_file', 'copy_path', 'copy_sha256_hash', 'copy_md5_hash', 'copy_sha1_hash', 'copy_sha512_hash', 'get_similar_to_exact', 'get_similar_to_very_similar', 'get_similar_to_similar', 'get_similar_to_speculative', 'duplicate_media_set_alternate', 'duplicate_media_set_alternate_collections', 'duplicate_media_set_custom', 'duplicate_media_set_focused_better', 'duplicate_media_set_focused_king', 'duplicate_media_set_same_quality', 'open_known_url' ]
SHORTCUTS_MEDIA_VIEWER_ACTIONS = [ 'pause_media', 'pause_play_media', 'move_animation_to_previous_frame', 'move_animation_to_next_frame', 'switch_between_fullscreen_borderless_and_regular_framed_window', 'pan_up', 'pan_down', 'pan_left', 'pan_right', 'pan_top_edge', 'pan_bottom_edge', 'pan_left_edge', 'pan_right_edge', 'pan_vertical_center', 'pan_horizontal_center', 'zoom_in', 'zoom_out', 'switch_between_100_percent_and_canvas_zoom', 'flip_darkmode', 'close_media_viewer' ]
SHORTCUTS_MEDIA_VIEWER_BROWSER_ACTIONS = [ 'view_next', 'view_first', 'view_last', 'view_previous', 'pause_play_slideshow', 'show_menu', 'close_media_viewer' ]
SHORTCUTS_MAIN_GUI_ACTIONS = [ 'refresh', 'refresh_all_pages', 'refresh_page_of_pages_pages', 'new_page', 'new_page_of_pages', 'new_duplicate_filter_page', 'new_gallery_downloader_page', 'new_url_downloader_page', 'new_simple_downloader_page', 'new_watcher_downloader_page', 'synchronised_wait_switch', 'set_media_focus', 'show_hide_splitters', 'set_search_focus', 'unclose_page', 'close_page', 'redo', 'undo', 'flip_darkmode', 'check_all_import_folders', 'flip_debug_force_idle_mode_do_not_set_this', 'show_and_focus_manage_tags_favourite_tags', 'show_and_focus_manage_tags_related_tags', 'show_and_focus_manage_tags_file_lookup_script_tags', 'show_and_focus_manage_tags_recent_tags', 'focus_media_viewer' ]
@ -751,6 +751,11 @@ class Shortcut( HydrusSerialisable.SerialisableBase ):
return self.shortcut_type
def IsAppropriateForPressRelease( self ):
return self.shortcut_key in SHORTCUT_MOUSE_CLICKS and self.shortcut_press_type != SHORTCUT_PRESS_TYPE_DOUBLE_CLICK
def IsDoubleClick( self ):
return self.shortcut_type == SHORTCUT_TYPE_MOUSE and self.shortcut_press_type == SHORTCUT_PRESS_TYPE_DOUBLE_CLICK

View File

@ -0,0 +1,265 @@
import os
import threading
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIAsync
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import QtPorting as QP
class FrameSplashPanel( QW.QWidget ):
def __init__( self, parent, controller ):
QW.QWidget.__init__( self, parent )
self._controller = controller
self._my_status = FrameSplashStatus( self._controller, self )
width = ClientGUIFunctions.ConvertTextToPixelWidth( self, 64 )
self.setMinimumWidth( width )
self.setMaximumWidth( width * 2 )
self._drag_last_pos = None
self._initial_position = self.parentWidget().pos()
# this is 124 x 166
self._hydrus_pixmap = QG.QPixmap( os.path.join( HC.STATIC_DIR, 'hydrus_splash.png' ) )
self._image_label = QW.QLabel( self )
self._image_label.setPixmap( self._hydrus_pixmap )
self._image_label.setAlignment( QC.Qt.AlignCenter )
self._title_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._status_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._status_sub_label = ClientGUICommon.BetterStaticText( self, label = ' ' )
self._title_label.setAlignment( QC.Qt.AlignCenter )
self._status_label.setAlignment( QC.Qt.AlignCenter )
self._status_sub_label.setAlignment( QC.Qt.AlignCenter )
vbox = QP.VBoxLayout()
QP.AddToLayout( vbox, self._image_label, CC.FLAGS_CENTER )
QP.AddToLayout( vbox, self._title_label, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._status_label, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._status_sub_label, CC.FLAGS_EXPAND_PERPENDICULAR )
margin = ClientGUIFunctions.ConvertTextToPixelWidth( self, 3 )
self._image_label.setMargin( margin )
self.setLayout( vbox )
def mouseMoveEvent( self, event ):
if ( event.buttons() & QC.Qt.LeftButton ) and self._drag_last_pos is not None:
mouse_pos = QG.QCursor.pos()
delta = mouse_pos - self._drag_last_pos
win = self.window()
win.move( win.pos() + delta )
self._drag_last_pos = QC.QPoint( mouse_pos )
event.accept()
return
QW.QWidget.mouseMoveEvent( self, event )
def mousePressEvent( self, event ):
if event.button() == QC.Qt.LeftButton:
self._drag_last_pos = QG.QCursor.pos()
event.accept()
return
QW.QWidget.mousePressEvent( self, event )
def mouseReleaseEvent( self, event ):
if event.button() == QC.Qt.LeftButton:
self._drag_last_pos = None
event.accept()
return
QW.QWidget.mouseReleaseEvent( self, event )
def SetDirty( self ):
( title_text, status_text, status_subtext ) = self._my_status.GetTexts()
self._title_label.setText( title_text )
self._status_label.setText( status_text )
self._status_sub_label.setText( status_subtext )
# We have this to be an off-Qt-thread-happy container for this info, as the framesplash has to deal with messages in the fuzzy time of shutdown
# all of a sudden, pubsubs are processed in non Qt-thread time, so this handles that safely and lets the gui know if the Qt controller is still running
class FrameSplashStatus( object ):
def __init__( self, controller, ui ):
self._controller = controller
self._lock = threading.Lock()
self._updater = ClientGUIAsync.FastThreadToGUIUpdater( ui, ui.SetDirty )
self._title_text = ''
self._status_text = ''
self._status_subtext = ''
self._controller.sub( self, 'SetTitleText', 'splash_set_title_text' )
self._controller.sub( self, 'SetText', 'splash_set_status_text' )
self._controller.sub( self, 'SetSubtext', 'splash_set_status_subtext' )
def _NotifyUI( self ):
self._updater.Update()
def GetTexts( self ):
with self._lock:
return ( self._title_text, self._status_text, self._status_subtext )
def SetText( self, text, print_to_log = True ):
if print_to_log and len( text ) > 0:
HydrusData.Print( text )
with self._lock:
self._status_text = text
self._status_subtext = ''
self._NotifyUI()
def SetSubtext( self, text ):
with self._lock:
self._status_subtext = text
self._NotifyUI()
def SetTitleText( self, text, clear_undertexts = True, print_to_log = True ):
if print_to_log:
HydrusData.DebugPrint( text )
with self._lock:
self._title_text = text
if clear_undertexts:
self._status_text = ''
self._status_subtext = ''
self._NotifyUI()
class FrameSplash( QW.QWidget ):
def __init__( self, controller, title ):
self._controller = controller
QW.QWidget.__init__( self, None )
self.setWindowFlag( QC.Qt.CustomizeWindowHint )
self.setWindowFlag( QC.Qt.WindowContextHelpButtonHint, on = False )
self.setWindowFlag( QC.Qt.WindowCloseButtonHint, on = False )
self.setWindowFlag( QC.Qt.WindowMaximizeButtonHint, on = False )
self.setAttribute( QC.Qt.WA_DeleteOnClose )
self.setWindowTitle( title )
self.setWindowIcon( QG.QIcon( self._controller.frame_icon_pixmap ) )
self._my_panel = FrameSplashPanel( self, self._controller )
self._cancel_shutdown_maintenance = ClientGUICommon.BetterButton( self, 'stop shutdown maintenance', self.CancelShutdownMaintenance )
self._cancel_shutdown_maintenance.hide()
#
self._vbox = QP.VBoxLayout()
QP.AddToLayout( self._vbox, self._cancel_shutdown_maintenance, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( self._vbox, self._my_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.setLayout( self._vbox )
screen = ClientGUIFunctions.GetMouseScreen()
if screen is not None:
self.move( screen.availableGeometry().center() - self.rect().center() )
self.show()
self.raise_()
def CancelShutdownMaintenance( self ):
self._cancel_shutdown_maintenance.setText( 'stopping\u2026' )
self._cancel_shutdown_maintenance.setEnabled( False )
HG.do_idle_shutdown_work = False
def ShowCancelShutdownButton( self ):
self._cancel_shutdown_maintenance.show()

View File

@ -14,6 +14,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusText
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientPaths
from hydrus.client.gui import ClientGUICommon
from hydrus.client.gui import ClientGUIDialogs
@ -181,19 +182,15 @@ class EditSubscriptionPanel( ClientGUIScrolledPanels.EditPanel ):
message = '''****Subscriptions are not for large one-time syncs****
tl;dr: Do not change the checker options or file limits until you really know what you are doing. The limits are now only 1000 (10000 in advanced mode) anyway, but you should leave them at 100/100.
tl;dr: Do not change the checker options or file limits until you really know what you are doing. There are good technical reasons for it, and you can screw yourself.
A subscription will start at a site's newest files and keep searching further and further back into the past. It will stop naturally if it reaches the end of results or starts to see files it saw in a previous check (and so assumes it has 'caught up' to where it was before). It will stop 'artificially' if it finds enough new files to hit the file limits here.
Normally, a subscription starts at a site's newest files and searches until it reaches the end of results or starts to see files it saw in a previous check (having 'caught up' to where it was before). It will stop early if it finds enough new files to hit the file limits here. Unless you have a very special reason, it is important to keep these file limit numbers low. Being automated, it is good to have some brakes to stop them if something goes wrong.
Unless you have a very special reason, it is important to keep these file limit numbers low. Being automated, subscriptions typically run when you are not looking at the client, and if they go wrong, it is good to have some brakes to stop them going very wrong.
You want a few initial files so the sub has some data to work with, but not too much that the first sync takes ages. You want a limit on regular checks in case a site changes its URL format (say from artistname.deviantart.com to deviantart.com/artistname) or changes its markup or otherwise starts delivering unusual results, and the subscription may not realise it is seeing the old urls as new. If the periodic limit is 100, this is no big deal--you'll likely get a popup message out of it and might need to update the respective downloader--but if it were 60000 (or infinite, and the site were somehow serving you random/full results!), you could run into a huge problem completely by accident.
First of all, making sure you only get a few dozen or hundred on the first check means you do not spend twenty minutes fetching all the search's thousands of file URLs that you may well have previously downloaded, but it is even more important for regular checks, where the sub is trying to find where it got to before: if a site changes its URL format (say from artistname.deviantart.com to deviantart.com/artistname) or changes its markup or otherwise starts delivering unusual results, the subscription may not realise it is seeing the wrong urls and will keep syncing until it hits its regular limit. If the periodic limit is 100, this is no big deal--you'll likely get a popup message out of it and might need to update the respective downloader--but if it were 60000 (or infinite, and the site were somehow serving you random/full results!), you could run into a huge problem completely by accident.
Subscription sync searches are also 'fragile' (they cannot pause/resume the gallery pagewalk, only completely cancel), so it is best if they are short--say, no more than five pages. It is better for a sub to regularly pick up a small number of new files than trying to catch up in a giant rush.
Subscription sync searches are somewhat 'fragile' (they cannot pause/resume the gallery pagewalk, only completely cancel), so it is best if they are short--say, no more than five pages. It is better for a sub to pick up a small number of new files every few weeks than trying to catch up in a giant rush once a year.
If you are not experienced with subscriptions, I strongly suggest you set these to something like 100 for the first check and 100 thereafter, which is likely your default. This works great for typical artist and character queries.
If you want to get all of an artist's files from a site, use the manual gallery download page first. A good routine is to check that you have the right search text and it all works correctly and that you know what tags you want, and then once that big queue is fully downloaded synced, start a new sub with the same settings to continue checking for anything posted in future.'''
If you want to get all of an artist's files from a site, use the manual gallery download page first. Check you have the right search text and it all works correctly there, and then once that big queue is fully downloaded, start a new sub with the same settings to continue checking for anything posted in future.'''
help_button = ClientGUICommon.BetterBitmapButton( self._file_limits_panel, CC.global_pixmaps().help, QW.QMessageBox.information, None, 'Information', message )
@ -473,7 +470,7 @@ But if 2 is--and is also perhaps accompanied by many 'could not parse' errors--t
else:
pretty_latest_new_file_time = HydrusData.TimestampToPrettyTimeDelta( latest_new_file_time )
pretty_latest_new_file_time = ClientData.TimestampToPrettyTimeDelta( latest_new_file_time )
if last_check_time is None or last_check_time == 0:
@ -482,7 +479,7 @@ But if 2 is--and is also perhaps accompanied by many 'could not parse' errors--t
else:
pretty_last_check_time = HydrusData.TimestampToPrettyTimeDelta( last_check_time )
pretty_last_check_time = ClientData.TimestampToPrettyTimeDelta( last_check_time )
pretty_next_check_time = query_header.GetNextCheckStatusString()
@ -994,7 +991,7 @@ But if 2 is--and is also perhaps accompanied by many 'could not parse' errors--t
else:
status = 'delayed--retrying ' + HydrusData.TimestampToPrettyTimeDelta( self._no_work_until, just_now_threshold = 0 ) + ' because: ' + self._no_work_until_reason
status = 'delayed--retrying ' + ClientData.TimestampToPrettyTimeDelta( self._no_work_until, just_now_threshold = 0 ) + ' because: ' + self._no_work_until_reason
self._delay_st.setText( status )
@ -1420,7 +1417,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
else:
pretty_latest_new_file_time = HydrusData.TimestampToPrettyTimeDelta( latest_new_file_time )
pretty_latest_new_file_time = ClientData.TimestampToPrettyTimeDelta( latest_new_file_time )
if last_checked is None or last_checked == 0:
@ -1429,7 +1426,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
else:
pretty_last_checked = HydrusData.TimestampToPrettyTimeDelta( last_checked )
pretty_last_checked = ClientData.TimestampToPrettyTimeDelta( last_checked )
#
@ -1515,7 +1512,7 @@ class EditSubscriptionsPanel( ClientGUIScrolledPanels.EditPanel ):
else:
pretty_delay = 'delayed--retrying ' + HydrusData.TimestampToPrettyTimeDelta( no_work_until, just_now_threshold = 0 ) + ' - because: ' + no_work_until_reason
pretty_delay = 'delayed--retrying ' + ClientData.TimestampToPrettyTimeDelta( no_work_until, just_now_threshold = 0 ) + ' - because: ' + no_work_until_reason
delay = HydrusData.GetTimeDeltaUntilTime( no_work_until )

View File

@ -3098,7 +3098,7 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
export_string = self._GetExportString()
with QP.FileDialog( self, 'Set the export path.', defaultFile = 'parents.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
with QP.FileDialog( self, 'Set the export path.', default_filename = 'parents.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
@ -3961,7 +3961,7 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
export_string = self._GetExportString()
with QP.FileDialog( self, 'Set the export path.', defaultFile = 'siblings.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
with QP.FileDialog( self, 'Set the export path.', default_filename = 'siblings.txt', acceptMode = QW.QFileDialog.AcceptSave ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:

View File

@ -1260,14 +1260,6 @@ def ClearLayout( layout, delete_widgets = False ):
layout.removeItem( item )
def ListWidgetGetStringSelection( widget ):
for i in range( widget.count() ):
if widget.item( i ).isSelected(): return widget.item( i ).text()
return None
def GetClientData( widget, idx ):
if isinstance( widget, QW.QComboBox ):
@ -1341,15 +1333,6 @@ def ListWidgetGetStrings( widget ):
return strings
def ListWidgetIndexForString( widget, string ):
for i in range( widget.count() ):
if widget.item( i ).text() == string: return i
return -1
def ListWidgetIsSelected( widget, idx ):
if idx == -1: return False
@ -1599,8 +1582,8 @@ class UIActionSimulator:
ev1 = QG.QKeyEvent( QC.QEvent.KeyPress, key, QC.Qt.NoModifier, text = text )
ev2 = QG.QKeyEvent( QC.QEvent.KeyRelease, key, QC.Qt.NoModifier, text = text )
QW.QApplication.postEvent( widget, ev1 )
QW.QApplication.postEvent( widget, ev2 )
QW.QApplication.instance().postEvent( widget, ev1 )
QW.QApplication.instance().postEvent( widget, ev2 )
class AboutBox( QW.QDialog ):
@ -2141,17 +2124,30 @@ class DirDialog( QW.QFileDialog ):
class FileDialog( QW.QFileDialog ):
def __init__( self, parent = None, message = None, acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFile, defaultFile = None, wildcard = None ):
def __init__( self, parent = None, message = None, acceptMode = QW.QFileDialog.AcceptOpen, fileMode = QW.QFileDialog.ExistingFile, default_filename = None, default_directory = None, wildcard = None, defaultSuffix = None ):
QW.QFileDialog.__init__( self, parent )
if message is not None: self.setWindowTitle( message )
self.setAcceptMode( acceptMode )
self.setFileMode( fileMode )
if defaultFile: self.setDirectory( defaultFile )
if default_directory is not None:
self.setDirectory( default_directory )
if defaultSuffix is not None:
self.setDefaultSuffix( defaultSuffix )
if default_filename is not None:
self.selectFile( default_filename )
if wildcard: self.setNameFilter( wildcard )

View File

@ -123,7 +123,7 @@ class FileImportJob( object ):
def GenerateHashAndStatus( self ):
HydrusImageHandling.ConvertToPngIfBmp( self._temp_path )
HydrusImageHandling.ConvertToPNGIfBMP( self._temp_path )
self._hash = HydrusFileHandling.GetHashFromPath( self._temp_path )

View File

@ -10,6 +10,7 @@ from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusThreading
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientDownloading
from hydrus.client import ClientThreading
from hydrus.client.importing import ClientImporting
@ -243,7 +244,7 @@ class SubscriptionQueryLegacy( HydrusSerialisable.SerialisableBase ):
else:
s = HydrusData.TimestampToPrettyTimeDelta( self._next_check_time )
s = ClientData.TimestampToPrettyTimeDelta( self._next_check_time )
if self._paused:

View File

@ -4,6 +4,7 @@ from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client.importing import ClientImporting
from hydrus.client.importing import ClientImportFileSeeds
from hydrus.client.importing import ClientImportGallerySeeds
@ -470,7 +471,7 @@ class SubscriptionQueryHeader( HydrusSerialisable.SerialisableBase ):
else:
s = HydrusData.TimestampToPrettyTimeDelta( self._next_check_time )
s = ClientData.TimestampToPrettyTimeDelta( self._next_check_time )
if self._paused:

View File

@ -11,6 +11,7 @@ from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusThreading
from hydrus.client import ClientData
from hydrus.client import ClientThreading
from hydrus.client import ClientConstants as CC
from hydrus.client.importing import ClientImporting
@ -1734,7 +1735,7 @@ class SubscriptionsManager( object ):
message += os.linesep * 2
message += '{} not runnable: {}'.format( HydrusData.ToHumanInt( len( self._names_that_cannot_run ) ), ', '.join( cannot_run ) )
message += os.linesep * 2
message += '{} next times: {}'.format( HydrusData.ToHumanInt( len( self._names_to_next_work_time ) ), ', '.join( ( '{}: {}'.format( name, HydrusData.TimestampToPrettyTimeDelta( next_work_time ) ) for ( name, next_work_time ) in next_times ) ) )
message += '{} next times: {}'.format( HydrusData.ToHumanInt( len( self._names_to_next_work_time ) ), ', '.join( ( '{}: {}'.format( name, ClientData.TimestampToPrettyTimeDelta( next_work_time ) ) for ( name, next_work_time ) in next_times ) ) )
HydrusData.ShowText( message )

View File

@ -2,12 +2,13 @@ import threading
import time
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientTags
from hydrus.client.importing import ClientImporting
from hydrus.client.importing import ClientImportOptions
from hydrus.client.importing import ClientImportFileSeeds
from hydrus.client.importing import ClientImportGallerySeeds
from hydrus.client.networking import ClientNetworkingJobs
from hydrus.client import ClientTags
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
@ -1162,7 +1163,7 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
elif not HydrusData.TimeHasPassed( self._no_work_until ):
return self._no_work_until_reason + ' - ' + 'next check ' + HydrusData.TimestampToPrettyTimeDelta( self._next_check_time )
return self._no_work_until_reason + ' - ' + 'next check ' + ClientData.TimestampToPrettyTimeDelta( self._next_check_time )
elif self._watcher_status != '':
@ -1195,7 +1196,7 @@ class WatcherImport( HydrusSerialisable.SerialisableBase ):
elif not HydrusData.TimeHasPassed( self._no_work_until ):
no_work_text = self._no_work_until_reason + ' - ' + 'next check ' + HydrusData.TimestampToPrettyTimeDelta( self._next_check_time )
no_work_text = self._no_work_until_reason + ' - ' + 'next check ' + ClientData.TimestampToPrettyTimeDelta( self._next_check_time )
file_status = no_work_text
watcher_status = no_work_text

View File

@ -3,6 +3,7 @@ import random
import typing
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client import ClientTags
from hydrus.client.media import ClientMediaManagers
from hydrus.client.media import ClientMediaResult
@ -382,7 +383,7 @@ def GetDuplicateComparisonStatements( shown_media, comparison_media ):
score = 0
statement = '{} {} {}'.format( HydrusData.TimestampToPrettyTimeDelta( s_ts ), operator, HydrusData.TimestampToPrettyTimeDelta( c_ts ) )
statement = '{} {} {}'.format( ClientData.TimestampToPrettyTimeDelta( s_ts ), operator, ClientData.TimestampToPrettyTimeDelta( c_ts ) )
statements_and_scores[ 'time_imported' ] = ( statement, score )
@ -2321,14 +2322,14 @@ class MediaSingleton( Media ):
timestamp = locations_manager.GetTimestamp( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
lines.append( 'imported ' + HydrusData.TimestampToPrettyTimeDelta( timestamp ) )
lines.append( 'imported ' + ClientData.TimestampToPrettyTimeDelta( timestamp ) )
if CC.TRASH_SERVICE_KEY in current_service_keys:
timestamp = locations_manager.GetTimestamp( CC.TRASH_SERVICE_KEY )
lines.append( 'trashed ' + HydrusData.TimestampToPrettyTimeDelta( timestamp ) )
lines.append( 'trashed ' + ClientData.TimestampToPrettyTimeDelta( timestamp ) )
if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in deleted_service_keys:
@ -2340,7 +2341,7 @@ class MediaSingleton( Media ):
if file_modified_timestamp is not None:
lines.append( 'file modified: {}'.format( HydrusData.TimestampToPrettyTimeDelta( file_modified_timestamp ) ) )
lines.append( 'file modified: {}'.format( ClientData.TimestampToPrettyTimeDelta( file_modified_timestamp ) ) )
for service_key in current_service_keys:
@ -2372,7 +2373,7 @@ class MediaSingleton( Media ):
status = 'uploaded '
lines.append( status + 'to ' + service.GetName() + ' ' + HydrusData.TimestampToPrettyTimeDelta( timestamp ) )
lines.append( status + 'to ' + service.GetName() + ' ' + ClientData.TimestampToPrettyTimeDelta( timestamp ) )
return lines

View File

@ -7,6 +7,7 @@ import time
import urllib
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client.networking import ClientNetworkingContexts
from hydrus.client.networking import ClientNetworkingDomain
from hydrus.core import HydrusConstants as HC
@ -640,7 +641,7 @@ class NetworkJob( object ):
with self._lock:
self._status_text = status_text + ' - retrying in {}'.format( HydrusData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._connection_error_wake_time ) )
time.sleep( 1 )
@ -668,7 +669,7 @@ class NetworkJob( object ):
with self._lock:
self._status_text = status_text + ' - retrying in {}'.format( HydrusData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
self._status_text = status_text + ' - retrying in {}'.format( ClientData.TimestampToPrettyTimeDelta( self._serverside_bandwidth_wake_time ) )
time.sleep( 1 )
@ -1334,7 +1335,7 @@ class NetworkJob( object ):
else:
self._status_text = 'waiting for a ' + self._gallery_token_name + ' slot: next ' + HydrusData.TimestampToPrettyTimeDelta( next_timestamp, just_now_threshold = 1 )
self._status_text = 'waiting for a ' + self._gallery_token_name + ' slot: next ' + ClientData.TimestampToPrettyTimeDelta( next_timestamp, just_now_threshold = 1 )
self._Sleep( 1 )
@ -1379,13 +1380,13 @@ class NetworkJob( object ):
waiting_duration = override_waiting_duration
waiting_str = 'overriding bandwidth ' + HydrusData.TimestampToPrettyTimeDelta( self._bandwidth_manual_override_delayed_timestamp, just_now_string = 'imminently', just_now_threshold = just_now_threshold )
waiting_str = 'overriding bandwidth ' + ClientData.TimestampToPrettyTimeDelta( self._bandwidth_manual_override_delayed_timestamp, just_now_string = 'imminently', just_now_threshold = just_now_threshold )
else:
waiting_duration = bandwidth_waiting_duration
waiting_str = 'bandwidth free ' + HydrusData.TimestampToPrettyTimeDelta( HydrusData.GetNow() + waiting_duration, just_now_string = 'imminently', just_now_threshold = just_now_threshold )
waiting_str = 'bandwidth free ' + ClientData.TimestampToPrettyTimeDelta( HydrusData.GetNow() + waiting_duration, just_now_string = 'imminently', just_now_threshold = just_now_threshold )
waiting_str += '\u2026 (' + bandwidth_network_context.ToHumanString() + ')'

View File

@ -73,8 +73,8 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 401
CLIENT_API_VERSION = 12
SOFTWARE_VERSION = 402
CLIENT_API_VERSION = 13
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -23,6 +23,9 @@ class HydrusController( object ):
self._name = 'hydrus'
self._last_shutdown_was_bad = False
self._i_own_running_file = False
self.db_dir = db_dir
self.db = None
@ -64,6 +67,8 @@ class HydrusController( object ):
self._system_busy = False
self._doing_fast_exit = False
def _GetCallToThread( self ):
@ -349,6 +354,14 @@ class HydrusController( object ):
call_to_thread.put( callable, *args, **kwargs )
def CleanRunningFile( self ):
if self._i_own_running_file:
HydrusData.CleanRunningFile( self.db_dir, self._name )
def ClearCaches( self ):
for cache in list(self._caches.values()): cache.Clear()
@ -389,6 +402,11 @@ class HydrusController( object ):
HydrusData.ShowText( summary )
def DoingFastExit( self ) -> bool:
return self._doing_fast_exit
def GetBootTime( self ):
return self._timestamps[ 'boot' ]
@ -539,6 +557,11 @@ class HydrusController( object ):
def LastShutdownWasBad( self ):
return self._last_shutdown_was_bad
def MaintainDB( self, maintenance_mode = HC.MAINTENANCE_IDLE, stop_time = None ):
pass
@ -587,6 +610,15 @@ class HydrusController( object ):
return self._Read( action, *args, **kwargs )
def RecordRunningStart( self ):
self._last_shutdown_was_bad = HydrusData.LastShutdownWasBad( self.db_dir, self._name )
self._i_own_running_file = True
HydrusData.RecordRunningStart( self.db_dir, self._name )
def ReleaseThreadSlot( self, thread_type ):
with self._thread_slot_lock:
@ -617,6 +649,11 @@ class HydrusController( object ):
self._timestamps[ 'last_user_action' ] = HydrusData.GetNow()
def SetDoingFastExit( self, value: bool ):
self._doing_fast_exit = value
def ShouldStopThisWork( self, maintenance_mode, stop_time = None ):
if maintenance_mode == HC.MAINTENANCE_IDLE:

View File

@ -15,6 +15,7 @@ import time
import traceback
import yaml
import itertools
import typing
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusExceptions
@ -68,11 +69,6 @@ def CleanRunningFile( db_path, instance ):
# just to be careful
if HG.shutting_down_due_to_already_running:
return
path = os.path.join( db_path, instance + '_running' )
try:
@ -414,11 +410,6 @@ def TimestampToPrettyTimeDelta( timestamp, just_now_string = 'now', just_now_thr
timestamp = 0
if HG.client_controller.new_options.GetBoolean( 'always_show_iso_time' ):
return ConvertTimestampToPrettyTime( timestamp )
if not show_seconds:
just_now_threshold = max( just_now_threshold, 60 )
@ -1036,6 +1027,18 @@ def MergeKeyToListDicts( key_to_list_dicts ):
return result
def PartitionIterator( pred: typing.Callable[ [ object ], bool ], stream: typing.Iterable[ object ] ):
( t1, t2 ) = itertools.tee( stream )
return ( itertools.filterfalse( pred, t1 ), filter( pred, t2 ) )
def PartitionIteratorIntoLists( pred: typing.Callable[ [ object ], bool ], stream: typing.Iterable[ object ] ):
( a, b ) = PartitionIterator( pred, stream )
return ( list( a ), list( b ) )
def Print( text ):
try:

View File

@ -40,13 +40,9 @@ no_page_limit_mode = False
thumbnail_debug_mode = False
currently_uploading_pending = False
shutting_down_due_to_already_running = False
do_idle_shutdown_work = False
program_is_shutting_down = False
shutdown_complete = False
restart = False
emergency_exit = False
twisted_is_broke = False

View File

@ -103,7 +103,7 @@ except:
OPENCV_OK = False
def ConvertToPngIfBmp( path ):
def ConvertToPNGIfBMP( path ):
with open( path, 'rb' ) as f:

View File

@ -1990,6 +1990,14 @@ class Metadata( HydrusSerialisable.SerialisableBase ):
def HasDoneInitialSync( self ):
with self._lock:
return self._next_update_due != 0
def HasUpdateHash( self, update_hash ):
with self._lock:

View File

@ -210,7 +210,7 @@ def GenerateEris( service ):
def ParseFileArguments( path, decompression_bombs_ok = False ):
HydrusImageHandling.ConvertToPngIfBmp( path )
HydrusImageHandling.ConvertToPNGIfBMP( path )
hash = HydrusFileHandling.GetHashFromPath( path )

View File

@ -70,7 +70,7 @@ def GetThreadInfo( thread = None ):
def IsThreadShuttingDown():
if HG.emergency_exit:
if HG.controller.DoingFastExit():
return True

View File

@ -209,10 +209,7 @@ class Controller( HydrusController.HydrusController ):
self.ShutdownModel()
if not HG.shutting_down_due_to_already_running:
HydrusData.CleanRunningFile( self.db_dir, 'server' )
self.CleanRunningFile()
def GetFilesDir( self ):
@ -292,7 +289,7 @@ class Controller( HydrusController.HydrusController ):
def Run( self ):
HydrusData.RecordRunningStart( self.db_dir, 'server' )
self.RecordRunningStart()
HydrusData.Print( 'Initialising db\u2026' )

View File

@ -438,8 +438,6 @@ class DB( HydrusDB.HydrusDB ):
def _DeleteService( self, service_key ):
# assume foreign keys is on here
service_id = self._GetServiceId( service_key )
service_type = self._GetServiceType( service_id )
@ -1190,12 +1188,6 @@ class DB( HydrusDB.HydrusDB ):
account.CheckPermission( HC.CONTENT_TYPE_SERVICES, HC.PERMISSION_ACTION_OVERRULE )
self._Commit()
self._c.execute( 'PRAGMA foreign_keys = ON;' )
self._BeginImmediate()
current_service_keys = { service_key for ( service_key, ) in self._c.execute( 'SELECT service_key FROM services;' ) }
future_service_keys = { service.GetServiceKey() for service in services }

View File

@ -1598,6 +1598,7 @@ class TestClientAPI( unittest.TestCase ):
metadata = []
detailed_known_urls_metadata = []
services_manager = HG.client_controller.services_manager
@ -1649,8 +1650,18 @@ class TestClientAPI( unittest.TestCase ):
metadata.append( metadata_row )
detailed_known_urls_metadata_row = dict( metadata_row )
detailed_known_urls_metadata_row[ 'detailed_known_urls' ] = [
{'normalised_url': 'https://gelbooru.com/index.php?id=4841557&page=post&s=view', 'url_type': 0, 'url_type_string': 'post url', 'match_name': 'gelbooru file page', 'can_parse': True},
{'normalised_url': 'https://img2.gelbooru.com//images/80/c8/80c8646b4a49395fb36c805f316c49a9.jpg', 'url_type': 5, 'url_type_string': 'unknown url', 'match_name': 'unknown url', 'can_parse': False}
]
detailed_known_urls_metadata.append( detailed_known_urls_metadata_row )
expected_metadata_result = { 'metadata' : metadata }
expected_detailed_known_urls_metadata_result = { 'metadata' : detailed_known_urls_metadata }
HG.test_controller.SetRead( 'hash_ids_to_hashes', file_ids_to_hashes )
HG.test_controller.SetRead( 'media_results', media_results )
@ -1762,6 +1773,24 @@ class TestClientAPI( unittest.TestCase ):
self.assertEqual( d, expected_metadata_result )
# metadata from hashes with detailed url info
path = '/get_files/file_metadata?hashes={}&detailed_url_information=true'.format( urllib.parse.quote( json.dumps( [ hash.hex() for hash in file_ids_to_hashes.values() ] ) ) )
connection.request( 'GET', path, headers = headers )
response = connection.getresponse()
data = response.read()
text = str( data, 'utf-8' )
self.assertEqual( response.status, 200 )
d = json.loads( text )
self.assertEqual( d, expected_detailed_known_urls_metadata_result )
# files and thumbs
hash = b'\xadm5\x99\xa6\xc4\x89\xa5u\xeb\x19\xc0&\xfa\xce\x97\xa9\xcdey\xe7G(\xb0\xce\x94\xa6\x01\xd22\xf3\xc3'

View File

@ -10,7 +10,7 @@ from hydrus.client.gui import QtPorting as QP
def DoClick( click, panel, do_delayed_ok_afterwards = False ):
QW.QApplication.postEvent( panel.widget(), click )
QW.QApplication.instance().postEvent( panel.widget(), click )
if do_delayed_ok_afterwards:

View File

@ -472,6 +472,11 @@ class Controller( object ):
return False
def DoingFastExit( self ):
return False
def GetCurrentSessionPageAPIInfoDict( self ):
return {

View File

@ -11,7 +11,7 @@ Pillow>=6.0.0
psutil>=5.0.0
pylzma>=0.5.0
pyOpenSSL>=19.1.0
PySide2<=5.13.0
PySide2>=5.15.0
PySocks>=1.7.0
python-mpv>=0.4.5
PyYAML>=5.0.0

View File

@ -1,23 +0,0 @@
beautifulsoup4>=4.0.0
chardet>=3.0.4
cloudscraper>=1.2.33
html5lib>=1.0.1
lxml>=4.5.0
lz4>=3.0.0
nose>=1.3.0
numpy>=1.16.0
opencv-python-headless>=4.0.0
Pillow>=6.0.0
psutil>=5.0.0
pylzma>=0.5.0
pyOpenSSL>=19.1.0
PySide2==5.15.0
PySocks>=1.7.0
python-mpv>=0.4.5
PyYAML>=5.0.0
QtPy>=1.9.0
requests>=2.23.0
Send2Trash>=1.5.0
service-identity>=18.1.0
six>=1.14.0
Twisted>=20.3.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB