Version 526

closes #1362, closes #1353
This commit is contained in:
Hydrus Network Developer 2023-05-03 15:34:44 -05:00
parent 07980588ae
commit 7bfe474a5d
No known key found for this signature in database
GPG Key ID: 76249F053212133C
21 changed files with 368 additions and 94 deletions

View File

@ -7,6 +7,38 @@ title: Changelog
!!! note
This is the new changelog, only the most recent builds. For all versions, see the [old changelog](old_changelog.html).
## [Version 526](https://github.com/hydrusnetwork/hydrus/releases/tag/v526)
### there will be an important update next week
* next week's release will have two important program changes--I will integrate an OpenCV update, which will require 'extract' users to perform a clean install, and the executables are finally changing from 'client' and 'server' to 'hydrus_client' and 'hydrus_server'! be prepared to update your shortcuts and launch scripts
### time
* fixed a stupid logical bug in my new date code, which was throwing errors on system:time predicates that had a month value equal to the current month (e.g. 'x years, 5 months' during May)--sorry! (issue #1362)
* when a subscription dies, the popup note about it says the death velocity period in the neat '180 days', as you set in UI, rather than converting to a date and stating the number of months and days using the recent calendar calculation updates
* I unified some more 'xxxified date' UI labels to be 'xxxified time'. we're generally moving to the latter format as the ideal while still accepting various combinations for system parsing input
### shortcuts
* added 'media play-pause/previous/next' and 'volume up/down/mute' key recognition to the shortcut system. if your keyboard/headphones have media keys, they _should_ be mappable now. note, however, that, at least on Windows, while these capture in the hydrus UI, they seem to have global OS-level hooks, and as far as I can tell Qt can't stop that event propagating, so these may have limited effectiveness if you also have an mp3 player open, since Windows will also send the 'next' call to that etc... it may be there is a nice way to properly register a Qt app as a media thing for Windows to global-hook these events to, but I'm not sure!
* also added 'mouse task button' to the mappable buttons. this is apparently a common Mouse6 mapping, so if you have it, knock yourself out
* the code in the shortcut system that tries to detect and merge many small scroll wheel events (such as the emulated scroll that a trackpad may generate) now applies to all mouse devices, not just synthesised events. with luck, this will mean that mice that generate like 15 smoothscroll events of one degree instead of one of fifteen degrees for every wheel tick will no longer spam-navigate the media viewer wew
### misc
* to save you typing/pasting time, the 'enter your reason' prompts in manage tags, tag siblings, and tag parents now remember the last five custom reasons you enter! you can change the number saved using the new option under _options->tags_, including setting it to 0 to disable the system
* fixed pasting tags in the manage tags dialog when the number of tags you are pasting is larger than the number of allowed 'recent tags'. previously it was saying 'did not understand what was in the clipboard', so hooray for the new error reporting
* every multi-column list in the program now has a 'reset column widths' item in its header right-click menu! when these reset events happen, the respective lists also resize themselves immediately, no restart required
* when you set 'try again' on an import object, it now clears all saved hashes from the import object (including the SHA256 which may have been linked from the database in an 'already in db'/'previously deleted' result). this will ensure the next attempt is not poisoned by these hashes (which can happen for various reasons) in the subsequent attempt. basically 'try again' resets better now (issue #1353)
### some build stuff
* the main build script now only uses Node16 sub-Actions (Node12 support is deprecated and being dropped in June)
* the main build script no longer uses set-output commands (these are deprecated and being dropped later in the year I think, in favour of some ENV stuff)
* tidied some cruft from the main build script
* I moved the 'new' python-mpv in the requirements.txts from 1.0.1 to 1.0.3. source users might like to rebuild their venvs again, particularly Windows users who updated to the new mpv dll recently
## [Version 525](https://github.com/hydrusnetwork/hydrus/releases/tag/v525)
### library updates
@ -358,26 +390,3 @@ title: Changelog
* cleaned up how popup file buttons are set and cleared
* cleaned up how popup main and secondary texts are set and cleared
* misc linting cleanup
## [Version 516](https://github.com/hydrusnetwork/hydrus/releases/tag/v516)
### misc
* the 'manage sidecar routers' control, which is on manage import folders, manage export folders, path-tagging-before-manual-import, and manual export files, now has import/export/duplicate buttons. you can save and transfer your work now! if you try to import 'export to sidecar' routers to an 'import from sidecar' context or _vice versa_, it should give you a nicely worded error
* fixed the error that was raising when you turn related tags off with the suggestions set to side-by-side layout. very sorry for the trouble!
* apngs that are set to 'loop x times' (usually once) now only loop that many times, on both mpv and my native renderer! like gifs, the 'always loop animations' setting under _options->media_ overrides it!
* fixed an issue with my native renderer not updating on scanbar scrubs very well. should be back to nice smooth instant draw as you scrub
* thanks to a user, folded in another deviant art parser update to the defaults
* updated the setuptools version in the requirements.txt due to a security note--I don't think the problem (which was about some vulnerable regex when fetching malicious package info) applies to us, but running from source users might like to run setup_venv again this week anyway
### related tags
* a new 'concurrence threshold' setting under _options->tag suggestions_ allows you to set how 'strict' the related tags search is. a higher percentage causes fewer but more relevant results. I'm increasing the default this week from 4% to 6%
* two new 'namespace to weight' settings under _options->tag suggestions_ now manage how much weight the 'search' and 'suggestion' sides of related tags have. you can say 'rank the suggestions from character tags highly' or 'rank unnamespaced suggestions lower', and 'do not search x tags' and 'do not suggest y tags'. I have prepped it with some 'creator/character/series namespaces are better than unnamespaced, and title/filename/page/chapter/volume are useless' defaults, but feel free to play around with it
* the related tags algorithm takes a larger sample now, resulting in a _little_ less ranking-variability
### client api
* changed and fixed an issue in the client api's new `get_file_relationships` call. previously, I said 'king' would be null if it was not on the given file domain, but this was not working correctly--it was giving pseudorandom 'fallback' kings. now it always gives the king, no matter what! a new param, `king_is_on_file_domain` says whether the king is on the given domain. `king_is_local` says whether the king is available on disk
* added some discussion and a list of the 8 possible 'better than' and 'same quality' logical combinations to the `set_file_relationships` help so you can see how group merge involving non-kings works
* client api is now version 42

View File

@ -34,6 +34,31 @@
<div class="content">
<h1 id="changelog"><a href="#changelog">changelog</a></h1>
<ul>
<li>
<h2 id="version_526"><a href="#version_526">version 526</a></h2>
<ul>
<li><h3>there will be an important update next week</h3></li>
<li>next week's release will have two important program changes--I will integrate an OpenCV update, which will require 'extract' users to perform a clean install, and the executables are finally changing from 'client' and 'server' to 'hydrus_client' and 'hydrus_server'! be prepared to update your shortcuts and launch scripts</li>
<li><h3>time</h3></li>
<li>fixed a stupid logical bug in my new date code, which was throwing errors on system:time predicates that had a month value equal to the current month (e.g. 'x years, 5 months' during May)--sorry! (issue #1362)</li>
<li>when a subscription dies, the popup note about it says the death velocity period in the neat '180 days', as you set in UI, rather than converting to a date and stating the number of months and days using the recent calendar calculation updates</li>
<li>I unified some more 'xxxified date' UI labels to be 'xxxified time'. we're generally moving to the latter format as the ideal while still accepting various combinations for system parsing input</li>
<li><h3>shortcuts</h3></li>
<li>added 'media play-pause/previous/next' and 'volume up/down/mute' key recognition to the shortcut system. if your keyboard/headphones have media keys, they _should_ be mappable now. note, however, that, at least on Windows, while these capture in the hydrus UI, they seem to have global OS-level hooks, and as far as I can tell Qt can't stop that event propagating, so these may have limited effectiveness if you also have an mp3 player open, since Windows will also send the 'next' call to that etc... it may be there is a nice way to properly register a Qt app as a media thing for Windows to global-hook these events to, but I'm not sure!</li>
<li>also added 'mouse task button' to the mappable buttons. this is apparently a common Mouse6 mapping, so if you have it, knock yourself out</li>
<li>the code in the shortcut system that tries to detect and merge many small scroll wheel events (such as the emulated scroll that a trackpad may generate) now applies to all mouse devices, not just synthesised events. with luck, this will mean that mice that generate like 15 smoothscroll events of one degree instead of one of fifteen degrees for every wheel tick will no longer spam-navigate the media viewer wew</li>
<li><h3>misc</h3></li>
<li>to save you typing/pasting time, the 'enter your reason' prompts in manage tags, tag siblings, and tag parents now remember the last five custom reasons you enter! you can change the number saved using the new option under _options->tags_, including setting it to 0 to disable the system</li>
<li>fixed pasting tags in the manage tags dialog when the number of tags you are pasting is larger than the number of allowed 'recent tags'. previously it was saying 'did not understand what was in the clipboard', so hooray for the new error reporting</li>
<li>every multi-column list in the program now has a 'reset column widths' item in its header right-click menu! when these reset events happen, the respective lists also resize themselves immediately, no restart required</li>
<li>when you set 'try again' on an import object, it now clears all saved hashes from the import object (including the SHA256 which may have been linked from the database in an 'already in db'/'previously deleted' result). this will ensure the next attempt is not poisoned by these hashes (which can happen for various reasons) in the subsequent attempt. basically 'try again' resets better now (issue #1353)</li>
<li><h3>some build stuff</h3></li>
<li>the main build script now only uses Node16 sub-Actions (Node12 support is deprecated and being dropped in June)</li>
<li>the main build script no longer uses set-output commands (these are deprecated and being dropped later in the year I think, in favour of some ENV stuff)</li>
<li>tidied some cruft from the main build script</li>
<li>I moved the 'new' python-mpv in the requirements.txts from 1.0.1 to 1.0.3. source users might like to rebuild their venvs again, particularly Windows users who updated to the new mpv dll recently</li>
</ul>
</li>
<li>
<h2 id="version_525"><a href="#version_525">version 525</a></h2>
<ul>

View File

@ -67,7 +67,7 @@ regen_file_enum_to_str_lookup = {
REGENERATE_FILE_DATA_JOB_FIX_PERMISSIONS : 'fix file read/write permissions',
REGENERATE_FILE_DATA_JOB_CHECK_SIMILAR_FILES_MEMBERSHIP : 'check for membership in the similar files search system',
REGENERATE_FILE_DATA_JOB_SIMILAR_FILES_METADATA : 'regenerate similar files metadata',
REGENERATE_FILE_DATA_JOB_FILE_MODIFIED_TIMESTAMP : 'regenerate file modified date',
REGENERATE_FILE_DATA_JOB_FILE_MODIFIED_TIMESTAMP : 'regenerate file modified time',
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF : 'determine if the file has EXIF metadata',
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA : 'determine if the file has non-EXIF human-readable embedded metadata',
REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE : 'determine if the file has an icc profile',

View File

@ -325,6 +325,10 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
#
self._dictionary[ 'recent_petition_reasons' ] = HydrusSerialisable.SerialisableDictionary()
#
self._dictionary[ 'duplicate_action_options' ] = HydrusSerialisable.SerialisableDictionary()
duplicate_content_merge_options = ClientDuplicates.DuplicateContentMergeOptions()
@ -392,6 +396,8 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
self._dictionary[ 'integers' ][ 'default_new_page_goes' ] = CC.NEW_PAGE_GOES_FAR_RIGHT
self._dictionary[ 'integers' ][ 'num_recent_petition_reasons' ] = 5
self._dictionary[ 'integers' ][ 'max_page_name_chars' ] = 20
self._dictionary[ 'integers' ][ 'page_file_count_display' ] = CC.PAGE_FILE_COUNT_DISPLAY_ALL
@ -1377,6 +1383,14 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
return self._dictionary[ name ]
def GetRecentPetitionReasons( self, content_type, action ):
with self._lock:
return list( self._dictionary[ 'recent_petition_reasons' ].get( ( content_type, action ), [] ) )[ : self._dictionary[ 'integers' ][ 'num_recent_petition_reasons' ] ]
def GetRecentPredicates( self, predicate_types ):
with self._lock:
@ -1467,6 +1481,30 @@ class ClientOptions( HydrusSerialisable.SerialisableBase ):
def PushRecentPetitionReason( self, content_type, action, reason ):
with self._lock:
key = ( content_type, action )
if key not in self._dictionary[ 'recent_petition_reasons' ]:
self._dictionary[ 'recent_petition_reasons' ][ key ] = []
reasons = self._dictionary[ 'recent_petition_reasons' ][ key ]
if reason in reasons:
reasons.remove( reason )
reasons.insert( 0, reason )
self._dictionary[ 'recent_petition_reasons' ][ key ] = reasons[ : self._dictionary[ 'integers' ][ 'num_recent_petition_reasons' ] ]
def PushRecentPredicates( self, predicates ):
with self._lock:

View File

@ -2476,7 +2476,7 @@ class Predicate( HydrusSerialisable.SerialisableBase ):
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME:
base = 'last view time'
base = 'last viewed time'
elif self._predicate_type == PREDICATE_TYPE_SYSTEM_MODIFIED_TIME:

View File

@ -1329,7 +1329,7 @@ class EditDuplicateContentMergeOptionsPanel( ClientGUIScrolledPanels.EditPanel )
rows = []
rows.append( ( 'sync archived status?: ', self._sync_archive_action ) )
rows.append( ( 'sync file modified date?: ', self._sync_file_modified_date_action ) )
rows.append( ( 'sync file modified time?: ', self._sync_file_modified_date_action ) )
rows.append( ( 'sync known urls?: ', self._sync_urls_action ) )
rows.append( ( 'sync notes?: ', self._sync_notes_action ) )
rows.append( ( '', self._sync_note_import_options_button ) )
@ -2713,11 +2713,11 @@ class EditFileTimestampsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUISc
if HydrusPaths.FileModifiedTimeIsOk( timestamp_data.timestamp ):
self._file_modified_timestamp_warning_st.setText( 'This will also change the modified date of the file on disk!' )
self._file_modified_timestamp_warning_st.setText( 'This will also change the modified time of the file on disk!' )
else:
self._file_modified_timestamp_warning_st.setText( 'File modified date on disk will not be changed--the timestamp is too early.' )
self._file_modified_timestamp_warning_st.setText( 'File modified time on disk will not be changed--the timestamp is too early.' )
return

View File

@ -3570,6 +3570,10 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._show_sibling_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
self._show_sibling_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
self._num_recent_petition_reasons = ClientGUICommon.BetterSpinBox( general_panel, initial = 5, min = 0, max = 100 )
tt = 'In manage tags, tag siblings, and tag parents, you may be asked to provide a reason with a petition you make to a hydrus repository. There are some fixed reasons, but the dialog can also remember what you recently typed. This controls how many recent reasons it will remember.'
self._num_recent_petition_reasons.setToolTip( tt )
self._ac_select_first_with_count = QW.QCheckBox( general_panel )
#
@ -3606,6 +3610,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._show_sibling_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
self._num_recent_petition_reasons.setValue( self._new_options.GetInteger( 'num_recent_petition_reasons' ) )
self._ac_select_first_with_count.setChecked( self._new_options.GetBoolean( 'ac_select_first_with_count' ) )
#
@ -3624,6 +3630,7 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
rows.append( ( 'Show parents expanded by default on edit/write autocomplete taglists: ', self._expand_parents_on_storage_autocomplete_taglists ) )
rows.append( ( 'Show sibling info by default on edit/write taglists: ', self._show_sibling_decorators_on_storage_taglists ) )
rows.append( ( 'Show sibling info by default on edit/write autocomplete taglists: ', self._show_sibling_decorators_on_storage_autocomplete_taglists ) )
rows.append( ( 'Number of recent petition reasons to remember in dialogs: ', self._num_recent_petition_reasons ) )
rows.append( ( 'By default, select the first tag result with actual count in write-autocomplete: ', self._ac_select_first_with_count ) )
gridbox = ClientGUICommon.WrapInGrid( general_panel, rows )
@ -3670,6 +3677,8 @@ class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_taglists', self._show_sibling_decorators_on_storage_taglists.isChecked() )
self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists', self._show_sibling_decorators_on_storage_autocomplete_taglists.isChecked() )
self._new_options.SetInteger( 'num_recent_petition_reasons', self._num_recent_petition_reasons.value() )
self._new_options.SetBoolean( 'ac_select_first_with_count', self._ac_select_first_with_count.isChecked() )
#

View File

@ -70,6 +70,12 @@ SHORTCUT_KEY_SPECIAL_F9 = 25
SHORTCUT_KEY_SPECIAL_F10 = 26
SHORTCUT_KEY_SPECIAL_F11 = 27
SHORTCUT_KEY_SPECIAL_F12 = 28
SHORTCUT_KEY_SPECIAL_MEDIA_PLAY_PAUSE = 29
SHORTCUT_KEY_SPECIAL_MEDIA_PREVIOUS = 30
SHORTCUT_KEY_SPECIAL_MEDIA_NEXT = 31
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_DOWN = 32
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_UP = 33
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_MUTE_UNMUTE = 32
if HC.PLATFORM_MACOS:
@ -111,7 +117,15 @@ special_key_shortcut_enum_lookup = {
QC.Qt.Key_F9 : SHORTCUT_KEY_SPECIAL_F9,
QC.Qt.Key_F10 : SHORTCUT_KEY_SPECIAL_F10,
QC.Qt.Key_F11 : SHORTCUT_KEY_SPECIAL_F11,
QC.Qt.Key_F12 : SHORTCUT_KEY_SPECIAL_F12
QC.Qt.Key_F12 : SHORTCUT_KEY_SPECIAL_F12,
QC.Qt.Key_MediaTogglePlayPause : SHORTCUT_KEY_SPECIAL_MEDIA_PLAY_PAUSE,
QC.Qt.Key_MediaPlay : SHORTCUT_KEY_SPECIAL_MEDIA_PLAY_PAUSE,
QC.Qt.Key_MediaPause : SHORTCUT_KEY_SPECIAL_MEDIA_PLAY_PAUSE,
QC.Qt.Key_MediaPrevious : SHORTCUT_KEY_SPECIAL_MEDIA_PREVIOUS,
QC.Qt.Key_MediaNext : SHORTCUT_KEY_SPECIAL_MEDIA_NEXT,
QC.Qt.Key_VolumeDown : SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_DOWN,
QC.Qt.Key_VolumeUp : SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_UP,
QC.Qt.Key_VolumeMute : SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_MUTE_UNMUTE
}
special_key_shortcut_str_lookup = {
@ -143,7 +157,13 @@ special_key_shortcut_str_lookup = {
SHORTCUT_KEY_SPECIAL_F9 : 'f9',
SHORTCUT_KEY_SPECIAL_F10 : 'f10',
SHORTCUT_KEY_SPECIAL_F11 : 'f11',
SHORTCUT_KEY_SPECIAL_F12 : 'f12'
SHORTCUT_KEY_SPECIAL_F12 : 'f12',
SHORTCUT_KEY_SPECIAL_MEDIA_PLAY_PAUSE : 'media: play/pause',
SHORTCUT_KEY_SPECIAL_MEDIA_PREVIOUS : 'media: previous',
SHORTCUT_KEY_SPECIAL_MEDIA_NEXT : 'media: next',
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_DOWN : 'volume down',
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_UP : 'volume up',
SHORTCUT_KEY_SPECIAL_MEDIA_VOLUME_MUTE_UNMUTE : 'volume mute/unmute'
}
SHORTCUT_MOUSE_LEFT = 0
@ -155,15 +175,17 @@ SHORTCUT_MOUSE_SCROLL_LEFT = 5
SHORTCUT_MOUSE_SCROLL_RIGHT = 6
SHORTCUT_MOUSE_BACK = 7
SHORTCUT_MOUSE_FORWARD = 8
SHORTCUT_MOUSE_TASK = 9
SHORTCUT_MOUSE_CLICKS = { SHORTCUT_MOUSE_LEFT, SHORTCUT_MOUSE_MIDDLE, SHORTCUT_MOUSE_RIGHT, SHORTCUT_MOUSE_BACK, SHORTCUT_MOUSE_FORWARD }
SHORTCUT_MOUSE_CLICKS = { SHORTCUT_MOUSE_LEFT, SHORTCUT_MOUSE_MIDDLE, SHORTCUT_MOUSE_RIGHT, SHORTCUT_MOUSE_BACK, SHORTCUT_MOUSE_FORWARD, SHORTCUT_MOUSE_TASK }
qt_mouse_buttons_to_hydrus_mouse_buttons = {
QC.Qt.LeftButton : SHORTCUT_MOUSE_LEFT,
QC.Qt.MiddleButton : SHORTCUT_MOUSE_MIDDLE,
QC.Qt.RightButton : SHORTCUT_MOUSE_RIGHT,
QC.Qt.BackButton : SHORTCUT_MOUSE_BACK,
QC.Qt.ForwardButton : SHORTCUT_MOUSE_FORWARD
QC.Qt.ForwardButton : SHORTCUT_MOUSE_FORWARD,
QC.Qt.TaskButton : SHORTCUT_MOUSE_TASK
}
shortcut_mouse_string_lookup = {
@ -172,6 +194,7 @@ shortcut_mouse_string_lookup = {
SHORTCUT_MOUSE_MIDDLE : 'middle-click',
SHORTCUT_MOUSE_BACK : 'back',
SHORTCUT_MOUSE_FORWARD : 'forward',
SHORTCUT_MOUSE_TASK : 'task button',
SHORTCUT_MOUSE_SCROLL_UP : 'scroll up',
SHORTCUT_MOUSE_SCROLL_DOWN : 'scroll down',
SHORTCUT_MOUSE_SCROLL_LEFT : 'scroll left',
@ -387,8 +410,9 @@ def ConvertKeyEventToSimpleTuple( event ):
return ( modifier, key )
GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS = 0
ONE_TICK_ON_A_NORMAL_MOUSE_IN_DEGREES = 15 * 8 # fifteen degrees, in eighths of a degree
ONE_TICK_ON_A_NORMAL_MOUSE_IN_EIGHTS_OF_A_DEGREE = 15 * 8 # fifteen degrees, in eighths of a degree
def ConvertMouseEventToShortcut( event: QG.QMouseEvent ):
@ -440,26 +464,25 @@ def ConvertMouseEventToShortcut( event: QG.QMouseEvent ):
angle_delta = angle_delta_point.y()
if QP.WheelEventIsSynthesised( event ):
# we used to do QP.WheelEventIsSynthesised here, but it seems some normal mice produce a billion small wheel events for smoothscroll gubbins or something, lfg
# so let's just try this tech for everyone
if abs( angle_delta ) < ONE_TICK_ON_A_NORMAL_MOUSE_IN_EIGHTS_OF_A_DEGREE:
if abs( angle_delta ) < ONE_TICK_ON_A_NORMAL_MOUSE_IN_DEGREES:
# likely using a trackpad to generate artificial wheel events
global GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS
GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS += angle_delta
if abs( GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS ) > ONE_TICK_ON_A_NORMAL_MOUSE_IN_EIGHTS_OF_A_DEGREE:
# likely using a trackpad to generate artificial wheel events
angle_delta = GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS
global GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS
GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS = 0
GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS += angle_delta
else:
if abs( GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS ) > ONE_TICK_ON_A_NORMAL_MOUSE_IN_DEGREES:
angle_delta = GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS
GLOBAL_MOUSE_SCROLL_DELTA_FOR_TRACKPADS = 0
else:
return None
return None

View File

@ -2518,12 +2518,16 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
message = 'Enter a reason for ' + tag_text + ' to be removed. A janitor will review your petition.'
suggestions = []
fixed_suggestions = [
'mangled parse/typo',
'not applicable/incorrect',
'clearing mass-pasted junk',
'splitting filename/title/etc... into individual tags'
]
suggestions.append( 'mangled parse/typo' )
suggestions.append( 'not applicable/incorrect' )
suggestions.append( 'clearing mass-pasted junk' )
suggestions.append( 'splitting filename/title/etc... into individual tags' )
suggestions = HG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_DELETE )
suggestions.extend( fixed_suggestions )
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
@ -2531,6 +2535,11 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
reason = dlg.GetValue()
if reason not in fixed_suggestions:
HG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_MAPPINGS, HC.CONTENT_UPDATE_DELETE, reason )
else:
return
@ -2602,6 +2611,8 @@ class ManageTagsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPa
if len( recent_tags ) > 0 and num_recent_tags is not None:
recent_tags = list( recent_tags )
if len( recent_tags ) > num_recent_tags:
recent_tags = random.sample( recent_tags, num_recent_tags )
@ -3232,10 +3243,14 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your request.'
suggestions = []
fixed_suggestions = [
'obvious by definition (a sword is a weapon)',
'character/series/studio/etc... belonging (character x belongs to series y)'
]
suggestions.append( 'obvious by definition (a sword is a weapon)' )
suggestions.append( 'character/series/studio/etc... belonging (character x belongs to series y)' )
suggestions = HG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_ADD )
suggestions.extend( fixed_suggestions )
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
@ -3243,6 +3258,11 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
reason = dlg.GetValue()
if reason not in fixed_suggestions:
HG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_ADD, reason )
else:
do_it = False
@ -3309,9 +3329,13 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
message += os.linesep * 2
message += 'to be removed. A janitor will review your petition.'
suggestions = []
fixed_suggestions = [
'obvious typo/mistake'
]
suggestions.append( 'obvious typo/mistake' )
suggestions = HG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE )
suggestions.extend( fixed_suggestions )
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
@ -3319,6 +3343,12 @@ class ManageTagParents( ClientGUIScrolledPanels.ManagePanel ):
reason = dlg.GetValue()
if reason not in fixed_suggestions:
HG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_PARENTS, HC.CONTENT_UPDATE_DELETE, reason )
else:
do_it = False
@ -4376,10 +4406,14 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
pair_strings = os.linesep.join( ( old + '->' + new for ( old, new ) in new_pairs ) )
suggestions = []
fixed_suggestions = [
'merging underscores/typos/phrasing/unnamespaced to a single uncontroversial good tag',
'rewording/namespacing based on preference'
]
suggestions.append( 'merging underscores/typos/phrasing/unnamespaced to a single uncontroversial good tag' )
suggestions.append( 'rewording/namespacing based on preference' )
suggestions = HG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_ADD )
suggestions.extend( fixed_suggestions )
message = 'Enter a reason for:' + os.linesep * 2 + pair_strings + os.linesep * 2 + 'To be added. A janitor will review your petition.'
@ -4389,6 +4423,11 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
reason = dlg.GetValue()
if reason not in fixed_suggestions:
HG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_ADD, reason )
else:
do_it = False
@ -4472,11 +4511,15 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
message += os.linesep * 2
message += 'to be removed. You will see the delete as soon as you upload, but a janitor will review your petition to decide if all users should receive it as well.'
suggestions = []
fixed_suggestions = [
'obvious typo/mistake',
'disambiguation',
'correcting to repository standard'
]
suggestions.append( 'obvious typo/mistake' )
suggestions.append( 'disambiguation' )
suggestions.append( 'correcting to repository standard' )
suggestions = HG.client_controller.new_options.GetRecentPetitionReasons( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE )
suggestions.extend( fixed_suggestions )
with ClientGUIDialogs.DialogTextEntry( self, message, suggestions = suggestions ) as dlg:
@ -4484,6 +4527,11 @@ class ManageTagSiblings( ClientGUIScrolledPanels.ManagePanel ):
reason = dlg.GetValue()
if reason not in fixed_suggestions:
HG.client_controller.new_options.PushRecentPetitionReason( HC.CONTENT_TYPE_TAG_SIBLINGS, HC.CONTENT_UPDATE_DELETE, reason )
else:
do_it = False

View File

@ -179,6 +179,9 @@ class BetterListCtrl( QW.QTreeWidget ):
self.header().setContextMenuPolicy( QC.Qt.CustomContextMenu )
self.header().customContextMenuRequested.connect( self._ShowHeaderMenu )
HG.client_controller.sub( self, 'NotifySettingsUpdated', 'reset_all_listctrl_status' )
HG.client_controller.sub( self, 'NotifySettingsUpdated', 'reset_listctrl_status' )
def _AddDataInfo( self, data_info ):
@ -387,7 +390,7 @@ class BetterListCtrl( QW.QTreeWidget ):
name = CGLC.column_list_type_name_lookup[ self._column_list_type ]
ClientGUIMenus.AppendMenuLabel( menu, f'multi-column list: {name}', 'This is the name of this multi-column list widget.' )
ClientGUIMenus.AppendMenuItem( menu, f'reset default column widths for "{name}" lists', 'Reset the column widths and other display settings for all lists of this type', HG.client_controller.column_list_manager.ResetToDefaults, self._column_list_type )
CGC.core().PopupMenu( self, menu )
@ -685,6 +688,57 @@ class BetterListCtrl( QW.QTreeWidget ):
return len( self.selectedItems() ) > 0
def NotifySettingsUpdated( self, column_list_type = None ):
if column_list_type is not None and column_list_type != self._column_list_type:
return
self.blockSignals( True )
self.header().blockSignals( True )
self._column_list_status: ClientGUIListStatus.ColumnListStatus = HG.client_controller.column_list_manager.GetStatus( self._column_list_type )
self._original_column_list_status = self._column_list_status
#
( self._sort_column_type, self._sort_asc ) = self._column_list_status.GetSort()
#
main_tlw = HG.client_controller.GetMainTLW()
MIN_SECTION_SIZE_CHARS = 3
last_column_index = self._column_list_status.GetColumnCount() - 1
for ( i, column_type ) in enumerate( self._column_list_status.GetColumnTypes() ):
if i == last_column_index:
width_chars = MIN_SECTION_SIZE_CHARS
else:
width_chars = self._column_list_status.GetColumnWidth( column_type )
width_chars = max( width_chars, MIN_SECTION_SIZE_CHARS )
width_pixels = ClientGUIFunctions.ConvertTextToPixelWidth( main_tlw, width_chars )
self.setColumnWidth( i, width_pixels )
self.header().blockSignals( False )
self.blockSignals( False )
#
self.Sort() # note this saves the current status, so don't do it until we resize stuff
def ProcessActivateAction( self ):
if self._activation_callback is not None:

View File

@ -1,5 +1,7 @@
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusGlobals as HG
from hydrus.client.gui.lists import ClientGUIListStatus
class ColumnListManager( HydrusSerialisable.SerialisableBase ):
@ -50,9 +52,23 @@ class ColumnListManager( HydrusSerialisable.SerialisableBase ):
return column_list_status
def ResetToDefaults( self ):
def ResetToDefaults( self, column_list_type = None ):
self._column_list_types_to_statuses = HydrusSerialisable.SerialisableDictionary()
if column_list_type is None:
self._column_list_types_to_statuses = HydrusSerialisable.SerialisableDictionary()
HG.client_controller.pub( 'reset_all_listctrl_status' )
else:
if column_list_type in self._column_list_types_to_statuses:
del self._column_list_types_to_statuses[ column_list_type ]
HG.client_controller.pub( 'reset_listctrl_status', column_list_type )
self._dirty = True

View File

@ -472,7 +472,7 @@ class PanelPredicateSystemLastViewedDate( PanelPredicateSystemDate ):
def _GetSystemPredicateLabel( self ) -> str:
return 'system:last viewed date'
return 'system:last viewed time'
def _GetPredicateType( self ) -> int:
@ -485,7 +485,7 @@ class PanelPredicateSystemArchivedDate( PanelPredicateSystemDate ):
def _GetSystemPredicateLabel( self ) -> str:
return 'system:archived date'
return 'system:archived time'
def _GetPredicateType( self ) -> int:
@ -498,7 +498,7 @@ class PanelPredicateSystemModifiedDate( PanelPredicateSystemDate ):
def _GetSystemPredicateLabel( self ) -> str:
return 'system:modified date'
return 'system:modified time'
def _GetPredicateType( self ) -> int:
@ -597,7 +597,7 @@ class PanelPredicateSystemLastViewedDelta( PanelPredicateSystemSingle ):
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:last viewed'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:last viewed time'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._years, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'years'), CC.FLAGS_CENTER_PERPENDICULAR )
@ -656,7 +656,7 @@ class PanelPredicateSystemArchivedDelta( PanelPredicateSystemSingle ):
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:archived date'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:archived time'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._years, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'years'), CC.FLAGS_CENTER_PERPENDICULAR )
@ -715,7 +715,7 @@ class PanelPredicateSystemModifiedDelta( PanelPredicateSystemSingle ):
hbox = QP.HBoxLayout()
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:modified date'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'system:modified time'), CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._sign, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, self._years, CC.FLAGS_CENTER_PERPENDICULAR )
QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText(self,'years'), CC.FLAGS_CENTER_PERPENDICULAR )

View File

@ -1237,7 +1237,7 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
def SetReferralURL( self, referral_url: str ):
self._referral_url = referral_url
def SetRequestHeaders( self, request_headers: dict ):
@ -1261,6 +1261,14 @@ class FileSeed( HydrusSerialisable.SerialisableBase ):
self.status = status
self.note = note
if status == CC.STATUS_UNKNOWN:
# if user is 'try again'ing for a complicated 'the destination file changed' situation,
# we want to scrub any db-assigned sha256 that was allocated by a previous 'already in db' file import result
# also kill any md5 or whatever, just in case that was causing the original mess-up
self._hashes = {}
self._UpdateModified()

View File

@ -703,7 +703,7 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
( death_files_found, death_time_delta ) = death_file_velocity
HydrusData.ShowText( 'The query "{}" for subscription "{}" found fewer than {} files in the last {}, so it appears to be dead!'.format( query_name, self._name, HydrusData.ToHumanInt( death_files_found ), HydrusTime.TimeDeltaToPrettyTimeDelta( death_time_delta ) ) )
HydrusData.ShowText( 'The query "{}" for subscription "{}" found fewer than {} files in the last {}, so it appears to be dead!'.format( query_name, self._name, HydrusData.ToHumanInt( death_files_found ), HydrusTime.TimeDeltaToPrettyTimeDelta( death_time_delta, no_bigger_than_days = True ) ) )
else:

View File

@ -1886,7 +1886,7 @@ class MediaSingleton( Media ):
if len( modified_timestamp_lines ) > 1:
lines.append( ( False, ( 'all modified dates', modified_timestamp_lines ) ) )
lines.append( ( False, ( 'all modified times', modified_timestamp_lines ) ) )

View File

@ -100,7 +100,7 @@ options = {}
# Misc
NETWORK_VERSION = 20
SOFTWARE_VERSION = 525
SOFTWARE_VERSION = 526
CLIENT_API_VERSION = 44
SERVER_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -516,6 +516,11 @@ class SerialisableDictionary( SerialisableBase, dict ):
key = ConvertMetaSerialisableTupleToObject( meta_key )
if isinstance( key, list ):
key = tuple( key )
value = ConvertMetaSerialisableTupleToObject( meta_value )
except HydrusExceptions.SerialisationException as e:

View File

@ -130,16 +130,29 @@ def CalendarDeltaToDateTime( years : int, months : int, days : int, hours : int
result = now - day_and_hour_delta
while months > result.month:
years += 1
months -= 12
new_year = result.year - years
new_month = result.month - months
dayrange = calendar.monthrange( new_year, new_month )
while new_month < 1:
new_year -= 1
new_month += 12
while new_month > 12:
new_year += 1
new_month -= 12
try:
dayrange = calendar.monthrange( new_year, new_month )
except:
dayrange = ( 0, 30 )
new_day = min( dayrange[1], result.day )
@ -160,7 +173,7 @@ def CalendarDeltaToRoughDateTimeTimeDelta( years : int, months : int, days : int
return datetime.timedelta( days = days + ( months * ( 365.25 / 12 ) ) + ( years * 365.25 ), hours = hours )
def TimeDeltaToPrettyTimeDelta( seconds, show_seconds = True ):
def TimeDeltaToPrettyTimeDelta( seconds, show_seconds = True, no_bigger_than_days = False ):
if seconds is None:
@ -189,8 +202,12 @@ def TimeDeltaToPrettyTimeDelta( seconds, show_seconds = True ):
lines = []
lines.append( ( 'year', YEAR ) )
lines.append( ( 'month', MONTH ) )
if not no_bigger_than_days:
lines.append( ( 'year', YEAR ) )
lines.append( ( 'month', MONTH ) )
lines.append( ( 'day', DAY ) )
lines.append( ( 'hour', HOUR ) )
lines.append( ( 'minute', MINUTE ) )

View File

@ -2051,12 +2051,12 @@ class TestTagObjects( unittest.TestCase ):
( 'system:modified time: since 1 day ago', "system:modified date < 1 day" ),
( 'system:modified time: since 1 day ago', "system:modified time < 1 day" ),
( 'system:modified time: since 1 month 1 day ago', "system:date modified < 0 years 1 month 1 day 1 hour" ),
( 'system:last view time: since 7 years 45 days ago', "system:last viewed time < 7 years 45 days 700h" ),
( 'system:last view time: since 7 years 45 days ago', "system:last viewed date < 7 years 45 days 700h" ),
( 'system:last view time: since 7 years 45 days ago', "system:last view time < 7 years 45 days 700h" ),
( 'system:last view time: since 7 years 45 days ago', "system:last view date < 7 years 45 days 700h" ),
( 'system:last view time: since 7 years 45 days ago', "system:time last viewed < 7 years 45 days 700h" ),
( 'system:last view time: since 7 years 45 days ago', "system:date last viewed < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:last viewed time < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:last viewed date < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:last view time < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:last view date < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:time last viewed < 7 years 45 days 700h" ),
( 'system:last viewed time: since 7 years 45 days ago', "system:date last viewed < 7 years 45 days 700h" ),
( 'system:import time: since 7 years 45 days ago', "system:time_imported < 7 years 45 days 700h" ),
( 'system:import time: since 2011-06-04', "system:time imported > 2011-06-04" ),
( 'system:import time: before 7 years 2 months ago', "system:time imported > 7 years 2 months" ),

View File

@ -68,6 +68,7 @@ from hydrus.test import TestHydrusSerialisable
from hydrus.test import TestHydrusServer
from hydrus.test import TestHydrusSessions
from hydrus.test import TestHydrusTags
from hydrus.test import TestHydrusTime
from hydrus.test import TestServerDB
DB_DIR = None
@ -784,6 +785,7 @@ class Controller( object ):
TestClientDBDuplicates,
TestClientDBTags,
TestHydrusData,
TestHydrusTime,
TestHydrusNATPunch,
TestClientNetworking,
TestHydrusNetworking,
@ -818,6 +820,7 @@ class Controller( object ):
TestFunctions,
TestHydrusData,
TestHydrusTags,
TestHydrusTime,
TestHydrusSerialisable,
TestHydrusSessions
]

View File

@ -0,0 +1,19 @@
import unittest
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusTime
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientData
from hydrus.client.metadata import ClientTags
class TestHydrusTime( unittest.TestCase ):
def test_quick( self ):
self.assertEqual( HydrusTime.TimeDeltaToPrettyTimeDelta( 86400 * 5 ), '5 days' )
self.assertIn( 'month', HydrusTime.TimeDeltaToPrettyTimeDelta( 86400 * 35 ) )
self.assertEqual( HydrusTime.TimeDeltaToPrettyTimeDelta( 86400 * 35, no_bigger_than_days = True ), '35 days' )