Merge remote-tracking branch 'hydrusnetwork/master' into mkdocs
This commit is contained in:
commit
9550648a27
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
|
@ -139,7 +139,7 @@ jobs:
|
|||
retention-days: 2
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
|
@ -211,6 +211,8 @@ jobs:
|
|||
pyinstaller client-win.spec
|
||||
dir -r
|
||||
-
|
||||
# yo pretty sure we'll need to install this manually once we are on windows server 2022
|
||||
# https://github.com/actions/virtual-environments/issues/4856
|
||||
name: InnoSetup
|
||||
run: |
|
||||
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
|
||||
|
|
|
@ -33,6 +33,37 @@
|
|||
<div class="content">
|
||||
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
|
||||
<ul>
|
||||
<li><h3 id="version_474"><a href="#version_474">version 474</a></h3></li>
|
||||
<ul>
|
||||
<li>command palette:</li>
|
||||
<li>the guy who put the command pallete together has fixed a 'show palette' bug some people encountered (issue #1060)</li>
|
||||
<li>he also added mouse support!</li>
|
||||
<li>he added support to show checkable menu items too, and I integrated this for the menubar (lightning bolt icon) items</li>
|
||||
<li>I added a line to the default QSS that I think fixes the odd icon/text background colours some users saw in the command palette</li>
|
||||
<li>.</li>
|
||||
<li>misc:</li>
|
||||
<li>file archive times are now recorded in the background. there's no load/search/sort yet, but this will be added in future</li>
|
||||
<li>under 'manage shortcuts', there is a new checkbox to rename left- and right-click to primary- and secondary- in the shortcuts UI. if you have a flipped mouse or any other odd situation, try it out</li>
|
||||
<li>if a file storage location does not have enough free disk space for a file, or if it just has <100MB generally, the client now throws up a popup to say what happened specifically with instructions to shut down and fix now and automatically pauses subscriptions, paged file import queues, and import folders. this test occurs before the attempt to copy the file into place. free space isn't actually checked over and over, it is cached for up to an hour depending on the last free space amount</li>
|
||||
<li>this 'paused all regular imports' mode is also now fired any time any simple file-add action fails to copy. at this stage, we are talking 'device disconnected' and 'device failed' style errors, so might as well pause everything just to be careful</li>
|
||||
<li>when the downloader hits a post url that spawns several subsidiary downloads (for instance on pixiv and artstation when you have a multi-file post), the status of that parent post is now 'completed', a new status to represent 'good, but not direct file'. new download queues will then present '3N' and '3 successful' summary counts that actually correspond to number of files rather than number of successful items</li>
|
||||
<li>pages now give a concise 'summary name' of 'name - num_files - import progress' (it also eli...des for longer names) for menus and the new command palette, which unlike the older status-bar-based strings are always available and will stop clients with many pages becoming multi-wide-column-menu-hell</li>
|
||||
<li>improved apng parsing. hydrus can now detect that pngs are actually apngs for (hopefully) all types of valid apng. it turns out some weird apngs have some additional header data, but I wrote a new chunk parser that should figure it all out</li>
|
||||
<li>with luck, users who have window focus issues when closing a child window (e.g. close review services, the main gui does not get focus back), should now see that happen (issue #1063). this may need some more work, so let me know</li>
|
||||
<li>the session weight count in the 'pages' menu now updates on any add thumbs, remove thumbs, or thumbnail panel swap. this _should_ be fast all the time, and buffer nicely if it is ever overwhelmed, but let me know if you have a madlad session and get significant new lag when you watch a downloader bring in new files</li>
|
||||
<li>a user came up with a clever idea to efficiently target regenerations for the recent fix to pixel duplicate calculations for images with opaque alpha channels, so this week I will queue up some pixel hash regeneration. it does not fix every file with an opaque alpha channel, but it should help out. it also shouldn't take _all_ that long to clear this queue out. lastly, I renamed that file maintenance job from 'calculate file pixel hash' to 'regenerate pixel duplicate data'</li>
|
||||
<li>the various duplicate system actions on thumbnails now specify the number of files being acted on in the yes/no dialog</li>
|
||||
<li>fixed a bug when searching in complicated multi-file-service domains on a client that has been on for a long time (some data used here was being reset in regular db maintenance)</li>
|
||||
<li>fixed a bug where for very unlucky byte sizes, for instance 188213746, the client was flipping between two different output values (e.g. 179MB/180MB) on subsequent calls (issue #1068)</li>
|
||||
<li>after some user profiles and experimental testing, rebalanced some optimisations in sibling and parent calculation. fingers crossed, some larger sibling groups with worst-case numbers should calculate more efficiently</li>
|
||||
<li>if sibling/parent calculation hits a heavy bump and takes a really long time to do a job during 'normal' time, the whole system now takes a much longer break (half an hour) before continuing</li>
|
||||
<li>.</li>
|
||||
<li>boring stuff:</li>
|
||||
<li>the delete dialog has basic multiple local file service support ready for that expansion. it no longer refers to the old static 'my files' service identifier. I think it will need some user-friendly more polish once that feature is in</li>
|
||||
<li>the 'migrate tags' dialog's file service filtering now supports n local file services, and 'all local files'</li>
|
||||
<li>updated the build scripts to force windows server 2019 (and macos-11). github is rolling out windows 2022 as the new latest, and there's a couple of things to iron out first on our end. this is probably going to happen this year though, along with Qt6 and python 3.9, which will all mean end of life for windows 7 in our built hydrus release</li>
|
||||
<li>removed the spare platform-specific github workflow scripts from the static folder--I wanted these as a sort of backup, but they never proved useful and needed to be synced on all changes</li>
|
||||
</ul>
|
||||
<li><h3 id="version_473"><a href="#version_473">version 473</a></h3></li>
|
||||
<ul>
|
||||
<li>misc:</li>
|
||||
|
|
|
@ -418,6 +418,7 @@ STATUS_NEW = 5 # no longer used
|
|||
STATUS_PAUSED = 6 # not used
|
||||
STATUS_VETOED = 7
|
||||
STATUS_SKIPPED = 8
|
||||
STATUS_SUCCESSFUL_AND_CHILD_FILES = 9
|
||||
|
||||
status_string_lookup = {}
|
||||
|
||||
|
@ -430,8 +431,9 @@ status_string_lookup[ STATUS_NEW ] = 'new'
|
|||
status_string_lookup[ STATUS_PAUSED ] = 'paused'
|
||||
status_string_lookup[ STATUS_VETOED ] = 'ignored'
|
||||
status_string_lookup[ STATUS_SKIPPED ] = 'skipped'
|
||||
status_string_lookup[ STATUS_SUCCESSFUL_AND_CHILD_FILES ] = 'completed'
|
||||
|
||||
SUCCESSFUL_IMPORT_STATES = { STATUS_SUCCESSFUL_AND_NEW, STATUS_SUCCESSFUL_BUT_REDUNDANT }
|
||||
SUCCESSFUL_IMPORT_STATES = { STATUS_SUCCESSFUL_AND_NEW, STATUS_SUCCESSFUL_BUT_REDUNDANT, STATUS_SUCCESSFUL_AND_CHILD_FILES }
|
||||
UNSUCCESSFUL_IMPORT_STATES = { STATUS_DELETED, STATUS_ERROR, STATUS_VETOED }
|
||||
FAILED_IMPORT_STATES = { STATUS_ERROR, STATUS_VETOED }
|
||||
|
||||
|
|
|
@ -1238,6 +1238,8 @@ class Controller( HydrusController.HydrusController ):
|
|||
|
||||
ClientGUIShortcuts.ShortcutsManager( shortcut_sets = shortcut_sets )
|
||||
|
||||
ClientGUIShortcuts.SetMouseLabels( self.new_options.GetBoolean( 'call_mouse_buttons_primary_secondary' ) )
|
||||
|
||||
ClientGUIStyle.InitialiseDefaults()
|
||||
|
||||
qt_style_name = self.new_options.GetNoneableString( 'qt_style_name' )
|
||||
|
|
|
@ -61,7 +61,7 @@ regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_CHECK_SIMILAR_FILES_MEMB
|
|||
regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_SIMILAR_FILES_METADATA ] = 'regenerate similar files metadata'
|
||||
regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_FILE_MODIFIED_TIMESTAMP ] = 'regenerate file modified date'
|
||||
regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE ] = 'determine if the file has an icc profile'
|
||||
regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_PIXEL_HASH ] = 'calculate file pixel hash'
|
||||
regen_file_enum_to_str_lookup[ REGENERATE_FILE_DATA_JOB_PIXEL_HASH ] = 'regenerate pixel duplicate data'
|
||||
|
||||
regen_file_enum_to_description_lookup = {}
|
||||
|
||||
|
@ -181,6 +181,8 @@ class ClientFilesManager( object ):
|
|||
|
||||
self._new_physical_file_deletes = threading.Event()
|
||||
|
||||
self._locations_to_free_space = {}
|
||||
|
||||
self._bad_error_occurred = False
|
||||
self._missing_locations = set()
|
||||
|
||||
|
@ -189,6 +191,59 @@ class ClientFilesManager( object ):
|
|||
self._controller.sub( self, 'shutdown', 'shutdown' )
|
||||
|
||||
|
||||
def _GetFileStorageFreeSpace( self, hash: bytes ) -> int:
|
||||
|
||||
hash_encoded = hash.hex()
|
||||
|
||||
prefix = 'f' + hash_encoded[:2]
|
||||
|
||||
location = self._prefixes_to_locations[ prefix ]
|
||||
|
||||
if location in self._locations_to_free_space:
|
||||
|
||||
( free_space, time_fetched ) = self._locations_to_free_space[ location ]
|
||||
|
||||
if free_space > 100 * ( 1024 ** 3 ):
|
||||
|
||||
check_period = 3600
|
||||
|
||||
elif free_space > 15 * ( 1024 ** 3 ):
|
||||
|
||||
check_period = 600
|
||||
|
||||
else:
|
||||
|
||||
check_period = 60
|
||||
|
||||
|
||||
if HydrusData.TimeHasPassed( time_fetched + check_period ):
|
||||
|
||||
free_space = HydrusPaths.GetFreeSpace( location )
|
||||
|
||||
self._locations_to_free_space[ location ] = ( free_space, HydrusData.GetNow() )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
free_space = HydrusPaths.GetFreeSpace( location )
|
||||
|
||||
self._locations_to_free_space[ location ] = ( free_space, HydrusData.GetNow() )
|
||||
|
||||
|
||||
return free_space
|
||||
|
||||
|
||||
def _HandleCriticalDriveError( self ):
|
||||
|
||||
HC.options['pause_import_folders_sync'] = True
|
||||
HC.options[ 'pause_subs_sync' ] = True
|
||||
self._controller.new_options.SetBoolean( 'pause_all_file_queues', True )
|
||||
|
||||
HydrusData.ShowText( 'All paged file import queues, subscriptions, and import folders have been paused. Resume them after restart under the file and network menus!' )
|
||||
|
||||
self._controller.pub( 'notify_refresh_network_menu' )
|
||||
|
||||
|
||||
def _AddFile( self, hash, mime, source_path ):
|
||||
|
||||
dest_path = self._GenerateExpectedFilePath( hash, mime )
|
||||
|
@ -198,10 +253,31 @@ class ClientFilesManager( object ):
|
|||
HydrusData.ShowText( 'Adding file to client file structure: from {} to {}'.format( source_path, dest_path ) )
|
||||
|
||||
|
||||
file_size = os.path.getsize( source_path )
|
||||
|
||||
dest_free_space = self._GetFileStorageFreeSpace( hash )
|
||||
|
||||
if dest_free_space < 100 * 1048576 or dest_free_space < file_size:
|
||||
|
||||
message = 'The disk for path "{}" is almost full and cannot take the file "{}", which is {}! Shut the client down now and fix this!'.format( dest_path, hash.hex(), HydrusData.ToHumanBytes( file_size ) )
|
||||
|
||||
HydrusData.ShowText( message )
|
||||
|
||||
self._HandleCriticalDriveError()
|
||||
|
||||
raise Exception( message )
|
||||
|
||||
|
||||
successful = HydrusPaths.MirrorFile( source_path, dest_path )
|
||||
|
||||
if not successful:
|
||||
|
||||
message = 'Copying the file from "{}" to "{}" failed! Details should be shown and other import queues should be paused. You should shut the client down now and fix this!'.format( source_path, dest_path )
|
||||
|
||||
HydrusData.ShowText( message )
|
||||
|
||||
self._HandleCriticalDriveError()
|
||||
|
||||
raise Exception( 'There was a problem copying the file from ' + source_path + ' to ' + dest_path + '!' )
|
||||
|
||||
|
||||
|
|
|
@ -246,6 +246,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
self._dictionary[ 'booleans' ][ 'force_animation_scanbar_show' ] = False
|
||||
|
||||
self._dictionary[ 'booleans' ][ 'call_mouse_buttons_primary_secondary' ] = False
|
||||
|
||||
#
|
||||
|
||||
self._dictionary[ 'colours' ] = HydrusSerialisable.SerialisableDictionary()
|
||||
|
|
|
@ -8110,6 +8110,15 @@ class DB( HydrusDB.HydrusDB ):
|
|||
continue
|
||||
|
||||
|
||||
if len( with_tag_ids ) > 1:
|
||||
|
||||
# ok, when we are using with_tag_ids_weight as a 'this is how long the hash_ids list is' in later weight calculations, it does not account for overlap
|
||||
# in real world data, bad siblings tend to have a count of anywhere from 8% to 600% of the ideal (30-50% is common), but the overlap is significant, often 98%
|
||||
# so just to fudge this number a bit better, let's multiply it by 0.75
|
||||
|
||||
with_tag_ids_weight = int( with_tag_ids_weight * 0.75 )
|
||||
|
||||
|
||||
# ultimately here, we are doing "delete all display mappings with hash_ids that have a storage mapping for a removee tag and no storage mappings for a keep tag
|
||||
# in order to reduce overhead, we go full meme and do a bunch of different situations
|
||||
|
||||
|
@ -13648,6 +13657,45 @@ class DB( HydrusDB.HydrusDB ):
|
|||
|
||||
|
||||
|
||||
if version == 473:
|
||||
|
||||
result = self._Execute( 'SELECT 1 FROM sqlite_master WHERE name = ?;', ( 'archive_timestamps', ) ).fetchone()
|
||||
|
||||
if result is None:
|
||||
|
||||
self._Execute( 'CREATE TABLE IF NOT EXISTS archive_timestamps ( hash_id INTEGER PRIMARY KEY, archived_timestamp INTEGER );' )
|
||||
self._CreateIndex( 'archive_timestamps', [ 'archived_timestamp' ] )
|
||||
|
||||
|
||||
try:
|
||||
|
||||
location_context = ClientLocation.LocationContext( current_service_keys = ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, ) )
|
||||
|
||||
db_location_context = self.modules_files_storage.GetDBLocationContext( location_context )
|
||||
|
||||
operator = '>'
|
||||
num_relationships = 0
|
||||
dupe_type = HC.DUPLICATE_POTENTIAL
|
||||
|
||||
dupe_hash_ids = self.modules_files_duplicates.DuplicatesGetHashIdsFromDuplicateCountPredicate( db_location_context, operator, num_relationships, dupe_type )
|
||||
|
||||
with self._MakeTemporaryIntegerTable( dupe_hash_ids, 'hash_id' ) as temp_hash_ids_table_name:
|
||||
|
||||
hash_ids = self._STS( self._Execute( 'SELECT hash_id FROM {} CROSS JOIN files_info USING ( hash_id ) WHERE mime IN {};'.format( temp_hash_ids_table_name, HydrusData.SplayListForDB( ( HC.IMAGE_GIF, HC.IMAGE_PNG, HC.IMAGE_TIFF ) ) ), ) )
|
||||
|
||||
self.modules_files_maintenance_queue.AddJobs( hash_ids, ClientFiles.REGENERATE_FILE_DATA_JOB_PIXEL_HASH )
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
HydrusData.PrintException( e )
|
||||
|
||||
message = 'Some pixel hash regen scheduling failed to set! This is not super important, but hydev would be interested in seeing the error that was printed to the log.'
|
||||
|
||||
self.pub_initial_message( message )
|
||||
|
||||
|
||||
|
||||
self._controller.frame_splash_status.SetTitleText( 'updated db to v{}'.format( HydrusData.ToHumanInt( version + 1 ) ) )
|
||||
|
||||
self._Execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
|
||||
|
|
|
@ -3,6 +3,7 @@ import sqlite3
|
|||
import typing
|
||||
|
||||
from hydrus.core import HydrusConstants as HC
|
||||
from hydrus.core import HydrusData
|
||||
from hydrus.core import HydrusDB
|
||||
from hydrus.core import HydrusExceptions
|
||||
|
||||
|
@ -32,6 +33,10 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
|
|||
( [ 'num_frames' ], False, 400 )
|
||||
]
|
||||
|
||||
index_generation_dict[ 'main.archive_timestamps' ] = [
|
||||
( [ 'archived_timestamp' ], False, 474 )
|
||||
]
|
||||
|
||||
return index_generation_dict
|
||||
|
||||
|
||||
|
@ -40,7 +45,8 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
|
|||
return {
|
||||
'main.file_inbox' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 400 ),
|
||||
'main.files_info' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY, size INTEGER, mime INTEGER, width INTEGER, height INTEGER, duration INTEGER, num_frames INTEGER, has_audio INTEGER_BOOLEAN, num_words INTEGER );', 400 ),
|
||||
'main.has_icc_profile' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 465 )
|
||||
'main.has_icc_profile' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 465 ),
|
||||
'main.archive_timestamps' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY, archived_timestamp INTEGER );', 474 )
|
||||
}
|
||||
|
||||
|
||||
|
@ -82,6 +88,10 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
|
|||
|
||||
self.inbox_hash_ids.difference_update( archiveable_hash_ids )
|
||||
|
||||
now = HydrusData.GetNow()
|
||||
|
||||
self._ExecuteMany( 'REPLACE INTO archive_timestamps ( hash_id, archived_timestamp ) VALUES ( ?, ? );', ( ( hash_id, now ) for hash_id in archiveable_hash_ids ) )
|
||||
|
||||
|
||||
return archiveable_hash_ids
|
||||
|
||||
|
@ -138,7 +148,8 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
|
|||
return [
|
||||
( 'file_inbox', 'hash_id' ),
|
||||
( 'files_info', 'hash_id' ),
|
||||
( 'has_icc_profile', 'hash_id' )
|
||||
( 'has_icc_profile', 'hash_id' ),
|
||||
( 'archive_timestamps', 'hash_id' )
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ class ClientDBFilesStorage( ClientDBModule.ClientDBModule ):
|
|||
|
||||
ClientDBModule.ClientDBModule.__init__( self, 'client file locations', cursor )
|
||||
|
||||
self.temp_file_storage_table_name = None
|
||||
self.temp_file_storage_table_name = 'mem.temp_file_storage_hash_id'
|
||||
|
||||
|
||||
def _GetInitialTableGenerationDict( self ) -> dict:
|
||||
|
@ -819,9 +819,9 @@ class ClientDBFilesStorage( ClientDBModule.ClientDBModule ):
|
|||
|
||||
# maybe I should stick this guy in 'temp' to live through db connection resets, but we'll see I guess. it is generally ephemeral, not going to linger through weird vacuum maintenance or anything right?
|
||||
|
||||
if self.temp_file_storage_table_name is None:
|
||||
|
||||
self.temp_file_storage_table_name = 'mem.temp_file_storage_hash_id'
|
||||
result = self._Execute( 'SELECT 1 FROM mem.sqlite_master WHERE name = ?;', ( self.temp_file_storage_table_name, ) ).fetchone()
|
||||
|
||||
if result is None:
|
||||
|
||||
self._Execute( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );'.format( self.temp_file_storage_table_name ) )
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ from hydrus.client import ClientExporting
|
|||
from hydrus.client import ClientLocation
|
||||
from hydrus.client import ClientParsing
|
||||
from hydrus.client import ClientPaths
|
||||
from hydrus.client import ClientRendering
|
||||
from hydrus.client import ClientServices
|
||||
from hydrus.client import ClientThreading
|
||||
from hydrus.client.gui import ClientGUIAsync
|
||||
|
@ -511,6 +510,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
self._controller.sub( self, 'NotifyNewImportFolders', 'notify_new_import_folders' )
|
||||
self._controller.sub( self, 'NotifyNewOptions', 'notify_new_options' )
|
||||
self._controller.sub( self, 'NotifyNewPages', 'notify_new_pages' )
|
||||
self._controller.sub( self, 'NotifyNewPagesCount', 'notify_new_pages_count' )
|
||||
self._controller.sub( self, 'NotifyNewPending', 'notify_new_pending' )
|
||||
self._controller.sub( self, 'NotifyNewPermissions', 'notify_new_permissions' )
|
||||
self._controller.sub( self, 'NotifyNewPermissions', 'notify_account_sync_due' )
|
||||
|
@ -518,6 +518,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
self._controller.sub( self, 'NotifyNewSessions', 'notify_new_sessions' )
|
||||
self._controller.sub( self, 'NotifyNewUndo', 'notify_new_undo' )
|
||||
self._controller.sub( self, 'NotifyPendingUploadFinished', 'notify_pending_upload_finished' )
|
||||
self._controller.sub( self, 'NotifyRefreshNetworkMenu', 'notify_refresh_network_menu' )
|
||||
self._controller.sub( self, 'PresentImportedFilesToPage', 'imported_files_to_page' )
|
||||
self._controller.sub( self, 'SetDBLockedStatus', 'db_locked_status' )
|
||||
self._controller.sub( self, 'SetStatusBarDirty', 'set_status_bar_dirty' )
|
||||
|
@ -645,15 +646,15 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
library_versions.append( ( 'OpenCV', cv2.__version__ ) )
|
||||
library_versions.append( ( 'Pillow', PIL.__version__ ) )
|
||||
|
||||
if HC.RUNNING_FROM_FROZEN_BUILD and HC.PLATFORM_MACOS:
|
||||
if ClientGUIMPV.MPV_IS_AVAILABLE:
|
||||
|
||||
library_versions.append( ( 'mpv: ', 'is not currently available on macOS' ) )
|
||||
library_versions.append( ( 'mpv api version: ', ClientGUIMPV.GetClientAPIVersionString() ) )
|
||||
|
||||
else:
|
||||
|
||||
if ClientGUIMPV.MPV_IS_AVAILABLE:
|
||||
if HC.RUNNING_FROM_FROZEN_BUILD and HC.PLATFORM_MACOS:
|
||||
|
||||
library_versions.append( ( 'mpv api version: ', ClientGUIMPV.GetClientAPIVersionString() ) )
|
||||
library_versions.append( ( 'mpv: ', 'is not currently available on macOS' ) )
|
||||
|
||||
else:
|
||||
|
||||
|
@ -2129,6 +2130,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
self._menu_updater_services = self._InitialiseMenubarGetMenuUpdaterServices()
|
||||
self._menu_updater_undo = self._InitialiseMenubarGetMenuUpdaterUndo()
|
||||
|
||||
self._menu_updater_pages_count = ClientGUIAsync.FastThreadToGUIUpdater( self, self._UpdateMenuPagesCount )
|
||||
|
||||
self._boned_updater = self._InitialiseMenubarGetBonesUpdater()
|
||||
|
||||
self.setMenuBar( self._menubar )
|
||||
|
@ -2371,6 +2374,8 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
self._menubar_network_subscriptions_paused.setChecked( HC.options[ 'pause_subs_sync' ] )
|
||||
|
||||
self._menubar_network_paged_import_queues_paused.setChecked( HG.client_controller.new_options.GetBoolean( 'pause_all_file_queues' ) )
|
||||
|
||||
|
||||
return ClientGUIAsync.AsyncQtUpdater( self, loading_callable, work_callable, publish_callable )
|
||||
|
||||
|
@ -2402,27 +2407,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
def publish_callable( result ):
|
||||
|
||||
(
|
||||
total_active_page_count,
|
||||
total_active_num_hashes,
|
||||
total_active_num_seeds,
|
||||
total_closed_page_count,
|
||||
total_closed_num_hashes,
|
||||
total_closed_num_seeds
|
||||
) = self.GetTotalPageCounts()
|
||||
|
||||
total_active_weight = ClientGUIPages.ConvertNumHashesAndSeedsToWeight( total_active_num_hashes, total_active_num_seeds )
|
||||
|
||||
if total_active_weight > 10000000 and self._controller.new_options.GetBoolean( 'show_session_size_warnings' ) and not self._have_shown_session_size_warning:
|
||||
|
||||
self._have_shown_session_size_warning = True
|
||||
|
||||
HydrusData.ShowText( 'Your session weight is {}, which is pretty big! To keep your UI lag-free, please try to close some pages or clear some finished downloaders!'.format( HydrusData.ToHumanInt( total_active_weight ) ) )
|
||||
|
||||
|
||||
ClientGUIMenus.SetMenuItemLabel( self._menubar_pages_page_count, '{} pages open'.format( HydrusData.ToHumanInt( total_active_page_count ) ) )
|
||||
|
||||
ClientGUIMenus.SetMenuItemLabel( self._menubar_pages_session_weight, 'total session weight: {}'.format( HydrusData.ToHumanInt( total_active_weight ) ) )
|
||||
self._UpdateMenuPagesCount()
|
||||
|
||||
#
|
||||
|
||||
|
@ -2866,9 +2851,9 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
for ( i, ( time_closed, page ) ) in enumerate( self._closed_pages ):
|
||||
|
||||
name = page.GetName()
|
||||
menu_name = page.GetNameForMenu()
|
||||
|
||||
args.append( ( i, name + ' - ' + page.GetPrettyStatus() ) )
|
||||
args.append( ( i, menu_name ) )
|
||||
|
||||
|
||||
args.reverse() # so that recently closed are at the top
|
||||
|
@ -3275,7 +3260,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
ClientGUIMenus.AppendSeparator( submenu )
|
||||
|
||||
ClientGUIMenus.AppendMenuCheckItem( submenu, 'paged file import queues', 'Pause all file import queues.', self._controller.new_options.GetBoolean( 'pause_all_file_queues' ), self._controller.new_options.FlipBoolean, 'pause_all_file_queues' )
|
||||
self._menubar_network_paged_import_queues_paused = ClientGUIMenus.AppendMenuCheckItem( submenu, 'paged file import queues', 'Pause all file import queues.', self._controller.new_options.GetBoolean( 'pause_all_file_queues' ), self._controller.new_options.FlipBoolean, 'pause_all_file_queues' )
|
||||
ClientGUIMenus.AppendMenuCheckItem( submenu, 'gallery searches', 'Pause all gallery imports\' searching.', self._controller.new_options.GetBoolean( 'pause_all_gallery_searches' ), self._controller.new_options.FlipBoolean, 'pause_all_gallery_searches' )
|
||||
ClientGUIMenus.AppendMenuCheckItem( submenu, 'watcher checkers', 'Pause all watchers\' checking.', self._controller.new_options.GetBoolean( 'pause_all_watcher_checkers' ), self._controller.new_options.FlipBoolean, 'pause_all_watcher_checkers' )
|
||||
|
||||
|
@ -4791,7 +4776,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
|
|||
|
||||
else:
|
||||
|
||||
media_status = page.GetPrettyStatus()
|
||||
media_status = page.GetPrettyStatusForStatusBar()
|
||||
|
||||
|
||||
if self._controller.CurrentlyIdle():
|
||||
|
@ -6393,6 +6378,31 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
self._controller.CallToThread( THREADUploadPending, service_key )
|
||||
|
||||
|
||||
def _UpdateMenuPagesCount( self ):
|
||||
|
||||
(
|
||||
total_active_page_count,
|
||||
total_active_num_hashes,
|
||||
total_active_num_seeds,
|
||||
total_closed_page_count,
|
||||
total_closed_num_hashes,
|
||||
total_closed_num_seeds
|
||||
) = self.GetTotalPageCounts()
|
||||
|
||||
total_active_weight = ClientGUIPages.ConvertNumHashesAndSeedsToWeight( total_active_num_hashes, total_active_num_seeds )
|
||||
|
||||
if total_active_weight > 10000000 and self._controller.new_options.GetBoolean( 'show_session_size_warnings' ) and not self._have_shown_session_size_warning:
|
||||
|
||||
self._have_shown_session_size_warning = True
|
||||
|
||||
HydrusData.ShowText( 'Your session weight is {}, which is pretty big! To keep your UI lag-free, please try to close some pages or clear some finished downloaders!'.format( HydrusData.ToHumanInt( total_active_weight ) ) )
|
||||
|
||||
|
||||
ClientGUIMenus.SetMenuItemLabel( self._menubar_pages_page_count, '{} pages open'.format( HydrusData.ToHumanInt( total_active_page_count ) ) )
|
||||
|
||||
ClientGUIMenus.SetMenuItemLabel( self._menubar_pages_session_weight, 'total session weight: {}'.format( HydrusData.ToHumanInt( total_active_weight ) ) )
|
||||
|
||||
|
||||
def _UpdateSystemTrayIcon( self, currently_booting = False ):
|
||||
|
||||
if not ClientGUISystemTray.SystemTrayAvailable() or ( not HC.PLATFORM_WINDOWS and not HG.client_controller.new_options.GetBoolean( 'advanced_mode' ) ):
|
||||
|
@ -7097,6 +7107,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
self._menu_updater_pages.update()
|
||||
|
||||
|
||||
def NotifyRefreshNetworkMenu( self ):
|
||||
|
||||
self._menu_updater_network.update()
|
||||
|
||||
|
||||
def NotifyNewExportFolders( self ):
|
||||
|
||||
self._menu_updater_file.update()
|
||||
|
@ -7118,6 +7133,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
|
|||
self._menu_updater_pages.update()
|
||||
|
||||
|
||||
def NotifyNewPagesCount( self ):
|
||||
|
||||
self._menu_updater_pages_count.Update()
|
||||
|
||||
|
||||
def NotifyNewPending( self ):
|
||||
|
||||
self._menu_updater_pending.update()
|
||||
|
|
|
@ -529,7 +529,7 @@ class FileSeedCacheButton( ClientGUICommon.ButtonWithMenuArrow ):
|
|||
|
||||
if num_successful > 0:
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'delete {} \'successful\' file import items from the queue'.format( HydrusData.ToHumanInt( num_successful ) ), 'Tell this log to clear out successful files, reducing the size of the queue.', self._ClearFileSeeds, ( CC.STATUS_SUCCESSFUL_AND_NEW, CC.STATUS_SUCCESSFUL_BUT_REDUNDANT ) )
|
||||
ClientGUIMenus.AppendMenuItem( menu, 'delete {} \'successful\' file import items from the queue'.format( HydrusData.ToHumanInt( num_successful ) ), 'Tell this log to clear out successful files, reducing the size of the queue.', self._ClearFileSeeds, ( CC.STATUS_SUCCESSFUL_AND_NEW, CC.STATUS_SUCCESSFUL_BUT_REDUNDANT, CC.STATUS_SUCCESSFUL_AND_CHILD_FILES ) )
|
||||
|
||||
|
||||
if num_deleted > 0:
|
||||
|
|
|
@ -101,11 +101,12 @@ class PagesSearchProvider( QAbstractLocatorSearchProvider ):
|
|||
|
||||
selectable_media_page = widget
|
||||
|
||||
label = '{} - {}'.format( selectable_media_page.GetName(), selectable_media_page.GetPrettyStatus() )
|
||||
label = selectable_media_page.GetNameForMenu()
|
||||
|
||||
if not query in label:
|
||||
|
||||
continue
|
||||
|
||||
|
||||
primary_text = highlight_result_text( label, query )
|
||||
secondary_text = 'top level page' if not parent_name else "child of '" + escape( parent_name ) + "'"
|
||||
|
@ -215,11 +216,24 @@ class MainMenuSearchProvider( QAbstractLocatorSearchProvider ):
|
|||
if not query in action.text() and not query in actionText:
|
||||
|
||||
continue
|
||||
|
||||
|
||||
primary_text = highlight_result_text( actionText, query )
|
||||
secondary_text = escape( parent_name )
|
||||
|
||||
result.append( QLocatorSearchResult( self.result_id_counter, 'lightning.png', 'lightning.png', True, [ primary_text, secondary_text ] ) )
|
||||
normal_png = 'lightning.png'
|
||||
toggled = False
|
||||
toggled_png = 'lightning.png'
|
||||
|
||||
if action.isCheckable():
|
||||
|
||||
toggled = action.isChecked()
|
||||
|
||||
normal_png = 'lightning_unchecked.png'
|
||||
toggled_png = 'lightning_checked.png'
|
||||
|
||||
|
||||
result.append( QLocatorSearchResult( self.result_id_counter, normal_png, normal_png, True, [ primary_text, secondary_text ], toggled, toggled_png, toggled_png ) )
|
||||
|
||||
self.result_ids_to_actions[ self.result_id_counter ] = action
|
||||
|
||||
|
|
|
@ -351,6 +351,13 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
local_file_services = list( HG.client_controller.services_manager.GetServices( ( HC.LOCAL_FILE_DOMAIN, ) ) )
|
||||
|
||||
if suggested_file_service_key is None:
|
||||
|
||||
suggested_file_service_key = local_file_services[0].GetServiceKey()
|
||||
|
||||
|
||||
self._media = self._FilterForDeleteLock( ClientMedia.FlattenMedia( media ), suggested_file_service_key )
|
||||
|
||||
self._question_is_already_resolved = len( self._media ) == 0
|
||||
|
@ -362,7 +369,7 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self._permitted_action_choices = []
|
||||
self._this_dialog_includes_service_keys = False
|
||||
|
||||
self._InitialisePermittedActionChoices( suggested_file_service_key = suggested_file_service_key )
|
||||
self._InitialisePermittedActionChoices( suggested_file_service_key )
|
||||
|
||||
self._action_radio = ClientGUICommon.BetterRadioBox( self, choices = self._permitted_action_choices, vertical = True )
|
||||
|
||||
|
@ -525,17 +532,12 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
self.widget().setLayout( vbox )
|
||||
|
||||
|
||||
def _FilterForDeleteLock( self, media, suggested_file_service_key ):
|
||||
def _FilterForDeleteLock( self, media, suggested_file_service_key: bytes ):
|
||||
|
||||
delete_lock_for_archived_files = HG.client_controller.new_options.GetBoolean( 'delete_lock_for_archived_files' )
|
||||
|
||||
if delete_lock_for_archived_files:
|
||||
|
||||
if suggested_file_service_key is None:
|
||||
|
||||
suggested_file_service_key = CC.LOCAL_FILE_SERVICE_KEY
|
||||
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( suggested_file_service_key )
|
||||
|
||||
if service.GetServiceType() in HC.LOCAL_FILE_SERVICES:
|
||||
|
@ -601,20 +603,21 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
return reason
|
||||
|
||||
|
||||
def _InitialisePermittedActionChoices( self, suggested_file_service_key = None ):
|
||||
def _InitialisePermittedActionChoices( self, suggested_file_service_key: bytes ):
|
||||
|
||||
possible_file_service_keys = []
|
||||
|
||||
if suggested_file_service_key is None:
|
||||
|
||||
suggested_file_service_key = CC.LOCAL_FILE_SERVICE_KEY
|
||||
|
||||
local_file_services = list( HG.client_controller.services_manager.GetServices( ( HC.LOCAL_FILE_DOMAIN, ) ) )
|
||||
|
||||
if suggested_file_service_key not in ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ):
|
||||
|
||||
possible_file_service_keys.append( ( suggested_file_service_key, suggested_file_service_key ) )
|
||||
|
||||
|
||||
local_file_services = [ lfs for lfs in local_file_services if lfs.GetServiceKey() != suggested_file_service_key ]
|
||||
|
||||
possible_file_service_keys.extend( ( lfs.GetServiceKey(), lfs.GetServiceKey() ) for lfs in local_file_services )
|
||||
|
||||
possible_file_service_keys.append( ( CC.TRASH_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
|
||||
possible_file_service_keys.append( ( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, CC.COMBINED_LOCAL_FILE_SERVICE_KEY ) )
|
||||
|
||||
|
@ -628,6 +631,8 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
del keys_to_hashes[ trashed_key ]
|
||||
|
||||
|
||||
num_local_file_services = 0
|
||||
|
||||
for fsk in possible_file_service_keys:
|
||||
|
||||
if fsk not in keys_to_hashes:
|
||||
|
@ -643,20 +648,25 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
( selection_file_service_key, deletee_file_service_key ) = fsk
|
||||
|
||||
# update this stuff to say 'send to trash?' vs 'remove from blah? (it is still in bleh)'. for multiple local file services
|
||||
deletee_service = HG.client_controller.services_manager.GetService( deletee_file_service_key )
|
||||
|
||||
if deletee_file_service_key == CC.LOCAL_FILE_SERVICE_KEY:
|
||||
deletee_service_type = deletee_service.GetServiceType()
|
||||
|
||||
if deletee_service_type == HC.LOCAL_FILE_DOMAIN:
|
||||
|
||||
self._this_dialog_includes_service_keys = True
|
||||
|
||||
if not HC.options[ 'confirm_trash' ]:
|
||||
|
||||
# this dialog will never show
|
||||
self._question_is_already_resolved = True
|
||||
|
||||
num_local_file_services += 1
|
||||
|
||||
if num_to_delete == 1: text = 'Send this file to the trash?'
|
||||
else: text = 'Send these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files to the trash?'
|
||||
if num_to_delete == 1: text = 'Send one file from {} to the trash?'.format( deletee_service.GetName() )
|
||||
else: text = 'Send {} files from {} to the trash?'.format( HydrusData.ToHumanInt( num_to_delete ), deletee_service.GetName() )
|
||||
|
||||
elif deletee_service_type == HC.FILE_REPOSITORY:
|
||||
|
||||
self._this_dialog_includes_service_keys = True
|
||||
|
||||
if num_to_delete == 1: text = 'Admin-delete this file?'
|
||||
else: text = 'Admin-delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
|
||||
|
||||
elif deletee_file_service_key == CC.COMBINED_LOCAL_FILE_SERVICE_KEY:
|
||||
|
||||
|
@ -675,18 +685,17 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
else: text = 'Permanently delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self._this_dialog_includes_service_keys = True
|
||||
|
||||
if num_to_delete == 1: text = 'Admin-delete this file?'
|
||||
else: text = 'Admin-delete these ' + HydrusData.ToHumanInt( num_to_delete ) + ' files?'
|
||||
|
||||
|
||||
self._permitted_action_choices.append( ( text, ( deletee_file_service_key, hashes, text ) ) )
|
||||
|
||||
|
||||
|
||||
if num_local_file_services == 1 and not HC.options[ 'confirm_trash' ]:
|
||||
|
||||
# this dialog will never show
|
||||
self._question_is_already_resolved = True
|
||||
|
||||
|
||||
if HG.client_controller.new_options.GetBoolean( 'use_advanced_file_deletion_dialog' ):
|
||||
|
||||
hashes = [ m.GetHash() for m in self._media if CC.COMBINED_LOCAL_FILE_SERVICE_KEY in m.GetLocationsManager().GetCurrent() ]
|
||||
|
@ -718,8 +727,10 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
( file_service_key, hashes, description ) = self._action_radio.GetValue()
|
||||
|
||||
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
||||
|
||||
# 'this includes service keys' because if we are deleting physically from the trash, then reason is already set
|
||||
reason_permitted = file_service_key in ( CC.LOCAL_FILE_SERVICE_KEY, 'physical_delete' ) and self._this_dialog_includes_service_keys
|
||||
reason_permitted = ( file_service_key in local_file_service_keys or file_service_key == 'physical_delete' ) and self._this_dialog_includes_service_keys
|
||||
|
||||
if reason_permitted:
|
||||
|
||||
|
@ -752,9 +763,9 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
save_reason = False
|
||||
|
||||
local_file_services = ( CC.LOCAL_FILE_SERVICE_KEY, )
|
||||
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
|
||||
|
||||
if file_service_key in local_file_services:
|
||||
if file_service_key in local_file_service_keys:
|
||||
|
||||
# split them into bits so we don't hang the gui with a huge delete transaction
|
||||
|
||||
|
|
|
@ -1031,7 +1031,11 @@ class MigrateTagsPanel( ClientGUIScrolledPanels.ReviewPanel ):
|
|||
|
||||
self._migration_source_file_filter = ClientGUICommon.BetterChoice( self._migration_panel )
|
||||
|
||||
for service_key in ( CC.LOCAL_FILE_SERVICE_KEY, CC.COMBINED_FILE_SERVICE_KEY ):
|
||||
source_file_service_keys = list( HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )
|
||||
source_file_service_keys.append( CC.COMBINED_LOCAL_FILE_SERVICE_KEY )
|
||||
source_file_service_keys.append( CC.COMBINED_FILE_SERVICE_KEY )
|
||||
|
||||
for service_key in source_file_service_keys:
|
||||
|
||||
service = HG.client_controller.services_manager.GetService( service_key )
|
||||
|
||||
|
|
|
@ -26,17 +26,23 @@ def ManageShortcuts( win: QW.QWidget ):
|
|||
|
||||
shortcuts_manager = ClientGUIShortcuts.shortcuts_manager()
|
||||
|
||||
call_mouse_buttons_primary_secondary = HG.client_controller.new_options.GetBoolean( 'call_mouse_buttons_primary_secondary' )
|
||||
|
||||
all_shortcuts = shortcuts_manager.GetShortcutSets()
|
||||
|
||||
with ClientGUITopLevelWindowsPanels.DialogEdit( win, 'manage shortcuts' ) as dlg:
|
||||
|
||||
panel = EditShortcutsPanel( dlg, all_shortcuts )
|
||||
panel = EditShortcutsPanel( dlg, call_mouse_buttons_primary_secondary, all_shortcuts )
|
||||
|
||||
dlg.SetPanel( panel )
|
||||
|
||||
if dlg.exec() == QW.QDialog.Accepted:
|
||||
|
||||
shortcut_sets = panel.GetValue()
|
||||
( call_mouse_buttons_primary_secondary, shortcut_sets ) = panel.GetValue()
|
||||
|
||||
HG.client_controller.new_options.SetBoolean( 'call_mouse_buttons_primary_secondary', call_mouse_buttons_primary_secondary )
|
||||
|
||||
ClientGUIShortcuts.SetMouseLabels( call_mouse_buttons_primary_secondary )
|
||||
|
||||
dupe_shortcut_sets = [ shortcut_set.Duplicate() for shortcut_set in shortcut_sets ]
|
||||
|
||||
|
@ -296,13 +302,16 @@ class EditShortcutSetPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
|
||||
|
||||
def __init__( self, parent, all_shortcuts ):
|
||||
def __init__( self, parent, call_mouse_buttons_primary_secondary, all_shortcuts ):
|
||||
|
||||
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
|
||||
|
||||
help_button = ClientGUICommon.BetterBitmapButton( self, CC.global_pixmaps().help, self._ShowHelp )
|
||||
help_button.setToolTip( 'Show help regarding editing shortcuts.' )
|
||||
|
||||
self._call_mouse_buttons_primary_secondary = QW.QCheckBox( self )
|
||||
self._call_mouse_buttons_primary_secondary.setToolTip( 'Useful if you swap your buttons around.' )
|
||||
|
||||
reserved_panel = ClientGUICommon.StaticBox( self, 'built-in hydrus shortcut sets' )
|
||||
|
||||
self._reserved_shortcuts = ClientGUIListCtrl.BetterListCtrl( reserved_panel, CGLC.COLUMN_LIST_SHORTCUT_SETS.ID, 6, data_to_tuples_func = self._GetTuples, activation_callback = self._EditReserved )
|
||||
|
@ -329,6 +338,8 @@ class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
self._call_mouse_buttons_primary_secondary.setChecked( call_mouse_buttons_primary_secondary )
|
||||
|
||||
reserved_shortcuts = [ shortcuts for shortcuts in all_shortcuts if shortcuts.GetName() in ClientGUIShortcuts.SHORTCUTS_RESERVED_NAMES ]
|
||||
custom_shortcuts = [ shortcuts for shortcuts in all_shortcuts if shortcuts.GetName() not in ClientGUIShortcuts.SHORTCUTS_RESERVED_NAMES ]
|
||||
|
||||
|
@ -349,6 +360,14 @@ class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
|
||||
#
|
||||
|
||||
rows = []
|
||||
|
||||
rows.append( ( 'Replace "left/right"-click with "primary/secondary": ', self._call_mouse_buttons_primary_secondary ) )
|
||||
|
||||
mouse_gridbox = ClientGUICommon.WrapInGrid( self, rows )
|
||||
|
||||
#
|
||||
|
||||
button_hbox = QP.HBoxLayout()
|
||||
|
||||
QP.AddToLayout( button_hbox, self._edit_reserved_button, CC.FLAGS_CENTER_PERPENDICULAR )
|
||||
|
@ -379,11 +398,14 @@ class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
vbox = QP.VBoxLayout()
|
||||
|
||||
QP.AddToLayout( vbox, help_button, CC.FLAGS_ON_RIGHT )
|
||||
QP.AddToLayout( vbox, mouse_gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
|
||||
QP.AddToLayout( vbox, reserved_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
QP.AddToLayout( vbox, custom_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
|
||||
|
||||
self.widget().setLayout( vbox )
|
||||
|
||||
self._call_mouse_buttons_primary_secondary.clicked.connect( self._UpdateMouseLabels )
|
||||
|
||||
|
||||
def _Add( self ):
|
||||
|
||||
|
@ -560,14 +582,23 @@ class EditShortcutsPanel( ClientGUIScrolledPanels.EditPanel ):
|
|||
QW.QMessageBox.information( self, 'Information', message )
|
||||
|
||||
|
||||
def _UpdateMouseLabels( self ):
|
||||
|
||||
swap_labels = self._call_mouse_buttons_primary_secondary.isChecked()
|
||||
|
||||
ClientGUIShortcuts.SetMouseLabels( swap_labels )
|
||||
|
||||
|
||||
def GetValue( self ) -> typing.List[ ClientGUIShortcuts.ShortcutSet ]:
|
||||
|
||||
call_mouse_buttons_primary_secondary = self._call_mouse_buttons_primary_secondary.isChecked()
|
||||
|
||||
shortcut_sets = []
|
||||
|
||||
shortcut_sets.extend( self._reserved_shortcuts.GetData() )
|
||||
shortcut_sets.extend( self._custom_shortcuts.GetData() )
|
||||
|
||||
return shortcut_sets
|
||||
return ( call_mouse_buttons_primary_secondary, shortcut_sets )
|
||||
|
||||
|
||||
class ShortcutWidget( QW.QWidget ):
|
||||
|
|
|
@ -176,6 +176,19 @@ shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_SCROLL_DOWN ] = 'scroll down'
|
|||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_SCROLL_LEFT ] = 'scroll left'
|
||||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_SCROLL_RIGHT ] = 'scroll right'
|
||||
|
||||
def SetMouseLabels( call_mouse_buttons_primary_secondary ):
|
||||
|
||||
if call_mouse_buttons_primary_secondary:
|
||||
|
||||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_LEFT ] = 'primary-click'
|
||||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_RIGHT ] = 'secondary-click'
|
||||
|
||||
else:
|
||||
|
||||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_LEFT ] = 'left-click'
|
||||
shortcut_mouse_string_lookup[ SHORTCUT_MOUSE_RIGHT ] = 'right-click'
|
||||
|
||||
|
||||
shortcut_names_to_pretty_names = {}
|
||||
|
||||
shortcut_names_to_pretty_names[ 'global' ] = 'global'
|
||||
|
|
|
@ -605,7 +605,6 @@ class Frame( QW.QWidget ):
|
|||
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
|
||||
|
||||
self._widget_event_filter = QP.WidgetEventFilter( self )
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventAboutToClose )
|
||||
self._widget_event_filter.EVT_MOVE( self.EventMove )
|
||||
|
||||
HG.client_controller.ResetIdleTimer()
|
||||
|
@ -616,12 +615,10 @@ class Frame( QW.QWidget ):
|
|||
pass
|
||||
|
||||
|
||||
def EventAboutToClose( self, event ):
|
||||
def closeEvent( self, event ):
|
||||
|
||||
self.CleanBeforeDestroy()
|
||||
|
||||
return True # was: event.ignore()
|
||||
|
||||
|
||||
def EventMove( self, event ):
|
||||
|
||||
|
@ -648,7 +645,6 @@ class MainFrame( QW.QMainWindow ):
|
|||
self.setWindowIcon( QG.QIcon( HG.client_controller.frame_icon_pixmap ) )
|
||||
|
||||
self._widget_event_filter = QP.WidgetEventFilter( self )
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventAboutToClose )
|
||||
|
||||
HG.client_controller.ResetIdleTimer()
|
||||
|
||||
|
@ -658,12 +654,10 @@ class MainFrame( QW.QMainWindow ):
|
|||
pass
|
||||
|
||||
|
||||
def EventAboutToClose( self, event ):
|
||||
def closeEvent( self, event ):
|
||||
|
||||
self.CleanBeforeDestroy()
|
||||
|
||||
return True # was: event.ignore()
|
||||
|
||||
|
||||
class FrameThatResizes( Frame ):
|
||||
|
||||
|
@ -675,10 +669,17 @@ class FrameThatResizes( Frame ):
|
|||
|
||||
self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_MAXIMIZE( self.EventSizeAndPositionChanged )
|
||||
|
||||
|
||||
def CleanBeforeDestroy( self ):
|
||||
|
||||
MainFrame.CleanBeforeDestroy( self )
|
||||
|
||||
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
||||
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
|
||||
|
||||
|
||||
def EventSizeAndPositionChanged( self, event ):
|
||||
|
||||
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
||||
|
@ -699,9 +700,17 @@ class MainFrameThatResizes( MainFrame ):
|
|||
|
||||
self._widget_event_filter.EVT_SIZE( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_MOVE_END( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_CLOSE( self.EventSizeAndPositionChanged )
|
||||
self._widget_event_filter.EVT_MAXIMIZE( self.EventSizeAndPositionChanged )
|
||||
|
||||
|
||||
def CleanBeforeDestroy( self ):
|
||||
|
||||
MainFrame.CleanBeforeDestroy( self )
|
||||
|
||||
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
||||
HG.client_controller.CallLaterQtSafe( self, 0.1, 'save frame size and position: {}'.format( self._frame_key ), SaveTLWSizeAndPosition, self, self._frame_key )
|
||||
|
||||
|
||||
def EventSizeAndPositionChanged( self, event ):
|
||||
|
||||
# maximise sends a pre-maximise size event that poisons last_size if this is immediate
|
||||
|
|
|
@ -22,7 +22,6 @@ import math
|
|||
import re
|
||||
|
||||
def elideRichText(richText: str, maxWidth: int, widget, elideFromLeft: bool):
|
||||
|
||||
doc = QG.QTextDocument()
|
||||
opt = QG.QTextOption()
|
||||
opt.setWrapMode(QG.QTextOption.NoWrap)
|
||||
|
@ -40,7 +39,7 @@ def elideRichText(richText: str, maxWidth: int, widget, elideFromLeft: bool):
|
|||
elidedPostfix = "…"
|
||||
metric = QG.QFontMetrics(widget.font())
|
||||
postfixWidth = metric.horizontalAdvance(elidedPostfix)
|
||||
|
||||
|
||||
while doc.size().width() > maxWidth - postfixWidth:
|
||||
if elideFromLeft:
|
||||
cursor.deleteChar()
|
||||
|
@ -65,12 +64,15 @@ class FocusEventFilter(QC.QObject):
|
|||
return False
|
||||
|
||||
class QLocatorSearchResult:
|
||||
def __init__(self, id: int, defaultIconPath: str, selectedIconPath: str, closeOnActivated: False, text: list):
|
||||
def __init__(self, id: int, defaultIconPath: str, selectedIconPath: str, closeOnActivated: bool, text: list, toggled: bool = False, toggledIconPath: str = "", toggledSelectedIconPath: str = ""):
|
||||
self.id = id
|
||||
self.defaultIconPath = defaultIconPath
|
||||
self.selectedIconPath = selectedIconPath
|
||||
self.closeOnActivated = closeOnActivated
|
||||
self.text = text
|
||||
self.toggled = toggled
|
||||
self.toggledIconPath = toggledIconPath
|
||||
self.toggledSelectedIconPath = toggledSelectedIconPath
|
||||
|
||||
class QLocatorTitleWidget(QW.QWidget):
|
||||
def __init__(self, title: str, iconPath: str, height: int, shouldRemainHidden: bool, parent = None):
|
||||
|
@ -98,7 +100,7 @@ class QLocatorTitleWidget(QW.QWidget):
|
|||
self.titleLabel.setFont(titleFont)
|
||||
self.setFixedHeight(height)
|
||||
self.shouldRemainHidden = shouldRemainHidden
|
||||
|
||||
|
||||
def updateData(self, count: int):
|
||||
self.countLabel.setText(str(count))
|
||||
|
||||
|
@ -112,22 +114,25 @@ class QLocatorResultWidget(QW.QWidget):
|
|||
up = QC.Signal()
|
||||
down = QC.Signal()
|
||||
activated = QC.Signal(int, int, bool)
|
||||
entered = QC.Signal()
|
||||
def __init__(self, keyEventTarget: QW.QWidget, height: int, primaryTextWidth: int, secondaryTextWidth: int, parent = None):
|
||||
super().__init__(parent)
|
||||
self.iconHeight = height - 2
|
||||
self.setObjectName("unselectedLocatorResult")
|
||||
self.keyEventTarget = keyEventTarget
|
||||
self.setLayout(QW.QHBoxLayout())
|
||||
self.iconLabel = QW.QLabel( self )
|
||||
self.iconLabel = QW.QLabel(self)
|
||||
self.iconLabel.setFixedHeight(self.iconHeight)
|
||||
self.mainTextLabel = QW.QLabel( self )
|
||||
self.mainTextLabel = QW.QLabel(self)
|
||||
self.primaryTextWidth = primaryTextWidth
|
||||
self.mainTextLabel.setMinimumWidth(primaryTextWidth)
|
||||
self.mainTextLabel.setTextFormat(QC.Qt.RichText)
|
||||
self.secondaryTextLabel = QW.QLabel( self )
|
||||
self.mainTextLabel.setTextInteractionFlags(QC.Qt.NoTextInteraction)
|
||||
self.secondaryTextLabel = QW.QLabel(self)
|
||||
self.secondaryTextWidth = secondaryTextWidth
|
||||
self.secondaryTextLabel.setMaximumWidth(secondaryTextWidth)
|
||||
self.secondaryTextLabel.setTextFormat(QC.Qt.RichText)
|
||||
self.secondaryTextLabel.setTextInteractionFlags(QC.Qt.NoTextInteraction)
|
||||
self.layout().setContentsMargins(4, 1, 4, 1)
|
||||
self.layout().addWidget(self.iconLabel)
|
||||
self.layout().addWidget(self.mainTextLabel)
|
||||
|
@ -150,19 +155,18 @@ class QLocatorResultWidget(QW.QWidget):
|
|||
self.selectedPalette = self.palette()
|
||||
self.selectedPalette.setColor(QG.QPalette.Window, QG.QPalette().color(QG.QPalette.WindowText))
|
||||
self.selectedPalette.setColor(QG.QPalette.WindowText, QG.QPalette().color(QG.QPalette.Window))
|
||||
|
||||
|
||||
self.id = -1
|
||||
self.providerIndex = -1
|
||||
self.closeOnActivated = False
|
||||
self.selected = False
|
||||
self.defaultStylingEnabled = True
|
||||
self.currentIcon = QG.QIcon()
|
||||
|
||||
def handleActivated():
|
||||
self.activated.emit(self.providerIndex, self.id, self.closeOnActivated)
|
||||
self.currentToggledIcon = QG.QIcon()
|
||||
self.toggled = False
|
||||
|
||||
self.activateEnterShortcut.activated.connect(handleActivated)
|
||||
self.activateReturnShortcut.activated.connect(handleActivated)
|
||||
self.activateEnterShortcut.activated.connect(self.activate)
|
||||
self.activateReturnShortcut.activated.connect(self.activate)
|
||||
|
||||
self.upShortcut.activated.connect(self.up)
|
||||
self.downShortcut.activated.connect(self.down)
|
||||
|
@ -173,11 +177,32 @@ class QLocatorResultWidget(QW.QWidget):
|
|||
p = QG.QPainter(self)
|
||||
self.style().drawPrimitive(QW.QStyle.PE_Widget, opt, p, self)
|
||||
|
||||
def enterEvent(self, event):
|
||||
self.entered.emit()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.entered.emit()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self.activate()
|
||||
|
||||
def activate(self):
|
||||
if not self.closeOnActivated:
|
||||
self.toggled = not self.toggled
|
||||
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
|
||||
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
|
||||
self.activated.emit(self.providerIndex, self.id, self.closeOnActivated)
|
||||
|
||||
def updateData(self, providerIndex: int, data: QLocatorSearchResult):
|
||||
self.toggled = data.toggled
|
||||
self.currentIcon = QG.QIcon()
|
||||
self.currentIcon.addFile(data.defaultIconPath, QC.QSize(), QG.QIcon.Normal)
|
||||
self.currentIcon.addFile(data.selectedIconPath, QC.QSize(), QG.QIcon.Selected)
|
||||
self.iconLabel.setPixmap(self.currentIcon.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
|
||||
self.currentToggledIcon = QG.QIcon()
|
||||
self.currentToggledIcon.addFile(data.toggledIconPath, QC.QSize(), QG.QIcon.Normal)
|
||||
self.currentToggledIcon.addFile(data.toggledSelectedIconPath, QC.QSize(), QG.QIcon.Selected)
|
||||
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
|
||||
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
|
||||
self.mainTextLabel.clear()
|
||||
self.secondaryTextLabel.clear()
|
||||
if len(data.text) > 0:
|
||||
|
@ -205,7 +230,8 @@ class QLocatorResultWidget(QW.QWidget):
|
|||
if self.defaultStylingEnabled: self.setPalette(QG.QPalette())
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
self.iconLabel.setPixmap(self.currentIcon.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
|
||||
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
|
||||
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
|
||||
|
||||
def keyPressEvent(self, ev: QG.QKeyEvent):
|
||||
if ev.key() != QC.Qt.Key_Up and ev.key() != QC.Qt.Key_Down and ev.key() != QC.Qt.Key_Enter and ev.key() != QC.Qt.Key_Return:
|
||||
|
@ -231,7 +257,7 @@ class QExampleSearchProvider(QAbstractLocatorSearchProvider):
|
|||
|
||||
def resultSelected(self, resultID: int):
|
||||
pass
|
||||
|
||||
|
||||
def processQuery(self, query: str, context, jobID: int):
|
||||
resCount = random.randint(0, 50)
|
||||
results = []
|
||||
|
@ -242,15 +268,15 @@ class QExampleSearchProvider(QAbstractLocatorSearchProvider):
|
|||
txt = []
|
||||
txt.append("Result <b>text</b> #" + str(i) + randomStr)
|
||||
txt.append("Secondary result text")
|
||||
results.append(QLocatorSearchResult(0, "icon.svg", "icon.svg", True, txt))
|
||||
results.append(QLocatorSearchResult(0, "icon.svg", "icon.svg", True, txt, False, "icon.svg", "icon.svg"))
|
||||
self.resultsAvailable.emit(jobID, results)
|
||||
|
||||
def stopJobs(self, jobs):
|
||||
pass
|
||||
|
||||
|
||||
def hideTitle(self):
|
||||
return False
|
||||
|
||||
|
||||
def titleIconPath(self):
|
||||
return "icon.svg"
|
||||
|
||||
|
@ -296,7 +322,7 @@ class QCalculatorSearchProvider(QAbstractLocatorSearchProvider):
|
|||
'factorial': math.factorial
|
||||
}
|
||||
self.safePattern = re.compile("^("+"|".join(self.safeEnv.keys())+r"|[0-9.*+\-%/()]|\s" + ")+$")
|
||||
|
||||
|
||||
def processQuery(self, query: str, context, jobID: int):
|
||||
try:
|
||||
if len(query.strip()) and self.safePattern.match(query):
|
||||
|
@ -305,7 +331,7 @@ class QCalculatorSearchProvider(QAbstractLocatorSearchProvider):
|
|||
int(result)
|
||||
except:
|
||||
result = str(float(result))
|
||||
self.resultsAvailable.emit(jobID, [QLocatorSearchResult(0, self.iconPath(), self.selectedIconPath(), False, [result,"Calculator"])])
|
||||
self.resultsAvailable.emit(jobID, [QLocatorSearchResult(0, self.iconPath(), self.selectedIconPath(), False, [result,"Calculator"], False, self.iconPath(), self.selectedIconPath())])
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -320,16 +346,16 @@ class QCalculatorSearchProvider(QAbstractLocatorSearchProvider):
|
|||
|
||||
def stopJobs(self, jobs):
|
||||
pass
|
||||
|
||||
|
||||
def hideTitle(self):
|
||||
return True
|
||||
|
||||
|
||||
def titleIconPath(self):
|
||||
return str()
|
||||
|
||||
|
||||
def selectedIconPath(self):
|
||||
return str()
|
||||
|
||||
|
||||
def iconPath(self):
|
||||
return str()
|
||||
|
||||
|
@ -377,7 +403,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
self.editorDownShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Down), self.searchEdit)
|
||||
self.editorDownShortcut.setContext(QC.Qt.WidgetShortcut)
|
||||
self.editorDownShortcut.activated.connect(self.handleEditorDown)
|
||||
|
||||
|
||||
def handleTextEdited():
|
||||
for i in range(len(self.resultItems)):
|
||||
for it in self.resultItems[i]: self.setResultVisible(it, False)
|
||||
|
@ -416,7 +442,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
elif alignment == QC.Qt.AlignTop:
|
||||
self.alignment = alignment
|
||||
self.updateAlignment()
|
||||
|
||||
|
||||
def updateAlignment( self ):
|
||||
widget = self
|
||||
while True:
|
||||
|
@ -425,7 +451,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
break
|
||||
else:
|
||||
widget = parent
|
||||
|
||||
|
||||
screenRect = QW.QApplication.primaryScreen().availableGeometry()
|
||||
if widget != self: # there is a parent
|
||||
screenRect = widget.geometry()
|
||||
|
@ -437,7 +463,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
elif self.alignment == QC.Qt.AlignTop:
|
||||
rect = QW.QStyle.alignedRect(QC.Qt.LeftToRight, QC.Qt.AlignHCenter | QC.Qt.AlignTop, self.size(), screenRect)
|
||||
self.setGeometry(rect)
|
||||
|
||||
|
||||
def paintEvent(self, event):
|
||||
opt = QW.QStyleOption()
|
||||
opt.initFrom(self)
|
||||
|
@ -473,10 +499,10 @@ class QLocatorWidget(QW.QWidget):
|
|||
if self.locator:
|
||||
self.locator.providerAdded.disconnect(self.providerAdded)
|
||||
self.locator.resultsAvailable.disconnect(self.handleResultsAvailable)
|
||||
|
||||
|
||||
self.reset()
|
||||
self.locator = locator
|
||||
|
||||
|
||||
if self.locator:
|
||||
for provider in self.locator.providers:
|
||||
self.providerAdded(provider.title(), self.locator.iconBasePath + provider.titleIconPath(), provider.suggestedReservedItemCount(), provider.hideTitle())
|
||||
|
@ -601,7 +627,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
self.resultList.ensureVisible(0, widget.pos().y() + widget.height(), 0, 0)
|
||||
break
|
||||
i = i + 1
|
||||
|
||||
|
||||
def handleEditorDown(self):
|
||||
for i in range(self.resultLayout.count()):
|
||||
widget = self.resultLayout.itemAt(i).widget()
|
||||
|
@ -611,6 +637,19 @@ class QLocatorWidget(QW.QWidget):
|
|||
widget.setSelected(True)
|
||||
break
|
||||
|
||||
def handleEntered(self):
|
||||
resultWidget = self.sender()
|
||||
i = 0
|
||||
while i < self.resultLayout.count():
|
||||
widget = self.resultLayout.itemAt(i).widget()
|
||||
if widget and widget.isVisible() and isinstance(widget, QLocatorResultWidget):
|
||||
if widget == resultWidget:
|
||||
self.selectedLayoutItemIndex = i
|
||||
widget.setSelected(True)
|
||||
else:
|
||||
widget.setSelected(False)
|
||||
i = i + 1
|
||||
|
||||
def handleResultActivated(self, provider: int, id: int, closeOnSelected: bool):
|
||||
currJobIdsTmp = self.currentJobIds[:]
|
||||
if closeOnSelected: self.finish(True)
|
||||
|
@ -643,7 +682,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
j += 1
|
||||
k = self.reservedItemCounts[i]
|
||||
while k < resultItemCount:
|
||||
self.resultLayout.takeAt(titleIndex + k).widget().deleteLater()
|
||||
self.resultLayout.takeAt(titleIndex + self.reservedItemCounts[i] + 1).widget().deleteLater()
|
||||
k += 1
|
||||
self.resultItems[i] = self.resultItems[i][:self.reservedItemCounts[i]]
|
||||
|
||||
|
@ -691,6 +730,7 @@ class QLocatorWidget(QW.QWidget):
|
|||
widget.up.connect(self.handleResultUp)
|
||||
widget.down.connect(self.handleResultDown)
|
||||
widget.activated.connect(self.handleResultActivated)
|
||||
widget.entered.connect(self.handleEntered)
|
||||
widget.setDefaultStylingEnabled(self.defaultStylingEnabled)
|
||||
|
||||
class QLocator(QC.QObject):
|
||||
|
@ -748,6 +788,8 @@ class QLocator(QC.QObject):
|
|||
for dataItem in self.savedProviderData[jobID]:
|
||||
dataItem.defaultIconPath = self.iconBasePath + dataItem.defaultIconPath
|
||||
dataItem.selectedIconPath = self.iconBasePath + dataItem.selectedIconPath
|
||||
dataItem.toggledIconPath = self.iconBasePath + dataItem.toggledIconPath
|
||||
dataItem.toggledSelectedIconPath = self.iconBasePath + dataItem.toggledSelectedIconPath
|
||||
self.resultsAvailable.emit(providerIndex, jobID)
|
||||
|
||||
def stopJobs(self, ids = []) -> None:
|
||||
|
|
|
@ -558,6 +558,8 @@ class Page( QW.QSplitter ):
|
|||
|
||||
self._controller.pub( 'refresh_page_name', self._page_key )
|
||||
|
||||
self._controller.pub( 'notify_new_pages_count' )
|
||||
|
||||
def clean_up_old_panel():
|
||||
|
||||
if CGC.core().MenuIsOpen():
|
||||
|
@ -682,6 +684,25 @@ class Page( QW.QSplitter ):
|
|||
return self._management_controller.GetPageName()
|
||||
|
||||
|
||||
def GetNameForMenu( self ) -> str:
|
||||
|
||||
name_for_menu = self.GetName()
|
||||
|
||||
( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary()
|
||||
|
||||
if num_files > 0:
|
||||
|
||||
name_for_menu = '{} - {} files'.format( name_for_menu, HydrusData.ToHumanInt( num_files ) )
|
||||
|
||||
|
||||
if num_value != num_range:
|
||||
|
||||
name_for_menu = '{} - {}'.format( name_for_menu, HydrusData.ConvertValueRangeToPrettyString( num_value, num_range ) )
|
||||
|
||||
|
||||
return HydrusText.ElideText( name_for_menu, 32, elide_center = True )
|
||||
|
||||
|
||||
def GetNumFileSummary( self ):
|
||||
|
||||
if self._initialised:
|
||||
|
@ -718,6 +739,11 @@ class Page( QW.QSplitter ):
|
|||
return self._parent_notebook
|
||||
|
||||
|
||||
def GetPrettyStatusForStatusBar( self ):
|
||||
|
||||
return self._pretty_status
|
||||
|
||||
|
||||
def GetSerialisablePage( self, only_changed_page_data, about_to_save ):
|
||||
|
||||
if only_changed_page_data and not self.IsCurrentSessionPageDirty():
|
||||
|
@ -763,11 +789,6 @@ class Page( QW.QSplitter ):
|
|||
return root
|
||||
|
||||
|
||||
def GetPrettyStatus( self ):
|
||||
|
||||
return self._pretty_status
|
||||
|
||||
|
||||
def GetSashPositions( self ):
|
||||
|
||||
hpos = HC.options[ 'hpos' ]
|
||||
|
@ -1747,7 +1768,7 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
|
||||
for selectable_media_page in selectable_media_pages:
|
||||
|
||||
label = '{} - {}'.format( selectable_media_page.GetName(), selectable_media_page.GetPrettyStatus() )
|
||||
label = selectable_media_page.GetNameForMenu()
|
||||
|
||||
ClientGUIMenus.AppendMenuItem( select_menu, label, 'select this page', self.ShowPage, selectable_media_page )
|
||||
|
||||
|
@ -2224,6 +2245,25 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
return self._name
|
||||
|
||||
|
||||
def GetNameForMenu( self ) -> str:
|
||||
|
||||
name_for_menu = self.GetName()
|
||||
|
||||
( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary()
|
||||
|
||||
if num_files > 0:
|
||||
|
||||
name_for_menu = '{} - {} files'.format( name_for_menu, HydrusData.ToHumanInt( num_files ) )
|
||||
|
||||
|
||||
if num_value != num_range:
|
||||
|
||||
name_for_menu = '{} - {}'.format( name_for_menu, HydrusData.ConvertValueRangeToPrettyString( num_value, num_range ) )
|
||||
|
||||
|
||||
return HydrusText.ElideText( name_for_menu, 32, elide_center = True )
|
||||
|
||||
|
||||
def GetNumFileSummary( self ):
|
||||
|
||||
total_num_files = 0
|
||||
|
@ -2418,6 +2458,25 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
return self._parent_notebook
|
||||
|
||||
|
||||
def GetPages( self ):
|
||||
|
||||
return self._GetPages()
|
||||
|
||||
|
||||
def GetPrettyStatusForStatusBar( self ):
|
||||
|
||||
( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary()
|
||||
|
||||
num_string = HydrusData.ToHumanInt( num_files )
|
||||
|
||||
if num_range > 0 and num_value != num_range:
|
||||
|
||||
num_string += ', ' + HydrusData.ConvertValueRangeToPrettyString( num_value, num_range )
|
||||
|
||||
|
||||
return HydrusData.ToHumanInt( self.count() ) + ' pages, ' + num_string + ' files'
|
||||
|
||||
|
||||
def GetSerialisablePage( self, only_changed_page_data, about_to_save ):
|
||||
|
||||
page_containers = []
|
||||
|
@ -2467,25 +2526,6 @@ class PagesNotebook( QP.TabWidgetWithDnD ):
|
|||
return root
|
||||
|
||||
|
||||
def GetPages( self ):
|
||||
|
||||
return self._GetPages()
|
||||
|
||||
|
||||
def GetPrettyStatus( self ):
|
||||
|
||||
( num_files, ( num_value, num_range ) ) = self.GetNumFileSummary()
|
||||
|
||||
num_string = HydrusData.ToHumanInt( num_files )
|
||||
|
||||
if num_range > 0 and num_value != num_range:
|
||||
|
||||
num_string += ', ' + HydrusData.ConvertValueRangeToPrettyString( num_value, num_range )
|
||||
|
||||
|
||||
return HydrusData.ToHumanInt( self.count() ) + ' pages, ' + num_string + ' files'
|
||||
|
||||
|
||||
def GetTestAbleToCloseStatement( self ):
|
||||
|
||||
count = collections.Counter()
|
||||
|
|
|
@ -307,7 +307,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
def _Delete( self, file_service_key = None, only_those_in_file_service_key = None ):
|
||||
|
||||
media_to_delete = self._selected_media
|
||||
media_to_delete = ClientMedia.FlattenMedia( self._selected_media )
|
||||
|
||||
if only_those_in_file_service_key is not None:
|
||||
|
||||
|
@ -523,7 +523,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
return sum( [ media.GetNumFiles() for media in self._selected_media ] )
|
||||
|
||||
|
||||
def _GetPrettyStatus( self ) -> str:
|
||||
def _GetPrettyStatusForStatusBar( self ) -> str:
|
||||
|
||||
num_files = len( self._hashes )
|
||||
|
||||
|
@ -1171,7 +1171,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
self.selectedMediaTagPresentationChanged.emit( tags_media, tags_changed )
|
||||
|
||||
self.statusTextChanged.emit( self._GetPrettyStatus() )
|
||||
self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() )
|
||||
|
||||
if tags_changed:
|
||||
|
||||
|
@ -1192,7 +1192,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
self.selectedMediaTagPresentationIncremented.emit( medias )
|
||||
|
||||
self.statusTextChanged.emit( self._GetPrettyStatus() )
|
||||
self.statusTextChanged.emit( self._GetPrettyStatusForStatusBar() )
|
||||
|
||||
else:
|
||||
|
||||
|
@ -1446,6 +1446,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
flat_media = ClientMedia.FlattenMedia( media_group )
|
||||
|
||||
|
||||
num_files_str = HydrusData.ToHumanInt( len( flat_media ) )
|
||||
|
||||
if len( flat_media ) < 2:
|
||||
|
||||
return False
|
||||
|
@ -1462,6 +1464,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
media_pairs = [ ( first_media, other_media ) for other_media in flat_media if other_media != first_media ]
|
||||
|
||||
|
||||
else:
|
||||
|
||||
num_files_str = HydrusData.ToHumanInt( len( self._GetSelectedFlatMedia() ) )
|
||||
|
||||
|
||||
if len( media_pairs ) == 0:
|
||||
|
||||
|
@ -1477,7 +1483,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
if duplicate_type == HC.DUPLICATE_FALSE_POSITIVE:
|
||||
|
||||
message = 'False positive records are complicated, and setting that relationship for many files at once is likely a mistake.'
|
||||
message = 'False positive records are complicated, and setting that relationship for {} files at once is likely a mistake.'.format( num_files_str )
|
||||
message += os.linesep * 2
|
||||
message += 'Are you sure all of these files are all potential duplicates and that they are all false positive matches with each other? If not, I recommend you step back for now.'
|
||||
|
||||
|
@ -1486,7 +1492,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
elif duplicate_type == HC.DUPLICATE_ALTERNATE:
|
||||
|
||||
message = 'Are you certain all these files are alternates with every other member of the selection, and that none are duplicates?'
|
||||
message = 'Are you certain all these {} files are alternates with every other member of the selection, and that none are duplicates?'.format( num_files_str )
|
||||
message += os.linesep * 2
|
||||
message += 'If some of them may be duplicates, I recommend you either deselect the possible duplicates and try again, or just leave this group to be processed in the normal duplicate filter.'
|
||||
|
||||
|
@ -1496,7 +1502,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
else:
|
||||
|
||||
message = 'Are you sure you want to ' + yes_no_text + ' for the selected files?'
|
||||
message = 'Are you sure you want to ' + yes_no_text + ' for the {} selected files?'.format( num_files_str )
|
||||
|
||||
|
||||
result = ClientGUIDialogsQuick.GetYesNo( self, message, yes_label = yes_label, no_label = no_label )
|
||||
|
@ -1589,9 +1595,21 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
worse_flat_media = [ media for media in flat_media if media.GetHash() != focused_hash ]
|
||||
|
||||
if len( worse_flat_media ) == 0:
|
||||
|
||||
return
|
||||
|
||||
|
||||
media_pairs = [ ( better_media, worse_media ) for worse_media in worse_flat_media ]
|
||||
|
||||
self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs )
|
||||
message = 'Are you sure you want to set the focused file as better than the {} other files in the selection?'.format( HydrusData.ToHumanInt( len( worse_flat_media ) ) )
|
||||
|
||||
result = ClientGUIDialogsQuick.GetYesNo( self, message )
|
||||
|
||||
if result == QW.QDialog.Accepted:
|
||||
|
||||
self._SetDuplicates( HC.DUPLICATE_BETTER, media_pairs = media_pairs, silent = True )
|
||||
|
||||
|
||||
else:
|
||||
|
||||
|
@ -1783,6 +1801,8 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea ):
|
|||
|
||||
self.newMediaAdded.emit()
|
||||
|
||||
HG.client_controller.pub( 'notify_new_pages_count' )
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
@ -2216,7 +2236,7 @@ class MediaPanelLoading( MediaPanel ):
|
|||
HG.client_controller.sub( self, 'SetNumQueryResults', 'set_num_query_results' )
|
||||
|
||||
|
||||
def _GetPrettyStatus( self ):
|
||||
def _GetPrettyStatusForStatusBar( self ):
|
||||
|
||||
s = 'Loading\u2026'
|
||||
|
||||
|
@ -2866,6 +2886,8 @@ class MediaPanelThumbnails( MediaPanel ):
|
|||
|
||||
HG.client_controller.pub( 'refresh_page_name', self._page_key )
|
||||
|
||||
HG.client_controller.pub( 'notify_new_pages_count' )
|
||||
|
||||
self.widget().update()
|
||||
|
||||
|
||||
|
|
|
@ -463,7 +463,7 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
file_seed_cache.AddFileSeeds( file_seeds )
|
||||
|
||||
status = CC.STATUS_SUCCESSFUL_AND_NEW
|
||||
status = CC.STATUS_SUCCESSFUL_AND_CHILD_FILES
|
||||
|
||||
note = 'was redirected on file download to a post url, which has been queued in the parent file log'
|
||||
|
||||
|
@ -1267,7 +1267,7 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
num_urls_added = file_seed_cache.InsertFileSeeds( insertion_index, file_seeds )
|
||||
|
||||
status = CC.STATUS_SUCCESSFUL_AND_NEW
|
||||
status = CC.STATUS_SUCCESSFUL_AND_CHILD_FILES
|
||||
note = 'Found {} new URLs.'.format( HydrusData.ToHumanInt( num_urls_added ) )
|
||||
|
||||
self.SetStatus( status, note = note )
|
||||
|
@ -1363,7 +1363,7 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
|
|||
|
||||
num_urls_added = file_seed_cache.InsertFileSeeds( insertion_index, child_file_seeds )
|
||||
|
||||
status = CC.STATUS_SUCCESSFUL_AND_NEW
|
||||
status = CC.STATUS_SUCCESSFUL_AND_CHILD_FILES
|
||||
note = 'Found {} new URLs.'.format( HydrusData.ToHumanInt( num_urls_added ) )
|
||||
|
||||
self.SetStatus( status, note = note )
|
||||
|
|
|
@ -290,7 +290,7 @@ class TagDisplayMaintenanceManager( object ):
|
|||
self._controller.sub( self, 'NotifyNewDisplayData', 'notify_new_tag_display_application' )
|
||||
|
||||
|
||||
def _GetAfterWorkWaitTime( self, service_key ):
|
||||
def _GetAfterWorkWaitTime( self, service_key, expected_work_time, actual_work_time ):
|
||||
|
||||
with self._lock:
|
||||
|
||||
|
@ -311,7 +311,16 @@ class TagDisplayMaintenanceManager( object ):
|
|||
|
||||
else:
|
||||
|
||||
return 30
|
||||
if actual_work_time > expected_work_time * 5:
|
||||
|
||||
# if suddenly a job blats the user for ten seconds or _ten minutes_ during normal time, we are going to take a big break
|
||||
|
||||
return 1800
|
||||
|
||||
else:
|
||||
|
||||
return 30
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -479,11 +488,17 @@ class TagDisplayMaintenanceManager( object ):
|
|||
|
||||
work_time = self._GetWorkTime( service_key )
|
||||
|
||||
start_time = HydrusData.GetNowPrecise()
|
||||
|
||||
still_needs_work = self._controller.WriteSynchronous( 'sync_tag_display_maintenance', service_key, work_time )
|
||||
|
||||
finish_time = HydrusData.GetNowPrecise()
|
||||
|
||||
total_time_took = finish_time - start_time
|
||||
|
||||
self._service_keys_to_needs_work[ service_key ] = still_needs_work
|
||||
|
||||
wait_time = self._GetAfterWorkWaitTime( service_key )
|
||||
wait_time = self._GetAfterWorkWaitTime( service_key, work_time, total_time_took )
|
||||
|
||||
self._last_loop_work_time = work_time
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ options = {}
|
|||
# Misc
|
||||
|
||||
NETWORK_VERSION = 20
|
||||
SOFTWARE_VERSION = 473
|
||||
SOFTWARE_VERSION = 474
|
||||
CLIENT_API_VERSION = 25
|
||||
|
||||
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
|
||||
|
|
|
@ -1658,6 +1658,12 @@ def BaseToHumanBytes( size, sig_figs = 3 ):
|
|||
|
||||
suffix_index = 0
|
||||
|
||||
ctx = decimal.getcontext()
|
||||
|
||||
# yo, ctx is actually a global, you set prec later, it'll hang around for the /= stuff here lmaaaaaoooooo
|
||||
# 188213746 was flipping between 179MB/180MB because the prec = 10 was being triggered later on 180MB
|
||||
ctx.prec = 28
|
||||
|
||||
d = decimal.Decimal( size )
|
||||
|
||||
while d >= 1024:
|
||||
|
@ -1669,8 +1675,6 @@ def BaseToHumanBytes( size, sig_figs = 3 ):
|
|||
|
||||
suffix = suffixes[ suffix_index ]
|
||||
|
||||
ctx = decimal.getcontext()
|
||||
|
||||
# ok, if we have 237KB, we still want all 237, even if user said 2 sf
|
||||
while d.log10() >= sig_figs:
|
||||
|
||||
|
|
|
@ -411,7 +411,7 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ):
|
|||
|
||||
def IsPNGAnimated( file_header_bytes ):
|
||||
|
||||
apng_actl_bytes = HydrusVideoHandling.GetAPNGACTLChunk( file_header_bytes )
|
||||
apng_actl_bytes = HydrusVideoHandling.GetAPNGACTLChunkData( file_header_bytes )
|
||||
|
||||
if apng_actl_bytes is not None:
|
||||
|
||||
|
|
|
@ -43,35 +43,58 @@ def CheckFFMPEGError( lines ):
|
|||
raise HydrusExceptions.DamagedOrUnusualFileException( 'FFMPEG could not parse.' )
|
||||
|
||||
|
||||
def GetAPNGACTLChunk( file_header_bytes: bytes ):
|
||||
def GetAPNGChunks( file_header_bytes: bytes ):
|
||||
|
||||
# https://wiki.mozilla.org/APNG_Specification
|
||||
# a chunk is:
|
||||
# 4 bytes of data size, unsigned int
|
||||
# 4 bytes of chunk name
|
||||
# n bytes of data
|
||||
# 4 bytes of CRC
|
||||
|
||||
# lop off 8 bytes of 'this is a PNG' at the top
|
||||
remaining_chunk_bytes = file_header_bytes[8:]
|
||||
|
||||
chunks = []
|
||||
|
||||
while len( remaining_chunk_bytes ) > 12:
|
||||
|
||||
( num_data_bytes, ) = struct.unpack( '>I', remaining_chunk_bytes[ : 4 ] )
|
||||
|
||||
chunk_name = remaining_chunk_bytes[ 4 : 8 ]
|
||||
|
||||
chunk_data = remaining_chunk_bytes[ 8 : 8 + num_data_bytes ]
|
||||
|
||||
chunks.append( ( chunk_name, chunk_data ) )
|
||||
|
||||
remaining_chunk_bytes = remaining_chunk_bytes[ 8 + num_data_bytes + 4 : ]
|
||||
|
||||
|
||||
return chunks
|
||||
|
||||
def GetAPNGACTLChunkData( file_header_bytes: bytes ):
|
||||
|
||||
# the acTL chunk can be in different places, but it has to be near the top
|
||||
# although it is almost always in fixed position (I think byte 29), we have seen both pHYs and sRGB chunks appear before it
|
||||
# so to be proper we need to parse chunks and find the right one
|
||||
apng_actl_chunk_header = b'acTL'
|
||||
apng_phys_chunk_header = b'pHYs'
|
||||
|
||||
first_guess_header = file_header_bytes[ 37:128 ]
|
||||
chunks = GetAPNGChunks( file_header_bytes )
|
||||
|
||||
if first_guess_header.startswith( apng_actl_chunk_header ):
|
||||
|
||||
return first_guess_header
|
||||
|
||||
elif first_guess_header.startswith( apng_phys_chunk_header ):
|
||||
|
||||
# aha, some weird other png chunk
|
||||
# https://wiki.mozilla.org/APNG_Specification
|
||||
|
||||
if apng_actl_chunk_header in first_guess_header:
|
||||
|
||||
i = first_guess_header.index( apng_actl_chunk_header )
|
||||
|
||||
return first_guess_header[i:]
|
||||
|
||||
|
||||
chunks = dict( chunks )
|
||||
|
||||
return None
|
||||
if apng_actl_chunk_header in chunks:
|
||||
|
||||
return chunks[ apng_actl_chunk_header ]
|
||||
|
||||
else:
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def GetAPNGNumFrames( apng_actl_bytes ):
|
||||
|
||||
( num_frames, ) = struct.unpack( '>I', apng_actl_bytes[ 4 : 8 ] )
|
||||
( num_frames, ) = struct.unpack( '>I', apng_actl_bytes[ : 4 ] )
|
||||
|
||||
return num_frames
|
||||
|
||||
|
@ -257,7 +280,7 @@ def GetFFMPEGAPNGProperties( path ):
|
|||
file_header_bytes = f.read( 256 )
|
||||
|
||||
|
||||
apng_actl_bytes = GetAPNGACTLChunk( file_header_bytes )
|
||||
apng_actl_bytes = GetAPNGACTLChunkData( file_header_bytes )
|
||||
|
||||
if apng_actl_bytes is None:
|
||||
|
||||
|
@ -874,7 +897,14 @@ def ParseFFMPEGVideoResolution( lines, png_ok = False ):
|
|||
width = int( width_string )
|
||||
height = int( height_string )
|
||||
|
||||
sar_match = re.search( "[\\[\\s]SAR [0-9]*:[0-9]* ", line )
|
||||
# if a vid has an SAR, this 'sample' aspect ratio basically just stretches it
|
||||
# when you convert the width using SAR, the resulting resolution should match the DAR, 'display' aspect ratio, which is what we actually want in final product
|
||||
# MPC-HC seems to agree with this calculation, Firefox does not
|
||||
# examples:
|
||||
# ' Stream #0:0: Video: hevc (Main), yuv420p(tv, bt709), 1280x720 [SAR 69:80 DAR 23:15], 30 fps, 30 tbr, 1k tbn (default)'
|
||||
# ' Stream #0:0: Video: vp9 (Profile 0), yuv420p(tv, progressive), 1120x1080, SAR 10:11 DAR 280:297, 30 fps, 30 tbr, 1k tbn (default)'
|
||||
|
||||
sar_match = re.search( "[\\[\\s]SAR [0-9]*:[0-9]*[,\\s]", line )
|
||||
|
||||
if sar_match is not None:
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-ubuntu:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: hydrus
|
||||
-
|
||||
name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
architecture: x64
|
||||
#- name: Cache Qt
|
||||
# id: cache-qt
|
||||
# uses: actions/cache@v1
|
||||
# with:
|
||||
# path: Qt
|
||||
# key: ${{ runner.os }}-QtCache
|
||||
#-
|
||||
# name: Install Qt
|
||||
# uses: jurplel/install-qt-action@v2
|
||||
# with:
|
||||
# install-deps: true
|
||||
# setup-python: 'false'
|
||||
# modules: qtcharts qtwidgets qtgui qtcore
|
||||
# cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
-
|
||||
name: APT Install
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libmpv1
|
||||
-
|
||||
name: Pip Installer
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: pyinstaller
|
||||
requirements: hydrus/static/build_files/linux/requirements.txt
|
||||
-
|
||||
name: Build Hydrus
|
||||
run: |
|
||||
cp hydrus/static/build_files/linux/client.spec client.spec
|
||||
cp hydrus/static/build_files/linux/server.spec server.spec
|
||||
pyinstaller server.spec
|
||||
pyinstaller client.spec
|
||||
-
|
||||
name: Remove Chonk
|
||||
run: |
|
||||
find dist/client/ -type f -name "*.pyc" -delete
|
||||
while read line; do find dist/client/ -type f -name "${line}" -delete ; done < hydrus/static/build_files/linux/files_to_delete.txt
|
||||
-
|
||||
name: Set Permissions
|
||||
run: |
|
||||
sudo chown --recursive 1000:1000 dist/client
|
||||
sudo find dist/client -type d -exec chmod 0755 {} \;
|
||||
sudo chmod +x dist/client/client dist/client/server dist/client/bin/swfrender_linux
|
||||
-
|
||||
name: Compress Client
|
||||
run: |
|
||||
mv dist/client "dist/Hydrus Network"
|
||||
tar -czvf Ubuntu-Extract.tar.gz -C dist "Hydrus Network"
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Ubuntu-Extract
|
||||
path: Ubuntu-Extract.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
|
@ -1,54 +0,0 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup FFMPEG
|
||||
uses: FedericoCarboni/setup-ffmpeg@v1
|
||||
id: setup_ffmpeg
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Install PyOxidizer
|
||||
run: python3 -m pip install pyoxidizer
|
||||
-
|
||||
name: Build Hydrus
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
cp ${{ steps.setup_ffmpeg.outputs.ffmpeg-path }} bin/
|
||||
cp static/build_files/macos/pyoxidizer.bzl pyoxidizer.bzl
|
||||
cp static/build_files/linux/requirements.txt requirements.txt
|
||||
basename $(rustc --print sysroot) | sed -e "s/^stable-//" > triple.txt
|
||||
pyoxidizer build --release
|
||||
cd build/$(head -n 1 triple.txt)/release
|
||||
mkdir -p "Hydrus Network.app/Contents/MacOS"
|
||||
mkdir -p "Hydrus Network.app/Contents/Resources"
|
||||
mkdir -p "Hydrus Network.app/Contents/Frameworks"
|
||||
mv install/static/icon.icns "Hydrus Network.app/Contents/Resources/icon.icns"
|
||||
cp install/static/build_files/macos/Info.plist "Hydrus Network.app/Contents/Info.plist"
|
||||
cp install/static/build_files/macos/ReadMeFirst.rtf ./ReadMeFirst.rtf
|
||||
cp install/static/build_files/macos/running_from_app "install/running_from_app"
|
||||
ln -s /Applications ./Applications
|
||||
mv install/* "Hydrus Network.app/Contents/MacOS/"
|
||||
rm -rf install
|
||||
cd $GITHUB_WORKSPACE
|
||||
temp_dmg="$(mktemp).dmg"
|
||||
hdiutil create "$temp_dmg" -ov -volname "HydrusNetwork" -fs HFS+ -srcfolder "$GITHUB_WORKSPACE/build/$(head -n 1 triple.txt)/release"
|
||||
hdiutil convert "$temp_dmg" -format UDZO -o HydrusNetwork.dmg
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
with:
|
||||
name: MacOS-DMG
|
||||
path: HydrusNetwork.dmg
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
|
@ -1,99 +0,0 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: [windows-latest]
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: hydrus
|
||||
-
|
||||
name: Setup FFMPEG
|
||||
uses: FedericoCarboni/setup-ffmpeg@v1
|
||||
id: setup_ffmpeg
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
architecture: x64
|
||||
-
|
||||
name: Cache Qt
|
||||
id: cache_qt
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ../Qt
|
||||
key: ${{ runner.os }}-QtCache
|
||||
-
|
||||
name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
install-deps: true
|
||||
setup-python: 'false'
|
||||
modules: qtcharts qtwidgets qtgui qtcore
|
||||
cached: ${{ steps.cache_qt.outputs.cache-hit }}
|
||||
-
|
||||
name: PIP Install Packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: pyinstaller
|
||||
requirements: hydrus\static\build_files\windows\requirements.txt
|
||||
-
|
||||
name: Download mpv-dev
|
||||
uses: carlosperate/download-file-action@v1.0.3
|
||||
id: download_mpv
|
||||
with:
|
||||
file-url: 'https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210228-git-d1be8bb.7z'
|
||||
file-name: 'mpv-dev-x86_64.7z'
|
||||
location: '.'
|
||||
-
|
||||
name: Process mpv-dev
|
||||
run: |
|
||||
7z x ${{ steps.download_mpv.outputs.file-path }}
|
||||
move mpv-1.dll hydrus\
|
||||
-
|
||||
name: Build Hydrus
|
||||
run: |
|
||||
move ${{ steps.setup_ffmpeg.outputs.ffmpeg-path }} hydrus\bin\
|
||||
move hydrus\static\build_files\windows\sqlite3.dll hydrus\
|
||||
move hydrus\static\build_files\windows\sqlite3.exe hydrus\db
|
||||
move hydrus\static\build_files\windows\client-win.spec client-win.spec
|
||||
move hydrus\static\build_files\windows\server-win.spec server-win.spec
|
||||
pyinstaller server-win.spec
|
||||
pyinstaller client-win.spec
|
||||
dir -r
|
||||
-
|
||||
name: InnoSetup
|
||||
run: |
|
||||
move hydrus\static\build_files\windows\InnoSetup.iss InnoSetup.iss
|
||||
ISCC.exe InnoSetup.iss /DVersion=${GITHUB_REF##*/v}
|
||||
-
|
||||
name: Compress Client
|
||||
run: |
|
||||
cd .\dist
|
||||
7z.exe a -tzip -mm=Deflate -mx=5 ..\Windows-Extract.zip 'Hydrus Network'
|
||||
cd ..
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Windows-Install
|
||||
path: dist\HydrusInstaller.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
||||
-
|
||||
name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Windows-Extract
|
||||
path: Windows-Extract.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 2
|
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
|
@ -131,6 +131,11 @@ QLocatorResultWidget#selectedLocatorResult
|
|||
background-color: #006ffa
|
||||
}
|
||||
|
||||
QLocatorResultWidget QWidget
|
||||
{
|
||||
background: transparent
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Place a .css or .qss Qt Stylesheet file in here, and hydrus will provide it as an UI stylesheet option.
|
||||
|
||||
Don't edit any of the files in here--they'll just be overwritten the next time you install. Copy to your own custom filenames if you want to edit anything.
|
||||
Don't edit any of the files in here in place--they'll just be overwritten the next time you update. Copy to your own custom filenames if you want to edit anything.
|
||||
|
||||
The default_hydrus.qss is used by the client to draw some custom widget colours. It is prepended to any custom stylesheet that is loaded, check it out for the class names you want want to override in your own custom QSS.
|
||||
|
||||
|
|
Loading…
Reference in New Issue