Version 484

closes #1139, closes #1143
This commit is contained in:
Hydrus Network Developer 2022-05-11 16:16:33 -05:00
parent ef19e2167e
commit 16bf34db22
26 changed files with 1079 additions and 537 deletions

View File

@ -3,6 +3,33 @@
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 484](https://github.com/hydrusnetwork/hydrus/releases/tag/v484)
### misc
* fixed the simple delete files dialog for trashed files. due to a logical oversight, the simple version was not testing 'trashed' status and so didn't see anything to permanently delete and would immediately dump out. now it shows the option for trashed files again, and if the selection includes trash and non-trash, it shows multiple options
* fixed an error in the 'show next pair' logic of the new duplicate filter queue where if it needed to auto-skip through the end of the current batch and load up the next batch (issues #1139, #1143)
* a new setting on _options->media_ now lets you set the scanbar to be small and simple instead of hidden when the mouse is moved away. I liked this so much personally it is now the default for new users. try it out!
* the media viewer's taglist hover window will now never send a mouse wheel event up to the media viewer canvas (so scrolling the tags won't accidentally do previous/next if you hit the end of the list scrollbar)
* I think I have fixed the bug where going on the media viewer from borderless fullscreen to a regular window would not trigger a media container resize if the media perfectly fitted the ratio of the fullscreen monitor!
* the system tray icon now has minimise/restore entries
* to reduce confusion, when a content parser vetoes, it now prepends the file import 'note' with 'veto: '
* the 'clear service info cache' job under _database->regenerate_ is renamed to 'service info numbers' and now has a service selector so you can, let's say, regen your miscounted 'number of files in trash' count without triggering a complete recount of every single mapping on the PTR the next time you open review services
* hydrus now recognises most (and maybe all) windows executables so it can discard them from imports confidently. a user discovered an interesting exe with embedded audio that ffmpeg was seeing as an mp3--this no longer occurs
* the 'edit string conversion step' dialog now saves a new default (which is used on 'add' events) every time you ok it. 'append extra text' is no longer the universal default!
* the 'edit tag rule' dialog in the parsing system now starts with the tag name field focused
* updated 'getting started/installing' help to talk more about mpv on Linux. the 'libgmodule' problem _seems_ to have a solid fix now, which is properly written out there. thanks to the users who figured all this out and provided feedback
### multiple local file services
* the media viewer menu now offers add/move actions just like the thumb grid
* added a new shortcut action that lets you specify add/move jobs. it is available in the media shortcut set and will work in the thumbnail grid and the media viewer
* add/move is now nicer in edge cases. files are filtered better to ensure only local media files end up in a job (e.g. if you were to try to move files out of the repository update domain using a shortcut), and 'add' commands from trashed files are naturally and silently converted to a pure undelete
### boring code cleanup
* refactored the UI side of multiple local file services add/move commands. various functions to select, filter, and question the user on actions are now pulled to a separate simple module where other parts of the UI can also access them, and there is now just one isolated pipeline for file service add/move content updates.
* if a 'move' job is started without a source service and multiple services could apply, the main routine will now ask the user which to use using a selector that shows how many files each choice will affect
* also rewrote the add/move menu population code, fixed a couple little issues, and refactored it to a module the media viewer canvas can use
* wrote a new menu builder that can place a list of items either as a single item (if the list is length 1), or make a submenu if there are more. it drives the new add/move commands and now the behind the scenes of all other service-based menu population
## [Version 483](https://github.com/hydrusnetwork/hydrus/releases/tag/v483)
### multiple local file services
@ -339,26 +366,3 @@
* the 'migrate tags' dialog's file service filtering now supports n local file services, and 'all local files'
* 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
* 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
## [Version 473](https://github.com/hydrusnetwork/hydrus/releases/tag/v473)
### misc
* fixed the recent problem with drag and dropping thumbnails to a level below the top row of pages. sorry for the trouble!
* fixed a bug where the client would not load results sorting by 'import time' when the search file domain was a single deleted file domain
* fixed a list display bug in the edit page parser dialog when a subsidiary page parser has two complicated string-match based content parsers
* collections now sort by modified time, using the largest known modified time in their collection
* added sqlite3.exe console back into the windows build--sorry, it was missing since the github build changeover!
* added a note to the help about backing up when tight on space, which I will repeat here: the sqlite database files are very compressible (70GB->17GB on default 7zip settings!), so if you need more space on your backup drive, this is a good way to reclaim it
### command palette
* a user has written a cool 'command palette' for the program! it brings up a type-and-search interface to navigate to pages or menu entries.
* I have integrated his first version and set the default shortcut to Ctrl+P. users who update will get this shortcut if they have nothing else on Ctrl+P on 'main window' set. if you prefer Ctrl+K or anything else, you can change it under _file->shortcuts->the main window_
* regular users will get a page list they can search and select, advanced users will also get the (potentially dangerous) full scan of the menubar and current thumbnail right-click menu. I will be polishing this latter feature in future to filter out big maintenance jobs and show checkbox status and similar, so if you are advanced, please be careful for now
* try it out, and let me know how it goes. the underlying widget is neat, and I can change its behaviour and extend it significantly
### (mostly advanced) deleted file improvements
* files that have been deleted from a local file domain are now aware of their file deletion reason. this is visible in the right-click menu of thumb or media canvas
* the advanced file deletion dialog now initialises using this stored reason. if all pending deletees have the same existing reason stored, it will display it, and if they are all set but differ, this will be noted and an option to not alter them is also available. this will come up later in niche advanced situations with mutiple file services
* reversing a recent change, local file deletion reasons are no longer cleared on undelete or (re)import. they'll now hang around invisibly and initialise any future advanced file deletion dialog
* updated the thumbnail and canvas undelete mechanism to handle multiple services. now, if the files are deleted in more than one domain, you will be asked to multiple-select which you wish to undelete for. if there is only one eligible undelete service, the process remains unchanged--you'll just get a yes/no confirmation if the 'confirm trash' option is set
* misc multiple local file services code conversion work

View File

@ -32,10 +32,22 @@ I try to release a new version every Wednesday by 8pm EST and write an accompany
* Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. The build is made on Ubuntu, so if you run something else, compatibility is hit and miss.
* If you have problems running the Ubuntu build, users with some python experience generally find running from source works well.
* You might need to get 'libmpv1' to get mpv working and playing video/audio. This is the mpv library, not the player. Check help->about to see if it is available--if not, see if you can get it with _apt_.
* You might need to get 'libmpv1' to get mpv working and playing video/audio. This is the mpv _library_, not the necessarily the player. Check _help->about_ to see if it is available--if not, see if you can get it with `apt`.
If the about window provides you an error popup like this:
```
OSError: /lib/x86_64-linux-gnu/libgio-2.0.so.0: undefined symbol: g_module_open_full
(traceback)
pyimod04_ctypes.install.<locals>.PyInstallerImportError: Failed to load dynlib/dll 'libmpv.so.1'. Most likely this dynlib/dll was not found when the application was frozen.
```
Then please do this:
1. Search your /usr/ dir for `libgmodule*`. You are looking for something like `libgmodule-2.0.so`. Users report finding it in `/usr/lib64/` and `/usr/lib/x86_64-linux-gnu`.
2. Copy that .so file to the hydrus install base directory.
3. Boot the client and hit _help->about_ to see if it reports a version.
4. If it all seems good, hit _options->media_ to set up mpv as your player for video/audio and try to view some things.
* You can also try [running the Windows version in wine](wine.md).
* **Third parties (not maintained by Hydrus Developer)**:
If you use Arch Linux, you can check out the AUR package a user maintains [here](https://aur.archlinux.org/packages/hydrus/).
=== "From Source"

View File

@ -33,6 +33,33 @@
<div class="content">
<h3 id="changelog"><a href="#changelog">changelog</a></h3>
<ul>
<li><h3 id="version_484"><a href="#version_484">version 484</a></h3></li>
<ul>
<li>misc:</li>
<li>fixed the simple delete files dialog for trashed files. due to a logical oversight, the simple version was not testing 'trashed' status and so didn't see anything to permanently delete and would immediately dump out. now it shows the option for trashed files again, and if the selection includes trash and non-trash, it shows multiple options</li>
<li>fixed an error in the 'show next pair' logic of the new duplicate filter queue where if it needed to auto-skip through the end of the current batch and load up the next batch (issues #1139, #1143)</li>
<li>a new setting on _options->media_ now lets you set the scanbar to be small and simple instead of hidden when the mouse is moved away. I liked this so much personally it is now the default for new users. try it out!</li>
<li>the media viewer's taglist hover window will now never send a mouse wheel event up to the media viewer canvas (so scrolling the tags won't accidentally do previous/next if you hit the end of the list scrollbar)</li>
<li>I think I have fixed the bug where going on the media viewer from borderless fullscreen to a regular window would not trigger a media container resize if the media perfectly fitted the ratio of the fullscreen monitor!</li>
<li>the system tray icon now has minimise/restore entries</li>
<li>to reduce confusion, when a content parser vetoes, it now prepends the file import 'note' with 'veto: '</li>
<li>the 'clear service info cache' job under _database->regenerate_ is renamed to 'service info numbers' and now has a service selector so you can, let's say, regen your miscounted 'number of files in trash' count without triggering a complete recount of every single mapping on the PTR the next time you open review services</li>
<li>hydrus now recognises most (and maybe all) windows executables so it can discard them from imports confidently. a user discovered an interesting exe with embedded audio that ffmpeg was seeing as an mp3--this no longer occurs</li>
<li>the 'edit string conversion step' dialog now saves a new default (which is used on 'add' events) every time you ok it. 'append extra text' is no longer the universal default!</li>
<li>the 'edit tag rule' dialog in the parsing system now starts with the tag name field focused</li>
<li>updated 'getting started/installing' help to talk more about mpv on Linux. the 'libgmodule' problem _seems_ to have a solid fix now, which is properly written out there. thanks to the users who figured all this out and provided feedback</li>
<li>.</li>
<li>multiple local file services:</li>
<li>the media viewer menu now offers add/move actions just like the thumb grid</li>
<li>added a new shortcut action that lets you specify add/move jobs. it is available in the media shortcut set and will work in the thumbnail grid and the media viewer</li>
<li>add/move is now nicer in edge cases. files are filtered better to ensure only local media files end up in a job (e.g. if you were to try to move files out of the repository update domain using a shortcut), and 'add' commands from trashed files are naturally and silently converted to a pure undelete</li>
<li>.</li>
<li>boring code cleanup:</li>
<li>refactored the UI side of multiple local file services add/move commands. various functions to select, filter, and question the user on actions are now pulled to a separate simple module where other parts of the UI can also access them, and there is now just one isolated pipeline for file service add/move content updates.</li>
<li>if a 'move' job is started without a source service and multiple services could apply, the main routine will now ask the user which to use using a selector that shows how many files each choice will affect</li>
<li>also rewrote the add/move menu population code, fixed a couple little issues, and refactored it to a module the media viewer canvas can use</li>
<li>wrote a new menu builder that can place a list of items either as a single item (if the list is length 1), or make a submenu if there are more. it drives the new add/move commands and now the behind the scenes of all other service-based menu population</li>
</ul>
<li><h3 id="version_483"><a href="#version_483">version 483</a></h3></li>
<ul>
<li>multiple local file services:</li>

View File

@ -84,7 +84,11 @@ As well as the python wrapper, 'python-mpv' as in the requirements.txt, you also
For Windows, the dll builds are [here](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/), although getting the right version for the current wrapper can be difficult (you will get errors when you try to load video if it is not correct). Just put it in your hydrus base install directory. You can also just grab the 'mpv-1.dll' I bundle in my release. In my experience, [this](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/mpv-dev-x86_64-20210228-git-d1be8bb.7z/download) works with python-mpv 0.5.2.
If you are on Linux/macOS, you can usually get 'libmpv1' with _apt_. You might have to adjust your python-mpv version (e.g. `pip3 install python-mpv==0.4.5`) to get it to work.
If you are on Linux, you can usually get 'libmpv1' with _apt_. You might have to adjust your python-mpv version (e.g. `pip3 install python-mpv==0.4.5`) to get it to work.
On macOS, you should be able to get it with `brew install mpv`, but you may find mpv crashes the program when it tries to load. Hydev is working on this.
Hit _help->about_ to see your mpv status. If you don't have it, it will present an error popup box with more info.
## SQLite { id="sqlite" }
@ -121,4 +125,4 @@ When running from source you will also need to [build the hydrus help docs](abou
My coding style is unusual and unprofessional. Everything is pretty much hacked together. If you are interested in how things work, please do look through the source and ask me if you don't understand something.
I'm constantly throwing new code together and then cleaning and overhauling it down the line. I work strictly alone, however, so while I am very interested in detailed bug reports or suggestions for good libraries to use, I am not looking for pull requests or suggestions on style. I know a lot of things are a mess. Everything I do is [WTFPL](https://github.com/sirkris/WTFPL/blob/master/WTFPL.md), so feel free to fork and play around with things on your end as much as you like.
I'm constantly throwing new code together and then cleaning and overhauling it down the line. I work strictly alone, however, so while I am very interested in detailed bug reports or suggestions for good libraries to use, I am not looking for pull requests or suggestions on style. I know a lot of things are a mess. Everything I do is [WTFPL](https://github.com/sirkris/WTFPL/blob/master/WTFPL.md), so feel free to fork and play around with things on your end as much as you like.

View File

@ -445,6 +445,11 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
( service_key, content_type, action, value ) = self._data
if content_type == HC.CONTENT_TYPE_FILES and action == HC.CONTENT_UPDATE_MOVE and value is not None and isinstance( value, bytes ):
value = value.hex()
serialisable_data = ( service_key.hex(), content_type, action, value )
@ -470,6 +475,18 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
( serialisable_service_key, content_type, action, value ) = serialisable_data
if content_type == HC.CONTENT_TYPE_FILES and action == HC.CONTENT_UPDATE_MOVE and value is not None and isinstance( value, str ):
try:
value = bytes.fromhex( value )
except:
value = None
self._data = ( bytes.fromhex( serialisable_service_key ), content_type, action, value )
@ -638,6 +655,8 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
components.append( HC.content_update_string_lookup[ action ] )
components.append( HC.content_type_string_lookup[ content_type ] )
value_string = ''
if content_type == HC.CONTENT_TYPE_RATINGS:
if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
@ -663,7 +682,20 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
value_string = '' # only 1 up/down allowed atm
else:
elif content_type == HC.CONTENT_TYPE_FILES and action == HC.CONTENT_UPDATE_MOVE and value is not None:
try:
from_name = HG.client_controller.services_manager.GetName( value )
value_string = '(from {})'.format( from_name )
except:
value_string = ''
elif value is not None:
value_string = '"{}"'.format( value )
@ -673,7 +705,14 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
components.append( value_string )
components.append( 'for' )
if content_type == HC.CONTENT_TYPE_FILES:
components.append( 'to' )
else:
components.append( 'for' )
services_manager = HG.client_controller.services_manager

View File

@ -473,6 +473,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'noneable_integers' ][ 'system_busy_cpu_count' ] = 1
self._dictionary[ 'noneable_integers' ][ 'animated_scanbar_hide_height' ] = 5
#
self._dictionary[ 'simple_downloader_formulae' ] = HydrusSerialisable.SerialisableList()
@ -518,6 +520,10 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
#
from hydrus.client import ClientStrings
self._dictionary[ 'last_used_string_conversion_step' ] = ClientStrings.StringConverter( [ ( ClientStrings.STRING_CONVERSION_APPEND_TEXT, 'extra text' ) ] )
self._dictionary[ 'custom_default_predicates' ] = HydrusSerialisable.SerialisableList()
self._dictionary[ 'predicate_types_to_recent_predicates' ] = HydrusSerialisable.SerialisableDictionary()
@ -1189,6 +1195,13 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def GetRawSerialisable( self, name ):
with self._lock:
return self._dictionary[ name ]
def GetRecentPredicates( self, predicate_types ):
with self._lock:
@ -1532,6 +1545,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def SetRawSerialisable( self, name, value ):
with self._lock:
self._dictionary[ name ] = value
def SetSimpleDownloaderFormulae( self, simple_downloader_formulae ):
with self._lock:

View File

@ -2159,7 +2159,7 @@ class ContentParser( HydrusSerialisable.SerialisableBase ):
if do_veto:
raise HydrusExceptions.VetoException( self._name )
raise HydrusExceptions.VetoException( 'veto: {}'.format( self._name ) )
else:

View File

@ -300,6 +300,11 @@ class StringConverter( StringProcessingStep ):
return s
def GetConversions( self ):
return list( self.conversions )
def GetConversionStrings( self ):
return [ self.ConversionToString( conversion ) for conversion in self.conversions ]

View File

@ -1691,14 +1691,31 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
message = 'This clears the cached counts for things like the number of files or tags on a service. Due to unusual situations and little counting bugs, these numbers can sometimes become unsynced. Clearing them forces an accurate recount from source.'
message += os.linesep * 2
message += 'Some GUI elements (review services, mainly) may be slow the next time they launch.'
message += 'Some GUI elements (review services, mainly) may be slow the next time they launch. Especially if you clear for all services.'
result = ClientGUIDialogsQuick.GetYesNo( self, message )
if result == QW.QDialog.Accepted:
self._controller.Write( 'delete_service_info', types_to_delete = types_to_delete )
services = HG.client_controller.services_manager.GetServices()
choice_tuples = [ ( service.GetName(), service.GetServiceKey(), service.GetName() ) for service in services ]
choice_tuples.sort()
choice_tuples.insert( 0, ( 'all services', None, 'Do it for everything. Can take a long time!' ) )
try:
service_key = ClientGUIDialogsQuick.SelectFromListButtons( self, 'Which service?', choice_tuples )
except HydrusExceptions.CancelledException:
return
self._controller.Write( 'delete_service_info', types_to_delete = types_to_delete, service_key = service_key )
@ -1845,6 +1862,21 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
def _FlipMinimiseRestore( self ):
if not self._currently_minimised_to_system_tray:
if self.isMinimized():
self.RestoreOrActivateWindow()
else:
self.showMinimized()
def _FlipShowHideWholeUI( self ):
if not self._currently_minimised_to_system_tray:
@ -3035,7 +3067,7 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes, CAC.ApplicationCo
ClientGUIMenus.AppendSeparator( regen_submenu )
ClientGUIMenus.AppendMenuItem( regen_submenu, 'clear service info cache', 'Delete all cached service info like total number of mappings or files, in case it has become desynchronised. Some parts of the gui may be laggy immediately after this as these numbers are recalculated.', self._DeleteServiceInfo )
ClientGUIMenus.AppendMenuItem( regen_submenu, 'service info numbers', 'Delete all cached service info like total number of mappings or files, in case it has become desynchronised. Some parts of the gui may be laggy immediately after this as these numbers are recalculated.', self._DeleteServiceInfo )
ClientGUIMenus.AppendMenuItem( regen_submenu, 'similar files search tree', 'Delete and recreate the similar files search tree.', self._RegenerateSimilarFilesTree )
ClientGUIMenus.AppendMenu( menu, regen_submenu, 'regenerate' )
@ -6556,6 +6588,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._system_tray_icon.highlight.connect( self.RestoreOrActivateWindow )
self._system_tray_icon.flip_show_ui.connect( self._FlipShowHideWholeUI )
self._system_tray_icon.flip_minimise_ui.connect( self._FlipMinimiseRestore )
self._system_tray_icon.exit_client.connect( self.TryToExit )
self._system_tray_icon.flip_pause_network_jobs.connect( self.FlipNetworkTrafficPaused )
self._system_tray_icon.flip_pause_subscription_jobs.connect( self.FlipSubscriptionsPaused )
@ -6567,6 +6600,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._system_tray_icon.SetShouldAlwaysShow( always_show_system_tray_icon )
self._system_tray_icon.SetUIIsCurrentlyShown( not self._currently_minimised_to_system_tray )
self._system_tray_icon.SetUIIsCurrentlyMinimised( self.isMinimized() )
self._system_tray_icon.SetNetworkTrafficPaused( new_options.GetBoolean( 'pause_all_new_network_traffic' ) )
self._system_tray_icon.SetSubscriptionsPaused( new_options.GetBoolean( 'pause_subs_sync' ) )
@ -6846,6 +6880,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def EventIconize( self, event: QG.QWindowStateChangeEvent ):
if self._have_system_tray_icon:
self._system_tray_icon.SetUIIsCurrentlyMinimised( self.isMinimized() )
if self.isMinimized():
self._was_maximised = event.oldState() & QC.Qt.WindowMaximized

View File

@ -171,7 +171,7 @@ def SelectMultipleFromList( win, title, choice_tuples ):
def SelectServiceKey( service_types = HC.ALL_SERVICES, service_keys = None, unallowed = None ):
def SelectServiceKey( service_types = HC.ALL_SERVICES, service_keys = None, unallowed = None, message = 'select service' ):
if service_keys is None:
@ -205,7 +205,7 @@ def SelectServiceKey( service_types = HC.ALL_SERVICES, service_keys = None, unal
tlw = HG.client_controller.GetMainTLW()
service_key = SelectFromList( tlw, 'select service', choice_tuples )
service_key = SelectFromList( tlw, message, choice_tuples )
return service_key

View File

@ -12,6 +12,7 @@ from hydrus.core import HydrusPaths
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientPaths
from hydrus.client import ClientThreading
@ -593,6 +594,77 @@ def AddKnownURLsViewCopyMenu( win, menu, focus_media, selected_media = None ):
ClientGUIMenus.AppendMenu( menu, urls_menu, 'known urls' )
def AddLocalFilesMoveAddToMenu( win: QW.QWidget, menu: QW.QMenu, local_duplicable_to_file_service_keys: typing.Collection[ bytes ], local_moveable_from_and_to_file_service_keys: typing.Collection[ typing.Tuple[ bytes, bytes ] ], multiple_selected: bool, process_application_command_call ):
if len( local_duplicable_to_file_service_keys ) == 0 and len( local_moveable_from_and_to_file_service_keys ) == 0:
return
local_action_menu = QW.QMenu( menu )
if len( local_duplicable_to_file_service_keys ) > 0:
menu_tuples = []
for s_k in local_duplicable_to_file_service_keys:
application_command = CAC.ApplicationCommand(
command_type = CAC.APPLICATION_COMMAND_TYPE_CONTENT,
data = ( s_k, HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ADD, None )
)
label = HG.client_controller.services_manager.GetName( s_k )
description = 'Duplicate the files to this local file service.'
call = HydrusData.Call( process_application_command_call, application_command )
menu_tuples.append( ( label, description, call ) )
if multiple_selected:
submenu_name = 'add selected to'
else:
submenu_name = 'add to'
ClientGUIMenus.AppendMenuOrItem( local_action_menu, submenu_name, menu_tuples )
if len( local_moveable_from_and_to_file_service_keys ) > 0:
menu_tuples = []
for ( source_s_k, dest_s_k ) in local_moveable_from_and_to_file_service_keys:
application_command = CAC.ApplicationCommand(
command_type = CAC.APPLICATION_COMMAND_TYPE_CONTENT,
data = ( dest_s_k, HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_MOVE, source_s_k )
)
label = 'from {} to {}'.format( HG.client_controller.services_manager.GetName( source_s_k ), HG.client_controller.services_manager.GetName( dest_s_k ) )
description = 'Add the files to the destination and delete from the source.'
call = HydrusData.Call( process_application_command_call, application_command )
menu_tuples.append( ( label, description, call ) )
if multiple_selected:
submenu_name = 'move selected'
else:
submenu_name = 'move'
ClientGUIMenus.AppendMenuOrItem( local_action_menu, submenu_name, menu_tuples )
ClientGUIMenus.AppendMenu( menu, local_action_menu, 'local services' )
def AddManageFileViewingStatsMenu( win: QW.QWidget, menu: QW.QMenu, flat_medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
# add test here for if media actually has stats, edit them, all that
@ -631,57 +703,20 @@ def AddServiceKeyLabelsToMenu( menu, service_keys, phrase ):
ClientGUIMenus.AppendMenu( menu, submenu, phrase + '\u2026' )
def AddServiceKeysToMenu( event_handler, menu, service_keys, phrase, description, call ):
def AddServiceKeysToMenu( menu, service_keys, submenu_name, description, bare_call ):
menu_tuples = []
services_manager = HG.client_controller.services_manager
if len( service_keys ) == 1:
for service_key in service_keys:
( service_key, ) = service_keys
label = services_manager.GetName( service_key )
name = services_manager.GetName( service_key )
this_call = HydrusData.Call( bare_call, service_key )
label = phrase + ' ' + name
ClientGUIMenus.AppendMenuItem( menu, label, description, call, service_key )
else:
submenu = QW.QMenu( menu )
for service_key in service_keys:
name = services_manager.GetName( service_key )
ClientGUIMenus.AppendMenuItem( submenu, name, description, call, service_key )
ClientGUIMenus.AppendMenu( menu, submenu, phrase + '\u2026' )
menu_tuples.append( ( label, description, this_call ) )
def AddDoubleServiceKeysToMenu( event_handler, menu, service_key_pairs, verb, format_phrase, description, call ):
services_manager = HG.client_controller.services_manager
if len( service_key_pairs ) == 1:
( ( service_key_1, service_key_2 ), ) = service_key_pairs
label = verb + ' ' + format_phrase.format( services_manager.GetName( service_key_1 ), services_manager.GetName( service_key_2 ) )
ClientGUIMenus.AppendMenuItem( menu, label, description, call, service_key_1, service_key_2 )
else:
submenu = QW.QMenu( menu )
for ( service_key_1, service_key_2 ) in service_key_pairs:
label = format_phrase.format( services_manager.GetName( service_key_1 ), services_manager.GetName( service_key_2 ) )
ClientGUIMenus.AppendMenuItem( submenu, label, description, call, service_key_1, service_key_2 )
ClientGUIMenus.AppendMenu( menu, submenu, verb + '\u2026' )
ClientGUIMenus.AppendMenuOrItem( menu, submenu_name, menu_tuples )

View File

@ -1,4 +1,5 @@
import collections
import itertools
import typing
from qtpy import QtWidgets as QW
@ -16,7 +17,7 @@ from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientTags
def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.ApplicationCommand, media: typing.Collection[ ClientMedia.Media ] ):
def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.ApplicationCommand, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
if not command.IsContentCommand():
@ -40,76 +41,40 @@ def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.Appl
service_type = service.GetServiceType()
hashes = set()
for m in media:
if service_type == HC.LOCAL_FILE_DOMAIN:
hashes.add( m.GetHash() )
if service_type in HC.REAL_TAG_SERVICES:
tag = value
rows = [ ( tag, hashes ) ]
can_add = False
can_pend = False
can_delete = False
can_petition = True
can_rescind_pend = False
can_rescind_petition = False
for m in media:
if value is not None:
tags_manager = m.GetTagsManager()
source_service_key = value
current = tags_manager.GetCurrent( service_key, ClientTags.TAG_DISPLAY_STORAGE )
pending = tags_manager.GetPending( service_key, ClientTags.TAG_DISPLAY_STORAGE )
petitioned = tags_manager.GetPetitioned( service_key, ClientTags.TAG_DISPLAY_STORAGE )
else:
if tag not in current:
can_add = True
if tag not in current and tag not in pending:
can_pend = True
if tag in current and action == HC.CONTENT_UPDATE_FLIP:
can_delete = True
if tag in current and tag not in petitioned and action == HC.CONTENT_UPDATE_FLIP:
can_petition = True
if tag in pending and action == HC.CONTENT_UPDATE_FLIP:
can_rescind_pend = True
if tag in petitioned:
can_rescind_petition = True
source_service_key = None
reason = None
MoveOrDuplicateLocalFiles( parent, service_key, action, media, source_service_key = source_service_key )
if service_type == HC.LOCAL_TAG:
else:
if service_type in HC.REAL_TAG_SERVICES:
if can_add:
tag = value
content_updates = GetContentUpdatesForAppliedContentApplicationCommandTags( parent, service_key, service_type, action, media, tag )
elif service_type in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ):
if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
content_update_action = HC.CONTENT_UPDATE_ADD
rating = value
elif can_delete:
content_updates = GetContentUpdatesForAppliedContentApplicationCommandRatingsSetFlip( service_key, action, media, rating )
content_update_action = HC.CONTENT_UPDATE_DELETE
elif action in ( HC.CONTENT_UPDATE_INCREMENT, HC.CONTENT_UPDATE_DECREMENT ) and service_type == HC.LOCAL_RATING_NUMERICAL:
one_star_value = service.GetOneStarValue()
content_updates = GetContentUpdatesForAppliedContentApplicationCommandRatingsIncDec( service_key, one_star_value, action, media )
else:
@ -118,149 +83,18 @@ def ApplyContentApplicationCommandToMedia( parent: QW.QWidget, command: CAC.Appl
else:
if can_rescind_petition:
content_update_action = HC.CONTENT_UPDATE_RESCIND_PETITION
elif can_pend:
content_update_action = HC.CONTENT_UPDATE_PEND
elif can_rescind_pend:
content_update_action = HC.CONTENT_UPDATE_RESCIND_PEND
elif can_petition:
message = 'Enter a reason for this tag to be removed. A janitor will review your petition.'
from hydrus.client.gui import ClientGUIDialogs
with ClientGUIDialogs.DialogTextEntry( parent, message ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
content_update_action = HC.CONTENT_UPDATE_PETITION
reason = dlg.GetValue()
else:
return True
else:
return True
return False
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_update_action, row, reason = reason ) for row in rows ]
elif service_type in ( HC.LOCAL_RATING_LIKE, HC.LOCAL_RATING_NUMERICAL ):
if action in ( HC.CONTENT_UPDATE_SET, HC.CONTENT_UPDATE_FLIP ):
if len( content_updates ) > 0:
rating = value
HG.client_controller.Write( 'content_updates', { service_key : content_updates } )
can_set = False
can_unset = False
for m in media:
ratings_manager = m.GetRatingsManager()
current_rating = ratings_manager.GetRating( service_key )
if current_rating == rating and action == HC.CONTENT_UPDATE_FLIP:
can_unset = True
else:
can_set = True
if can_set:
row = ( rating, hashes )
elif can_unset:
row = ( None, hashes )
else:
return True
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, row ) ]
elif action in ( HC.CONTENT_UPDATE_INCREMENT, HC.CONTENT_UPDATE_DECREMENT ):
if service_type == HC.LOCAL_RATING_NUMERICAL:
if action == HC.CONTENT_UPDATE_INCREMENT:
direction = 1
initialisation_rating = 0.0
elif action == HC.CONTENT_UPDATE_DECREMENT:
direction = -1
initialisation_rating = 1.0
one_star_value = service.GetOneStarValue()
ratings_to_hashes = collections.defaultdict( set )
for m in media:
ratings_manager = m.GetRatingsManager()
current_rating = ratings_manager.GetRating( service_key )
if current_rating is None:
new_rating = initialisation_rating
else:
new_rating = current_rating + ( one_star_value * direction )
new_rating = max( min( new_rating, 1.0 ), 0.0 )
if current_rating != new_rating:
ratings_to_hashes[ new_rating ].add( m.GetHash() )
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) ) for ( rating, hashes ) in ratings_to_hashes.items() ]
else:
return True
else:
return False
if len( content_updates ) > 0:
HG.client_controller.Write( 'content_updates', { service_key : content_updates } )
return True
def EditFileNotes( win: QW.QWidget, media: ClientMedia.Media, name_to_start_on = typing.Optional[ str ] ):
def EditFileNotes( win: QW.QWidget, media: ClientMedia.MediaSingleton, name_to_start_on = typing.Optional[ str ] ):
names_to_notes = media.GetNotesManager().GetNamesToNotes()
@ -287,6 +121,397 @@ def EditFileNotes( win: QW.QWidget, media: ClientMedia.Media, name_to_start_on =
def GetContentUpdatesForAppliedContentApplicationCommandRatingsSetFlip( service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], rating: typing.Optional[ float ] ):
hashes = set()
for m in media:
hashes.add( m.GetHash() )
can_set = False
can_unset = False
for m in media:
ratings_manager = m.GetRatingsManager()
current_rating = ratings_manager.GetRating( service_key )
if current_rating == rating and action == HC.CONTENT_UPDATE_FLIP:
can_unset = True
else:
can_set = True
if can_set:
row = ( rating, hashes )
elif can_unset:
row = ( None, hashes )
else:
return []
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, row ) ]
return content_updates
def GetContentUpdatesForAppliedContentApplicationCommandRatingsIncDec( service_key: bytes, one_star_value: float, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ] ):
if action == HC.CONTENT_UPDATE_INCREMENT:
direction = 1
initialisation_rating = 0.0
elif action == HC.CONTENT_UPDATE_DECREMENT:
direction = -1
initialisation_rating = 1.0
else:
return []
ratings_to_hashes = collections.defaultdict( set )
for m in media:
ratings_manager = m.GetRatingsManager()
current_rating = ratings_manager.GetRating( service_key )
if current_rating is None:
new_rating = initialisation_rating
else:
new_rating = current_rating + ( one_star_value * direction )
new_rating = max( min( new_rating, 1.0 ), 0.0 )
if current_rating != new_rating:
ratings_to_hashes[ new_rating ].add( m.GetHash() )
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_RATINGS, HC.CONTENT_UPDATE_ADD, ( rating, hashes ) ) for ( rating, hashes ) in ratings_to_hashes.items() ]
return content_updates
def GetContentUpdatesForAppliedContentApplicationCommandTags( parent: QW.QWidget, service_key: bytes, service_type: int, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], tag: str ):
hashes = set()
for m in media:
hashes.add( m.GetHash() )
rows = [ ( tag, hashes ) ]
can_add = False
can_pend = False
can_delete = False
can_petition = True
can_rescind_pend = False
can_rescind_petition = False
for m in media:
tags_manager = m.GetTagsManager()
current = tags_manager.GetCurrent( service_key, ClientTags.TAG_DISPLAY_STORAGE )
pending = tags_manager.GetPending( service_key, ClientTags.TAG_DISPLAY_STORAGE )
petitioned = tags_manager.GetPetitioned( service_key, ClientTags.TAG_DISPLAY_STORAGE )
if tag not in current:
can_add = True
if tag not in current and tag not in pending:
can_pend = True
if tag in current and action == HC.CONTENT_UPDATE_FLIP:
can_delete = True
if tag in current and tag not in petitioned and action == HC.CONTENT_UPDATE_FLIP:
can_petition = True
if tag in pending and action == HC.CONTENT_UPDATE_FLIP:
can_rescind_pend = True
if tag in petitioned:
can_rescind_petition = True
reason = None
if service_type == HC.LOCAL_TAG:
if can_add:
content_update_action = HC.CONTENT_UPDATE_ADD
elif can_delete:
content_update_action = HC.CONTENT_UPDATE_DELETE
else:
return []
else:
if can_rescind_petition:
content_update_action = HC.CONTENT_UPDATE_RESCIND_PETITION
elif can_pend:
content_update_action = HC.CONTENT_UPDATE_PEND
elif can_rescind_pend:
content_update_action = HC.CONTENT_UPDATE_RESCIND_PEND
elif can_petition:
message = 'Enter a reason for this tag to be removed. A janitor will review your petition.'
from hydrus.client.gui import ClientGUIDialogs
with ClientGUIDialogs.DialogTextEntry( parent, message ) as dlg:
if dlg.exec() == QW.QDialog.Accepted:
content_update_action = HC.CONTENT_UPDATE_PETITION
reason = dlg.GetValue()
else:
return []
else:
return []
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_update_action, row, reason = reason ) for row in rows ]
return content_updates
def GetLocalFileActionServiceKeys( media: typing.Collection[ ClientMedia.MediaSingleton ] ):
local_media_file_service_keys = set( HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) ) )
local_duplicable_to_file_service_keys = set()
local_moveable_from_and_to_file_service_keys = set()
for m in media:
locations_manager = m.GetLocationsManager()
current = locations_manager.GetCurrent()
if locations_manager.IsLocal():
can_send_to = local_media_file_service_keys.difference( current )
can_send_from = local_media_file_service_keys.intersection( current )
if len( can_send_to ) > 0:
local_duplicable_to_file_service_keys.update( can_send_to )
if len( can_send_from ) > 0:
# can_send_from does not include trash. we won't say 'move from trash to blah' since that's a little complex. we'll just say 'add to blah' in that case I think
local_moveable_from_and_to_file_service_keys.update( list( itertools.product( can_send_from, can_send_to ) ) )
return ( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys )
def MoveOrDuplicateLocalFiles( parent: QW.QWidget, dest_service_key: bytes, action: int, media: typing.Collection[ ClientMedia.MediaSingleton ], source_service_key: typing.Optional[ bytes ] = None ):
dest_service_name = HG.client_controller.services_manager.GetName( dest_service_key )
applicable_media = [ m for m in media if m.GetLocationsManager().IsLocal() and dest_service_key not in m.GetLocationsManager().GetCurrent() and m.GetMime() not in HC.HYDRUS_UPDATE_FILES ]
if len( applicable_media ) == 0:
return
( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = GetLocalFileActionServiceKeys( media )
do_yes_no = True
yes_no_text = 'Add {} files to {}?'.format( HydrusData.ToHumanInt( len( applicable_media ) ), dest_service_name )
if action == HC.CONTENT_UPDATE_MOVE:
local_moveable_from_and_to_file_service_keys = { pair for pair in local_moveable_from_and_to_file_service_keys if pair[1] == dest_service_key }
potential_source_service_keys = { pair[0] for pair in local_moveable_from_and_to_file_service_keys }
potential_source_service_keys_to_applicable_media = collections.defaultdict( list )
for m in applicable_media:
current = m.GetLocationsManager().GetCurrent()
for potential_source_service_key in potential_source_service_keys:
if potential_source_service_key in current:
potential_source_service_keys_to_applicable_media[ potential_source_service_key ].append( m )
if source_service_key is None:
if len( potential_source_service_keys ) == 0:
return
elif len( potential_source_service_keys ) == 1:
( source_service_key, ) = potential_source_service_keys
else:
do_yes_no = False
num_applicable_media = len( applicable_media )
choice_tuples = []
for potential_source_service_key in potential_source_service_keys:
potential_source_service_name = HG.client_controller.services_manager.GetName( potential_source_service_key )
text = 'move {} in "{}" to "{}"'.format( len( potential_source_service_keys_to_applicable_media[ potential_source_service_key ] ), potential_source_service_name, dest_service_name )
description = 'Move from {} to {}.'.format( potential_source_service_name, dest_service_name )
choice_tuples.append( ( text, potential_source_service_key, description ) )
choice_tuples.sort()
try:
source_service_key = ClientGUIDialogsQuick.SelectFromListButtons( parent, 'select source service', choice_tuples, message = 'Select where we are moving from. Note this may not cover all files.' )
except HydrusExceptions.CancelledException:
return
source_service_name = HG.client_controller.services_manager.GetName( source_service_key )
applicable_media = potential_source_service_keys_to_applicable_media[ source_service_key ]
yes_no_text = 'Move {} files from {} to {}?'.format( HydrusData.ToHumanInt( len( applicable_media ) ), source_service_name, dest_service_name )
if len( applicable_media ) == 0:
return
if do_yes_no:
result = ClientGUIDialogsQuick.GetYesNo( parent, yes_no_text )
if result != QW.QDialog.Accepted:
return
now = HydrusData.GetNow()
# make this async with a popup and pausable/cancellable, with the interleaved fix so cancel works
for block_of_media in HydrusData.SplitListIntoChunks( applicable_media, 64 ):
content_updates = []
undelete_hashes = set()
for m in block_of_media:
if dest_service_key in m.GetLocationsManager().GetDeleted():
undelete_hashes.add( m.GetHash() )
else:
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ADD, ( m.GetMediaResult().GetFileInfoManager(), now ) ) )
if len( undelete_hashes ) > 0:
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, undelete_hashes ) )
HG.client_controller.Write( 'content_updates', { dest_service_key : content_updates } )
if action == HC.CONTENT_UPDATE_MOVE:
# interleave this into above
for block_of_media in HydrusData.SplitListIntoChunks( applicable_media, 64 ):
block_of_hashes = [ m.GetHash() for m in block_of_media ]
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, block_of_hashes, reason = 'Moved to {}'.format( dest_service_name ) ) ]
HG.client_controller.Write( 'content_updates', { source_service_key : content_updates } )
def UndeleteFiles( hashes ):
local_file_service_keys = HG.client_controller.services_manager.GetServiceKeys( ( HC.LOCAL_FILE_DOMAIN, ) )
@ -324,6 +549,7 @@ def UndeleteFiles( hashes ):
def UndeleteMedia( win, media ):
media_deleted_service_keys = HydrusData.MassUnion( ( m.GetLocationsManager().GetDeleted() for m in media ) )

View File

@ -130,6 +130,42 @@ def AppendMenuLabel( menu, label, description = '' ):
return menu_item
def AppendMenuOrItem( menu, submenu_name, menu_tuples, sort_tuples = True ):
if sort_tuples:
try:
menu_tuples = sorted( menu_tuples )
except:
pass
if len( menu_tuples ) == 1:
submenu = menu
item_prefix = '{} '.format( submenu_name )
else:
submenu = QW.QMenu( menu )
AppendMenu( menu, submenu, submenu_name )
item_prefix = ''
for ( label, description, call ) in menu_tuples:
label = '{}{}'.format( item_prefix, label )
AppendMenuItem( submenu, label, description, call )
def AppendSeparator( menu ):
num_items = len( menu.actions() )

View File

@ -1083,6 +1083,8 @@ class EditHTMLTagRulePanel( ClientGUIScrolledPanels.EditPanel ):
self._should_test_tag_string.clicked.connect( self.EventShouldTestChanged )
ClientGUIFunctions.SetFocusLater( self._tag_name )
def _UpdateShouldTest( self ):

View File

@ -599,9 +599,10 @@ class EditDeleteFilesPanel( ClientGUIScrolledPanels.EditPanel ):
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 ) )
if HG.client_controller.new_options.GetBoolean( 'use_advanced_file_deletion_dialog' ):
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 ) )

View File

@ -2001,6 +2001,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._mpv_conf_path = QP.FilePickerCtrl( self, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( self, min=1, max=255 )
self._animated_scanbar_hide_height = ClientGUICommon.NoneableSpinCtrl( self, none_phrase = 'no, hide it', min = 1, max = 255, unit = 'px' )
self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( self, min=1, max=63 )
self._media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer filetype handling' )
@ -2028,6 +2029,9 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._animated_scanbar_height.setValue( self._new_options.GetInteger( 'animated_scanbar_height' ) )
self._animated_scanbar_nub_width.setValue( self._new_options.GetInteger( 'animated_scanbar_nub_width' ) )
self._animated_scanbar_hide_height.SetValue( 5 )
self._animated_scanbar_hide_height.SetValue( self._new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) )
self._media_viewer_zoom_center.SetValue( self._new_options.GetInteger( 'media_viewer_zoom_center' ) )
media_zooms = self._new_options.GetMediaZooms()
@ -2067,6 +2071,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Media zooms:', self._media_zooms ) )
rows.append( ( 'Set a new mpv.conf on dialog ok?:', self._mpv_conf_path ) )
rows.append( ( 'Animation scanbar height:', self._animated_scanbar_height ) )
rows.append( ( 'Animation scanbar height when mouse away:', self._animated_scanbar_hide_height ) )
rows.append( ( 'Animation scanbar nub width:', self._animated_scanbar_nub_width ) )
rows.append( ( 'Time until mouse cursor autohides on media viewer:', self._media_viewer_cursor_autohide_time_ms ) )
rows.append( ( 'RECOMMEND WINDOWS ONLY: Hide and anchor mouse cursor on media viewer drags:', self._anchor_and_hide_canvas_drags ) )
@ -2315,6 +2320,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetInteger( 'animated_scanbar_height', self._animated_scanbar_height.value() )
self._new_options.SetInteger( 'animated_scanbar_nub_width', self._animated_scanbar_nub_width.value() )
self._new_options.SetNoneableInteger( 'animated_scanbar_hide_height', self._animated_scanbar_hide_height.GetValue() )
self._new_options.SetInteger( 'media_viewer_zoom_center', self._media_viewer_zoom_center.GetValue() )
try:

View File

@ -7,6 +7,7 @@ from qtpy import QtWidgets as QW
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientParsing
@ -362,8 +363,22 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
def _AddConversion( self ):
conversion_type = ClientStrings.STRING_CONVERSION_APPEND_TEXT
data = 'extra text'
try:
default_string_converter = HG.client_controller.new_options.GetRawSerialisable( 'last_used_string_conversion_step' )
default_conversions = default_string_converter.GetConversions()
if len( default_conversions ) > 0:
( conversion_type, data ) = default_conversions[0]
except:
conversion_type = ClientStrings.STRING_CONVERSION_APPEND_TEXT
data = 'extra text'
try:
@ -388,6 +403,10 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
( conversion_type, data ) = panel.GetValue()
new_default_string_converter = ClientStrings.StringConverter( conversions = [ ( conversion_type, data ) ] )
HG.client_controller.new_options.SetRawSerialisable( 'last_used_string_conversion_step', new_default_string_converter )
enumerated_conversion = ( number, conversion_type, data )
self._conversions.AddDatas( ( enumerated_conversion, ) )
@ -542,6 +561,10 @@ class EditStringConverterPanel( ClientGUIScrolledPanels.EditPanel ):
( conversion_type, data ) = panel.GetValue()
new_default_string_converter = ClientStrings.StringConverter( conversions = [ ( conversion_type, data ) ] )
HG.client_controller.new_options.SetRawSerialisable( 'last_used_string_conversion_step', new_default_string_converter )
enumerated_conversion = ( number, conversion_type, data )
self._conversions.AddDatas( ( enumerated_conversion, ) )

View File

@ -18,6 +18,7 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
flip_pause_network_jobs = QC.Signal()
flip_pause_subscription_jobs = QC.Signal()
highlight = QC.Signal()
flip_minimise_ui = QC.Signal()
exit_client = QC.Signal()
def __init__( self, parent: QW.QWidget ):
@ -25,6 +26,7 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
QW.QSystemTrayIcon.__init__( self, parent )
self._ui_is_currently_shown = True
self._ui_is_currently_minimised = False
self._should_always_show = False
self._network_traffic_paused = False
self._subscriptions_paused = False
@ -60,6 +62,8 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
self._show_hide_menu_item = ClientGUIMenus.AppendMenuItem( new_menu, 'show/hide', 'Hide or show the hydrus client', self.flip_show_ui.emit )
self._minimise_restore_menu_item = ClientGUIMenus.AppendMenuItem( new_menu, 'restore/minimise', 'Restore or minimise the hydrus client window', self.flip_minimise_ui.emit )
self._UpdateShowHideMenuItemLabel()
ClientGUIMenus.AppendSeparator( new_menu )
@ -97,12 +101,25 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
self._network_traffic_menu_item.setText( label )
def _UpdateRestoreMinimiseMenuItemLabel( self ):
label = 'restore' if self._ui_is_currently_minimised else 'minimise'
self._minimise_restore_menu_item.setText( label )
show_it = self._ui_is_currently_shown and not HG.client_controller.new_options.GetBoolean( 'minimise_client_to_system_tray' )
self._minimise_restore_menu_item.setVisible( show_it )
def _UpdateShowHideMenuItemLabel( self ):
label = 'hide' if self._ui_is_currently_shown else 'show'
self._show_hide_menu_item.setText( label )
self._UpdateRestoreMinimiseMenuItemLabel()
def _UpdateShowSelf( self ) -> bool:
@ -216,6 +233,16 @@ class ClientSystemTrayIcon( QW.QSystemTrayIcon ):
def SetUIIsCurrentlyMinimised( self, ui_is_currently_minimised: bool ):
if ui_is_currently_minimised != self._ui_is_currently_minimised:
self._ui_is_currently_minimised = ui_is_currently_minimised
self._UpdateRestoreMinimiseMenuItemLabel()
def SetUIIsCurrentlyShown( self, ui_is_currently_shown: bool ):
if ui_is_currently_shown != self._ui_is_currently_shown:

View File

@ -156,12 +156,12 @@ def CalculateCanvasMediaSize( media, canvas_size: QC.QSize, show_action ):
canvas_width = canvas_size.width()
canvas_height = canvas_size.height()
if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
animated_scanbar_height = 0
canvas_height -= animated_scanbar_height
canvas_height -= animated_scanbar_height
'''
canvas_width = max( canvas_width, 80 )
canvas_height = max( canvas_height, 60 )
@ -313,12 +313,12 @@ def CalculateMediaContainerSize( media, zoom, show_action ):
( media_width, media_height ) = CalculateMediaSize( media, zoom )
if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
'''if ClientGUICanvasMedia.ShouldHaveAnimationBar( media, show_action ):
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
animated_scanbar_height = 0
media_height += animated_scanbar_height
media_height += animated_scanbar_height
'''
return QC.QSize( media_width, media_height )
@ -1078,6 +1078,10 @@ class Canvas( QW.QWidget, CAC.ApplicationCommandProcessorMixin ):
self._last_drag_pos = None
# this forces a resize if needed, which may have been missed in natural event handling if the window is bigger than its parent and counts as out of view in a weird way
# this fixes the 'going from borderless to regular window on a 16:9 video on a perfect 16:9 screen doesn't scale it down until I pan it' bug
self._DrawCurrentMedia()
def _SaveCurrentMediaViewTime( self ):
@ -3101,7 +3105,13 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def _GetNumRemainingDecisions( self ):
return len( self._batch_of_pairs_to_process ) - self._current_pair_index
# this looks a little weird, but I want to be clear that we make a decision on the final index
last_decision_index = len( self._batch_of_pairs_to_process ) - 1
number_of_decisions_after_the_current = last_decision_index - self._current_pair_index
return 1 + number_of_decisions_after_the_current
def _GoBack( self ):
@ -3435,9 +3445,7 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
self._processed_pairs.append( ( self._batch_of_pairs_to_process[ self._current_pair_index ], None, None, None, {}, was_auto_skipped ) )
self._current_pair_index += 1
if self._GetNumRemainingDecisions() == 0:
if self._GetNumRemainingDecisions() == 1: # we are at the end of the queue, this decision we just appended is the last
if self._GetNumCommittableDecisions() == 0:
@ -3456,6 +3464,10 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
return
else:
self._current_pair_index += 1
self._ShowCurrentPair()
@ -4697,6 +4709,12 @@ class CanvasMediaListBrowser( CanvasMediaListNavigable ):
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaActions.GetLocalFileActionServiceKeys( ( self._current_media, ) )
multiple_selected = False
ClientGUIMedia.AddLocalFilesMoveAddToMenu( self, menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
ClientGUIMedia.AddKnownURLsViewCopyMenu( self, menu, self._current_media )
open_menu = QW.QMenu( menu )

View File

@ -867,11 +867,6 @@ class CanvasHoverFrameTop( CanvasHoverFrame ):
event.ignore()
def wheelEvent( self, event ):
QW.QApplication.sendEvent( self.parentWidget(), event )
def ProcessContentUpdates( self, service_keys_to_content_updates ):
if self._current_media is not None:
@ -1244,11 +1239,6 @@ class CanvasHoverFrameTopRight( CanvasHoverFrame ):
self._SizeAndPosition()
def wheelEvent( self, event ):
QW.QApplication.sendEvent( self.parentWidget(), event )
def ProcessContentUpdates( self, service_keys_to_content_updates ):
if self._current_media is not None:
@ -1510,11 +1500,6 @@ class CanvasHoverFrameRightNotes( CanvasHoverFrame ):
return CanvasHoverFrame._ShouldBeHidden( self )
def wheelEvent( self, event ):
QW.QApplication.sendEvent( self.parentWidget(), event )
def ProcessContentUpdates( self, service_keys_to_content_updates ):
if self._current_media is not None:
@ -1840,11 +1825,6 @@ class CanvasHoverFrameRightDuplicates( CanvasHoverFrame ):
def wheelEvent( self, event ):
QW.QApplication.sendEvent( self.parentWidget(), event )
def SetDuplicatePair( self, canvas_key, shown_media, comparison_media ):
if canvas_key == self._canvas_key:
@ -1959,3 +1939,18 @@ class CanvasHoverFrameTags( CanvasHoverFrame ):
def wheelEvent( self, event ):
# need the mouse test here since some weird event passing happens on mouse events on other stuff, I think because this hover is child 0 of the parent, it somehow gets 'focus'
if self.rect().contains( self.mapFromGlobal( QG.QCursor.pos() ) ):
# we do not want to send taglist wheel events up to the canvas lad
event.accept()
return
CanvasHoverFrame.wheelEvent( self, event )

View File

@ -586,6 +586,8 @@ class AnimationBar( QW.QWidget ):
self._num_frames = 1
self._last_drawn_info = None
self._show_text = True
self._currently_in_a_drag = False
self._it_was_playing_before_drag = False
@ -670,7 +672,7 @@ class AnimationBar( QW.QWidget ):
#
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
my_height = self.height()
if buffer_indices is not None:
@ -693,13 +695,13 @@ class AnimationBar( QW.QWidget ):
if rendered_to_x > start_x:
painter.drawRect( start_x, 0, rendered_to_x - start_x, animated_scanbar_height )
painter.drawRect( start_x, 0, rendered_to_x - start_x, my_height )
else:
painter.drawRect( start_x, 0, my_width - start_x, animated_scanbar_height )
painter.drawRect( start_x, 0, my_width - start_x, my_height )
painter.drawRect( 0, 0, rendered_to_x, animated_scanbar_height )
painter.drawRect( 0, 0, rendered_to_x, my_height )
@ -711,13 +713,13 @@ class AnimationBar( QW.QWidget ):
if end_x > rendered_to_x:
painter.drawRect( rendered_to_x, 0, end_x - rendered_to_x, animated_scanbar_height )
painter.drawRect( rendered_to_x, 0, end_x - rendered_to_x, my_height )
else:
painter.drawRect( rendered_to_x, 0, my_width - rendered_to_x, animated_scanbar_height )
painter.drawRect( rendered_to_x, 0, my_width - rendered_to_x, my_height )
painter.drawRect( 0, 0, end_x, animated_scanbar_height )
painter.drawRect( 0, 0, end_x, my_height )
@ -741,35 +743,38 @@ class AnimationBar( QW.QWidget ):
if nub_x is not None:
painter.drawRect( nub_x, 0, animated_scanbar_nub_width, animated_scanbar_height )
painter.drawRect( nub_x, 0, animated_scanbar_nub_width, my_height )
#
painter.setPen( QG.QPen() )
progress_strings = []
if num_frames_are_useful:
if self._show_text:
progress_strings.append( HydrusData.ConvertValueRangeToPrettyString( current_frame_index + 1, self._num_frames ) )
painter.setPen( QG.QPen() )
if current_timestamp_ms is not None:
progress_strings = []
progress_strings.append( HydrusData.ConvertValueRangeToScanbarTimestampsMS( current_timestamp_ms, self._duration_ms ) )
if num_frames_are_useful:
progress_strings.append( HydrusData.ConvertValueRangeToPrettyString( current_frame_index + 1, self._num_frames ) )
s = ' - '.join( progress_strings )
if len( s ) > 0:
if current_timestamp_ms is not None:
progress_strings.append( HydrusData.ConvertValueRangeToScanbarTimestampsMS( current_timestamp_ms, self._duration_ms ) )
( text_size, s ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, s )
s = ' - '.join( progress_strings )
x = my_width - text_size.width() - 3
y = ( my_height - text_size.height() ) / 2
ClientGUIFunctions.DrawText( painter, x, y, s )
if len( s ) > 0:
( text_size, s ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, s )
x = my_width - text_size.width() - 3
y = ( my_height - text_size.height() ) / 2
ClientGUIFunctions.DrawText( painter, x, y, s )
#
@ -927,6 +932,11 @@ class AnimationBar( QW.QWidget ):
self.update()
def SetShowText( self, show_text: bool ):
self._show_text = show_text
def TIMERAnimationUpdate( self ):
if self._CurrentMediaWindowIsBad():
@ -1026,6 +1036,7 @@ class MediaContainer( QW.QWidget ):
self._static_image_window.readyForNeighbourPrefetch.connect( self.readyForNeighbourPrefetch )
self._controls_bar = QW.QWidget( self )
self._controls_bar_show_full = True
# We need this to force-fill some blanks at times
self.setAutoFillBackground( True )
@ -1224,7 +1235,7 @@ class MediaContainer( QW.QWidget ):
self._media_window.move( QC.QPoint( 0, 0 ) )
controls_bar_rect = self.GetIdealControlsBarRect()
controls_bar_rect = self.GetIdealControlsBarRect( full_size = self._controls_bar_show_full )
self._controls_bar.setFixedSize( controls_bar_rect.size() )
self._controls_bar.move( controls_bar_rect.topLeft() )
@ -1275,14 +1286,26 @@ class MediaContainer( QW.QWidget ):
def GetIdealControlsBarRect( self ):
def GetIdealControlsBarRect( self, full_size = True ):
my_size = self.size()
my_width = my_size.width()
my_height = my_size.height()
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
if full_size:
animated_scanbar_height = HG.client_controller.new_options.GetInteger( 'animated_scanbar_height' )
else:
animated_scanbar_height = HG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' )
if animated_scanbar_height is None:
animated_scanbar_height = 5
return QC.QRect(
QC.QPoint( 0, my_height - animated_scanbar_height ),
@ -1491,7 +1514,7 @@ class MediaContainer( QW.QWidget ):
return False
return isinstance( self._media_window, ClientGUIMPV.mpvWidget ) and self._media.HasAudio()
return isinstance( self._media_window, ClientGUIMPV.mpvWidget ) and self._media.HasAudio() and self._controls_bar_show_full
def StopForSlideshow( self, value ):
@ -1504,19 +1527,36 @@ class MediaContainer( QW.QWidget ):
def TIMERUIUpdate( self ):
is_near = False
show_small_instead_of_hiding = None
force_show = False
if not ShouldHaveAnimationBar( self._media, self._show_action ):
should_show_controls = False
else:
my_window = self.window()
is_near = self.MouseIsNearAnimationBar()
show_small_instead_of_hiding = HG.client_controller.new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) is not None
force_show = self._volume_control.PopupIsVisible() or self._animation_bar.DoingADrag() or HG.client_controller.new_options.GetBoolean( 'force_animation_scanbar_show' )
should_show_controls = self.MouseIsNearAnimationBar() or self._volume_control.PopupIsVisible() or self._animation_bar.DoingADrag() or HG.client_controller.new_options.GetBoolean( 'force_animation_scanbar_show' )
should_show_controls = is_near or show_small_instead_of_hiding or force_show
if should_show_controls:
should_show_full = is_near or force_show
if should_show_full != self._controls_bar_show_full:
self._controls_bar_show_full = should_show_full
self._animation_bar.SetShowText( self._controls_bar_show_full )
self._SizeAndPositionChildren()
if not self._controls_bar.isVisible():
self._animation_bar.SetMediaAndWindow( self._media, self._media_window )

View File

@ -604,7 +604,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
if file_service_key is None:
if len( self._location_context.current_service_keys ) == 1:
( possible_suggested_file_service_key, ) = self._location_context.current_service_keys
if HG.client_controller.services_manager.GetServiceType( possible_suggested_file_service_key ) in HC.SPECIFIC_LOCAL_FILE_SERVICES + ( HC.FILE_REPOSITORY, ):
@ -727,74 +727,6 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
def _LaunchMediaViewer( self, first_media = None ):
if self._HasFocusSingleton():
media = self._GetFocusSingleton()
new_options = HG.client_controller.new_options
( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( media.GetMime() )
if media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY:
hash = media.GetHash()
mime = media.GetMime()
client_files_manager = HG.client_controller.client_files_manager
path = client_files_manager.GetFilePath( hash, mime )
new_options = HG.client_controller.new_options
launch_path = new_options.GetMimeLaunch( mime )
HydrusPaths.LaunchFile( path, launch_path )
return
elif media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW:
return
media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, for_media_viewer = True )
if len( media_results ) > 0:
if first_media is None and self._focused_media is not None:
first_media = self._focused_media
if first_media is not None:
first_media = first_media.GetDisplayMedia()
if first_media is not None and first_media.GetLocationsManager().IsLocal():
first_hash = first_media.GetHash()
else:
first_hash = None
self.SetFocusedMedia( None )
canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() )
canvas_window = ClientGUICanvas.CanvasMediaListBrowser( canvas_frame, self._page_key, self._location_context, media_results, first_hash )
canvas_frame.SetCanvas( canvas_window )
canvas_window.exitFocusMedia.connect( self.SetFocusedMedia )
def _GetFocusSingleton( self ) -> ClientMedia.MediaSingleton:
if self._focused_media is not None:
@ -1273,81 +1205,72 @@ class MediaPanel( ClientMedia.ListeningMediaList, QW.QScrollArea, CAC.Applicatio
def _DOLocalDuplicateFiles( self, service_key, flat_media ):
def _LaunchMediaViewer( self, first_media = None ):
now = HydrusData.GetNow()
for block_of_flat_media in HydrusData.SplitListIntoChunks( flat_media, 64 ):
if self._HasFocusSingleton():
content_updates = []
undelete_hashes = set()
media = self._GetFocusSingleton()
for m in block_of_flat_media:
new_options = HG.client_controller.new_options
( media_show_action, media_start_paused, media_start_with_embed ) = new_options.GetMediaShowAction( media.GetMime() )
if media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW_ON_ACTIVATION_OPEN_EXTERNALLY:
if service_key in m.GetLocationsManager().GetDeleted():
undelete_hashes.add( m.GetHash() )
else:
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_ADD, ( m.GetMediaResult().GetFileInfoManager(), now ) ) )
hash = media.GetHash()
mime = media.GetMime()
if len( undelete_hashes ) > 0:
client_files_manager = HG.client_controller.client_files_manager
content_updates.append( HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_UNDELETE, undelete_hashes ) )
path = client_files_manager.GetFilePath( hash, mime )
HG.client_controller.Write( 'content_updates', { service_key : content_updates } )
def _LocalDuplicateFiles( self, service_key ):
flat_media = self._GetSelectedFlatMedia( is_not_in_file_service_key = service_key )
if len( flat_media ) > 0:
text = 'Add {} files to {}?'.format( HydrusData.ToHumanInt( len( flat_media ) ), HG.client_controller.services_manager.GetName( service_key ) )
result = ClientGUIDialogsQuick.GetYesNo( self, text )
if result != QW.QDialog.Accepted:
new_options = HG.client_controller.new_options
launch_path = new_options.GetMimeLaunch( mime )
HydrusPaths.LaunchFile( path, launch_path )
return
elif media_show_action == CC.MEDIA_VIEWER_ACTION_DO_NOT_SHOW:
return
self._DOLocalDuplicateFiles( service_key, flat_media )
def _LocalMoveFiles( self, service_key_from, service_key_to ):
media_results = self.GenerateMediaResults( discriminant = CC.DISCRIMINANT_LOCAL, for_media_viewer = True )
flat_media = self._GetSelectedFlatMedia( is_in_file_service_key = service_key_from, is_not_in_file_service_key = service_key_to )
if len( flat_media ) > 0:
if len( media_results ) > 0:
text = 'Move {} files from {} to {}?'.format( HydrusData.ToHumanInt( len( flat_media ) ), HG.client_controller.services_manager.GetName( service_key_from ), HG.client_controller.services_manager.GetName( service_key_to ) )
result = ClientGUIDialogsQuick.GetYesNo( self, text )
if result != QW.QDialog.Accepted:
if first_media is None and self._focused_media is not None:
return
first_media = self._focused_media
self._DOLocalDuplicateFiles( service_key_to, flat_media )
if first_media is not None:
first_media = first_media.GetDisplayMedia()
for block_of_flat_media in HydrusData.SplitListIntoChunks( flat_media, 64 ):
if first_media is not None and first_media.GetLocationsManager().IsLocal():
block_of_hashes = [ m.GetHash() for m in block_of_flat_media ]
first_hash = first_media.GetHash()
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_FILES, HC.CONTENT_UPDATE_DELETE, block_of_hashes, reason = 'Moved to {}'.format( HG.client_controller.services_manager.GetName( service_key_to ) ) ) ]
else:
HG.client_controller.Write( 'content_updates', { service_key_from : content_updates } )
first_hash = None
self.SetFocusedMedia( None )
canvas_frame = ClientGUICanvasFrame.CanvasFrame( self.window() )
canvas_window = ClientGUICanvas.CanvasMediaListBrowser( canvas_frame, self._page_key, self._location_context, media_results, first_hash )
canvas_frame.SetCanvas( canvas_window )
canvas_window.exitFocusMedia.connect( self.SetFocusedMedia )
def _ManageNotes( self ):
@ -3752,9 +3675,6 @@ class MediaPanelThumbnails( MediaPanel ):
if multiple_selected:
local_duplicate_phrase = 'add all possible selected to'
local_move_from_to_phrase = 'all possible selected from {} to {}'
download_phrase = 'download all possible selected'
rescind_download_phrase = 'cancel downloads for all possible selected'
upload_phrase = 'upload all possible selected to'
@ -3779,9 +3699,6 @@ class MediaPanelThumbnails( MediaPanel ):
else:
local_duplicate_phrase = 'add to'
local_move_from_to_phrase = 'from {} to {}'
download_phrase = 'download'
rescind_download_phrase = 'cancel download'
upload_phrase = 'upload to'
@ -3857,9 +3774,6 @@ class MediaPanelThumbnails( MediaPanel ):
# valid commands for the files
local_duplicable_to_file_service_keys = set()
local_moveable_from_and_to_file_service_keys = set()
uploadable_file_service_keys = set()
downloadable_file_service_keys = set()
@ -3883,26 +3797,6 @@ class MediaPanelThumbnails( MediaPanel ):
pending = locations_manager.GetPending()
petitioned = locations_manager.GetPetitioned()
# LOCAL MIGRATION
if locations_manager.IsLocal():
can_send_to = local_media_file_service_keys.difference( current )
can_send_from = local_media_file_service_keys.intersection( current )
if len( can_send_to ) > 0:
local_duplicable_to_file_service_keys.update( can_send_to )
if len( can_send_from ) > 0:
# can_send_from does not include trash. we won't say 'move from trash to blah' since that's a little complex. we'll just say 'add to blah' in that case I think
local_moveable_from_and_to_file_service_keys.update( list( itertools.product( can_send_from, can_send_to ) ) )
# FILE REPOS
# we can upload (set pending) to a repo_id when we have permission, a file is local, not current, not pending, and either ( not deleted or we_can_overrule )
@ -4156,6 +4050,8 @@ class MediaPanelThumbnails( MediaPanel ):
ClientGUIMenus.AppendMenu( menu, manage_menu, 'manage' )
( local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys ) = ClientGUIMediaActions.GetLocalFileActionServiceKeys( flat_selected_medias )
len_interesting_local_service_keys = 0
len_interesting_local_service_keys += len( local_duplicable_to_file_service_keys )
@ -4195,19 +4091,7 @@ class MediaPanelThumbnails( MediaPanel ):
if len_interesting_local_service_keys > 0:
local_action_menu = QW.QMenu( files_parent_menu )
if len( local_duplicable_to_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, local_action_menu, local_duplicable_to_file_service_keys, local_duplicate_phrase, 'Duplicate the files to the local file service.', self._LocalDuplicateFiles )
if len( local_moveable_from_and_to_file_service_keys ) > 0:
ClientGUIMedia.AddDoubleServiceKeysToMenu( self, local_action_menu, local_moveable_from_and_to_file_service_keys, 'move', local_move_from_to_phrase, 'Move the files to the local file service.', self._LocalMoveFiles )
ClientGUIMenus.AppendMenu( files_parent_menu, local_action_menu, 'local services' )
ClientGUIMedia.AddLocalFilesMoveAddToMenu( self, files_parent_menu, local_duplicable_to_file_service_keys, local_moveable_from_and_to_file_service_keys, multiple_selected, self.ProcessApplicationCommand )
if len_interesting_remote_service_keys > 0:
@ -4226,57 +4110,57 @@ class MediaPanelThumbnails( MediaPanel ):
if len( uploadable_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, uploadable_file_service_keys, upload_phrase, 'Upload all selected files to the file repository.', self._UploadFiles )
if len( pending_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, pending_file_service_keys, rescind_upload_phrase, 'Rescind the pending upload to the file repository.', self._RescindUploadFiles )
if len( petitionable_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, petitionable_file_service_keys, petition_phrase, 'Petition these files for deletion from the file repository.', self._PetitionFiles )
if len( petitioned_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, petitioned_file_service_keys, rescind_petition_phrase, 'Rescind the petition to delete these files from the file repository.', self._RescindPetitionFiles )
if len( deletable_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, deletable_file_service_keys, remote_delete_phrase, 'Delete these files from the file repository.', self._Delete )
if len( modifyable_file_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, modifyable_file_service_keys, modify_account_phrase, 'Modify the account(s) that uploaded these files to the file repository.', self._ModifyUploaders )
if len( pinnable_ipfs_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, pinnable_ipfs_service_keys, pin_phrase, 'Pin these files to the ipfs service.', self._UploadFiles )
if len( pending_ipfs_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, pending_ipfs_service_keys, rescind_pin_phrase, 'Rescind the pending pin to the ipfs service.', self._RescindUploadFiles )
if len( unpinnable_ipfs_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, unpinnable_ipfs_service_keys, unpin_phrase, 'Unpin these files from the ipfs service.', self._PetitionFiles )
if len( petitioned_ipfs_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, petitioned_ipfs_service_keys, rescind_unpin_phrase, 'Rescind the pending unpin from the ipfs service.', self._RescindPetitionFiles )
if multiple_selected and len( ipfs_service_keys ) > 0:
ClientGUIMedia.AddServiceKeysToMenu( self, remote_action_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory )
ClientGUIMedia.AddServiceKeysToMenu( remote_action_menu, ipfs_service_keys, 'pin new directory to', 'Pin these files as a directory to the ipfs service.', self._UploadDirectory )
ClientGUIMenus.AppendMenu( files_parent_menu, remote_action_menu, 'remote services' )

View File

@ -14,6 +14,68 @@ from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.widgets import ClientGUICommon
class LocalFilesSubPanel( QW.QWidget ):
def __init__( self, parent: QW.QWidget ):
QW.QWidget.__init__( self, parent )
self._add_or_move_action = ClientGUICommon.BetterChoice( self )
self._add_or_move_action.addItem( 'add to', HC.CONTENT_UPDATE_ADD )
self._add_or_move_action.addItem( 'move to', HC.CONTENT_UPDATE_MOVE )
self._add_or_move_action.SetValue( HC.CONTENT_UPDATE_ADD )
self._service_keys = ClientGUICommon.BetterChoice( self )
#
services = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_FILE_DOMAIN, ) )
for service in services:
service_name = service.GetName()
service_key = service.GetServiceKey()
self._service_keys.addItem( service_name, service_key )
#
vbox = QP.VBoxLayout()
ratings_numerical_hbox = QP.HBoxLayout()
QP.AddToLayout( vbox, self._add_or_move_action, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._service_keys, CC.FLAGS_EXPAND_PERPENDICULAR )
self.setLayout( vbox )
def GetValue( self ):
service_key = self._service_keys.GetValue()
if service_key is None:
raise HydrusExceptions.VetoException( 'Please select a service!' )
action = self._add_or_move_action.GetValue()
value = None
return CAC.ApplicationCommand( CAC.APPLICATION_COMMAND_TYPE_CONTENT, ( service_key, HC.CONTENT_TYPE_FILES, action, value ) )
def SetValue( self, action: int, service_key: bytes ):
self._add_or_move_action.SetValue( action )
self._service_keys.SetValue( service_key )
class RatingLikeSubPanel( QW.QWidget ):
def __init__( self, parent: QW.QWidget ):
@ -572,6 +634,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
PANEL_RATING_LIKE = 2
PANEL_RATING_NUMERICAL = 3
PANEL_RATING_NUMERICAL_INCDEC = 4
PANEL_LOCAL_FILES = 5
def __init__( self, parent: QW.QWidget, command: CAC.ApplicationCommand, shortcuts_name: str ):
@ -588,6 +651,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
if is_custom_or_media:
self._panel_choice.addItem( 'tag command', self.PANEL_TAG )
self._panel_choice.addItem( 'local file command', self.PANEL_LOCAL_FILES )
self._panel_choice.addItem( 'like/dislike rating command', self.PANEL_RATING_LIKE )
self._panel_choice.addItem( 'numerical rating command', self.PANEL_RATING_NUMERICAL )
self._panel_choice.addItem( 'numerical rating increment/decrement command', self.PANEL_RATING_NUMERICAL_INCDEC )
@ -607,6 +671,8 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
self._rating_numerical_inc_dec_sub_panel = RatingNumericalIncDecSubPanel( self )
self._local_files_sub_panel = LocalFilesSubPanel( self )
#
if command.IsSimpleCommand():
@ -649,6 +715,12 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
self._panel_choice.SetValue( self.PANEL_TAG )
elif service_type == HC.LOCAL_FILE_DOMAIN:
self._local_files_sub_panel.SetValue( action, service_key )
self._panel_choice.SetValue( self.PANEL_LOCAL_FILES )
elif service_type == HC.LOCAL_RATING_LIKE:
rating = value
@ -685,6 +757,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
QP.AddToLayout( vbox, self._panel_choice, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._simple_sub_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
QP.AddToLayout( vbox, self._tag_sub_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._local_files_sub_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._rating_like_sub_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._rating_numerical_sub_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
QP.AddToLayout( vbox, self._rating_numerical_inc_dec_sub_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
@ -705,6 +778,7 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
self._simple_sub_panel.setVisible( panel_type == self.PANEL_SIMPLE )
self._tag_sub_panel.setVisible( panel_type == self.PANEL_TAG )
self._local_files_sub_panel.setVisible( panel_type == self.PANEL_LOCAL_FILES )
self._rating_like_sub_panel.setVisible( panel_type == self.PANEL_RATING_LIKE )
self._rating_numerical_sub_panel.setVisible( panel_type == self.PANEL_RATING_NUMERICAL )
self._rating_numerical_inc_dec_sub_panel.setVisible( panel_type == self.PANEL_RATING_NUMERICAL_INCDEC )
@ -722,6 +796,10 @@ class ApplicationCommandWidget( ClientGUIScrolledPanels.EditPanel ):
return self._tag_sub_panel.GetValue()
elif panel_type == self.PANEL_LOCAL_FILES:
return self._local_files_sub_panel.GetValue()
elif panel_type == self.PANEL_RATING_LIKE:
return self._rating_like_sub_panel.GetValue()

View File

@ -80,7 +80,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 483
SOFTWARE_VERSION = 484
CLIENT_API_VERSION = 31
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -194,6 +194,7 @@ CONTENT_UPDATE_FLIP = 16
CONTENT_UPDATE_CLEAR_DELETE_RECORD = 17
CONTENT_UPDATE_INCREMENT = 18
CONTENT_UPDATE_DECREMENT = 19
CONTENT_UPDATE_MOVE = 20
content_update_string_lookup = {
CONTENT_UPDATE_ADD : 'add',
@ -213,7 +214,8 @@ content_update_string_lookup = {
CONTENT_UPDATE_FLIP : 'flip on/off',
CONTENT_UPDATE_CLEAR_DELETE_RECORD : 'clear deletion record',
CONTENT_UPDATE_INCREMENT : 'increment',
CONTENT_UPDATE_DECREMENT : 'decrement'
CONTENT_UPDATE_DECREMENT : 'decrement',
CONTENT_UPDATE_MOVE : 'move',
}
DEFINITIONS_TYPE_HASHES = 0
@ -549,6 +551,7 @@ AUDIO_MKV = 48
AUDIO_MP4 = 49
UNDETERMINED_MP4 = 50
APPLICATION_CBOR = 51
APPLICATION_WINDOWS_EXE = 52
APPLICATION_OCTET_STREAM = 100
APPLICATION_UNKNOWN = 101
@ -685,6 +688,7 @@ mime_string_lookup = {
APPLICATION_ZIP : 'zip',
APPLICATION_RAR : 'rar',
APPLICATION_7Z : '7z',
APPLICATION_WINDOWS_EXE : 'windows exe',
APPLICATION_HYDRUS_ENCRYPTED_ZIP : 'application/hydrus-encrypted-zip',
APPLICATION_HYDRUS_UPDATE_CONTENT : 'application/hydrus-update-content',
APPLICATION_HYDRUS_UPDATE_DEFINITIONS : 'application/hydrus-update-definitions',
@ -742,6 +746,7 @@ mime_mimetype_string_lookup = {
APPLICATION_ZIP : 'application/zip',
APPLICATION_RAR : 'application/vnd.rar',
APPLICATION_7Z : 'application/x-7z-compressed',
APPLICATION_WINDOWS_EXE : 'application/octet-stream',
APPLICATION_HYDRUS_ENCRYPTED_ZIP : 'application/hydrus-encrypted-zip',
APPLICATION_HYDRUS_UPDATE_CONTENT : 'application/hydrus-update-content',
APPLICATION_HYDRUS_UPDATE_DEFINITIONS : 'application/hydrus-update-definitions',
@ -799,6 +804,7 @@ mime_ext_lookup = {
APPLICATION_ZIP : '.zip',
APPLICATION_RAR : '.rar',
APPLICATION_7Z : '.7z',
APPLICATION_WINDOWS_EXE : '.exe',
APPLICATION_HYDRUS_ENCRYPTED_ZIP : '.zip.encrypted',
APPLICATION_HYDRUS_UPDATE_CONTENT : '',
APPLICATION_HYDRUS_UPDATE_DEFINITIONS : '',

View File

@ -62,7 +62,8 @@ headers_and_mime.extend( [
( ( ( 0, b'fLaC' ), ), HC.AUDIO_FLAC ),
( ( ( 0, b'RIFF' ), ( 8, b'WAVE' ) ), HC.AUDIO_WAVE ),
( ( ( 8, b'AVI ' ), ), HC.VIDEO_AVI ),
( ( ( 0, b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C' ), ), HC.UNDETERMINED_WM )
( ( ( 0, b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C' ), ), HC.UNDETERMINED_WM ),
( ( ( 0, b'\x4D\x5A\x90\x00\x03', ), ), HC.APPLICATION_WINDOWS_EXE )
] )
def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, clip_rect = None, percentage_in = 35 ):

View File

@ -1,33 +1,45 @@
import ctypes.util
import os
import re
import shutil
import subprocess
otool_out = subprocess.check_output( [ 'otool', '-L', 'libmpv.1.dylib' ], encoding = 'utf-8' )
output_dir = 'mpv_dylibs'
print( 'Starting mpv dylib gather.' )
# /usr/local/opt/ffmpeg@4/lib/libavcodec.58.dylib (compatibility version 58.0.0, current version 58.134.100)
for line in otool_out.splitlines():
full_path = ctypes.util.find_library( 'mpv' )
if full_path is None:
# don't want /System/Library stuff, do want /usr/lib stuff
match = re.search( r'/usr/.*dylib(?=\s\()', line )
print( 'Failed to locate library!' )
if match is not None:
else:
filename = os.path.basename( full_path ) # libmpv.1.dylib or similar
otool_out = subprocess.check_output( [ 'otool', '-L', filename ], encoding = 'utf-8' )
output_dir = 'mpv_dylibs'
# /usr/local/opt/ffmpeg@4/lib/libavcodec.58.dylib (compatibility version 58.0.0, current version 58.134.100)
for line in otool_out.splitlines():
source_path = line[ match.start() : match.end() ]
# don't want /System/Library stuff, do want /usr/lib stuff
match = re.search( r'/usr/.*dylib(?=\s\()', line )
destination_filename = os.path.basename( source_path )
destination_path = os.path.join( output_dir, destination_filename )
print( 'Gathering {} to {}.'.format( source_path, destination_path ) )
shutil.copy2( source_path, destination_path )
else:
print( 'Not gathering {}.'.format( line ) )
if match is not None:
source_path = line[ match.start() : match.end() ]
destination_filename = os.path.basename( source_path )
destination_path = os.path.join( output_dir, destination_filename )
print( 'Gathering {} to {}.'.format( source_path, destination_path ) )
shutil.copy2( source_path, destination_path )
else:
print( 'Not gathering {}.'.format( line ) )