Version 206

This commit is contained in:
Hydrus Network Developer 2016-05-18 15:07:14 -05:00
parent ca18d65577
commit dab4e0131a
27 changed files with 705 additions and 421 deletions

View File

@ -1,3 +0,0 @@
"[[], [[0, 342, [null, ""manage_tags""]], [0, 378, [null, ""next""]], [0, 312, [null, ""last""]], [0, 127, [null, ""delete""]], [2, 78, [null, ""frame_next""]], [0, 317, [null, ""next""]], [0, 343, [null, ""manage_ratings""]], [0, 379, [null, ""next""]], [4, 316, [null, ""pan_right""]], [0, 346, [null, ""archive""]], [0, 70, [null, ""fullscreen_switch""]], [0, 313, [null, ""first""]], [0, 366, [null, ""previous""]], [4, 346, [null, ""inbox""]], [0, 380, [null, ""previous""]], [4, 317, [null, ""pan_down""]], [0, 314, [null, ""previous""]], [0, 367, [null, ""next""]], [0, 376, [null, ""previous""]], [4, 314, [null, ""pan_left""]], [2, 69, [null, ""open_externally""]], [0, 381, [null, ""next""]], [0, 315, [null, ""previous""]], [0, 377, [null, ""previous""]], [4, 315, [null, ""pan_up""]], [2, 66, [null, ""frame_back""]], [0, 382, [null, ""last""]], [0, 375, [null, ""first""]], [0, 316, [null, ""next""]]]]"
"[[], [[0, 342, [null, ""manage_tags""]], [0, 378, [null, ""next""]], [0, 312, [null, ""last""]], [0, 127, [null, ""delete""]], [2, 78, [null, ""frame_next""]], [0, 317, [null, ""next""]], [0, 343, [null, ""manage_ratings""]], [0, 379, [null, ""next""]], [4, 316, [null, ""pan_right""]], [0, 346, [null, ""archive""]], [0, 70, [null, ""fullscreen_switch""]], [0, 313, [null, ""first""]], [0, 366, [null, ""previous""]], [4, 346, [null, ""inbox""]], [0, 380, [null, ""previous""]], [4, 317, [null, ""pan_down""]], [0, 314, [null, ""previous""]], [0, 367, [null, ""next""]], [0, 376, [null, ""previous""]], [4, 314, [null, ""pan_left""]], [2, 69, [null, ""open_externally""]], [0, 381, [null, ""next""]], [0, 315, [null, ""previous""]], [0, 377, [null, ""previous""]], [4, 315, [null, ""pan_up""]], [2, 66, [null, ""frame_back""]], [0, 382, [null, ""last""]], [0, 375, [null, ""first""]], [0, 316, [null, ""next""]]]]"
"[[], [[0, 342, [null, ""manage_tags""]], [0, 378, [null, ""next""]], [0, 312, [null, ""last""]], [0, 127, [null, ""delete""]], [2, 78, [null, ""frame_next""]], [0, 317, [null, ""next""]], [0, 343, [null, ""manage_ratings""]], [0, 379, [null, ""next""]], [4, 316, [null, ""pan_right""]], [0, 346, [null, ""archive""]], [0, 70, [null, ""fullscreen_switch""]], [0, 313, [null, ""first""]], [0, 366, [null, ""previous""]], [4, 346, [null, ""inbox""]], [0, 380, [null, ""previous""]], [4, 317, [null, ""pan_down""]], [0, 314, [null, ""previous""]], [0, 367, [null, ""next""]], [0, 376, [null, ""previous""]], [4, 314, [null, ""pan_left""]], [2, 69, [null, ""open_externally""]], [0, 381, [null, ""next""]], [0, 315, [null, ""previous""]], [0, 377, [null, ""previous""]], [4, 315, [null, ""pan_up""]], [2, 66, [null, ""frame_back""]], [0, 382, [null, ""last""]], [0, 375, [null, ""first""]], [0, 316, [null, ""next""]]]]"

View File

@ -8,6 +8,30 @@
<div class="content">
<h3>changelog</h3>
<ul>
<li><h3>version 206</h3></li>
<ul>
<li>if the currently focussed thumbnail is removed from view, the earliest non-selected thumbnail will be remembered as a 'ghost' focus if the user then presses the arrow keys to continue to navigate</li>
<li>if the user presses an arrow key to navigate the thumbs when there is no known focus thumb, the first will be assumed</li>
<li>the listbook now supports duplicate display names</li>
<li>services of the same type can now have the same name</li>
<li>cleaned some listbook code</li>
<li>inbox or custom filters will now render first-file animations correctly on initialisation</li>
<li>fixed the 'change' event on the new timedeltabutton, which was not updating the thread checker on dialog ok</li>
<li>subscriptions will now naturally terminate their gallery sync on a 404, rather than dumping out with an error</li>
<li>fixed some false positive 'paths are different' testing in default directory mirroring code (used in db backup, restore) that was slowing these operations down</li>
<li>if database files have the same file and modified timestamp, they won't be re-copied in a backup or restore</li>
<li>client external storage location recovery/rebalancing will now skip over files with the same size and modified date</li>
<li>if you backup the client and have external file storage locations, a popup will remind you to back those locations up manually</li>
<li>did a quick pass over some help stuff, and added a 'can the client manage files from their origial locations' bit to the faq</li>
<li>created a simple no-reward patreon, wrote some help about it, and added links in the usual places</li>
<li>empty 'read' a/c dropdowns will be more careful about idly refreshing their system preds, hopefully leading to less accidental gui hang on large jobs</li>
<li>main gui menus will be more careful about refreshing themselves when called, hopefully leading to less accidental gui hang on large jobs</li>
<li>text snippets are stored inside the client db in a better way</li>
<li>added flat service directory structure storage support to the client db</li>
<li>fixed a mapping petition rescind sql typo</li>
<li>fixed the v198->v199 server update code</li>
<li>misc cleanup</li>
</ul>
<li><h3>version 205</h3></li>
<ul>
<li>fixed v201->v202 update code, which v204 retroactively broke</li>

View File

@ -14,13 +14,14 @@
<p>Anyway:</p>
<ul>
<li><a href="https://hydrusnetwork.github.io/hydrus/">homepage</a></li>
<li><a href="https://8ch.net/hydrus/index.html">8chan board</a></li>
<li><a href="http://hydrus.tumblr.com">tumblr</a> (<a href="http://hydrus.tumblr.com/rss">rss</a>)</li>
<li><a href="https://github.com/hydrusnetwork/hydrus/releases">new downloads</a></li>
<li><a href="https://www.mediafire.com/hydrus">old downloads</a></li>
<li><a href="https://github.com/hydrusnetwork/hydrus">github</a></li>
<li><a href="https://8ch.net/hydrus/index.html">8chan board</a></li>
<li><a href="https://twitter.com/hydrusnetwork">twitter</a></li>
<li><a href="http://hydrus.tumblr.com">tumblr</a> (<a href="http://hydrus.tumblr.com/rss">rss</a>)</li>
<li><a href="mailto:hydrus.admin@gmail.com">email</a></li>
<li><a href="https://www.patreon.com/hydrus_dev">patreon</a></li>
</ul>
<p>If you would like to send me something physical, you can use my PO Box:</p>
<ul>

View File

@ -21,7 +21,7 @@
<a name="namespaces"><h3>what is a namespace?</h3></a>
<p>A <i>namespace</i> is a category that in hydrus prefixes a tag. An example is 'person' in the tag 'person:ron paul'--it lets people and software know that 'ron paul' is a name. You can create any namespace you like; just type one or more words and then a colon, and then the next string of text will have that namespace.</p>
<p>The hydrus client gives namespaces different colours so you can pick out important tags more easily in a large list, and you can also search by a particular namespace, even creating complicated predicates like 'give all files that do not have any character tags', for instance.</p>
<a name="filenames"><h3>why not use existing filenames and folders?</h3></a>
<a name="filenames"><h3>why not use filenames and folders?</h3></a>
<p>As a retrieval method, filenames and folders are less and less useful as the number of files increases. Why?</p>
<ul>
<li>A filename is not unique; did you mean this "04.jpg" or <i>this</i> "04.jpg" in another folder? Perhaps "04 (3).jpg"?</li>
@ -32,16 +32,30 @@
</ul>
<p>So, the client tracks files by their <i>hash</i>.</p>
<p>Please do not tag your files with their exact original 'filename.jpg' on my public tag repo. <a href="https://www.youtube.com/watch?v=_yYS0ZZdsnA">Shed the concept of filenames as you would chains.</a></p>
<a name="external_files"><h3>can the client manage files from their original locations?</h3></a>
<p>When the client imports a file, it makes a quickly accessible but human-ugly copy in its internal database, by default under <i>install_dir/db/client_files</i>. When it needs to access that file again, it always knows where it is, and it can be confident it is what it expects it to be. It never accesses the original again.</p>
<p>This storage method is not always convenient, particularly for those who are hesitant about converting to using hydrus completely and also do not want to maintain two large copies of their collections. The question comes up--"can hydrus track files from their original locations, without having to copy them into the db?"</p>
<p>The technical answer is, "This support could be added," but I have decided not to, mainly because:</p>
<ul>
<li>Files stored in locations outside of hydrus's responsibility can change or go missing (particularly if a whole parent folder is moved!), which erodes the assumptions it makes about file access, meaning additional checks would have to be added before important operations, often with no simple recovery.</li>
<li>External duplicates would not be merged, and the file system would have to be extended to handle pointless 1->n hash->path relationships.</li>
<li>Many regular operations--like figuring out whether orphaned files should be physically deleted--are less simple.</li>
<li>Backing up or restoring a distributed external file system is much more complicated.</li>
<li>It would require more code to maintain and would mean a laggier db and interface.</li>
<li>Hydrus is an attempt to get <i>away</i> from files and folders--if a collection is too large and complicated to manage using explorer, what's the point in supporting that old system?</li>
</ul>
<p>It is not unusual for new users who ask for this feature to find their feelings change after getting more experience with the software. If desired, path text can be preserved as tags using regexes during import, and getting into the swing of searching by metadata rather than navigating folders often shows how very effective the former is over the latter. Most users eventually import most or all of their collection into hydrus permanently, deleting their old folder structure as they go.</p>
<p>For this reason, if you are hesitant about doing things the hydrus way, I advise you try running it on a smaller subset of your collection, say 5,000 files, leaving the original copies completely intact. After a month or two, think about how often you used hydrus to look at the files versus navigating through folders. If you barely used the folders, you probably do not need them any more, but if you used them a lot, then hydrus might not be for you, or it might only be for some sorts of files in your collection.</p>
<a name="hashes"><h3>what is a hash?</h3></a>
<p><a href="https://en.wikipedia.org/wiki/Hash_function">wiki</a></p>
<p>Hashes are a subject you usually has to be a software engineer to find interesting. The simple answer is that they are unique names for things. It can be proven that f099b5823f4e36a4bd6562812582f60e49e818cf445902b504b5533c6a5dad94 refers to one particular file and no other. Hashes make excellent identifiers inside software. In the client's normal operation, you will never encounter a file's hash. If you want to see a thumbnail bigger, double-click it; the software handles the mathematics.</p>
<p>Hashes are a subject you usually has to be a software engineer to find interesting. The simple answer is that they are unique names for things. Hashes make excellent identifiers inside software, as you can safely assume that f099b5823f4e36a4bd6562812582f60e49e818cf445902b504b5533c6a5dad94 refers to one particular file and no other. In the client's normal operation, you will never encounter a file's hash. If you want to see a thumbnail bigger, double-click it; the software handles the mathematics.</p>
<p><i>For those who </i>are<i> interested: hydrus uses SHA-256, which spits out 32-byte (256-bit) hashes. The software stores the hash densely, as 32 bytes, only encoding it to 64 hex characters when the user views it or copies to clipboard. SHA-256 is not perfect, but it is a great compromise candidate; it is secure for now, it is reasonably fast, it is available for most programming languages, and newer CPUs perform it more efficiently all the time.</i></p>
<a name="access_keys"><h3>what is an access key?</h3></a>
<p>The hydrus network's repositories do not use username/password, but instead a single strong identifier-password like this:</p>
<p><i>7ce4dbf18f7af8b420ee942bae42030aab344e91dc0e839260fcd71a4c9879e3</i></p>
<p>These hex numbers give you access to a particular account on a particular repository, and are often combined like so:</p>
<p><i>7ce4dbf18f7af8b420ee942bae42030aab344e91dc0e839260fcd71a4c9879e3@hostname.com:45871</i></p>
<p>They are long enough to be impossible to guess, and also randomly generated, so they reveal nothing personally identifying about you. Many people can use the same access key (and hence the same account) on a repository without consequence, although they will have to share bandwidth limits, and if one person screws around and gets the account banned, everyone will lose access.</p>
<p>They are long enough to be impossible to guess, and also randomly generated, so they reveal nothing personally identifying about you. Many people can use the same access key (and hence the same account) on a repository without consequence, although they will have to share any bandwidth limits, and if one person screws around and gets the account banned, everyone will lose access.</p>
<p>The access key is the account. Do not give it to anyone you do not want to have access to the account. An administrator will never need it; instead they will want your <i>account key</i>.</p>
<a name="account_keys"><h3>what is an account key?</h3></a>
<p>This is another long string of random hexadecimal that <i>identifies</i> your account without giving away access. If you need to identify yourself to a repository administrator (say, to get your account's permissions modified), you will need to tell them your account key. You can copy it to your clipboard in <i>services->review services</i>.</p>

View File

@ -20,10 +20,11 @@
<p><a href="import.png"><img src="import.png" width="960" height="540" /></a></p>
<p>The files are being imported into the client's database. <a href="faq.html#filenames">The client discards their filenames.</a></p>
<p>Notice your original folder and its files are untouched. You can move the originals somewhere else, delete them, and the client will still return searches fine. In the same way, you can delete from the client, and the original files will remain unchanged; import is a <b>copy</b>, not a move, operation. The client performs all its operations on its internal database. If you find yourself enjoying using the client and decide to completely switch over, you can delete the original files you import without worry. You can always export them back again later.</p>
<p><a href="faq.html#external_files">FAQ: can the client manage files from their original locations?</a></p>
<p>Now:</p>
<ul class="bulletpoint">
<li>Click on a thumbnail; it'll show in the preview screen, bottom left.</li>
<li>Now double- or middle-click the thumbnail to open the media viewer. You can hit 'f' to switch between giving the fullscreen a frame or not. You can use your scrollwheel or page up/down to browse the media, ctrl+scrollwheel to zoom in and out.</li>
<li>Double- or middle-click the thumbnail to open the media viewer. You can hit 'f' to switch between giving the fullscreen a frame or not. You can use your scrollwheel or page up/down to browse the media and ctrl+scrollwheel to zoom in and out.</li>
<li>
<p>Move your mouse to the top-left, top-middle and top-right of the media viewer. You should see some 'hover' panels pop into place.</p>
<p><img src="hover_command.png" /></p>
@ -31,7 +32,7 @@
</li>
<li>Press enter/return or double/middle-click again to close the media viewer.</li>
<li>You can quickly select multiple files by shift- or ctrl- clicking. Notice how the status bar at the bottom of the screen updates with the number selected and their total size. Right-clicking your selection will present another summary and many actions.</li>
<li>Hit F9 to bring up a new page chooser. It won't show much right now, because you are just started. You can navigate it with the arrow keys, your numpad, or your mouse.</li>
<li>Hit F9 to bring up a new page chooser. You can navigate it with the arrow keys, your numpad, or your mouse.</li>
<li>
<p>On the left of a normal search page is a text box. When it is focused, a dropdown window appears. It looks like this:</p>
<p><img src="ac_dropdown_system.png" /></p>
@ -43,7 +44,7 @@
</li>
<li>You can remove from the list of 'active tags' in the box above with a double-click, or by entering the exact same tag again through the dropdown.</li>
<li>Play with the system tags more if you like, and the sort-by dropdown. The collect-by dropdown is advanced, so wait until you understand <i>namespaces</i> before expecting it to do anything.</li>
<li>To close a page, middle click its tab.</li>
<li>To close a page, middle-click its tab.</li>
</ul>
<p>The client can currently import the following mimetypes:</p>
<ul>
@ -64,7 +65,7 @@
<p>Although some support is imperfect for the complicated filetypes. Most videos will not play audio yet, some animated gifs with unusual transparency will render like static, and flash cannot embed into Linux or OS X. When something does not render how you want, right-clicking on its thumbnail presents the option 'open externally', which will open the file in the appropriate default program (e.g. ACDSee, VLC).</p>
<p>The client can also download files from several websites, including 4chan and 8chan, many boorus, and gallery sites like deviant art and hentai foundry. The different download pages are under F9->download.</p>
<p><a href="screenshot_thread_watcher.png"><img src="screenshot_thread_watcher.png" width="960" height="540" /></a></p>
<p>Most of them have similar interfaces. Paste the url or type the query you are interested in, and press enter.</p>
<p>Most of them have similar interfaces. Paste the url or type the query you are interested in and press enter.</p>
<h3>inbox and archiving</h3>
<p>The client sends newly imported files to an <b>inbox</b>, just like your email. Inbox acts like a tag, matched by 'system:inbox'. A small envelope icon is drawn in the top corner of all inbox files:</p>
<p><img src="fresh_imports.png" /></p>
@ -86,7 +87,7 @@
<p>Your choices will not be committed until you finish filtering.</p>
<p>This saves time.</p>
<h3>lastly</h3>
<p>The hydrus client's workflows are not designed for half-finished files that you are still working on. Think of it as a giant archive for everything excellent you have decided to store away.</p>
<p>The hydrus client's workflows are not designed for half-finished files that you are still working on. Think of it as a giant archive for everything excellent you have decided to store away. It lets you find and remember these things quickly.</p>
<p class="right"><a href="getting_started_more_files.html">I want to learn more about files! ----></a></p>
<p class="right"><a href="getting_started_tags.html">No, let's learn about tags! ----></a></p>
</div>

View File

@ -9,12 +9,12 @@
<p><a href="introduction.html"><---- Back to the introduction</a></p>
<h3>downloading</h3>
<p>You can get the latest release at <a href="https://github.com/hydrusnetwork/hydrus/releases">my github releases page</a>.</p>
<p>I try to release a new version every Wednesday by 8pm EST and write an accompanying post on <a href="http://hydrus.tumblr.com/">my tumblr</a> and a sticky on my <a href="https://8ch.net/hydrus/index.html">8chan board</a>.</p>
<p>I try to release a new version every Wednesday by 8pm EST and write an accompanying post on <a href="http://hydrus.tumblr.com/">my tumblr</a> and a sticky on <a href="https://8ch.net/hydrus/index.html">my 8chan board</a>.</p>
<h3>installing</h3>
<p>for Windows:</p>
<ul>
<li>If you want the easy solution, download the .exe installer. Run it, hit ok several times.</li>
<li>If you know what you are doing and want a little more control, get the .zip. Don't extract it to Program Files unless you are willing to run it as administrator every time. You probably want something like D:\hydrus.</li>
<li>If you know what you are doing and want a little more control, get the .zip. Don't extract it to Program Files unless you are willing to run it as administrator every time (it stores all its user data inside its own folder). You probably want something like D:\hydrus.</li>
</ul>
<p>for OS X:</p>
<ul>
@ -24,24 +24,24 @@
<p>for Linux:</p>
<ul>
<li>Get the .tag.gz. Extract it somewhere useful and create shortcuts to 'client' and 'server' as you like. I build on Ubuntu, so if you run something else, compatibility is hit and miss.</li>
<li>Try <a href="wine.html">running the Windows version in wine</a>.</li>
<li>Or try <a href="wine.html">running the Windows version in wine</a>.</li>
<li>If you use Arch Linux, you can check out the AUR package a user maintains <a href="https://aur4.archlinux.org/packages/hydrus/">here</a>.</li>
</ul>
<p>from source:</p>
<ul>
<li>If you know Python, you can <a href="running_from_source.html">run from source</a>.</li>
</ul>
<p>Hydrus stores all its data&#x2014;options, files, subscriptions, <i>everything</i>&#x2014;entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine in a different location.</p>
<p>When you install, make sure your destination hard drive has enough space for what you intend to store. If you have 100GB of stuff, your system drive might not be appropriate.</p>
<p>Hydrus stores all its data&#x2014;options, files, subscriptions, <i>everything</i>&#x2014;entirely inside its own directory. You can extract it to a usb stick, move it from one place to another, have multiple installs for multiple purposes, wrap it all up inside a truecrypt volume, whatever you like. The .exe installer writes some unavoidable uninstall registry stuff to Windows, but the 'installed' client itself will run fine if you manually move it.</p>
<h3>updating</h3>
<p>You don't <i>have</i> to update every week, but I generally recommend it. If you leave it a while, updating multiple versions in a single step is usually fine, but I suggest skimming every intervening release post just in case there is an important update note in any of them (I usually <b>BOLD AND CAPITALISE</b> this sort of thing). If you run into an error doing a multiple update, try updating to every intervening version in turn (and please let me know, so I can fix the problem!).</p>
<p>Clients and servers of different versions can usually connect to one another, but every couple of months or so, I make a change to the network protocol, and you will get polite error messages if you try to connect to a newer server with an older client or <i>vice versa</i>. Read my release posts and judge for yourself what you want to do.</p>
<p>Clients and servers of different versions can usually connect to one another, but from time to time, I make a change to the network protocol, and you will get polite error messages if you try to connect to a newer server with an older client or <i>vice versa</i>. Read my release posts and judge for yourself what you want to do.</p>
<p>The update process:<p>
<ul>
<li>If the client or server you want to update is running, close it!</li>
<li>If you maintain a backup, run it now!</li>
<li>If you use the installer, just download the new installer and run it. It should detect where the last install was and overwrite everything automatically.</li>
<li>If you extract, then just extract the new version right on top of your current install and overwrite manually.</li>
<li>Start your client or server. It may take a few minutes to update its database.</li>
<li>Start your client or server. It may take a few minutes to update its database. I will say in the release post if it is likely to take longer.</li>
</ul>
<p>Unless the update specifically disables or reconfigures something, all your files and tags and settings will be remembered after the update.</p>
<h3>backing up</h3>

View File

@ -34,7 +34,7 @@
<p><i>Remember: If you are going to scrape anything from a site, be polite about it!</i></p>
<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>.</p>
<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>

View File

@ -21,7 +21,7 @@
<li>title:vitruvian man</li>
</ul>
<p>The client is set up to draw common namespaces in different colours, just like boorus do. You can change these colours in the options.</p>
<p>Once you are happy with your tags, hit 'apply' or just press enter on an empty text box.</p>
<p>Once you are happy with your tags, hit 'apply' or just press enter on the text box if it is empty.</p>
<p><a href="sororitas_local_done.png"><img src="sororitas_local_done.png" width="960" height="540" /></a></p>
<p>The tags are now saved to your database. Searching for any of them will return this file and anything else so tagged:</p>
<p><a href="sororitas_search.png"><img src="sororitas_search.png" width="960" height="540" /></a></p>
@ -36,7 +36,7 @@
<p>Here's the info so you can copy it:</p>
<ul><li>4a285629721ca442541ef2c15ea17d1f7f7578b0c3f4f5f2a05f8f0ab297786f@hydrus.no-ip.org:45871</li></ul>
<p>Over time, usually when it is idle, your client will download updates from the repository until it is fully synchronised. You can customise when this happens in <i>file->options->maintenance and processing</i>. As the repository synchronises, you should see some new tags appear, particularly on famous files that lots of people have.</p>
<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 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.</b></p>
<p>You can watch more detailed synchronisation progress in the <i>services->review services</i> window.</p>
<p><img src="tag_repo_review.png" /></p>
<p>Your new service should now be listed on the left of the manage tags dialog. Adding tags to a repository works very similarly to the local tags service except hitting 'apply' will not immediately confirm your changes—it will put them in a queue to be uploaded. These 'pending' tags will be counted with a plus '+' or minus '-' sign:</p>

View File

@ -39,6 +39,7 @@
<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>
</ul>

View File

@ -15,8 +15,8 @@
<p>This does a lot more than a normal image viewer. If you are totally new to the idea of personal media collections and tagging, I suggest you start slow, walk through the getting started guides, and experiment doing different things. If you aren't sure on what a button does, try clicking it! You'll be importing thousands of files and applying <i>tens</i> of thousands of tags in no time.</p>
<p>The client is chiefly a file database. It stores your files inside its own folders, managing them far better than an explorer window or some online gallery. Here's a screenshot of one of my test installs with a search showing all files:</p>
<p><a href="example_client.png"><img src="example_client.png" width="960" height="540" title="WELCOME TO INTERNET" /></a></p>
<p>As well as the client, there is also a server that anyone can run to store files or tags for sharing between many users. The mechanics of running a server is usually confusing to new users, so wait a little while before you explore this. I run a tag service with several million tags that you are welcome to access and contribute to.</p>
<p>I plan to expand the network to include peer-to-peer anonymous communication.</p>
<p>As well as the client, there is also a server that anyone can run to store files or tags for sharing between many users. The mechanics of running a server is usually confusing to new users, so wait a little while before you explore this. I run a public tag service with several million tags that you are welcome to access and contribute to.</p>
<p>I have many plans to expand the client and the network.</p>
<h3>statement of principles</h3>
<ul class="bulletpoint">
<li>No speech should be outlawed.</li>
@ -25,7 +25,7 @@
</ul>
<p>None of the above are currently true, but I would love to live in a world where they were. My software is an attempt to move us a little closer.</p>
<p>I try to side with the person over the authority, the distributed over the centralised. I still use gmail and youtube just like pretty much everyone, but I would rather be using different systems, especially in ten years. No one seemed to be making what I wanted, so I decided to do it myself, and here we are.</p>
<p>I'd like to eventually set up a paypal/kickstarter/patreon-similar way for people to gibe moni plos, but it'll be totally voluntary.</p>
<p>If, after a few months, you find you enjoy the software and would like to further support it, I have set up a simple no-reward patreon, which you can read more about <a href="support.html">here</a>.</p>
<h3>license</h3>
<p>These programs are free software. They come without any warranty, to the extent permitted by applicable law. You can redistribute them and/or modify them under the terms of the Do What The Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See http://sam.zoy.org/wtfpl/COPYING for more details.</p>
<p>Do what the fuck you want to with my software, and if shit breaks, <span class="dealwithit">DEAL WITH IT</span>.</p>

16
help/support.html Normal file
View File

@ -0,0 +1,16 @@
<html>
<head>
<title>support</title>
<link href="hydrus.ico" rel="shortcut icon" />
<link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="content">
<h3>can I contribute to hydrus development?</h3>
<p>I don't expect anything! I'm amazed and grateful that anyone wants to use my software or upload to the public tag repository. I enjoy the feedback and work, and I hope to keep putting completely free weekly releases out as long as there is more to do.</p>
<p>That said, as I have developed the software, several users have kindly offered to contribute money, either as thanks for a specific feature or just in general. I kept putting the thought off, but I eventually got over my hesitance and set something up.</p>
<p>I find the tactics of most internet fundraising very distasteful, especially when they promise something they then fail to deliver. I much prefer the 'if you like me and would like to contribute, then please do, meanwhile Ill keep doing what I do model. I support Red Letter Media and a few other 'put out regular free content' creators on Patreon in this way, and I get a lot out of it, even though I have no direct reward beyond the knowledge that I helped some people do something neat.</p>
<p>If you feel the same way about my work, I've set up a simple Patreon page <a href="https://www.patreon.com/hydrus_dev">here</a>.</p>
</div>
</body>
</html>

View File

@ -49,7 +49,7 @@
<h3>acronyms and synonyms</h3>
<p>I prefer the full 'series:the lord of the rings' rather than 'lotr'. If you are an advanced user, please help out with tag siblings to help induce this.</p>
<h3>character:anna (frozen)</h3>
<p>I am not fond of putting a series name after a character because it looks unusual and is applied unreliably. It is done to separate same-named characters from each other (particularly when they have no canon surname), which is useful in places that search slowly or usually only deal in single-tag searches. I would prefer that namespaces say their namespace and nothing else. Some sites even say things like 'anna (disney)'. I don't really mind this stuff, but if you are adding a sibling to collapse these divergent tags into the 'proper' one, I'd prefer it all went to the simple and reliable 'character:anna'. Even better would be migrating towards a canon-ok unique name, like 'character:pricess anna of arendelle'.</p>
<p>I am not fond of putting a series name after a character because it looks unusual and is applied unreliably. It is done to separate same-named characters from each other (particularly when they have no canon surname), which is useful in places that search slowly or usually only deal in single-tag searches. I would prefer that namespaces say their namespace and nothing else. Some sites even say things like 'anna (disney)'. I don't really mind this stuff, but if you are adding a sibling to collapse these divergent tags into the 'proper' one, I'd prefer it all went to the simple and reliable 'character:anna'. Even better would be migrating towards a canon-ok unique name, like 'character:princess anna of arendelle'.</p>
<h3>protip: reign in your spergitude</h3>
<p>Importing all the many tags from the boorus is totally fine, but if you are typing tags yourself, I suggest you do not try to tag <a href="http://safebooru.org/index.php?page=post&s=list&tags=card_on_necklace">everything</a> <a href="http://gelbooru.com/index.php?page=post&s=list&tags=collarbone">in</a> <a href="https://e621.net/post/index?tags=cum_on_neck">the</a> <a href="http://danbooru.donmai.us/posts?tags=brown_vest">image</a>--tag everything that you would search for. You will save a lot of time. Anyone can see what is in an image just by looking at it--tags are primarily for finding things. Character, series and creator namespaces are a great place to start. After that, add what you are interested in.</p>
<h3>siblings and parents</h3>

View File

@ -405,6 +405,7 @@ class GlobalBMPs( object ):
GlobalBMPs.eight_chan = wx.Bitmap( os.path.join( HC.STATIC_DIR, '8chan.png' ) )
GlobalBMPs.twitter = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'twitter.png' ) )
GlobalBMPs.tumblr = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'tumblr.png' ) )
GlobalBMPs.patreon = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'patreon.png' ) )
GlobalBMPs.first = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'first.png' ) )
GlobalBMPs.previous = wx.Bitmap( os.path.join( HC.STATIC_DIR, 'previous.png' ) )

View File

@ -1379,6 +1379,18 @@ class DB( HydrusDB.HydrusDB ):
def _Backup( self, path ):
client_files_locations = self._GetClientFilesLocations()
for location in client_files_locations.values():
if not location.startswith( HC.CLIENT_FILES_DIR ):
HydrusData.ShowText( 'Some of your files are stored outside of ' + HC.CLIENT_FILES_DIR + '. These files will not be backed up--please do this manually, yourself.' )
break
job_key = ClientThreading.JobKey( cancellable = True )
job_key.SetVariable( 'popup_title', 'backing up db' )
@ -1405,7 +1417,10 @@ class DB( HydrusDB.HydrusDB ):
source = os.path.join( self._db_dir, filename )
dest = os.path.join( path, filename )
shutil.copy2( source, dest )
if not HydrusPaths.PathsHaveSameSizeAndDate( source, dest ):
shutil.copy2( source, dest )
job_key.SetVariable( 'popup_text_1', 'copying archives directory' )
@ -2373,15 +2388,14 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE perceptual_hashes ( hash_id INTEGER PRIMARY KEY, phash BLOB_BYTES );' )
self._c.execute( 'CREATE TABLE reasons ( reason_id INTEGER PRIMARY KEY, reason TEXT );' )
self._c.execute( 'CREATE UNIQUE INDEX reasons_reason_index ON reasons ( reason );' )
self._c.execute( 'CREATE TABLE remote_ratings ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, count INTEGER, rating REAL, score REAL, PRIMARY KEY( service_id, hash_id ) );' )
self._c.execute( 'CREATE INDEX remote_ratings_hash_id_index ON remote_ratings ( hash_id );' )
self._c.execute( 'CREATE INDEX remote_ratings_rating_index ON remote_ratings ( rating );' )
self._c.execute( 'CREATE INDEX remote_ratings_score_index ON remote_ratings ( score );' )
self._c.execute( 'CREATE TABLE service_filenames ( service_id INTEGER REFERENCES services ON DELETE CASCADE, hash_id INTEGER, filename TEXT, PRIMARY KEY( service_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_directories ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, num_files INTEGER, total_size INTEGER, PRIMARY KEY( service_id, directory_id ) );' )
self._c.execute( 'CREATE TABLE service_directory_file_map ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, hash_id INTEGER, PRIMARY KEY( service_id, directory_id, hash_id ) );' )
self._c.execute( 'CREATE TABLE service_info ( service_id INTEGER REFERENCES services ON DELETE CASCADE, info_type INTEGER, info INTEGER, PRIMARY KEY ( service_id, info_type ) );' )
@ -2423,6 +2437,8 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE VIRTUAL TABLE IF NOT EXISTS external_master.tags_fts4 USING fts4( tag );' )
self._c.execute( 'CREATE TABLE IF NOT EXISTS external_master.texts ( text_id INTEGER PRIMARY KEY, text TEXT UNIQUE );' )
# inserts
location = HydrusPaths.ConvertAbsPathToPortablePath( HC.CLIENT_FILES_DIR )
@ -2750,6 +2766,14 @@ class DB( HydrusDB.HydrusDB ):
self.pub_service_updates_after_commit( service_keys_to_service_updates )
def _DeleteServiceDirectory( self, service_id, dirname ):
directory_id = self._GetTextId( dirname )
self._c.execute( 'DELETE FROM service_directories WHERE service_id = ? AND directory_id = ?;', ( service_id, directory_id ) )
self._c.execute( 'DELETE FROM service_directory_file_map WHERE service_id = ? AND directory_id = ?;', ( service_id, directory_id ) )
def _DeleteServiceInfo( self ):
self._c.execute( 'DELETE FROM service_info;' )
@ -3143,6 +3167,29 @@ class DB( HydrusDB.HydrusDB ):
return result
def _GetDirectoryHashes( self, service_key, dirname ):
service_id = self._GetServiceId( service_key )
directory_id = self._GetTextId( dirname )
hash_ids = [ hash_id for ( hash_id, ) in self._c.execute( 'SELECT hash_id FROM service_directory_file_map WHERE service_id = ? AND directory_id = ?;', ( service_id, directory_id ) ) ]
hashes = self._GetHashes( hash_ids )
return hashes
def _GetDirectoryInfo( self, service_key ):
service_id = self._GetServiceId( service_key )
incomplete_info = self._c.execute( 'SELECT directory_id, num_files, total_size FROM service_directories WHERE service_id = ?;', ( service_id, ) ).fetchall()
info = [ ( self._GetText( directory_id ), num_files, total_size ) for ( directory_id, num_files, total_size ) in incomplete_info ]
return info
def _GetDownloads( self ): return { hash for ( hash, ) in self._c.execute( 'SELECT hash FROM file_transfers, hashes USING ( hash_id ) WHERE service_id = ?;', ( self._local_file_service_id, ) ) }
def _GetFileHashes( self, given_hashes, given_hash_type, desired_hash_type ):
@ -4537,7 +4584,7 @@ class DB( HydrusDB.HydrusDB ):
for ( ( namespace_id, tag_id, reason_id ), hash_ids ) in petitioned_dict.items():
petitioned = ( self._GetNamespaceTag( namespace_id, tag_id ), hash_ids, self._GetReason( reason_id ) )
petitioned = ( self._GetNamespaceTag( namespace_id, tag_id ), hash_ids, self._GetText( reason_id ) )
content_data_dict[ HC.CONTENT_TYPE_MAPPINGS ][ HC.CONTENT_UPDATE_PETITION ].append( petitioned )
@ -4546,14 +4593,14 @@ class DB( HydrusDB.HydrusDB ):
# tag siblings
pending = [ ( ( self._GetNamespaceTag( old_namespace_id, old_tag_id ), self._GetNamespaceTag( new_namespace_id, new_tag_id ) ), self._GetReason( reason_id ) ) for ( old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id ) in self._c.execute( 'SELECT old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id FROM tag_sibling_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PENDING ) ).fetchall() ]
pending = [ ( ( self._GetNamespaceTag( old_namespace_id, old_tag_id ), self._GetNamespaceTag( new_namespace_id, new_tag_id ) ), self._GetText( reason_id ) ) for ( old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id ) in self._c.execute( 'SELECT old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id FROM tag_sibling_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PENDING ) ).fetchall() ]
if len( pending ) > 0:
content_data_dict[ HC.CONTENT_TYPE_TAG_SIBLINGS ][ HC.CONTENT_UPDATE_PEND ] = pending
petitioned = [ ( ( self._GetNamespaceTag( old_namespace_id, old_tag_id ), self._GetNamespaceTag( new_namespace_id, new_tag_id ) ), self._GetReason( reason_id ) ) for ( old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id ) in self._c.execute( 'SELECT old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id FROM tag_sibling_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PETITIONED ) ).fetchall() ]
petitioned = [ ( ( self._GetNamespaceTag( old_namespace_id, old_tag_id ), self._GetNamespaceTag( new_namespace_id, new_tag_id ) ), self._GetText( reason_id ) ) for ( old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id ) in self._c.execute( 'SELECT old_namespace_id, old_tag_id, new_namespace_id, new_tag_id, reason_id FROM tag_sibling_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PETITIONED ) ).fetchall() ]
if len( petitioned ) > 0:
@ -4562,14 +4609,14 @@ class DB( HydrusDB.HydrusDB ):
# tag parents
pending = [ ( ( self._GetNamespaceTag( child_namespace_id, child_tag_id ), self._GetNamespaceTag( parent_namespace_id, parent_tag_id ) ), self._GetReason( reason_id ) ) for ( child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id ) in self._c.execute( 'SELECT child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id FROM tag_parent_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PENDING ) ).fetchall() ]
pending = [ ( ( self._GetNamespaceTag( child_namespace_id, child_tag_id ), self._GetNamespaceTag( parent_namespace_id, parent_tag_id ) ), self._GetText( reason_id ) ) for ( child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id ) in self._c.execute( 'SELECT child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id FROM tag_parent_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PENDING ) ).fetchall() ]
if len( pending ) > 0:
content_data_dict[ HC.CONTENT_TYPE_TAG_PARENTS ][ HC.CONTENT_UPDATE_PEND ] = pending
petitioned = [ ( ( self._GetNamespaceTag( child_namespace_id, child_tag_id ), self._GetNamespaceTag( parent_namespace_id, parent_tag_id ) ), self._GetReason( reason_id ) ) for ( child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id ) in self._c.execute( 'SELECT child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id FROM tag_parent_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PETITIONED ) ).fetchall() ]
petitioned = [ ( ( self._GetNamespaceTag( child_namespace_id, child_tag_id ), self._GetNamespaceTag( parent_namespace_id, parent_tag_id ) ), self._GetText( reason_id ) ) for ( child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id ) in self._c.execute( 'SELECT child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id, reason_id FROM tag_parent_petitions WHERE service_id = ? AND status = ? ORDER BY reason_id LIMIT 100;', ( service_id, HC.PETITIONED ) ).fetchall() ]
if len( petitioned ) > 0:
@ -4640,35 +4687,6 @@ class DB( HydrusDB.HydrusDB ):
def _GetReason( self, reason_id ):
result = self._c.execute( 'SELECT reason FROM reasons WHERE reason_id = ?;', ( reason_id, ) ).fetchone()
if result is None:
raise HydrusExceptions.DataMissing( 'Reason error in database' )
( reason, ) = result
return reason
def _GetReasonId( self, reason ):
result = self._c.execute( 'SELECT reason_id FROM reasons WHERE reason=?;', ( reason, ) ).fetchone()
if result is None:
self._c.execute( 'INSERT INTO reasons ( reason ) VALUES ( ? );', ( reason, ) )
reason_id = self._c.lastrowid
else: ( reason_id, ) = result
return reason_id
def _GetRemoteThumbnailHashesIShouldHave( self, service_key ):
service_id = self._GetServiceId( service_key )
@ -5087,6 +5105,38 @@ class DB( HydrusDB.HydrusDB ):
def _GetText( self, text_id ):
result = self._c.execute( 'SELECT text FROM texts WHERE text_id = ?;', ( text_id, ) ).fetchone()
if result is None:
raise HydrusExceptions.DataMissing( 'Text lookup error in database' )
( text, ) = result
return text
def _GetTextId( self, text ):
result = self._c.execute( 'SELECT text_id FROM texts WHERE text = ?;', ( text, ) ).fetchone()
if result is None:
self._c.execute( 'INSERT INTO texts ( text ) VALUES ( ? );', ( text, ) )
text_id = self._c.lastrowid
else:
( text_id, ) = result
return text_id
def _GetURLStatus( self, url ):
result = self._c.execute( 'SELECT hash_id FROM urls WHERE url = ?;', ( url, ) ).fetchone()
@ -5651,7 +5701,7 @@ class DB( HydrusDB.HydrusDB ):
hash_ids = self._GetHashIds( hashes )
reason_id = self._GetReasonId( reason )
reason_id = self._GetTextId( reason )
self._c.execute( 'DELETE FROM file_petitions WHERE service_id = ? AND hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';', ( service_id, ) )
@ -5691,6 +5741,23 @@ class DB( HydrusDB.HydrusDB ):
elif action == HC.CONTENT_UPDATE_UNDELETE: self._UndeleteFiles( hash_ids )
elif data_type == HC.CONTENT_TYPE_DIRECTORIES:
if action == HC.CONTENT_UPDATE_ADD:
( hashes, dirname ) = row
hash_ids = self._GetHashIds( hashes )
self._SetServiceDirectory( service_id, hash_ids, dirname )
elif action == HC.CONTENT_UPDATE_DELETE:
dirname = row
self._DeleteServiceDirectory( service_id, dirname )
elif service_type in HC.TAG_SERVICES:
@ -5828,7 +5895,7 @@ class DB( HydrusDB.HydrusDB ):
elif action == HC.CONTENT_UPDATE_RESCIND_PEND: ultimate_pending_rescinded_mappings_ids.append( ( namespace_id, tag_id, hash_ids ) )
elif action == HC.CONTENT_UPDATE_PETITION:
reason_id = self._GetReasonId( reason )
reason_id = self._GetTextId( reason )
ultimate_petitioned_mappings_ids.append( ( namespace_id, tag_id, hash_ids, reason_id ) )
@ -5878,7 +5945,7 @@ class DB( HydrusDB.HydrusDB ):
continue
reason_id = self._GetReasonId( reason )
reason_id = self._GetTextId( reason )
self._c.execute( 'DELETE FROM tag_sibling_petitions WHERE service_id = ? AND old_namespace_id = ? AND old_tag_id = ?;', ( service_id, old_namespace_id, old_tag_id ) )
@ -5969,7 +6036,7 @@ class DB( HydrusDB.HydrusDB ):
except HydrusExceptions.SizeException: continue
reason_id = self._GetReasonId( reason )
reason_id = self._GetTextId( reason )
self._c.execute( 'DELETE FROM tag_parent_petitions WHERE service_id = ? AND child_namespace_id = ? AND child_tag_id = ? AND parent_namespace_id = ? AND parent_tag_id = ?;', ( service_id, child_namespace_id, child_tag_id, parent_namespace_id, parent_tag_id ) )
@ -6472,6 +6539,30 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'REPLACE INTO service_filenames ( service_id, hash_id, filename ) VALUES ( ?, ?, ? );', ( service_id, hash_id, filename ) )
def _SetServiceDirectory( self, service_id, hash_ids, dirname ):
directory_id = self._GetTextId( dirname )
self._c._execute( 'DELETE FROM service_directories WHERE service_id = ? AND directory_id = ?;', ( service_id, directory_id ) )
self._c._execute( 'DELETE FROM service_directory_file_map WHERE service_id = ? AND directory_id = ?;', ( service_id, directory_id ) )
num_files = len( hash_ids )
result = self._c.execute( 'SELECT SUM( size ) FROM files_info WHERE hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';' ).fetchone()
if result is None:
total_size = 0
else:
( total_size, ) = result
self._c.execute( 'INSERT INTO service_directories ( service_id, directory_id, num_files, total_size ) VALUES ( ?, ?, ?, ? );', ( service_id, directory_id, num_files, total_size ) )
self._c.executemany( 'INSERT INTO service_directory_file_map ( service_id, directory_id, hash_id ) VALUES ( ?, ?, ? );', ( ( service_id, directory_id, hash_id ) for hash_id in hash_ids ) )
def _SetTagCensorship( self, info ):
self._c.execute( 'DELETE FROM tag_censorship;' )
@ -8034,6 +8125,22 @@ class DB( HydrusDB.HydrusDB ):
self._c.execute( 'CREATE TABLE vacuum_timestamps ( name TEXT, timestamp INTEGER );' )
if version == 205:
self._c.execute( 'CREATE TABLE service_directories ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, num_files INTEGER, total_size INTEGER, PRIMARY KEY( service_id, directory_id ) );' )
self._c.execute( 'CREATE TABLE service_directory_file_map ( service_id INTEGER REFERENCES services ON DELETE CASCADE, directory_id INTEGER, hash_id INTEGER, PRIMARY KEY( service_id, directory_id, hash_id ) );' )
#
self._c.execute( 'CREATE TABLE IF NOT EXISTS external_master.texts ( text_id INTEGER PRIMARY KEY, text TEXT UNIQUE );' )
#
self._c.execute( 'INSERT OR IGNORE INTO texts SELECT reason_id, reason FROM reasons;' )
self._c.execute( 'DROP TABLE reasons;' )
self._controller.pub( 'splash_set_title_text', 'updated db to v' + str( version + 1 ) )
self._c.execute( 'UPDATE version SET version = ?;', ( version + 1, ) )
@ -8291,7 +8398,7 @@ class DB( HydrusDB.HydrusDB ):
for ( namespace_id, tag_id, hash_ids ) in petitioned_rescinded_mappings_ids:
self._c.execute( 'DELETE FROM ' + petitioned_mappings_table_name + ' WHERE AND namespace_id = ? AND tag_id = ? AND hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';', ( namespace_id, tag_id ) )
self._c.execute( 'DELETE FROM ' + petitioned_mappings_table_name + ' WHERE namespace_id = ? AND tag_id = ? AND hash_id IN ' + HydrusData.SplayListForDB( hash_ids ) + ';', ( namespace_id, tag_id ) )
num_petitions_deleted = self._GetRowCount()
@ -8699,7 +8806,10 @@ class DB( HydrusDB.HydrusDB ):
source = os.path.join( path, filename )
dest = os.path.join( self._db_dir, filename )
shutil.copy2( source, dest )
if not HydrusPaths.PathsHaveSameSizeAndDate( source, dest ):
shutil.copy2( source, dest )
HydrusPaths.MirrorTree( os.path.join( path, 'client_archives' ), HC.CLIENT_ARCHIVES_DIR )

View File

@ -501,14 +501,9 @@ class ExportFolder( HydrusSerialisable.SerialisableBaseNamed ):
do_copy = False
elif os.path.exists( dest_path ):
elif HydrusPaths.PathsHaveSameSizeAndDate( source_path, dest_path ):
dest_size = os.path.getsize( dest_path )
if dest_size == size:
do_copy = False
do_copy = False
if do_copy:

View File

@ -1105,8 +1105,6 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_tag_parents' ), p( '&Manage Tag Parents' ), p( 'Set certain tags to be automatically added with other tags.' ) )
menu.AppendSeparator()
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_boorus' ), p( 'Manage &Boorus' ), p( 'Change the html parsing information for boorus to download from.' ) )
#menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_imageboards' ), p( 'Manage &Imageboards' ), p( 'Change the html POST form information for imageboards to dump to.' ) )
#menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_4chan_pass' ), p( 'Manage &4chan Pass' ), p( 'Set up your 4chan pass, so you can dump without having to fill in a captcha.' ) )
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_pixiv_account' ), p( 'Manage &Pixiv Account' ), p( 'Set up your pixiv username and password.' ) )
menu.Append( ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'manage_subscriptions' ), p( 'Manage &Subscriptions' ), p( 'Change the queries you want the client to regularly import from.' ) )
menu.AppendSeparator()
@ -1140,10 +1138,13 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
twitter.SetBitmap( CC.GlobalBMPs.twitter )
tumblr = wx.MenuItem( links, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'tumblr' ), p( 'Tumblr' ) )
tumblr.SetBitmap( CC.GlobalBMPs.tumblr )
patreon = wx.MenuItem( links, ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'patreon' ), p( 'Patreon' ) )
patreon.SetBitmap( CC.GlobalBMPs.patreon )
links.AppendItem( site )
links.AppendItem( board )
links.AppendItem( twitter )
links.AppendItem( tumblr )
links.AppendItem( patreon )
menu.AppendMenu( wx.ID_NONE, p( 'Links' ), links )
db_profile_mode_id = ClientCaches.MENU_EVENT_ID_TO_ACTION_CACHE.GetPermanentId( 'db_profile_mode' )
@ -1319,11 +1320,6 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
self._controller.CallToThread( do_it )
def _Manage4chanPass( self ):
with ClientGUIDialogsManage.DialogManage4chanPass( self ) as dlg: dlg.ShowModal()
def _ManageAccountTypes( self, service_key ):
with ClientGUIDialogsManage.DialogManageAccountTypes( self, service_key ) as dlg: dlg.ShowModal()
@ -1339,11 +1335,6 @@ class FrameGUI( ClientGUICommon.FrameThatResizes ):
with ClientGUIDialogsManage.DialogManageExportFolders( self ) as dlg: dlg.ShowModal()
def _ManageImageboards( self ):
with ClientGUIDialogsManage.DialogManageImageboards( self ) as dlg: dlg.ShowModal()
def _ManageImportFolders( self ):
with ClientGUIDialogsManage.DialogManageImportFolders( self ) as dlg: dlg.ShowModal()
@ -2441,11 +2432,9 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
elif command == 'import_files': self._ImportFiles()
elif command == 'import_tags': self._ImportTags()
elif command == 'load_gui_session': self._LoadGUISession( data )
elif command == 'manage_4chan_pass': self._Manage4chanPass()
elif command == 'manage_account_types': self._ManageAccountTypes( data )
elif command == 'manage_boorus': self._ManageBoorus()
elif command == 'manage_export_folders': self._ManageExportFolders()
elif command == 'manage_imageboards': self._ManageImageboards()
elif command == 'manage_import_folders': self._ManageImportFolders()
elif command == 'manage_pixiv_account': self._ManagePixivAccount()
elif command == 'manage_server_services': self._ManageServer( data )
@ -2477,6 +2466,7 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
elif command == 'open_export_folder': self._OpenExportFolder()
elif command == 'open_install_folder': self._OpenInstallFolder()
elif command == 'options': self._ManageOptions()
elif command == 'patreon': webbrowser.open( 'https://www.patreon.com/hydrus_dev' )
elif command == 'pause_export_folders_sync': self._PauseSync( 'export_folders' )
elif command == 'pause_import_folders_sync': self._PauseSync( 'import_folders' )
elif command == 'pause_repo_sync': self._PauseSync( 'repo' )
@ -2782,6 +2772,15 @@ The password is cleartext here but obscured in the entry dialog. Enter a blank p
def RefreshMenu( self, name ):
db_going_to_hang_if_we_hit_it = HydrusGlobals.client_controller.GetDB().CurrentlyDoingJob()
if db_going_to_hang_if_we_hit_it:
wx.CallLater( 2500, self.RefreshMenu, name )
return
( menu, label, show ) = self._GenerateMenuInfo( name )
if HC.PLATFORM_OSX: menu.SetTitle( label ) # causes bugs in os x if this is not here
@ -2958,14 +2957,14 @@ class FrameReviewServices( ClientGUICommon.Frame ):
listbook_dict[ service_type ] = listbook
parent_listbook.AddPage( name, listbook )
parent_listbook.AddPage( name, name, listbook )
listbook = listbook_dict[ service_type ]
name = service.GetName()
listbook.AddPageArgs( name, self._Panel, ( listbook, self._controller, service.GetServiceKey() ), {} )
listbook.AddPageArgs( name, name, self._Panel, ( listbook, self._controller, service.GetServiceKey() ), {} )
wx.CallAfter( self._local_listbook.Layout )

View File

@ -2092,7 +2092,7 @@ class CanvasMediaListFilter( CanvasMediaList ):
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
self.SetMedia( self._GetFirst() )
wx.CallAfter( self.SetMedia, self._GetFirst() ) # don't set this until we have a size > (20, 20)!
def _Back( self ):
@ -2846,7 +2846,7 @@ class CanvasMediaListCustomFilter( CanvasMediaListNavigable ):
self.Bind( wx.EVT_CHAR_HOOK, self.EventCharHook )
self.SetMedia( self._GetFirst() )
wx.CallAfter( self.SetMedia, self._GetFirst() ) # don't set this until we have a size > (20, 20)!
self._hover_commands.AddCommand( 'edit shortcuts', self.EventShortcuts )

View File

@ -13,6 +13,7 @@ import traceback
import wx
import wx.combo
import wx.richtext
import wx.lib.newevent
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
from wx.lib.mixins.listctrl import ColumnSorterMixin
import HydrusTags
@ -220,6 +221,8 @@ class AutoCompleteDropdown( wx.Panel ):
self._cache_text = ''
self._cached_results = []
self._initial_matches_fetched = False
if self._float_mode:
self.Bind( wx.EVT_MOVE, self.EventMove )
@ -634,8 +637,6 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
self._current_namespace = ''
self._current_matches = []
self._cached_results = []
self._file_service_key = file_service_key
self._tag_service_key = tag_service_key
@ -700,6 +701,8 @@ class AutoCompleteDropdownTags( AutoCompleteDropdown ):
matches = self._GenerateMatches()
self._initial_matches_fetched = True
self._dropdown_list.SetPredicates( matches )
self._current_matches = matches
@ -919,9 +922,9 @@ class AutoCompleteDropdownTagsRead( AutoCompleteDropdownTags ):
input_just_changed = self._cache_text != ''
db_not_going_to_hang_if_we_hit_it = not HydrusGlobals.client_controller.CurrentlyIdle()
db_not_going_to_hang_if_we_hit_it = not HydrusGlobals.client_controller.GetDB().CurrentlyDoingJob()
if input_just_changed or db_not_going_to_hang_if_we_hit_it:
if input_just_changed or db_not_going_to_hang_if_we_hit_it or not self._initial_matches_fetched:
self._cache_text = ''
self._current_namespace = ''
@ -1947,16 +1950,16 @@ class ListBook( wx.Panel ):
self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
self._names_to_active_pages = {}
self._names_to_proto_pages = {}
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box = self.LB( self, style = wx.LB_SINGLE | wx.LB_SORT )
self._list_box = wx.ListBox( self, style = wx.LB_SINGLE | wx.LB_SORT )
self._empty_panel = wx.Panel( self )
self._empty_panel.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNFACE ) )
self._current_name = None
self._current_key = None
self._current_panel = self._empty_panel
@ -1976,55 +1979,67 @@ class ListBook( wx.Panel ):
self.Bind( wx.EVT_MENU, self.EventMenu )
class LB( wx.ListBox ):
def _ActivatePage( self, key ):
( classname, args, kwargs ) = self._keys_to_proto_pages[ key ]
def FindString( self, name ):
page = classname( *args, **kwargs )
page.Hide()
self._panel_sizer.AddF( page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._keys_to_active_pages[ key ] = page
del self._keys_to_proto_pages[ key ]
self._RecalcListBoxWidth()
def _GetIndex( self, key ):
for i in range( self._list_box.GetCount() ):
if HC.PLATFORM_WINDOWS: return wx.ListBox.FindString( self, name )
else:
i_key = self._list_box.GetClientData( i )
if i_key == key:
for i in range( self.GetCount() ):
if self.GetString( i ) == name: return i
return wx.NOT_FOUND
return i
return wx.NOT_FOUND
def _RecalcListBoxWidth( self ): self.Layout()
def _Select( self, selection ):
if selection == wx.NOT_FOUND: self._current_name = None
else: self._current_name = self._list_box.GetString( selection )
if selection == wx.NOT_FOUND:
self._current_key = None
else:
self._current_key = self._list_box.GetClientData( selection )
self._current_panel.Hide()
self._list_box.SetSelection( selection )
if selection == wx.NOT_FOUND: self._current_panel = self._empty_panel
if selection == wx.NOT_FOUND:
self._current_panel = self._empty_panel
else:
if self._current_name in self._names_to_proto_pages:
if self._current_key in self._keys_to_proto_pages:
( classname, args, kwargs ) = self._names_to_proto_pages[ self._current_name ]
page = classname( *args, **kwargs )
page.Hide()
self._panel_sizer.AddF( page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._names_to_active_pages[ self._current_name ] = page
del self._names_to_proto_pages[ self._current_name ]
self._RecalcListBoxWidth()
self._ActivatePage( self._current_key )
self._current_panel = self._names_to_active_pages[ self._current_name ]
self._current_panel = self._keys_to_active_pages[ self._current_key ]
self._current_panel.Show()
@ -2038,7 +2053,12 @@ class ListBook( wx.Panel ):
self.ProcessEvent( event )
def AddPage( self, name, page, select = False ):
def AddPage( self, display_name, key, page, select = False ):
if self._GetIndex( key ) != wx.NOT_FOUND:
raise HydrusExceptions.NameException( 'That entry already exists!' )
if not isinstance( page, tuple ):
@ -2047,30 +2067,41 @@ class ListBook( wx.Panel ):
self._panel_sizer.AddF( page, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._list_box.Append( name )
self._list_box.Append( display_name, key )
self._names_to_active_pages[ name ] = page
self._keys_to_active_pages[ key ] = page
self._RecalcListBoxWidth()
if self._list_box.GetCount() == 1: self._Select( 0 )
elif select: self._Select( self._list_box.FindString( name ) )
if self._list_box.GetCount() == 1:
self._Select( 0 )
elif select:
index = self._GetIndex( key )
self._Select( index )
def AddPageArgs( self, name, classname, args, kwargs ):
def AddPageArgs( self, display_name, key, classname, args, kwargs ):
if self.NameExists( name ):
if self._GetIndex( key ) != wx.NOT_FOUND:
raise HydrusExceptions.NameException( 'That name is already in use!' )
raise HydrusExceptions.NameException( 'That entry already exists!' )
self._list_box.Append( name )
self._list_box.Append( display_name, key )
self._names_to_proto_pages[ name ] = ( classname, args, kwargs )
self._keys_to_proto_pages[ key ] = ( classname, args, kwargs )
self._RecalcListBoxWidth()
if self._list_box.GetCount() == 1: self._Select( 0 )
if self._list_box.GetCount() == 1:
self._Select( 0 )
def DeleteAllPages( self ):
@ -2081,12 +2112,12 @@ class ListBook( wx.Panel ):
self._panel_sizer.AddF( self._empty_panel, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
self._current_name = None
self._current_key = None
self._current_panel = self._empty_panel
self._names_to_active_pages = {}
self._names_to_proto_pages = {}
self._keys_to_active_pages = {}
self._keys_to_proto_pages = {}
self._list_box.Clear()
@ -2097,23 +2128,32 @@ class ListBook( wx.Panel ):
if selection != wx.NOT_FOUND:
name_to_delete = self._current_name
key_to_delete = self._current_key
page_to_delete = self._current_panel
next_selection = selection + 1
previous_selection = selection - 1
if next_selection < self._list_box.GetCount(): self._Select( next_selection )
elif previous_selection >= 0: self._Select( previous_selection )
else: self._Select( wx.NOT_FOUND )
if next_selection < self._list_box.GetCount():
self._Select( next_selection )
elif previous_selection >= 0:
self._Select( previous_selection )
else:
self._Select( wx.NOT_FOUND )
self._panel_sizer.Detach( page_to_delete )
wx.CallAfter( page_to_delete.Destroy )
del self._names_to_active_pages[ name_to_delete ]
del self._keys_to_active_pages[ key_to_delete ]
self._list_box.Delete( self._list_box.FindString( name_to_delete ) )
self._list_box.Delete( selection )
self._RecalcListBoxWidth()
@ -2135,85 +2175,97 @@ class ListBook( wx.Panel ):
def EventSelection( self, event ):
if self._list_box.GetSelection() != self._list_box.FindString( self._current_name ):
if self._list_box.GetSelection() != self._GetIndex( self._current_key ):
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
self.GetEventHandler().ProcessEvent( event )
if event.IsAllowed(): self._Select( self._list_box.GetSelection() )
else: self._list_box.SetSelection( self._list_box.FindString( self._current_name ) )
if event.IsAllowed():
self._Select( self._list_box.GetSelection() )
else:
self._list_box.SetSelection( self._GetIndex( self._current_key ) )
def GetCurrentName( self ): return self._current_name
def GetCurrentKey( self ):
return self._current_key
def GetCurrentPage( self ):
if self._current_panel == self._empty_panel: return None
else: return self._current_panel
def GetNames( self ):
names = set()
names.update( self._names_to_proto_pages.keys() )
names.update( self._names_to_active_pages.keys() )
return names
def GetNamesToActivePages( self ):
return self._names_to_active_pages
def NameExists( self, name ): return self._list_box.FindString( name ) != wx.NOT_FOUND
def RenamePage( self, name, new_name ):
if self.NameExists( new_name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if self._current_name == name: self._current_name = new_name
if name in self._names_to_active_pages:
if self._current_panel == self._empty_panel:
dict_to_rename = self._names_to_active_pages
return None
else:
dict_to_rename = self._names_to_proto_pages
return self._current_panel
page_info = dict_to_rename[ name ]
def GetActivePages( self ):
del dict_to_rename[ name ]
return self._keys_to_active_pages.values()
dict_to_rename[ new_name ] = page_info
def GetPage( self, key ):
self._list_box.SetString( self._list_box.FindString( name ), new_name )
if key in self._keys_to_proto_pages:
self._ActivatePage( key )
if key in self._keys_to_active_pages:
return self._keys_to_active_pages[ key ]
raise Exception( 'That page not found!' )
def KeyExists( self, key ):
return key in self._keys_to_active_pages or key in self._keys_to_proto_pages
def RenamePage( self, key, new_name ):
index = self._GetIndex( key )
if index != wx.NOT_FOUND:
self._list_box.SetString( index, new_name )
self._RecalcListBoxWidth()
def Select( self, name ):
def Select( self, key ):
selection = self._list_box.FindString( name )
index = self._GetIndex( key )
if selection != wx.NOT_FOUND and selection != self._list_box.GetSelection():
if index != wx.NOT_FOUND and index != self._list_box.GetSelection():
event = wx.NotifyEvent( wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGING, -1 )
self.GetEventHandler().ProcessEvent( event )
if event.IsAllowed(): self._Select( selection )
if event.IsAllowed():
self._Select( index )
def SelectDown( self ):
current_selection = self._list_box.FindString( self._current_name )
current_selection = self._list_box.GetSelection()
if current_selection != wx.NOT_FOUND:
@ -2222,17 +2274,20 @@ class ListBook( wx.Panel ):
if current_selection == num_entries - 1: selection = 0
else: selection = current_selection + 1
if selection != current_selection: self._Select( selection )
if selection != current_selection:
self._Select( selection )
def SelectPage( self, page_to_select ):
for ( name, page ) in self._names_to_active_pages.items():
for ( key, page ) in self._keys_to_active_pages.items():
if page == page_to_select:
self._Select( self._list_box.FindString( name ) )
self._Select( self._GetIndex( key ) )
return
@ -2241,7 +2296,7 @@ class ListBook( wx.Panel ):
def SelectUp( self ):
current_selection = self._list_box.FindString( self._current_name )
current_selection = self._list_box.GetSelection()
if current_selection != wx.NOT_FOUND:
@ -2250,7 +2305,10 @@ class ListBook( wx.Panel ):
if current_selection == 0: selection = num_entries - 1
else: selection = current_selection - 1
if selection != current_selection: self._Select( selection )
if selection != current_selection:
self._Select( selection )
@ -6101,6 +6159,8 @@ class StaticBoxSorterForListBoxTags( StaticBox ):
self._tags_box.SetTagsByMedia( media, force_reload = force_reload )
( TimeDeltaEvent, EVT_TIME_DELTA ) = wx.lib.newevent.NewCommandEvent()
class TimeDeltaButton( wx.Button ):
def __init__( self, parent, min = 1, days = False, hours = False, minutes = False, seconds = False ):
@ -6187,6 +6247,10 @@ class TimeDeltaButton( wx.Button ):
self.SetValue( value )
new_event = TimeDeltaEvent( 0 )
wx.PostEvent( self, new_event )
@ -6266,7 +6330,9 @@ class TimeDeltaCtrl( wx.Panel ):
self.SetValue( self._min )
wx.PostEvent( self, event )
new_event = TimeDeltaEvent( 0 )
wx.PostEvent( self, new_event )
def GetValue( self ):

View File

@ -2986,7 +2986,7 @@ class DialogPathsToTags( Dialog ):
name = service.GetName()
self._tag_repositories.AddPageArgs( name, self._Panel, ( self._tag_repositories, service_key, paths ), {} )
self._tag_repositories.AddPageArgs( name, service_key, self._Panel, ( self._tag_repositories, service_key, paths ), {} )
@ -2994,13 +2994,11 @@ class DialogPathsToTags( Dialog ):
name = CC.LOCAL_TAG_SERVICE_KEY
self._tag_repositories.AddPage( name, page )
self._tag_repositories.AddPage( name, name, page )
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
default_tag_repository = HydrusGlobals.client_controller.GetServicesManager().GetService( default_tag_repository_key )
self._tag_repositories.Select( default_tag_repository.GetName() )
self._tag_repositories.Select( default_tag_repository_key )
#
@ -3067,7 +3065,7 @@ class DialogPathsToTags( Dialog ):
paths_to_tags = collections.defaultdict( dict )
for page in self._tag_repositories.GetNamesToActivePages().values():
for page in self._tag_repositories.GetActivePages():
( service_key, page_of_paths_to_tags ) = page.GetInfo()
@ -4394,7 +4392,7 @@ class DialogShortcuts( Dialog ):
page = self._Panel( self._shortcuts, default_shortcuts )
self._shortcuts.AddPage( 'default', page )
self._shortcuts.AddPage( 'default', 'default', page )
all_shortcuts = HydrusGlobals.client_controller.Read( 'serialisable_named', HydrusSerialisable.SERIALISABLE_TYPE_SHORTCUTS )
@ -4402,7 +4400,7 @@ class DialogShortcuts( Dialog ):
for ( name, shortcuts ) in names_to_shortcuts.items():
self._shortcuts.AddPageArgs( name, self._Panel, ( self._shortcuts, shortcuts ), {} )
self._shortcuts.AddPageArgs( name, name, self._Panel, ( self._shortcuts, shortcuts ), {} )
@ -4473,7 +4471,7 @@ class DialogShortcuts( Dialog ):
def _CurrentPageIsUntouchable( self ):
name = self._shortcuts.GetCurrentName()
name = self._shortcuts.GetCurrentKey()
if name == 'default': return True
else: return False
@ -4487,7 +4485,7 @@ class DialogShortcuts( Dialog ):
if not self._CurrentPageIsUntouchable():
name = self._shortcuts.GetCurrentName()
name = self._shortcuts.GetCurrentKey()
self._edit_log.append( HydrusData.EditLogActionDelete( name ) )
@ -4508,7 +4506,7 @@ class DialogShortcuts( Dialog ):
for ( name, page ) in self._shortcuts.GetNamesToActivePages().items():
for page in self._shortcuts.GetActivePages():
if name != 'default':
@ -4531,7 +4529,10 @@ class DialogShortcuts( Dialog ):
if name == '': return
while self._shortcuts.NameExists( name ): name += str( random.randint( 0, 9 ) )
while self._shortcuts.KeyExists( name ):
name += str( random.randint( 0, 9 ) )
shortcuts = ClientData.Shortcuts( name )
@ -4554,7 +4555,7 @@ class DialogShortcuts( Dialog ):
page = self._Panel( self._shortcuts, shortcuts )
self._shortcuts.AddPage( name, page, select = True )
self._shortcuts.AddPage( name, name, page, select = True )

View File

@ -412,7 +412,7 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
for ( name, booru ) in boorus.items():
self._boorus.AddPageArgs( name, self._Panel, ( self._boorus, booru ), {} )
self._boorus.AddPageArgs( name, name, self._Panel, ( self._boorus, booru ), {} )
@ -462,15 +462,21 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
name = dlg.GetValue()
if self._boorus.NameExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if self._boorus.KeyExists( name ):
raise HydrusExceptions.NameException( 'That name is already in use!' )
if name == '': raise HydrusExceptions.NameException( 'Please enter a nickname for the service.' )
if name == '':
raise HydrusExceptions.NameException( 'Please enter a nickname for the booru.' )
booru = ClientData.Booru( name, 'search_url', '+', 1, 'thumbnail', '', 'original image', {} )
page = self._Panel( self._boorus, booru, is_new = True )
self._boorus.AddPage( name, page, select = True )
self._boorus.AddPage( name, name, page, select = True )
except HydrusExceptions.NameException as e:
@ -488,7 +494,7 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
if booru_panel is not None:
name = self._boorus.GetCurrentName()
name = self._boorus.GetCurrentKey()
booru = booru_panel.GetBooru()
@ -513,12 +519,14 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
HydrusGlobals.client_controller.Write( 'delete_remote_booru', name )
for ( name, page ) in self._boorus.GetNamesToActivePages().items():
for page in self._boorus.GetActivePages():
if page.HasChanges():
booru = page.GetBooru()
name = booru.GetName()
HydrusGlobals.client_controller.Write( 'remote_booru', name, booru )
@ -532,7 +540,7 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
if booru_panel is not None:
name = self._boorus.GetCurrentName()
name = self._boorus.GetCurrentKey()
self._names_to_delete.append( name )
@ -556,18 +564,18 @@ class DialogManageBoorus( ClientGUIDialogs.Dialog ):
name = booru.GetName()
if not self._boorus.NameExists( name ):
if not self._boorus.KeyExists( name ):
new_booru = ClientData.Booru( name, 'search_url', '+', 1, 'thumbnail', '', 'original image', {} )
page = self._Panel( self._boorus, new_booru, is_new = True )
self._boorus.AddPage( name, page, select = True )
self._boorus.AddPage( name, name, page, select = True )
self._boorus.Select( name )
page = self._boorus.GetNamesToActivePages()[ name ]
page = self._boorus.GetPage( name )
page.Update( booru )
@ -1773,7 +1781,7 @@ If you select synchronise, be careful!'''
return self._export_folder
'''
class DialogManageImageboards( ClientGUIDialogs.Dialog ):
def __init__( self, parent ):
@ -1809,7 +1817,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
for ( name, imageboards ) in sites.items():
self._sites.AddPageArgs( name, self._Panel, ( self._sites, imageboards ), {} )
self._sites.AddPageArgs( name, name, self._Panel, ( self._sites, imageboards ), {} )
@ -1859,13 +1867,13 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
name = dlg.GetValue()
if self._sites.NameExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if self._sites.KeyExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if name == '': raise HydrusExceptions.NameException( 'Please enter a nickname for the service.' )
page = self._Panel( self._sites, [], is_new = True )
self._sites.AddPage( name, page, select = True )
self._sites.AddPage( name, name, page, select = True )
except HydrusExceptions.NameException as e:
@ -1883,7 +1891,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
if site_panel is not None:
name = self._sites.GetCurrentName()
name = self._sites.GetCurrentKey()
imageboards = site_panel.GetImageboards()
@ -1910,12 +1918,14 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
HydrusGlobals.client_controller.Write( 'delete_imageboard', name )
for ( name, page ) in self._sites.GetNamesToActivePages().items():
for page in self._sites.GetActivePages():
if page.HasChanges():
imageboards = page.GetImageboards()
name = 'this is old code'
HydrusGlobals.client_controller.Write( 'imageboard', name, imageboards )
@ -1929,7 +1939,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
if site_panel is not None:
name = self._sites.GetCurrentName()
name = self._sites.GetCurrentKey()
self._names_to_delete.append( name )
@ -1951,14 +1961,14 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
( name, imageboards ) = thing.items()[0]
if not self._sites.NameExists( name ):
if not self._sites.KeyExists( name ):
page = self._Panel( self._sites, [], is_new = True )
self._sites.AddPage( name, page, select = True )
self._sites.AddPage( name, name, page, select = True )
page = self._sites.GetNamesToActivePages()[ name ]
page = self._sites.GetPage( name )
for imageboard in imageboards:
@ -2009,7 +2019,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
name = imageboard.GetName()
self._imageboards.AddPageArgs( name, self._Panel, ( self._imageboards, imageboard ), {} )
self._imageboards.AddPageArgs( name, name, self._Panel, ( self._imageboards, imageboard ), {} )
@ -2057,7 +2067,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
name = dlg.GetValue()
if self._imageboards.NameExists( name ): raise HydrusExceptions.NameException()
if self._imageboards.KeyExists( name ): raise HydrusExceptions.NameException()
if name == '': raise Exception( 'Please enter a nickname for the service.' )
@ -2065,7 +2075,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
page = self._Panel( self._imageboards, imageboard, is_new = True )
self._imageboards.AddPage( name, page, select = True )
self._imageboards.AddPage( name, name, page, select = True )
self._has_changes = True
@ -2105,7 +2115,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
if imageboard_panel is not None:
name = self._imageboards.GetCurrentName()
name = self._imageboards.GetCurrentKey()
self._imageboards.DeleteCurrentPage()
@ -2115,11 +2125,9 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
def GetImageboards( self ):
current_names = self._imageboards.GetNames()
names_to_imageboards = { imageboard.GetName() : imageboard for imageboard in self._original_imageboards if self._imageboards.KeyExists( imageboard.GetName() ) }
names_to_imageboards = { imageboard.GetName() : imageboard for imageboard in self._original_imageboards if imageboard.GetName() in current_names }
for page in self._imageboards.GetNamesToActivePages().values():
for page in self._imageboards.GetActivePages():
imageboard = page.GetImageboard()
@ -2133,23 +2141,23 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
if self._is_new: return True
return self._has_changes or True in ( page.HasChanges() for page in self._imageboards.GetNamesToActivePages().values() )
return self._has_changes or True in ( page.HasChanges() for page in self._imageboards.GetActivePages() )
def UpdateImageboard( self, imageboard ):
name = imageboard.GetName()
if not self._imageboards.NameExists( name ):
if not self._imageboards.KeyExists( name ):
new_imageboard = ClientData.Imageboard( name, '', 60, [], {} )
page = self._Panel( self._imageboards, new_imageboard, is_new = True )
self._imageboards.AddPage( name, page, select = True )
self._imageboards.AddPage( name, name, page, select = True )
page = self._imageboards.GetNamesToActivePages()[ name ]
page = self._imageboards.GetPage( name )
page.Update( imageboard )
@ -2495,7 +2503,7 @@ class DialogManageImageboards( ClientGUIDialogs.Dialog ):
'''
class DialogManageImportFolders( ClientGUIDialogs.Dialog ):
def __init__( self, parent ):
@ -3130,22 +3138,22 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
self._listbook = ClientGUICommon.ListBook( self )
self._listbook.AddPage( 'connection', self._ConnectionPanel( self._listbook ) )
self._listbook.AddPage( 'files and trash', self._FilesAndTrashPanel( self._listbook ) )
self._listbook.AddPage( 'speed and memory', self._SpeedAndMemoryPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'maintenance and processing', self._MaintenanceAndProcessingPanel( self._listbook ) )
self._listbook.AddPage( 'media', self._MediaPanel( self._listbook ) )
self._listbook.AddPage( 'gui', self._GUIPanel( self._listbook ) )
#self._listbook.AddPage( 'sound', self._SoundPanel( self._listbook ) )
self._listbook.AddPage( 'default file system predicates', self._DefaultFileSystemPredicatesPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'default tag import options', self._DefaultTagImportOptionsPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'colours', self._ColoursPanel( self._listbook ) )
self._listbook.AddPage( 'local server', self._ServerPanel( self._listbook ) )
self._listbook.AddPage( 'sort/collect', self._SortCollectPanel( self._listbook ) )
self._listbook.AddPage( 'shortcuts', self._ShortcutsPanel( self._listbook ) )
self._listbook.AddPage( 'file storage locations', self._ClientFilesPanel( self._listbook ) )
self._listbook.AddPage( 'downloading', self._DownloadingPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'tags', self._TagsPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'connection', 'connection', self._ConnectionPanel( self._listbook ) )
self._listbook.AddPage( 'files and trash', 'files and trash', self._FilesAndTrashPanel( self._listbook ) )
self._listbook.AddPage( 'speed and memory', 'speed and memory', self._SpeedAndMemoryPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'maintenance and processing', 'maintenance and processing', self._MaintenanceAndProcessingPanel( self._listbook ) )
self._listbook.AddPage( 'media', 'media', self._MediaPanel( self._listbook ) )
self._listbook.AddPage( 'gui', 'gui', self._GUIPanel( self._listbook ) )
#self._listbook.AddPage( 'sound', 'sound', self._SoundPanel( self._listbook ) )
self._listbook.AddPage( 'default file system predicates', 'default file system predicates', self._DefaultFileSystemPredicatesPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'default tag import options', 'default tag import options', self._DefaultTagImportOptionsPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'colours', 'colours', self._ColoursPanel( self._listbook ) )
self._listbook.AddPage( 'local server', 'local server', self._ServerPanel( self._listbook ) )
self._listbook.AddPage( 'sort/collect', 'sort/collect', self._SortCollectPanel( self._listbook ) )
self._listbook.AddPage( 'shortcuts', 'shortcuts', self._ShortcutsPanel( self._listbook ) )
self._listbook.AddPage( 'file storage locations', 'file storage locations', self._ClientFilesPanel( self._listbook ) )
self._listbook.AddPage( 'downloading', 'downloading', self._DownloadingPanel( self._listbook, self._new_options ) )
self._listbook.AddPage( 'tags', 'tags', self._TagsPanel( self._listbook, self._new_options ) )
self._ok = wx.Button( self, id = wx.ID_OK, label = 'Save' )
self._ok.Bind( wx.EVT_BUTTON, self.EventOK )
@ -5036,7 +5044,7 @@ class DialogManageOptions( ClientGUIDialogs.Dialog ):
def EventOK( self, event ):
for ( name, page ) in self._listbook.GetNamesToActivePages().items():
for page in self._listbook.GetActivePages():
page.UpdateOptions()
@ -5642,7 +5650,7 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
page = self._Panel( self._services_listbook, service_key, service_type, options )
self._services_listbook.AddPage( name, page )
self._services_listbook.AddPage( name, service_key, page )
@ -5694,23 +5702,18 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
( service_key, service_type, options ) = service_panel.GetInfo()
for ( existing_service_key, existing_service_type, existing_options ) in [ page.GetInfo() for page in self._services_listbook.GetNamesToActivePages().values() if page != service_panel ]:
for ( existing_service_key, existing_service_type, existing_options ) in [ page.GetInfo() for page in self._services_listbook.GetActivePages() if page != service_panel ]:
if options[ 'port' ] == existing_options[ 'port' ]: raise Exception( 'That port is already in use!' )
if options[ 'port' ] == existing_options[ 'port' ]:
raise Exception( 'That port is already in use!' )
name = self._services_listbook.GetCurrentName()
new_name = HC.service_string_lookup[ service_type ] + '@' + str( options[ 'port' ] )
if name != new_name: self._services_listbook.RenamePage( name, new_name )
def EventAdd( self, event ):
self._CheckCurrentServiceIsValid()
service_key = HydrusData.GenerateKey()
service_type = self._service_types.GetClientData( self._service_types.GetSelection() )
@ -5719,7 +5722,10 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
existing_ports = set()
for ( existing_service_key, existing_service_type, existing_options ) in [ page.GetInfo() for page in self._services_listbook.GetNamesToActivePages().values() ]: existing_ports.add( existing_options[ 'port' ] )
for ( existing_service_key, existing_service_type, existing_options ) in [ page.GetInfo() for page in self._services_listbook.GetActivePages() ]:
existing_ports.add( existing_options[ 'port' ] )
while port in existing_ports: port += 1
@ -5733,7 +5739,7 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
name = HC.service_string_lookup[ service_type ] + '@' + str( port )
self._services_listbook.AddPage( name, page, select = True )
self._services_listbook.AddPage( name, service_key, page, select = True )
def EventOK( self, event ):
@ -5746,7 +5752,7 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
return
for ( name, page ) in self._services_listbook.GetNamesToActivePages().items():
for page in self._services_listbook.GetActivePages():
if page.HasChanges():
@ -5798,7 +5804,21 @@ class DialogManageServer( ClientGUIDialogs.Dialog ):
def EventServiceChanging( self, event ):
try: self._CheckCurrentServiceIsValid()
try:
self._CheckCurrentServiceIsValid()
service_panel = self._services_listbook.GetCurrentPage()
if service_panel is not None:
( service_key, service_type, options ) = service_panel.GetInfo()
new_name = HC.service_string_lookup[ service_type ] + '@' + str( options[ 'port' ] )
self._services_listbook.RenamePage( service_key, new_name )
except Exception as e:
wx.MessageBox( HydrusData.ToUnicode( e ) )
@ -5991,7 +6011,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
self._service_types_to_listbooks[ service_type ] = listbook
self._listbooks_to_service_types[ listbook ] = service_type
parent_listbook.AddPage( name, listbook )
parent_listbook.AddPage( name, name, listbook )
services = HydrusGlobals.client_controller.GetServicesManager().GetServices( ( service_type, ) )
@ -6001,7 +6021,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
name = service.GetName()
info = service.GetInfo()
listbook.AddPageArgs( name, self._Panel, ( listbook, service_key, service_type, name, info ), {} )
listbook.AddPageArgs( name, service_key, self._Panel, ( listbook, service_key, service_type, name, info ), {} )
@ -6041,7 +6061,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
wx.CallAfter( self._ok.SetFocus )
def _CheckCurrentServiceIsValid( self ):
def _RenameCurrentServiceIfNeeded( self ):
local_or_remote_listbook = self._notebook.GetCurrentPage()
@ -6057,14 +6077,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
( service_key, service_type, name, info ) = service_panel.GetInfo()
old_name = services_listbook.GetCurrentName()
if old_name is not None and name != old_name:
if services_listbook.NameExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
services_listbook.RenamePage( old_name, name )
services_listbook.RenamePage( service_key, name )
@ -6086,9 +6099,10 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
services_listbook = local_or_remote_listbook.GetCurrentPage()
if services_listbook.NameExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if name == '': raise HydrusExceptions.NameException( 'Please enter a nickname for the service.' )
if name == '':
raise HydrusExceptions.NameException( 'Please enter a nickname for the service.' )
service_key = HydrusData.GenerateKey()
service_type = self._listbooks_to_service_types[ services_listbook ]
@ -6175,7 +6189,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
page = self._Panel( services_listbook, service_key, service_type, name, info )
services_listbook.AddPage( name, page, select = True )
services_listbook.AddPage( name, service_key, page, select = True )
except HydrusExceptions.NameException as e:
@ -6190,14 +6204,6 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
def EventExport( self, event ):
try: self._CheckCurrentServiceIsValid()
except HydrusExceptions.NameException as e:
wx.MessageBox( str( e ) )
return
local_or_remote_listbook = self._notebook.GetCurrentPage()
if local_or_remote_listbook is not None:
@ -6240,19 +6246,11 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
def EventOK( self, event ):
try: self._CheckCurrentServiceIsValid()
except HydrusExceptions.NameException as e:
wx.MessageBox( str( e ) )
return
all_listbooks = self._service_types_to_listbooks.values()
for listbook in all_listbooks:
all_pages = listbook.GetNamesToActivePages().values()
all_pages = listbook.GetActivePages()
for page in all_pages:
@ -6262,7 +6260,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
for listbook in all_listbooks:
all_pages = listbook.GetNamesToActivePages().values()
all_pages = listbook.GetActivePages()
for page in all_pages:
@ -6284,13 +6282,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
def EventPageChanging( self, event ):
try: self._CheckCurrentServiceIsValid()
except HydrusExceptions.NameException as e:
wx.MessageBox( str( e ) )
event.Veto()
self._RenameCurrentServiceIfNeeded()
def EventRemove( self, event ):
@ -6343,25 +6335,11 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
def EventServiceChanging( self, event ):
try: self._CheckCurrentServiceIsValid()
except Exception as e:
HydrusData.ShowException( e )
event.Veto()
self._RenameCurrentServiceIfNeeded()
def Import( self, paths ):
try: self._CheckCurrentServiceIsValid()
except Exception as e:
wx.MessageBox( HydrusData.ToUnicode( e ) )
return
for path in paths:
with open( path, 'rb' ) as f: file = f.read()
@ -6370,15 +6348,15 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
services_listbook = self._service_types_to_listbooks[ service_type ]
if services_listbook.NameExists( name ):
if services_listbook.KeyExists( service_key ):
message = 'A service already exists with that name. Overwrite it?'
message = 'That service seems to already exist. Overwrite it?'
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
if dlg.ShowModal() == wx.ID_YES:
page = services_listbook.GetNamesToActivePages()[ name ]
page = services_listbook.GetPage[ service_key ]
page.Update( service_key, service_type, name, info )
@ -6390,7 +6368,7 @@ class DialogManageServices( ClientGUIDialogs.Dialog ):
page = self._Panel( services_listbook, service_key, service_type, name, info )
services_listbook.AddPage( name, page, select = True )
services_listbook.AddPage( name, service_key, page, select = True )
@ -7116,7 +7094,7 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
for name in self._original_subscription_names:
self._listbook.AddPageArgs( name, self._Panel, ( self._listbook, name ), {} )
self._listbook.AddPageArgs( name, name, self._Panel, ( self._listbook, name ), {} )
#
@ -7163,13 +7141,16 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
name = dlg.GetValue()
if self._listbook.NameExists( name ): raise HydrusExceptions.NameException( 'That name is already in use!' )
if self._listbook.KeyExists( name ):
raise HydrusExceptions.NameException( 'That name is already in use!' )
if name == '': raise HydrusExceptions.NameException( 'Please enter a nickname for the subscription.' )
page = self._Panel( self._listbook, name, is_new_subscription = True )
self._listbook.AddPage( name, page, select = True )
self._listbook.AddPage( name, name, page, select = True )
except HydrusExceptions.NameException as e:
@ -7222,7 +7203,7 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
def EventOK( self, event ):
all_pages = self._listbook.GetNamesToActivePages().values()
all_pages = self._listbook.GetActivePages()
try:
@ -7245,7 +7226,7 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
def EventRemove( self, event ):
name = self._listbook.GetCurrentName()
name = self._listbook.GetCurrentKey()
self._names_to_delete.add( name )
@ -7264,9 +7245,9 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
name = subscription.GetName()
if self._listbook.NameExists( name ):
if self._listbook.KeyExists( name ):
message = 'A service already exists with that name. Overwrite it?'
message = 'A subscription with that name already exists. Overwrite it?'
with ClientGUIDialogs.DialogYesNo( self, message ) as dlg:
@ -7274,7 +7255,7 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
self._listbook.Select( name )
page = self._listbook.GetNamesToActivePages()[ name ]
page = self._listbook.GetPage( name )
page.Update( subscription )
@ -7286,7 +7267,7 @@ class DialogManageSubscriptions( ClientGUIDialogs.Dialog ):
page.Update( subscription )
self._listbook.AddPage( name, page, select = True )
self._listbook.AddPage( name, name, page, select = True )
except:
@ -7638,7 +7619,7 @@ class DialogManageTagCensorship( ClientGUIDialogs.Dialog ):
page = self._Panel( self._tag_services, service_key, initial_value )
self._tag_services.AddPage( name, page )
self._tag_services.AddPage( name, service_key, page )
self._tag_services.Select( 'all known tags' )
@ -7686,7 +7667,7 @@ class DialogManageTagCensorship( ClientGUIDialogs.Dialog ):
try:
info = [ page.GetInfo() for page in self._tag_services.GetNamesToActivePages().values() if page.HasInfo() ]
info = [ page.GetInfo() for page in self._tag_services.GetActivePages() if page.HasInfo() ]
HydrusGlobals.client_controller.Write( 'tag_censorship', info )
@ -7804,7 +7785,7 @@ class DialogManageTagParents( ClientGUIDialogs.Dialog ):
name = service.GetName()
service_key = service.GetServiceKey()
self._tag_repositories.AddPageArgs( name, self._Panel, ( self._tag_repositories, service_key, tag ), {} )
self._tag_repositories.AddPageArgs( name, service_key, self._Panel, ( self._tag_repositories, service_key, tag ), {} )
@ -7812,13 +7793,11 @@ class DialogManageTagParents( ClientGUIDialogs.Dialog ):
name = CC.LOCAL_TAG_SERVICE_KEY
self._tag_repositories.AddPage( name, page )
self._tag_repositories.AddPage( name, name, page )
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
service = HydrusGlobals.client_controller.GetServicesManager().GetService( default_tag_repository_key )
self._tag_repositories.Select( service.GetName() )
self._tag_repositories.Select( default_tag_repository_key )
def ArrangeControls():
@ -7883,7 +7862,7 @@ class DialogManageTagParents( ClientGUIDialogs.Dialog ):
try:
for page in self._tag_repositories.GetNamesToActivePages().values():
for page in self._tag_repositories.GetActivePages():
( service_key, content_updates ) = page.GetContentUpdates()
@ -8393,7 +8372,7 @@ class DialogManageTagSiblings( ClientGUIDialogs.Dialog ):
name = CC.LOCAL_TAG_SERVICE_KEY
self._tag_repositories.AddPage( name, page )
self._tag_repositories.AddPage( name, name, page )
services = HydrusGlobals.client_controller.GetServicesManager().GetServices( ( HC.TAG_REPOSITORY, ) )
@ -8406,15 +8385,13 @@ class DialogManageTagSiblings( ClientGUIDialogs.Dialog ):
name = service.GetName()
service_key = service.GetServiceKey()
self._tag_repositories.AddPageArgs( name, self._Panel, ( self._tag_repositories, service_key, tag ), {} )
self._tag_repositories.AddPageArgs( name, service_key, self._Panel, ( self._tag_repositories, service_key, tag ), {} )
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
service = HydrusGlobals.client_controller.GetServicesManager().GetService( default_tag_repository_key )
self._tag_repositories.Select( service.GetName() )
self._tag_repositories.Select( default_tag_repository_key )
#
@ -8471,7 +8448,7 @@ class DialogManageTagSiblings( ClientGUIDialogs.Dialog ):
try:
for page in self._tag_repositories.GetNamesToActivePages().values():
for page in self._tag_repositories.GetActivePages():
( service_key, content_updates ) = page.GetContentUpdates()
@ -9096,8 +9073,6 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
services = HydrusGlobals.client_controller.GetServicesManager().GetServices( HC.TAG_SERVICES )
name_to_select = None
for service in services:
service_key = service.GetServiceKey()
@ -9106,12 +9081,12 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
page = self._Panel( self._tag_repositories, self._file_service_key, service.GetServiceKey(), media )
self._tag_repositories.AddPage( name, page )
if service_key == HC.options[ 'default_tag_repository' ]: name_to_select = name
self._tag_repositories.AddPage( name, service_key, page )
if name_to_select is not None: self._tag_repositories.Select( name_to_select )
default_tag_repository_key = HC.options[ 'default_tag_repository' ]
self._tag_repositories.Select( default_tag_repository_key )
#
@ -9167,14 +9142,14 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
def _ClearPanels( self ):
for page in self._tag_repositories.GetNamesToActivePages().values(): page.SetMedia( set() )
for page in self._tag_repositories.GetActivePages(): page.SetMedia( set() )
def _CommitCurrentChanges( self ):
service_keys_to_content_updates = {}
for page in self._tag_repositories.GetNamesToActivePages().values():
for page in self._tag_repositories.GetActivePages():
( service_key, content_updates ) = page.GetContentUpdates()
@ -9200,7 +9175,10 @@ class DialogManageTags( ClientGUIDialogs.Dialog ):
self._current_media = new_media
for page in self._tag_repositories.GetNamesToActivePages().values(): page.SetMedia( ( new_media, ) )
for page in self._tag_repositories.GetActivePages():
page.SetMedia( ( new_media, ) )

View File

@ -2850,7 +2850,7 @@ class ManagementPanelThreadWatcherImport( ManagementPanel ):
self._thread_check_period = ClientGUICommon.TimeDeltaButton( self._options_panel, min = 30, hours = True, minutes = True, seconds = True )
self._thread_check_period.SetValue( check_period )
self._thread_check_period.Bind( wx.EVT_SPINCTRL, self.EventCheckPeriod )
self._thread_check_period.Bind( ClientGUICommon.EVT_TIME_DELTA, self.EventCheckPeriod )
self._thread_check_now_button = wx.Button( self._options_panel, label = 'check now' )
self._thread_check_now_button.Bind( wx.EVT_BUTTON, self.EventCheckNow )

View File

@ -79,6 +79,7 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
self._page_key = page_key
self._focussed_media = None
self._next_best_media_after_focussed_media_removed = None
self._shift_focussed_media = None
self._selected_media = set()
@ -677,7 +678,10 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
self._DeselectSelect( ( media, ), () )
if self._focussed_media == media: self._SetFocussedMedia( None )
if self._focussed_media == media:
self._SetFocussedMedia( None )
self._shift_focussed_media = None
@ -974,6 +978,33 @@ class MediaPanel( ClientMedia.ListeningMediaList, wx.ScrolledWindow ):
def _SetFocussedMedia( self, media ):
if media is None and self._focussed_media is not None:
next_best_media = self._focussed_media
i = self._sorted_media.index( next_best_media )
while next_best_media in self._selected_media:
if i == 0:
next_best_media = None
break
i -= 1
next_best_media = self._sorted_media[ i ]
self._next_best_media_after_focussed_media_removed = next_best_media
else:
self._next_best_media_after_focussed_media_removed = None
self._focussed_media = media
HydrusGlobals.client_controller.pub( 'focus_changed', self._page_key, media )
@ -1581,7 +1612,29 @@ class MediaPanelThumbnails( MediaPanel ):
if self._focussed_media is not None:
current_position = self._sorted_media.index( self._focussed_media )
media_to_use = self._focussed_media
elif self._next_best_media_after_focussed_media_removed is not None:
media_to_use = self._next_best_media_after_focussed_media_removed
if columns == -1: # treat it as if the focussed area is between this and the next
columns = 0
elif len( self._sorted_media ) > 0:
media_to_use = self._sorted_media[ 0 ]
else:
media_to_use = None
if media_to_use is not None:
current_position = self._sorted_media.index( media_to_use )
new_position = current_position + columns + ( self._num_columns * rows )
@ -1590,9 +1643,10 @@ class MediaPanelThumbnails( MediaPanel ):
self._HitMedia( self._sorted_media[ new_position ], False, shift )
self._ScrollToMedia( self._focussed_media )
self._ScrollToMedia( media_to_use )
def _RecalculateVirtualSize( self ):
@ -1708,13 +1762,19 @@ class MediaPanelThumbnails( MediaPanel ):
def _RemoveMedia( self, singleton_media, collected_media ):
if self._focussed_media is not None:
if self._focussed_media in singleton_media or self._focussed_media in collected_media:
self._SetFocussedMedia( None )
MediaPanel._RemoveMedia( self, singleton_media, collected_media )
self._selected_media.difference_update( singleton_media )
self._selected_media.difference_update( collected_media )
if self._focussed_media not in self._selected_media: self._SetFocussedMedia( None )
self._shift_focussed_media = None
self._RecalculateVirtualSize()

View File

@ -2267,58 +2267,67 @@ class Subscription( HydrusSerialisable.SerialisableBaseNamed ):
new_urls_this_page = 0
( page_of_urls, definitely_no_more_pages ) = gallery.GetPage( self._query, page_index )
page_index += 1
if definitely_no_more_pages:
try:
keep_checking = False
( page_of_urls, definitely_no_more_pages ) = gallery.GetPage( self._query, page_index )
for url in page_of_urls:
page_index += 1
if this_is_initial_sync:
if self._initial_file_limit is not None and total_new_urls + 1 > self._initial_file_limit:
keep_checking = False
break
else:
if self._periodic_file_limit is not None and total_new_urls + 1 > self._periodic_file_limit:
keep_checking = False
break
if url in urls_to_add:
# this catches the occasional overflow when a new file is uploaded while gallery parsing is going on
continue
if self._seed_cache.HasSeed( url ):
if definitely_no_more_pages:
keep_checking = False
break
for url in page_of_urls:
else:
if this_is_initial_sync:
if self._initial_file_limit is not None and total_new_urls + 1 > self._initial_file_limit:
keep_checking = False
break
else:
if self._periodic_file_limit is not None and total_new_urls + 1 > self._periodic_file_limit:
keep_checking = False
break
urls_to_add.add( url )
urls_to_add_ordered.append( url )
if url in urls_to_add:
# this catches the occasional overflow when a new file is uploaded while gallery parsing is going on
continue
new_urls_this_page += 1
total_new_urls += 1
if self._seed_cache.HasSeed( url ):
keep_checking = False
break
else:
urls_to_add.add( url )
urls_to_add_ordered.append( url )
new_urls_this_page += 1
total_new_urls += 1
except HydrusExceptions.NotFoundException:
# paheal now 404s when no results, so just move on and naturally break
pass
if new_urls_this_page == 0:

View File

@ -53,7 +53,7 @@ options = {}
# Misc
NETWORK_VERSION = 17
SOFTWARE_VERSION = 205
SOFTWARE_VERSION = 206
UNSCALED_THUMBNAIL_DIMENSIONS = ( 200, 200 )
@ -72,6 +72,7 @@ CONTENT_TYPE_TAG_PARENTS = 2
CONTENT_TYPE_FILES = 3
CONTENT_TYPE_RATINGS = 4
CONTENT_TYPE_MAPPING = 5
CONTENT_TYPE_DIRECTORIES = 6
CONTENT_UPDATE_ADD = 0
CONTENT_UPDATE_DELETE = 1

View File

@ -142,7 +142,10 @@ def CopyAndMergeTree( source, dest ):
source_path = os.path.join( root, filename )
dest_path = os.path.join( dest_root, filename )
shutil.copy2( source_path, dest_path )
if not PathsHaveSameSizeAndDate( source, dest ):
shutil.copy2( source_path, dest_path )
@ -317,7 +320,7 @@ def PathsHaveSameSizeAndDate( path1, path2 ):
if os.path.exists( path1 ) and os.path.exists( path2 ):
same_size = os.path.getsize( path1 ) == os.path.getsize( path2 )
same_modified_time = os.path.getmtime( path1 ) == os.path.getmtime( path2 )
same_modified_time = int( os.path.getmtime( path1 ) ) == int( os.path.getmtime( path2 ) )
if same_size and same_modified_time:

View File

@ -493,7 +493,10 @@ class DB( HydrusDB.HydrusDB ):
source = os.path.join( self._db_dir, filename )
dest = os.path.join( backup_path, filename )
shutil.copy2( source, dest )
if not HydrusPaths.PathsHaveSameSizeAndDate( source, dest ):
shutil.copy2( source, dest )
HydrusData.Print( 'backing up: copying files' )
@ -2487,10 +2490,14 @@ class DB( HydrusDB.HydrusDB ):
HydrusData.Print( 'exporting mappings to external db' )
self._c.execute( 'CREATE TABLE IF NOT EXISTS external_mappings.mappings ( service_id INTEGER, tag_id INTEGER, hash_id INTEGER, account_id INTEGER, timestamp INTEGER, PRIMARY KEY( service_id, tag_id, hash_id ) );' )
self._c.execute( 'INSERT INTO external_mappings.mappings SELECT * FROM main.mappings;' )
self._c.execute( 'DROP TABLE main.mappings;' )
self._c.execute( 'CREATE TABLE IF NOT EXISTS external_mappings.mapping_petitions ( service_id INTEGER, account_id INTEGER, tag_id INTEGER, hash_id INTEGER, reason_id INTEGER, timestamp INTEGER, status INTEGER, PRIMARY KEY( service_id, account_id, tag_id, hash_id, status ) );' )
self._c.execute( 'INSERT INTO external_mappings.mapping_petitions SELECT * FROM main.mapping_petitions;' )
self._c.execute( 'DROP TABLE main.mapping_petitions;' )

BIN
static/patreon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B