Merge remote-tracking branch 'hydrusnetwork/master' into mkdocs

This commit is contained in:
Paul Friederichsen 2022-02-16 16:33:36 -06:00
commit 9550648a27
33 changed files with 624 additions and 420 deletions

View File

@ -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

View File

@ -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>

View File

@ -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 }

View File

@ -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' )

View File

@ -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 + '!' )

View File

@ -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()

View File

@ -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, ) )

View File

@ -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' )
]

View File

@ -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 ) )

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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 )

View File

@ -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 ):

View File

@ -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'

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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 )

View File

@ -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

View File

@ -81,7 +81,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 473
SOFTWARE_VERSION = 474
CLIENT_API_VERSION = 25
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -131,6 +131,11 @@ QLocatorResultWidget#selectedLocatorResult
background-color: #006ffa
}
QLocatorResultWidget QWidget
{
background: transparent
}
/*

View File

@ -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.