Version 254

This commit is contained in:
Hydrus Network Developer 2017-05-03 16:33:48 -05:00
parent f700ad6dc2
commit c1758952a1
40 changed files with 1359 additions and 921 deletions

View File

@ -18,7 +18,7 @@
<li>public tag repository: 4a285629721ca442541ef2c15ea17d1f7f7578b0c3f4f5f2a05f8f0ab297786f@hydrus.no-ip.org:45871</li>
<li>read-only file repository: 8f8a3685abc19e78a92ba61d84a0482b1cfac176fd853f46d93fe437a95e40a5@hydrus.no-ip.org:45872</li>
</ul>
<p><b>Tags are rich, cpu-intensive metadata. My repository has millions of mappings, and your client will download and store them all. It will take a few hundred MB and several <i>hours</i> total processing time to fully synchronise. It will mostly happen in the background, without you noticing. if you close the client, it will continue where it left off when it next boots.</b></p>
<p><b>Tags are rich, cpu-intensive metadata. My repository has millions of mappings, and your client will eventually download and store them all. It will take a few hundred MB and some <i>hours</i> total processing time to fully synchronise. It will mostly happen in the background, without you noticing--for most users, it takes a week or more to quietly catch up. If you interrupt its processing maintenance, it will continue where it left off the next time it runs.</b></p>
</div>
</body>
</html>

View File

@ -55,14 +55,8 @@
<p>There are two kinds of shortcuts in the program--<i>reserved</i>, which have fixed names, are undeletable, and are always active in certain contexts (related to their name), and <i>custom</i>, which you create and name and edit and are only active in a media viewer when you want them to. You can redefine some simple shortcut commands, but most importantly, you can create shortcuts for adding/removing a tag or setting/unsetting a rating.</p>
<p>Use the same 'keyboard' icon to set the current and default custom shortcuts. </p>
<h3>finding duplicates</h3>
<p>system:similar_to takes two arguments: a hash and an integer representing max hamming distance (0 means exactly the same, 64 means everything. 5 is good for finding dupes). You can quick-select it from a file's right-click menu. It returns all images that are <i>very</i> similar to the hash. For example:</p>
<p>Here are a couple of duplicates, found despite their different resolution.</p>
<p><i>system:similar_to</i> lets you run the duplicates processing page's searches manually. You can either insert the hash and hamming distance manually, or you can launch these searches automatically from the thumbnail <i>right-click->find similar files</i> menu. For example:</p>
<p><img src="similar_gununu.png" /></p>
<p>And some images of similar shape but not colour.</p>
<p><img src="similar_icons.png" /></p>
<p>If you are careful, you can find images that look only <i>somewhat</i> like your hash. You get a lot of false positives with hamming distance of much more than 12, though.</p>
<p><img src="similar_gununus.png" /></p>
<p>I will, <i>sometime</i>, write an algorithm that says 'show me all the dupes currently in the database'.</p>
<h3>PIL errors</h3>
<p>At some point, you will probably encounter a PIL error when importing a file. PIL is the Python Image Library, the code I use to manipulate image files. Some files are kooky, and just won't load with it. I can't fix these errors, since PIL is not mine. Just gotta deal with it.</p>
<p>If the PIL error'ing file is one you particularly care about, I suggest you import it into photoshop or similar and save it again. Photoshop should be clever enough to parse the file's weirdness, and then it'll hopefully save again to a simpler format that PIL, and hence the client, will be able to understand.</p>

View File

@ -8,6 +8,31 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 254</h3></li>
<ul>
<li>the duplicate processing page is now on the new page picker, under the new menu 'special'</li>
<li>improved the dupe pair selection algorithm--it should be faster for everyone</li>
<li>added help icon to dupe processing page--it can launch of a couple of message boxes with simple help, or you can open some html help</li>
<li>wrote some dupe processing html help!</li>
<li>the dupe processing page now refreshes its numbers when the dupe filter closes</li>
<li>the archive, inbox, delete, and undelete buttons on the duplicate filter's top hover frame now work</li>
<li>delete and shift+delete keys now work to delete/undelete a file in the duplicate filter</li>
<li>the mouse cursor now hides/shows on halt and new movement on the duplicate filter</li>
<li>you can now set 'content' shortcut actions (adding tags or ratings) to shortcuts in the 'media' reserved shortcut set (which are always on in media contexts), although they don't yet work in the thumbnail view yet</li>
<li>added a help button to the manage shortcuts panel</li>
<li>the edit shortcuts dialog will now now allow you to name a custom shortcut set to one of the reserved names and will explain the problem in a message box</li>
<li>import pages update more efficiently during periods of busy cpu</li>
<li>import pages will use less idle cpu time generally</li>
<li>import pages will adaptively use less cpu time when they are in the undo deletion queue or have no files to import</li>
<li>the page of images downloader will spam a little less idle time</li>
<li>the canvas background details and the top-right hover window will now only show 10 urls max</li>
<li>added a help button to the tag import options panel to better explain the namespace selection and explicit tags</li>
<li>added 'gui report mode' to the help menu, which will report key and mouse shortcut events and and matched commands</li>
<li>the adminside petition panel now sorts multiple petitions by number of files descending</li>
<li>misc improvements</li>
<li>misc help cleanup</li>
<li>misc fixes</li>
</ul>
<li><h3>version 253</h3></li>
<ul>
<li>created a new object to hold tag and rating merging and 'worse file' deletion options</li>

BIN
help/dupe_alternate_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
help/dupe_alternate_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
help/dupe_alternate_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
help/dupe_better_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
help/dupe_better_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
help/dupe_exact_match_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
help/dupe_exact_match_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
help/dupe_garbage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
help/dupe_icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
help/dupe_management.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
help/dupe_mug_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
help/dupe_mug_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
help/dupe_not_dupes_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
help/dupe_not_dupes_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

132
help/duplicates.html Normal file
View File

@ -0,0 +1,132 @@
<html>
<head>
<title>duplicates</title>
<link href="hydrus.ico" rel="shortcut icon" />
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="content">
<h3>duplicates</h3>
<p>As files are shared on the internet, they are often resized, cropped, converted to a different format, subsequently altered by the original or a new artist, or turned into a template and reinterpreted over and over and over. Even if you have a very restrictive importing workflow, your client is almost certainly going to get some <i>duplicates</i>. Some will be interesting alternate versions that you want to keep, and others will be thumbnails and other low-quality garbage you accidentally imported and would rather delete. Along the way, it would be nice to harmonise your ratings and tags to the better files so you don't lose any work.</p>
<p>Finding and processing duplicates within a large collection is impossible to do by hand, so I have written a system to do the heavy lifting for you. It is all on--</p>
<h3>the duplicates processing page</h3>
<p>On the normal 'new page' selection window, hit <i>special->duplicates processing</i>. This will open this page:</p>
<p><img src="dupe_management.png" /></p>
<p>There are three steps to this page:</p>
<ul>
<li>
<h3>preparation</h3>
<p>It takes a lot of CPU time to search for duplicates. The client has to generate difficult 'looks like' metadata about each file and then insert that into a complicated search tree, which itself must be carefully rebalanced to stay fast.</p>
<p>Thankfully, this first section is simple to use: if it tells you it wants to do work, then just hit the play buttons and leave the client alone for a bit--it will go down to the database and iterate through as many files and tree branches it needs to to make sure it can search efficiently.</p>
<p>This section doesn't usually need to do much work, but if it does, you should not try to do anything else with the client while it does its job--it will likely be burning 100% CPU, and it will aggressively lock the database in order to run as fast as possible. If you then try to run some normal search pages or tag some files on different pages in the client, the whole thing is likely to hang, and then you'll have to wait five or ten minutes (or eight hours, if it really does need to do a lot of work!) for it to all free up again. If you need to do some other work or shut down the client, just hit the stop button to pause its progress--you can continue where you left off whenever you like.</p>
<p>A future version of this will be neater and less rude, but this is all a first version, so please bear with it. If you end up using the duplicate filter regularly, you can set this heavy work to occur automatically in your regular idle/shutdown maintenance cycles from the cog icon on the same page.</p>
</li>
<li>
<h3>discovery</h3>
<p>Once the database is ready to search, you actually have to do it! You can set a 'search distance', which represents how 'fuzzy' or imprecise a match the database will consider a duplicate. I recommend you start with 'exact match', which looks for files that are as similar as it can understand. The smaller the search distance, the faster and better and fewer the results will be. I do not recommend you go above 8--the 'speculative' option--as you will be inundated with false positives.</p>
<p>Like the preparation step, this is very CPU intensive and will lock your db. Either leave it alone while it works or let the client handle everything automatically during idle time.</p>
<p>If you are interested, the current version of this system uses a <i>phash</i> (a 64-bit binary string 'perceptual hash' based on whether the values of an 8x8 DCT of a 32x32 greyscale version of the image are above or below the average value) to represent the image shape and a VPTree to search different files' phashes' relative <a href="https://en.wikipedia.org/wiki/Hamming_distance">hamming distance</a>. I expect to extend it in future with multiple phash generation (flips, rotations, crops on interesting parts of the image) and most-common colour comparisons.</p>
</li>
<li>
<h3>processing</h3>
<p>After you have searched your files, you should have a few dozen to a few ten-thousand 'potential' pairs. The number may be frighteningly high, but you will be able to cut it down quicker than you expect. There are several mathematical optimisations at the database level that can use one of your decisions to resolve multiple unknown relationships.</p>
<p>If you like, you can review some of these groups of potential pairs as thumbnails by hitting the 'show some random pairs' button. It is often surprising and interesting to discover what it has found.</p>
<p>You can do some manual filtering on these thumbnails--merging tags and deleting bad quality files, or even setting duplicate statuses manually through the right-click menu (PROTIP: This last bit isn't done yet!)--but like archiving and deleting from your inbox, this is much more quickly done through a specialised filter:</p>
</li>
</ul>
<h3>the duplicates filter</h3>
<p>Just like the archive/delete filter, this uses quick mouse-clicks or keyboard shortcuts to assign pairs of potential duplicates a particular new status that is saved back to the database. Depending on the status, different tag and rating and deletion actions will occur.</p>
<p>The system uses pairs because they are the simplest building block of the underlying network of similar files. Two similar files, A and B, have one relationship, <i>A-B</i>, but three similar files would have three: <i>A-B, B-C, and A-C</i>. Larger groups can get very complicated. Making decisions on just two files at a time is fast and easy, leaving the database to handle the difficult implications.</p>
<p>So, the filter works just like a normal media viewer window, except that it only ever presents two files at a time to scroll through. You can set shortcuts for any action, but by default, it uses:</p>
<ul>
<li>
<p>Left-click or space: <b>The files are dupes and the one I am looking at is better than the other.</b></p>
</li>
<li>
<p>Right-click: <b>The files are not dupes but <i>alternates</i>.</b></p>
</li>
<li>
<p>Middle-click: <b>Go back one decision.</b></p>
</li>
<li>
<p>Enter/Escape: <b>Stop filtering.</b></p>
</li>
</ul>
<p>The idea is to compare the two files by scrolling with your mouse wheel and then clicking to assign a status, at which point the next pair will be loaded. If you prefer different shortcuts, you can set them under <i>file->shortcuts</i> or the keyboard icon on the duplicate filter's top hover window. You can also access more 'duplicate decisions' through the labelled buttons and change what happens to the files and their tags and ratings on each different decision through the cog icon on the same top hover window.</p>
<p><a href="dupe_icons.png"><img src="dupe_icons.png" /></a></p>
<p><i>Move your move to the top of the media viewer to bring up the top hover window. Hit the cog or keyboard icons to edit how it works, and click the buttons if you do not have a shortcut mapped. 'Custom action' lets you one of the other four actions but with one-off content merge options--say if you want to set that files are alternate but still with to merge some tags.</i></p>
<p>Because of technical limitations, you will be asked to checkpoint (save your progress to the database and then continue) every fifty decisions or so.</p>
<h3>different duplicate statuses</h3>
<p>There are currently five possible statuses. The client uses different logic to apply them at the database level, so please treat them as described and not a different scheme.</p>
<ul>
<li>
<h3>potential</h3>
<p>This is the default state for new pairs the client thinks might be duplicates. It is essentially an 'unknown' state and represents the pool of pairs the client would like you, the human, to look at and make decisions about.</p>
<p>The duplicates filter shows these so you can filter them into the other four categories.</p>
</li>
<li>
<h3>better/worse</h3>
<p>This tells the client that the pair of files represent the exact same thing--except that the one you are looking for is 'better' in some way. What that means is up to you, but for most people this generally means:</p>
<ul>
<li>higher resolution</li>
<li>better image quality</li>
<li>png over jpg for screenshots</li>
<li>jpg over png for busy images</li>
<li>a slightly better crop</li>
<li>no watermark or other site-frame or undesired blemish</li>
<li>has been tagged by other people, so is likely to be the more 'popular'</li>
</ul>
<p>However these are not hard rules--sometimes a file has a larger resolution or filesize due to a bad upscaling or encoding decision by the person who 'reinterpreted' it. You really have to look at it and decide for yourself.</p>
<p>Here is a good example of a better/worse pair:</p>
<p><a href="dupe_better_1.png"><img src="dupe_better_1.png" /></a></p>
<p><a href="dupe_better_2.jpg"><img src="dupe_better_2.jpg" /></a></p>
<p>The first image is better because it is a png (pixel-perfect pngs are always better than jpgs for screenshots of applications--note how obvious the jpg's encoding artifacts are on the flat colour background) and it has a slightly higher (original) resolution, making it less blurry. I presume the second went through some FunnyJunk-tier trash meme site to get automatically cropped to 960px height and converted to the significantly smaller jpeg. Whatever happened, we do not care about being able to fit our images into monetised social media <i>&lt;div&gt;</i> elements nor about consuming a few more KB on our hard drives, so let's keep the first and drop the second.</p>
<p>The default action on setting a better/worse pair is to move all <i>local tags</i> from the worse file to the best (i.e. adding them to the better file and then deleting them from the worse) and then sending the worse file to the trash.</p>
</li>
<li>
<h3>exact duplicates</h3>
<p>This is the same as better/worse, except that you absolutely cannot discern a better file. This is typically a rare decision.</p>
<p>Here are two exact matches:</p>
<p><a href="dupe_exact_match_1.png"><img src="dupe_exact_match_1.png" /></a></p>
<p><a href="dupe_exact_match_2.png"><img src="dupe_exact_match_2.png" /></a></p>
<p>I cannot tell any difference, although the filesize is significantly different, so I suspect the smaller is a lossless png optimisation. Many of the big content providers--Facebook, Google, Clouflare--automatically 'optimise' the data that goes through their networks in order to save bandwidth. With pngs it is usually mostly harmless, but jpegs are often a slaughterhouse.</p>
<p>Given the filesize, you might decide that these are actually a better/worse pair--but if the larger image had tags and was the 'canonical' version on most boorus, the decision might not be so clear. Sometimes you just want to keep both without a firm decision on which is best, in which case you can just set this 'exact duplicates' status and move on.</p>
<p>The default action on setting an exact duplicates pair is to copy all <i>local tags</i> between the two files in both directions.</p>
</li>
<li>
<h3>alternates</h3>
<p>This tells the client the pair of files are not exactly the same but that one is an alteration of the other or they are both descended from a common original. Again, the precise definition is up to you, but it generally means something like:</p>
<ul>
<li>the files are recolours</li>
<li>the files are alternate versions of the same image produced by the same or different artists (e.g. clean/messy or with/without hair ribbon)</li>
<li>iterations on a close template</li>
<li>different versions of a file's progress, such as the steps from the initial draft sketch to a final shaded version</li>
</ul>
<p>Here are two alternate files:</p>
<p><a href="dupe_mug_1.png"><img src="dupe_mug_1.png" /></a></p>
<p><a href="dupe_mug_2.png"><img src="dupe_mug_2.png" /></a></p>
<p>Though they are mostly similar, they are not duplicates. If one is not an edit of the other, they are certainly children of the same original.</p>
<p>Here are three files you might or might not consider to be alternates:</p>
<p><a href="dupe_alternate_1.jpg"><img src="dupe_alternate_1.jpg" /></a></p>
<p><a href="dupe_alternate_2.jpg"><img src="dupe_alternate_2.jpg" /></a></p>
<p><a href="dupe_alternate_3.jpg"><img src="dupe_alternate_3.jpg" /></a></p>
<p>These are all based on the same template--which is why the dupe filter found them--but they are not so closely related, and the last one is joking about a different ideology entirely and might deserve to be in its own group. Ultimately, you might prefer just to give them some shared tag and consider them not alternates <i>per se</i>.</p>
<p>The default action here is to do nothing but record the alternate status. A future version of the client will support revisiting the large unsorted archive you build here and adding file relationship metadata, but creating that will be a complicated job that was not in the scope of this initial duplicate management system.</p>
</li>
<li>
<h3>not duplicates</h3>
<p>The duplicate finder sometimes has false positives, so this status is to tell the client that the potential pair are actually not duplicates of any kind. This usually happens when two images have a similar shape by accident.</p>
<p>Here are two such files:</p>
<p><a href="dupe_not_dupes_1.png"><img style="max-width: 100%;" src="dupe_not_dupes_1.png" /></a></p>
<p><a href="dupe_not_dupes_2.jpg"><img style="max-width: 100%;" src="dupe_not_dupes_2.jpg" /></a></p>
<p>Despite their similarity, they are neither duplicates nor of even the same topic. The only commonality is the medium. I would not consider them close enough to be alternates--just adding something like 'screenshot' and 'imageboard' as tags to both is probably the closest connection they have.</p>
<p>The default action here is obviously to do nothing but record the status and move on.</p>
<p>The incidence of false positives increases as you broaden the search distance--the less precise your search, the less likely it is to be correct. At distance 14, these files all match, but uselessly:</p>
<p><a href="dupe_garbage.png"><img style="max-width: 100%;" src="dupe_garbage.png" /></a></p>
</li>
</ul>
<h3>the future</h3>
<p>This only supports jpgs and pngs at the moment, but I will attempt to add video in a future iteration. And as I said above, I would like to add more search algorithms beyond this first <i>phash</i> system, and there is plenty of db and gui stuff to add to provide support for 'this image has a parent'-type notification and navigation actions for alternates.</p>
</div>
</body>
</html>

View File

@ -6,7 +6,6 @@
</head>
<body>
<div class="content">
<p><a href="getting_started_tags.html"><--- Back to tags</a></p>
<p class="warning">Do not try to create a subscription until you are comfortable with a normal gallery download page! If you haven't downloaded anything yet, go <i>F9->download->gallery->[your preference]</i> and run some searches, just so you get a feel before you make a mistake here.</p>
<h3>what are subs?</h3>
<p>Subscriptions are a way of telling the client to quietly and regularly repeat a gallery search. The client will sync with the gallery and download any new files behind the scenes, just as if you were running the download yourself.</p>
@ -35,7 +34,6 @@
<p>So, I suggest you start with artist searches to begin with. These usually top out at about 1,000 files total and a handful of new files every week/month, and also hence don't take all that long. Once you are more confident, try doing multiple-tag queries. I suggest you leave simple single-tag queries for the manual download page, where you can hit 'that's enough' yourself.</p>
<h3>help! it won't stop!</h3>
<p>If you <i>do</i> put in a huge search, and the 'found x new files for subscription y' message is climbing terrifyingly higher and higher with no end in sight, just hit the pause button on the popup. You can also pause all current subscriptions from even starting at <i>services->pause->subscriptions synchronisation</i>. Then you can go back into the dialog and remove or edit at your own pace.</p>
<p class="right"><a href="index.html">Go back to the index ---></a></p>
</div>
</body>
</html>

View File

@ -55,7 +55,6 @@
<p>The namespaces listed are those that hydrus knows how to parse from the gallery site or wherever you are downloading files from. Selecting one will tell hydrus to get those tags and set/pend them to the respective tag service.</p>
<p>You can quickly get thousands of tags in a few minutes this way!</p>
<p class="right"><a href="getting_started_ratings.html">Read about ratings ---></a></p>
<p class="right"><a href="getting_started_subscriptions.html">Read about subscriptions ---></a></p>
<p class="right"><a href="index.html">Go back to the index ---></a></p>
</div>
</body>

View File

@ -19,29 +19,42 @@
<h3>hydrus help</h3>
<p>Although I try to make hydrus's interface simple, some of the things it does are quite complicated. Please read the introduction and skim the simple getting started guides at the least. If you like, you can revisit the more complicated topics later, once you are experienced in the basics.</p>
<ul>
<li><a href="introduction.html">introduction and statement of principles</a></li>
<li><a href="getting_started_installing.html">installing and updating</a></li>
<li><a href="getting_started_files.html">getting started with files</a></li>
<li><a href="getting_started_more_files.html">more getting started with files</a></li>
<li><a href="getting_started_tags.html">getting started with tags</a></li>
<li><a href="getting_started_ratings.html">getting started with ratings</a></li>
<li><a href="getting_started_subscriptions.html">getting started with subscriptions</a></li>
<li><a href="getting_started_ipfs.html">getting started with ipfs</a></li>
<li><a href="getting_started_local_booru.html">getting started with the local booru</a></li>
<li><a href="reducing_lag.html">reducing program lag</a></li>
<li><a href="access_keys.html">access keys to my server</a></li>
<li><a href="tagging_schema.html">thoughts on a public tagging schema</a></li>
<li><a href="advanced.html">advanced usage - general</a></li>
<li><a href="advanced_siblings.html">advanced usage - tag siblings</a></li>
<li><a href="advanced_parents.html">advanced usage - tag parents</a></li>
<li><a href="privacy.html">privacy</a></li>
<li><a href="server.html">setting up your own server</a></li>
<li><a href="wine.html">running a client or server in wine</a></li>
<li><a href="running_from_source.html">running a client or server from source</a></li>
<li><a href="contact.html">developer contact and links</a></li>
<li><a href="support.html">financial support</a></li>
<li><a href="faq.html">faq</a></li>
<li><a href="changelog.html">changelog</a></li>
<li><h3>starting out</h3></li>
<ul>
<li><a href="introduction.html">introduction and statement of principles</a></li>
<li><a href="getting_started_installing.html">installing and updating</a></li>
<li><a href="getting_started_files.html">getting started with files</a></li>
<li><a href="getting_started_tags.html">getting started with tags</a></li>
<li><a href="getting_started_ratings.html">getting started with ratings</a></li>
<li><a href="access_keys.html">access keys to my server</a></li>
</ul>
<li><h3>the next step</h3></li>
<ul>
<li><a href="getting_started_more_files.html">more getting started with files</a></li>
<li><a href="tagging_schema.html">thoughts on a public tagging schema</a></li>
<li><a href="getting_started_subscriptions.html">getting started with subscriptions</a></li>
<li><a href="duplicates.html">filtering duplicates</a></li>
<li><a href="reducing_lag.html">reducing program lag</a></li>
</ul>
<li><h3>advanced usage</h3></li>
<ul>
<li><a href="advanced.html">advanced usage - general</a></li>
<li><a href="advanced_siblings.html">advanced usage - tag siblings</a></li>
<li><a href="advanced_parents.html">advanced usage - tag parents</a></li>
<li><a href="ipfs.html">ipfs</a></li>
<li><a href="local_booru.html">the local booru</a></li>
<li><a href="server.html">setting up your own server</a></li>
<li><a href="wine.html">running a client or server in wine</a></li>
<li><a href="running_from_source.html">running a client or server from source</a></li>
</ul>
<li><h3>misc</h3></li>
<ul>
<li><a href="privacy.html">privacy</a></li>
<li><a href="contact.html">developer contact and links</a></li>
<li><a href="support.html">financial support</a></li>
<li><a href="faq.html">faq</a></li>
<li><a href="changelog.html">changelog</a></li>
</ul>
</ul>
</body>
</html>

View File

@ -1,6 +1,6 @@
<html>
<head>
<title>getting started - ipfs</title>
<title>ipfs</title>
<link href="hydrus.ico" rel="shortcut icon" />
<link href="style.css" rel="stylesheet" type="text/css" />
</head>

View File

@ -1,6 +1,6 @@
<html>
<head>
<title>getting started - local booru</title>
<title>local booru</title>
<link href="hydrus.ico" rel="shortcut icon" />
<link href="style.css" rel="stylesheet" type="text/css" />
</head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@ -80,7 +80,10 @@ def BuildSimpleChildrenToParents( pairs ):
for ( child, parent ) in pairs:
if child == parent: continue
if child == parent:
continue
if LoopInSimpleChildrenToParents( simple_children_to_parents, child, parent ): continue
@ -2191,6 +2194,11 @@ class ShortcutsManager( object ):
if command is not None:
if HydrusGlobals.gui_report_mode:
HydrusData.ShowText( 'command matched: ' + repr( command ) )
return command

View File

@ -808,11 +808,11 @@ class Controller( HydrusController.HydrusController ):
wx.CallAfter( self.ProcessPubSub )
def PageDeleted( self, page_key ):
def PageCompletelyDestroyed( self, page_key ):
try:
return self._gui.PageDeleted( page_key )
return self._gui.PageCompletelyDestroyed( page_key )
except wx.PyDeadObjectError:
@ -820,9 +820,9 @@ class Controller( HydrusController.HydrusController ):
def PageHidden( self, page_key ):
def PageClosedButNotDestroyed( self, page_key ):
return self._gui.PageHidden( page_key )
return self._gui.PageClosedButNotDestroyed( page_key )
def PopupMenu( self, window, menu ):

View File

@ -1079,6 +1079,11 @@ class DB( HydrusDB.HydrusDB ):
if len( pairs_of_hash_ids ) >= MAX_BATCH_SIZE:
break
hash_ids_to_hashes = self._GetHashIdsToHashes( seen_hash_ids )

View File

@ -513,6 +513,11 @@ class ApplicationCommand( HydrusSerialisable.SerialisableBase ):
return cmp( self.ToString(), other.ToString() )
def __repr__( self ):
return self.ToString()
def _GetSerialisableInfo( self ):
if self._command_type == CC.APPLICATION_COMMAND_TYPE_SIMPLE:
@ -2110,6 +2115,11 @@ def ConvertKeyEventToShortcut( event ):
shortcut = Shortcut( CC.SHORTCUT_TYPE_KEYBOARD, key, modifiers )
if HydrusGlobals.gui_report_mode:
HydrusData.ShowText( 'key event caught: ' + repr( shortcut ) )
return shortcut
@ -2173,6 +2183,11 @@ def ConvertMouseEventToShortcut( event ):
shortcut = Shortcut( CC.SHORTCUT_TYPE_MOUSE, key, modifiers )
if HydrusGlobals.gui_report_mode:
HydrusData.ShowText( 'mouse event caught: ' + repr( shortcut ) )
return shortcut

View File

@ -98,6 +98,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
self._controller.sub( self, 'ClearClosedPages', 'clear_closed_pages' )
self._controller.sub( self, 'NewCompose', 'new_compose_frame' )
self._controller.sub( self, 'NewPageDuplicateFilter', 'new_duplicate_filter' )
self._controller.sub( self, 'NewPageImportBooru', 'new_import_booru' )
self._controller.sub( self, 'NewPageImportGallery', 'new_import_gallery' )
self._controller.sub( self, 'NewPageImportHDD', 'new_hdd_import' )
@ -1062,10 +1063,6 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
ClientGUIMenus.AppendMenuItem( self, search_menu, service.GetName(), 'Open a new search tab for ' + service.GetName() + '.', self._NewPageQuery, service.GetServiceKey() )
ClientGUIMenus.AppendSeparator( search_menu )
ClientGUIMenus.AppendMenuItem( self, search_menu, 'duplicates (under construction!)', 'Open a new tab to discover and filter duplicate files.', self._NewPageDuplicateFilter )
ClientGUIMenus.AppendMenu( menu, search_menu, 'new search page' )
#
@ -1121,6 +1118,8 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
ClientGUIMenus.AppendMenu( download_menu, gallery_menu, 'gallery' )
ClientGUIMenus.AppendMenu( menu, download_menu, 'new download page' )
#
download_popup_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, download_popup_menu, 'a youtube video', 'Enter a YouTube URL and choose which formats you would like to download', self._StartYoutubeDownload )
@ -1136,6 +1135,14 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
#
special_menu = wx.Menu()
ClientGUIMenus.AppendMenuItem( self, special_menu, 'duplicates processing', 'Open a new tab to discover and filter duplicate files.', self._NewPageDuplicateFilter )
ClientGUIMenus.AppendMenu( menu, special_menu, 'new special page' )
#
return ( menu, p( '&Pages' ), True )
@ -1414,6 +1421,7 @@ class FrameGUI( ClientGUITopLevelWindows.FrameThatResizes ):
ClientGUIMenus.AppendMenuItem( self, debug, 'make a popup in five seconds', 'Throw a delayed popup at the message manager, giving you time to minimise or otherwise alter the client before it arrives.', wx.CallLater, 5000, HydrusData.ShowText, 'This is a delayed popup message.' )
ClientGUIMenus.AppendMenuCheckItem( self, debug, 'db report mode', 'Have the db report query information, where supported.', HydrusGlobals.db_report_mode, self._SwitchBoolean, 'db_report_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug, 'db profile mode', 'Run detailed \'profiles\' on every database query and dump this information to the log (this is very useful for hydrus dev to have, if something is running slow for you!).', HydrusGlobals.db_profile_mode, self._SwitchBoolean, 'db_profile_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug, 'gui report mode', 'Have the gui report inside information, where supported.', HydrusGlobals.gui_report_mode, self._SwitchBoolean, 'gui_report_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug, 'pubsub profile mode', 'Run detailed \'profiles\' on every internal publisher/subscriber message and dump this information to the log. This can hammer your log with dozens of large dumps every second. Don\'t run it unless you know you need to.', HydrusGlobals.pubsub_profile_mode, self._SwitchBoolean, 'pubsub_profile_mode' )
ClientGUIMenus.AppendMenuCheckItem( self, debug, 'force idle mode', 'Make the client consider itself idle and fire all maintenance routines right now. This may hang the gui for a while.', HydrusGlobals.force_idle_mode, self._SwitchBoolean, 'force_idle_mode' )
ClientGUIMenus.AppendMenuItem( self, debug, 'force a gui layout now', 'Tell the gui to relayout--useful to test some gui bootup layout issues.', self.Layout )
@ -2494,6 +2502,10 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
HydrusGlobals.db_profile_mode = not HydrusGlobals.db_profile_mode
elif name == 'gui_report_mode':
HydrusGlobals.gui_report_mode = not HydrusGlobals.gui_report_mode
elif name == 'pubsub_profile_mode':
HydrusGlobals.pubsub_profile_mode = not HydrusGlobals.pubsub_profile_mode
@ -3104,6 +3116,11 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._ImportFiles( paths )
def NewPageDuplicateFilter( self ):
self._NewPageDuplicateFilter()
def NewPageImportBooru( self ):
self._NewPageImportBooru()
@ -3190,7 +3207,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
self._menu_updater.Update()
def PageDeleted( self, page_key ):
def PageCompletelyDestroyed( self, page_key ):
with self._lock:
@ -3198,7 +3215,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def PageHidden( self, page_key ):
def PageClosedButNotDestroyed( self, page_key ):
with self._lock:

View File

@ -1173,7 +1173,6 @@ class Canvas( wx.Window ):
self._current_zoom = 1.0
self._canvas_zoom = 1.0
self._drag_begin_coordinates = None
self._last_drag_coordinates = None
self._current_drag_is_touch = False
self._last_motion_coordinates = ( 0, 0 )
@ -2004,7 +2003,6 @@ class Canvas( wx.Window ):
( x, y ) = pos
self._drag_begin_coordinates = ( x, y )
self._last_drag_coordinates = ( x, y )
self._current_drag_is_touch = False
@ -2515,6 +2513,8 @@ class CanvasWithDetails( Canvas ):
urls.sort()
urls = urls[ : 10 ]
for url in urls:
parse = urlparse.urlparse( url )
@ -2627,12 +2627,119 @@ class CanvasWithHovers( CanvasWithDetails ):
self._hover_ratings = ClientGUIHoverFrames.FullscreenHoverFrameRatings( self, self._canvas_key )
#
self._timer_cursor_hide = wx.Timer( self, id = ID_TIMER_CURSOR_HIDE )
self.Bind( wx.EVT_TIMER, self.TIMEREventCursorHide, id = ID_TIMER_CURSOR_HIDE )
self.Bind( wx.EVT_MOTION, self.EventDrag )
def _GenerateHoverTopFrame( self ):
raise NotImplementedError()
def EventDrag( self, event ):
CC.CAN_HIDE_MOUSE = True
( x, y ) = event.GetPosition()
show_mouse = self.GetCursor() == wx.StockCursor( wx.CURSOR_ARROW )
is_dragging = event.Dragging() and self._last_drag_coordinates is not None
has_moved = ( x, y ) != self._last_motion_coordinates
if is_dragging:
( old_x, old_y ) = self._last_drag_coordinates
( delta_x, delta_y ) = ( x - old_x, y - old_y )
delta_distance = ( float( delta_x ) ** 2 + float( delta_y ) ** 2 ) ** 0.5
if delta_distance > 0:
if not self._current_drag_is_touch and delta_distance > 50:
# if user is able to generate such a large distance, they are almost certainly touching
self._current_drag_is_touch = True
if HC.PLATFORM_WINDOWS and not self._current_drag_is_touch:
# touch events obviously don't mix with warping well. the touch just warps it back and again and we get a massive delta!
show_mouse = False
self.WarpPointer( old_x, old_y )
else:
show_mouse = True
self._last_drag_coordinates = ( x, y )
( old_delta_x, old_delta_y ) = self._total_drag_delta
self._total_drag_delta = ( old_delta_x + delta_x, old_delta_y + delta_y )
self._DrawCurrentMedia()
elif has_moved:
self._last_motion_coordinates = ( x, y )
show_mouse = True
if show_mouse:
self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT )
else:
self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) )
def TIMEREventCursorHide( self, event ):
try:
if not CC.CAN_HIDE_MOUSE:
return
if HydrusGlobals.client_controller.MenuIsOpen():
self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT )
else:
self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) )
except wx.PyDeadObjectError:
self._timer_cursor_hide.Stop()
except:
self._timer_cursor_hide.Stop()
raise
class CanvasFilterDuplicates( CanvasWithHovers ):
def __init__( self, parent, file_service_key ):
@ -2663,10 +2770,15 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
wx.CallAfter( self._ShowNewPair ) # don't set this until we have a size > (20, 20)!
HydrusGlobals.client_controller.sub( self, 'ProcessContentUpdates', 'content_updates_gui' )
HydrusGlobals.client_controller.sub( self, 'Archive', 'canvas_archive' )
HydrusGlobals.client_controller.sub( self, 'Delete', 'canvas_delete' )
HydrusGlobals.client_controller.sub( self, 'Inbox', 'canvas_inbox' )
HydrusGlobals.client_controller.sub( self, 'Undelete', 'canvas_undelete' )
HydrusGlobals.client_controller.sub( self, 'SwitchMedia', 'canvas_show_next' )
HydrusGlobals.client_controller.sub( self, 'SwitchMedia', 'canvas_show_previous' )
HydrusGlobals.client_controller.sub( self, 'ShowNewPair', 'canvas_show_new_pair' )
def _Close( self ):
@ -2696,6 +2808,8 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
HydrusGlobals.client_controller.pub( 'refresh_dupe_numbers' )
self._closing = True
self.GetParent().Close()
@ -2943,6 +3057,22 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def Archive( self, canvas_key ):
if self._canvas_key == canvas_key:
self._Archive()
def Delete( self, canvas_key ):
if self._canvas_key == canvas_key:
self._Delete()
def EventCharHook( self, event ):
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
@ -2953,7 +3083,15 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
else:
CanvasWithHovers.EventCharHook( self, event )
( modifier, key ) = ClientData.ConvertKeyEventToSimpleTuple( event )
if modifier == wx.ACCEL_NORMAL and key in CC.DELETE_KEYS: self._Delete()
elif modifier == wx.ACCEL_SHIFT and key in CC.DELETE_KEYS: self._Undelete()
elif modifier == wx.ACCEL_CTRL and key == ord( 'C' ): self._CopyFileToClipboard()
else:
CanvasWithHovers.EventCharHook( self, event )
@ -2993,6 +3131,14 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def Inbox( self, canvas_key ):
if self._canvas_key == canvas_key:
self._Inbox()
def ProcessContentUpdates( self, service_keys_to_content_updates ):
def catch_up():
@ -3029,6 +3175,14 @@ class CanvasFilterDuplicates( CanvasWithHovers ):
def Undelete( self, canvas_key ):
if canvas_key == self._canvas_key:
self._Undelete()
class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
def __init__( self, parent, page_key, media_results ):
@ -3040,11 +3194,6 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
self._just_started = True
self._timer_cursor_hide = wx.Timer( self, id = ID_TIMER_CURSOR_HIDE )
self.Bind( wx.EVT_TIMER, self.TIMEREventCursorHide, id = ID_TIMER_CURSOR_HIDE )
self.Bind( wx.EVT_MOTION, self.EventDrag )
self.Bind( wx.EVT_LEFT_DOWN, self.EventDragBegin )
self.Bind( wx.EVT_LEFT_UP, self.EventDragEnd )
@ -3240,75 +3389,6 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
self._Close()
def EventDrag( self, event ):
CC.CAN_HIDE_MOUSE = True
( x, y ) = event.GetPosition()
show_mouse = self.GetCursor() == wx.StockCursor( wx.CURSOR_ARROW )
is_dragging = event.Dragging() and self._last_drag_coordinates is not None
has_moved = ( x, y ) != self._last_motion_coordinates
if is_dragging:
( old_x, old_y ) = self._last_drag_coordinates
( delta_x, delta_y ) = ( x - old_x, y - old_y )
delta_distance = ( float( delta_x ) ** 2 + float( delta_y ) ** 2 ) ** 0.5
if delta_distance > 0:
if not self._current_drag_is_touch and delta_distance > 50:
# if user is able to generate such a large distance, they are almost certainly touching
self._current_drag_is_touch = True
if HC.PLATFORM_WINDOWS and not self._current_drag_is_touch:
# touch events obviously don't mix with warping well. the touch just warps it back and again and we get a massive delta!
show_mouse = False
self.WarpPointer( old_x, old_y )
else:
show_mouse = True
self._last_drag_coordinates = ( x, y )
( old_delta_x, old_delta_y ) = self._total_drag_delta
self._total_drag_delta = ( old_delta_x + delta_x, old_delta_y + delta_y )
self._DrawCurrentMedia()
elif has_moved:
self._last_motion_coordinates = ( x, y )
show_mouse = True
if show_mouse:
self.SetCursor( wx.StockCursor( wx.CURSOR_ARROW ) )
self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT )
else:
self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) )
def EventDragBegin( self, event ):
( x, y ) = event.GetPosition()
@ -3375,36 +3455,6 @@ class CanvasMediaList( ClientMedia.ListeningMediaList, CanvasWithHovers ):
def TIMEREventCursorHide( self, event ):
try:
if not CC.CAN_HIDE_MOUSE:
return
if HydrusGlobals.client_controller.MenuIsOpen():
self._timer_cursor_hide.Start( 800, wx.TIMER_ONE_SHOT )
else:
self.SetCursor( wx.StockCursor( wx.CURSOR_BLANK ) )
except wx.PyDeadObjectError:
self._timer_cursor_hide.Stop()
except:
self._timer_cursor_hide.Stop()
raise
class CanvasMediaListFilterArchiveDelete( CanvasMediaList ):
def __init__( self, parent, page_key, media_results ):

View File

@ -1814,7 +1814,14 @@ class DialogPageChooser( Dialog ):
( entry_type, obj ) = entry
if entry_type == 'menu': button.SetLabelText( obj )
if entry_type == 'menu':
button.SetLabelText( obj )
elif entry_type == 'page_duplicate_filter':
button.SetLabelText( 'duplicates processing' )
elif entry_type in ( 'page_query', 'page_petitions' ):
name = HydrusGlobals.client_controller.GetServicesManager().GetService( obj ).GetName()
@ -1833,9 +1840,18 @@ class DialogPageChooser( Dialog ):
button.SetLabelText( text )
elif entry_type == 'page_import_page_of_images': button.SetLabelText( 'page of images' )
elif entry_type == 'page_import_thread_watcher': button.SetLabelText( 'thread watcher' )
elif entry_type == 'page_import_urls': button.SetLabelText( 'raw urls' )
elif entry_type == 'page_import_page_of_images':
button.SetLabelText( 'page of images' )
elif entry_type == 'page_import_thread_watcher':
button.SetLabelText( 'thread watcher' )
elif entry_type == 'page_import_urls':
button.SetLabelText( 'raw urls' )
button.Show()
@ -1856,6 +1872,10 @@ class DialogPageChooser( Dialog ):
HydrusGlobals.client_controller.pub( 'new_page_query', obj )
elif entry_type == 'page_duplicate_filter':
HydrusGlobals.client_controller.pub( 'new_duplicate_filter' )
elif entry_type == 'page_import_booru':
HydrusGlobals.client_controller.pub( 'new_import_booru' )
@ -1904,6 +1924,8 @@ class DialogPageChooser( Dialog ):
entries.append( ( 'menu', 'petitions' ) )
entries.append( ( 'menu', 'special' ) )
elif menu_keyword == 'files':
file_repos = [ ( 'page_query', service_key ) for service_key in [ service.GetServiceKey() for service in self._services if service.GetServiceType() == HC.FILE_REPOSITORY ] ]
@ -1957,6 +1979,10 @@ class DialogPageChooser( Dialog ):
entries = [ ( 'page_petitions', service_key ) for service_key in self._petition_service_keys ]
elif menu_keyword == 'special':
entries.append( ( 'page_duplicate_filter', None ) )
if len( entries ) <= 4:

View File

@ -737,6 +737,8 @@ class FullscreenHoverFrameRatings( FullscreenHoverFrame ):
urls.sort()
urls = urls[ : 10 ]
if urls != self._last_seen_urls:
self._last_seen_urls = list( urls )

File diff suppressed because it is too large Load Diff

View File

@ -955,7 +955,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
self._OpenExternally()
if action == 'launch_the_archive_delete_filter':
elif action == 'launch_the_archive_delete_filter':
self._ArchiveDeleteFilter()
@ -1313,7 +1313,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def PageHidden( self, page_key ):
if page_key == self._page_key: HydrusGlobals.client_controller.pub( 'preview_changed', self._page_key, None )
if page_key == self._page_key:
HydrusGlobals.client_controller.pub( 'preview_changed', self._page_key, None )
def PageShown( self, page_key ):

View File

@ -8,6 +8,7 @@ import ClientGUIDialogs
import collections
import HydrusConstants as HC
import HydrusData
import os
import wx
import wx.lib.masked.timectrl
import HydrusGlobals
@ -221,9 +222,32 @@ class OptionsPanelTags( OptionsPanel ):
self._service_keys_to_explicit_button_info = {}
self._button_ids_to_service_keys = {}
self._vbox = wx.BoxSizer( wx.VERTICAL )
#
self.SetSizer( self._vbox )
help_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.help, self._ShowHelp )
help_button.SetToolTipString( 'Show help regarding these tag options.' )
self._services_vbox = wx.BoxSizer( wx.VERTICAL )
#
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( help_button, CC.FLAGS_LONE_BUTTON )
vbox.AddF( self._services_vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self.SetSizer( vbox )
def _ShowHelp( self ):
message = 'Here you can select which kinds of tags you would like applied to the files that are imported.'
message += os.linesep * 2
message += 'If this import context can parse tags (such as a gallery downloader, which may provide \'creator\' or \'series\' tags, amongst others), then the namespaces it provides will be listed here with checkboxes--simply check which ones you are interested in for the tag services you want them to be applied to and it will all occur as the importer processes its files.'
message += os.linesep * 2
message += 'You can also set some fixed \'explicit\' tags to be applied to all successful files. For instance, you might want to add something like \'read later\' or \'from my unsorted folder\' or \'pixiv subscription\'.'
wx.MessageBox( message )
def EventChecked( self, event ):
@ -282,7 +306,7 @@ class OptionsPanelTags( OptionsPanel ):
self._service_keys_to_explicit_button_info = {}
self._button_ids_to_service_keys = {}
self._vbox.Clear( True )
self._services_vbox.Clear( True )
services = HydrusGlobals.client_controller.GetServicesManager().GetServices( HC.TAG_SERVICES, randomised = False )
@ -341,7 +365,7 @@ class OptionsPanelTags( OptionsPanel ):
outer_gridbox.AddF( vbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._vbox.AddF( outer_gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._services_vbox.AddF( outer_gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )

View File

@ -3735,6 +3735,9 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
help_button = ClientGUICommon.BetterBitmapButton( self, CC.GlobalBMPs.help, self._ShowHelp )
help_button.SetToolTipString( 'Show help regarding editing shortcuts.' )
reserved_panel = ClientGUICommon.StaticBox( self, 'reserved' )
self._reserved_shortcuts = ClientGUICommon.SaneListCtrlForSingleObject( reserved_panel, 180, [ ( 'name', -1 ), ( 'size', 100 ) ], activation_callback = self._EditReserved )
@ -3798,23 +3801,13 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
vbox = wx.BoxSizer( wx.VERTICAL )
vbox.AddF( help_button, CC.FLAGS_LONE_BUTTON )
vbox.AddF( reserved_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
vbox.AddF( custom_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
self.SetSizer( vbox )
def _GetTuples( self, shortcuts ):
name = shortcuts.GetName()
size = len( shortcuts )
display_tuple = ( name, HydrusData.ConvertIntToPrettyString( size ) )
sort_tuple = ( name, size )
return ( display_tuple, sort_tuple )
def _Add( self ):
shortcuts = ClientData.Shortcuts( 'new shortcuts' )
@ -3907,6 +3900,36 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
def _GetTuples( self, shortcuts ):
name = shortcuts.GetName()
size = len( shortcuts )
display_tuple = ( name, HydrusData.ConvertIntToPrettyString( size ) )
sort_tuple = ( name, size )
return ( display_tuple, sort_tuple )
def _ShowHelp( self ):
message = 'I am in the process of converting the multiple old messy shortcut systems to this single unified engine. Many actions are not yet available here, and mouse support is very limited. I expect to overwrite the reserved shortcut sets back to (new and expanded) defaults at least once more, so don\'t remap everything yet unless you are ok with doing it again.'
message += os.linesep * 2
message += '---'
message += os.linesep * 2
message += 'In hydrus, shortcuts are split into different sets that are active in different contexts. Depending on where the program focus is, multiple sets can be active at the same time. On a keyboard or mouse event, the active sets will be consulted one after another (typically from the smallest and most precise focus to the largest and broadest parent) until an action match is found.'
message += os.linesep * 2
message += 'There are two kinds--\'reserved\' and \'custom\':'
message += os.linesep * 2
message += 'Reserved shortcuts are always active in their contexts--the \'main_gui\' one is always consulted when you hit a key on the main gui window, for instance. They have limited actions to choose from, appropriate to their context. If you would prefer to, say, open the manage tags dialog with Ctrl+F3, edit or add that entry in the \'media\' set and that new shortcut will apply anywhere you are focused on some particular media.'
message += os.linesep * 2
message += 'Custom shortcuts sets are those you can create and rename at will. They are only ever active in the media viewer window, and only when you set them so from the top hover-window\'s keyboard icon. They are primarily meant for setting tags and ratings with shortcuts, and are intended to be turned on and off as you perform different \'filtering\' jobs--for instance, you might like to set the 1-5 keys to the different values of a five-star rating system, or assign a few simple keystrokes to a number of common tags.'
message += os.linesep * 2
message += 'The reserved \'media\' set also supports tag and rating actions, if you would like some of those to always be active.'
wx.MessageBox( message )
def CommitChanges( self ):
for shortcuts in self._reserved_shortcuts.GetObjects():
@ -3959,8 +3982,12 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._name.SetValue( name )
self._this_is_custom = True
if name in CC.SHORTCUTS_RESERVED_NAMES:
self._this_is_custom = False
self._name.Disable()
@ -4068,7 +4095,16 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
def GetValue( self ):
shortcuts = ClientData.Shortcuts( self._name.GetValue() )
name = self._name.GetValue()
if self._this_is_custom and name in CC.SHORTCUTS_RESERVED_NAMES:
wx.MessageBox( 'That name is reserved--please pick another!' )
raise HydrusExceptions.VetoException()
shortcuts = ClientData.Shortcuts( name )
for ( shortcut, command ) in self._shortcuts.GetClientData():
@ -4095,8 +4131,6 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
ClientGUIScrolledPanels.EditPanel.__init__( self, parent )
self._am_reserved = shortcuts_name in CC.SHORTCUTS_RESERVED_NAMES
self._final_command = 'simple'
self._current_ratings_like_service = None
@ -4112,7 +4146,7 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
self._none_panel = ClientGUICommon.StaticBox( self, 'simple actions' )
if self._am_reserved:
if shortcuts_name in CC.SHORTCUTS_RESERVED_NAMES:
choices = CC.simple_shortcut_name_to_action_lookup[ shortcuts_name ]
@ -4313,7 +4347,9 @@ class ManageShortcutsPanel( ClientGUIScrolledPanels.ManagePanel ):
vbox.AddF( self._ratings_like_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
vbox.AddF( self._ratings_numerical_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
if self._am_reserved:
is_custom_or_media = shortcuts_name not in CC.SHORTCUTS_RESERVED_NAMES or shortcuts_name == 'media'
if not is_custom_or_media:
self._set_simple.Hide()

View File

@ -405,16 +405,18 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
self._RegenerateSeedCacheStatus( page_key )
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageDeleted( page_key ) ):
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageCompletelyDestroyed( page_key ) ):
if HydrusGlobals.client_controller.PageHidden( page_key ):
if HydrusGlobals.client_controller.PageClosedButNotDestroyed( page_key ):
time.sleep( 0.1 )
time.sleep( 1 )
else:
try:
did_work = False
if not self._gallery_paused:
self._WorkOnGallery( page_key )
@ -422,10 +424,17 @@ class GalleryImport( HydrusSerialisable.SerialisableBase ):
if not self._files_paused:
self._WorkOnFiles( page_key )
did_work = self._WorkOnFiles( page_key )
time.sleep( 0.1 )
if did_work:
time.sleep( 0.1 )
else:
time.sleep( 1 )
HydrusGlobals.client_controller.WaitUntilPubSubsEmpty()
@ -674,9 +683,7 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
if path is None:
time.sleep( 1 )
return
return False
with self._lock:
@ -782,6 +789,8 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
HydrusGlobals.client_controller.pub( 'update_status', page_key )
return True
def _THREADWork( self, page_key ):
@ -792,17 +801,26 @@ class HDDImport( HydrusSerialisable.SerialisableBase ):
HydrusGlobals.client_controller.pub( 'update_status', page_key )
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageDeleted( page_key ) ):
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HydrusGlobals.client_controller.PageHidden( page_key ):
if self._paused or HydrusGlobals.client_controller.PageClosedButNotDestroyed( page_key ):
time.sleep( 0.1 )
time.sleep( 1 )
else:
try:
self._WorkOnFiles( page_key )
did_work = self._WorkOnFiles( page_key )
if did_work:
time.sleep( 0.05 )
else:
time.sleep( 1 )
HydrusGlobals.client_controller.WaitUntilPubSubsEmpty()
@ -1370,7 +1388,7 @@ class PageOfImagesImport( HydrusSerialisable.SerialisableBase ):
if file_url is None:
return
return False
try:
@ -1451,6 +1469,8 @@ class PageOfImagesImport( HydrusSerialisable.SerialisableBase ):
ClientData.WaitPolitely( page_key )
return True
def _WorkOnQueue( self, page_key ):
@ -1551,18 +1571,15 @@ class PageOfImagesImport( HydrusSerialisable.SerialisableBase ):
time.sleep( 5 )
HydrusGlobals.client_controller.pub( 'update_status', page_key )
ClientData.WaitPolitely( page_key )
else:
with self._lock:
self._SetParserStatus( page_key, '' )
if len( self._pending_page_urls ) == 0:
self._SetParserStatus( page_key, '' )
HydrusGlobals.client_controller.pub( 'update_status', page_key )
ClientData.WaitPolitely( page_key )
@ -1575,11 +1592,11 @@ class PageOfImagesImport( HydrusSerialisable.SerialisableBase ):
HydrusGlobals.client_controller.pub( 'update_status', page_key )
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageDeleted( page_key ) ):
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HydrusGlobals.client_controller.PageHidden( page_key ):
if self._paused or HydrusGlobals.client_controller.PageClosedButNotDestroyed( page_key ):
time.sleep( 0.1 )
time.sleep( 1 )
else:
@ -1587,9 +1604,16 @@ class PageOfImagesImport( HydrusSerialisable.SerialisableBase ):
self._WorkOnQueue( page_key )
self._WorkOnFiles( page_key )
did_work = self._WorkOnFiles( page_key )
time.sleep( 0.1 )
if did_work:
time.sleep( 0.1 )
else:
time.sleep( 1 )
HydrusGlobals.client_controller.WaitUntilPubSubsEmpty()
@ -2708,7 +2732,7 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
if file_url is None:
return
return False
try:
@ -2818,6 +2842,8 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
ClientData.WaitPolitely( page_key )
return True
def _WorkOnThread( self, page_key ):
@ -2959,24 +2985,35 @@ class ThreadWatcherImport( HydrusSerialisable.SerialisableBase ):
HydrusGlobals.client_controller.pub( 'update_status', page_key )
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageDeleted( page_key ) ):
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HydrusGlobals.client_controller.PageHidden( page_key ):
if self._paused or HydrusGlobals.client_controller.PageClosedButNotDestroyed( page_key ):
time.sleep( 0.1 )
time.sleep( 1 )
else:
try:
if self._thread_url != '':
if self._thread_url == '':
time.sleep( 1 )
else:
self._WorkOnThread( page_key )
self._WorkOnFiles( page_key )
did_work = self._WorkOnFiles( page_key )
if did_work:
time.sleep( 0.1 )
else:
time.sleep( 1 )
time.sleep( 0.1 )
HydrusGlobals.client_controller.WaitUntilPubSubsEmpty()
@ -3147,7 +3184,7 @@ class URLsImport( HydrusSerialisable.SerialisableBase ):
if file_url is None:
return
return False
try:
@ -3228,6 +3265,8 @@ class URLsImport( HydrusSerialisable.SerialisableBase ):
ClientData.WaitPolitely( page_key )
return True
def _THREADWork( self, page_key ):
@ -3238,19 +3277,26 @@ class URLsImport( HydrusSerialisable.SerialisableBase ):
HydrusGlobals.client_controller.pub( 'update_status', page_key )
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageDeleted( page_key ) ):
while not ( HydrusGlobals.view_shutdown or HydrusGlobals.client_controller.PageCompletelyDestroyed( page_key ) ):
if self._paused or HydrusGlobals.client_controller.PageHidden( page_key ):
if self._paused or HydrusGlobals.client_controller.PageClosedButNotDestroyed( page_key ):
time.sleep( 0.1 )
time.sleep( 1 )
else:
try:
self._WorkOnFiles( page_key )
did_work = self._WorkOnFiles( page_key )
time.sleep( 0.1 )
if did_work:
time.sleep( 0.1 )
else:
time.sleep( 1 )
HydrusGlobals.client_controller.WaitUntilPubSubsEmpty()

View File

@ -49,7 +49,7 @@ options = {}
# Misc
NETWORK_VERSION = 18
SOFTWARE_VERSION = 253
SOFTWARE_VERSION = 254
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )

View File

@ -9,6 +9,7 @@ model_shutdown = False
db_report_mode = False
db_profile_mode = False
gui_report_mode = False
pubsub_profile_mode = False
force_idle_mode = False
server_busy = False