mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
152f5f37ce | ||
|
|
3e42ad8469 | ||
|
|
c7c7a73384 | ||
|
|
47e636e949 | ||
|
|
0562729d8b | ||
|
|
4a36227a18 | ||
|
|
28b8b2e415 | ||
|
|
fd82464564 | ||
|
|
418acf6e5e | ||
|
|
d14d076989 | ||
|
|
cb8bb5a70e | ||
|
|
563c9aeff3 | ||
|
|
a0cc1f2e03 | ||
|
|
01403a3f92 | ||
|
|
7116674663 | ||
|
|
b6bc5de79c | ||
|
|
5a275db67d | ||
|
|
31395d8794 | ||
|
|
3734bd6f6c | ||
|
|
da06ef8cad | ||
|
|
0b00171655 | ||
|
|
c1cfa86ad1 | ||
|
|
c34c9562d3 | ||
|
|
0e542577b0 | ||
|
|
42be49da83 | ||
|
|
398ac9b7c6 | ||
|
|
508e9a5d94 | ||
|
|
10dbfa9b38 | ||
|
|
e8c42740cf | ||
|
|
4b6c4f048d | ||
|
|
7594cccf8c | ||
|
|
1d9573cf6f | ||
|
|
76f45fb5a6 | ||
|
|
12cf9b800b | ||
|
|
ba7e6494c6 | ||
|
|
72d8160b28 | ||
|
|
6d53511cee | ||
|
|
a563327723 | ||
|
|
096e2bb78a | ||
|
|
8e65f15e1a | ||
|
|
9ea9f60e92 | ||
|
|
8efefaf0bf | ||
|
|
33d9569427 | ||
|
|
2fdfacb34e | ||
|
|
97fcf1ffa8 |
44
README.md
44
README.md
@@ -1,7 +1,17 @@
|
||||
# dupeGuru
|
||||
|
||||
This package contains the source for dupeGuru. Its documentation is
|
||||
[available online][documentation]. Here's how this source tree is organised:
|
||||
[dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in
|
||||
a system. It's written mostly in Python 3 and has the peculiarity of using
|
||||
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
|
||||
is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt4.
|
||||
|
||||
dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same
|
||||
source tree. You choose the edition you want to build in a ``configure.py`` flag.
|
||||
|
||||
# Contents of this folder
|
||||
|
||||
This folder contains the source for dupeGuru. Its documentation is in ``help``, but is also
|
||||
[available online][documentation] in its built form. Here's how this source tree is organised:
|
||||
|
||||
* core: Contains the core logic code for dupeGuru. It's Python code.
|
||||
* core_*: Edition-specific-cross-toolkit code written in Python.
|
||||
@@ -36,7 +46,7 @@ and follow instructions from the script. You can then ignore the rest of the bui
|
||||
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
|
||||
to be installed manually.
|
||||
|
||||
* All systems: [Python 3.2+][python] and [setuptools][setuptools]
|
||||
* All systems: [Python 3.3+][python] and [setuptools][setuptools]
|
||||
* Mac OS X: The last XCode to have the 10.6 SDK included.
|
||||
* Windows: Visual Studio 2008, [PyQt 4.7+][pyqt], [cx_Freeze][cxfreeze] and
|
||||
[Advanced Installer][advinst] (you only need the last two if you want to create an installer)
|
||||
@@ -45,17 +55,16 @@ On Ubuntu, the apt-get command to install all pre-requisites is:
|
||||
|
||||
$ apt-get install python3-dev python3-pyqt4 pyqt4-dev-tools python3-setuptools
|
||||
|
||||
## Virtualenv setup
|
||||
## Setting up the virtual environment
|
||||
|
||||
First, you need `pip` and `virtualenv` in your system Python install:
|
||||
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
|
||||
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
|
||||
it doesn't install setuptools and pip, so it has to be installed manually:
|
||||
|
||||
$ sudo easy_install pip
|
||||
$ sudo pip install virtualenv
|
||||
|
||||
Then, in dupeGuru's source folder, create a virtual environment and activate it:
|
||||
|
||||
$ virtualenv --system-site-packages env
|
||||
$ pyvenv --system-site-packages env
|
||||
$ source env/bin/activate
|
||||
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
|
||||
$ easy_install pip
|
||||
|
||||
Then, you can install pip requirements in your virtualenv:
|
||||
|
||||
@@ -63,17 +72,6 @@ Then, you can install pip requirements in your virtualenv:
|
||||
|
||||
([osx|win] depends, of course, on your platform. On other platforms, just use requirements.txt).
|
||||
|
||||
## Alternative: pyvenv
|
||||
|
||||
If you're on Python 3.3+, you can use the built-in `pyvenv` instead of `virtualenv`. `pyvenv` is
|
||||
pretty much the same thing as `virtualenv`, except that it doesn't install setuptools and pip, so it
|
||||
has to be installed manually:
|
||||
|
||||
$ pyvenv --system-site-packages env
|
||||
$ source env/bin/activate
|
||||
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
|
||||
$ easy_install pip
|
||||
|
||||
## Actual building and running
|
||||
|
||||
With your virtualenv activated, you can build and run dupeGuru with these commands:
|
||||
@@ -86,6 +84,8 @@ You can also package dupeGuru into an installable package with:
|
||||
|
||||
$ python package.py
|
||||
|
||||
[dupeguru]: http://www.hardcoded.net/dupeguru/
|
||||
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
|
||||
[python]: http://www.python.org/
|
||||
[setuptools]: https://pypi.python.org/pypi/setuptools
|
||||
|
||||
26
build.py
26
build.py
@@ -19,7 +19,7 @@ from setuptools import setup, Extension
|
||||
|
||||
from hscommon import sphinxgen
|
||||
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||
get_module_version, move_all, copy_sysconfig_files_for_embed, copy_all, OSXAppStructure,
|
||||
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
||||
collect_stdlib_dependencies, copy)
|
||||
from hscommon import loc
|
||||
@@ -123,7 +123,6 @@ def build_cocoa(edition, dev):
|
||||
del sys.path[0]
|
||||
# Views are not referenced by python code, so they're not found by the collector.
|
||||
copy_all('build/inter/*.so', op.join(pydep_folder, 'inter'))
|
||||
copy_sysconfig_files_for_embed(pydep_folder)
|
||||
if not dev:
|
||||
# Important: Don't ever run delete_files_with_pattern('*.py') on dev builds because you'll
|
||||
# be deleting all py files in symlinked folders.
|
||||
@@ -135,6 +134,7 @@ def build_cocoa(edition, dev):
|
||||
print_and_do(cocoa_compile_command(edition))
|
||||
os.chdir('..')
|
||||
app.copy_executable('cocoa/build/dupeGuru')
|
||||
build_help(edition)
|
||||
print("Copying resources and frameworks")
|
||||
image_path = ed('cocoa/{}/dupeguru.icns')
|
||||
resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
|
||||
@@ -151,6 +151,7 @@ def build_qt(edition, dev, conf):
|
||||
print("Building Qt stuff")
|
||||
print_and_do("pyrcc4 -py3 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
|
||||
fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py'))
|
||||
build_help(edition)
|
||||
print("Creating the run.py file")
|
||||
filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition)
|
||||
|
||||
@@ -168,17 +169,12 @@ def build_help(edition):
|
||||
conftmpl = op.join(current_path, 'help', 'conf.tmpl')
|
||||
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
|
||||
|
||||
def build_base_localizations():
|
||||
loc.compile_all_po('locale')
|
||||
loc.compile_all_po(op.join('hscommon', 'locale'))
|
||||
loc.merge_locale_dir(op.join('hscommon', 'locale'), 'locale')
|
||||
|
||||
def build_qt_localizations():
|
||||
loc.compile_all_po(op.join('qtlib', 'locale'))
|
||||
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
|
||||
|
||||
def build_localizations(ui, edition):
|
||||
build_base_localizations()
|
||||
loc.compile_all_po('locale')
|
||||
if ui == 'cocoa':
|
||||
app = cocoa_app(edition)
|
||||
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'))
|
||||
@@ -220,8 +216,6 @@ def build_updatepot():
|
||||
# We want to merge the generated pot with the old pot in the most preserving way possible.
|
||||
ui_packages = ['qt', op.join('cocoa', 'inter')]
|
||||
loc.generate_pot(ui_packages, op.join('locale', 'ui.pot'), ['tr'], merge=(not ISOSX))
|
||||
print("Building hscommon.pot")
|
||||
loc.generate_pot(['hscommon'], op.join('hscommon', 'locale', 'hscommon.pot'), ['tr'])
|
||||
print("Building qtlib.pot")
|
||||
loc.generate_pot(['qtlib'], op.join('qtlib', 'locale', 'qtlib.pot'), ['tr'])
|
||||
if ISOSX:
|
||||
@@ -236,13 +230,11 @@ def build_updatepot():
|
||||
def build_mergepot():
|
||||
print("Updating .po files using .pot files")
|
||||
loc.merge_pots_into_pos('locale')
|
||||
loc.merge_pots_into_pos(op.join('hscommon', 'locale'))
|
||||
loc.merge_pots_into_pos(op.join('qtlib', 'locale'))
|
||||
loc.merge_pots_into_pos(op.join('cocoalib', 'locale'))
|
||||
|
||||
def build_normpo():
|
||||
loc.normalize_all_pos('locale')
|
||||
loc.normalize_all_pos(op.join('hscommon', 'locale'))
|
||||
loc.normalize_all_pos(op.join('qtlib', 'locale'))
|
||||
loc.normalize_all_pos(op.join('cocoalib', 'locale'))
|
||||
|
||||
@@ -264,7 +256,7 @@ def build_cocoa_bridging_interfaces(edition):
|
||||
add_to_pythonpath('cocoalib')
|
||||
from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
|
||||
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
|
||||
PyFairware, PyTextField, ProgressWindowView, PyProgressWindow)
|
||||
PyTextField, ProgressWindowView, PyProgressWindow)
|
||||
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
||||
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
||||
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
|
||||
@@ -276,7 +268,7 @@ def build_cocoa_bridging_interfaces(edition):
|
||||
from inter.stats_label import PyStatsLabel, StatsLabelView
|
||||
from inter.app import PyDupeGuruBase, DupeGuruView
|
||||
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
||||
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyFairware,
|
||||
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
||||
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru]
|
||||
@@ -317,7 +309,6 @@ def build_pe_modules(ui):
|
||||
def build_normal(edition, ui, dev, conf):
|
||||
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
|
||||
add_to_pythonpath('.')
|
||||
build_help(edition)
|
||||
print("Building dupeGuru")
|
||||
if edition == 'pe':
|
||||
build_pe_modules(ui)
|
||||
@@ -335,8 +326,9 @@ def main():
|
||||
if dev:
|
||||
print("Building in Dev mode")
|
||||
if options.clean:
|
||||
if op.exists('build'):
|
||||
shutil.rmtree('build')
|
||||
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
|
||||
if op.exists(path):
|
||||
shutil.rmtree(path)
|
||||
if not op.exists('build'):
|
||||
os.mkdir('build')
|
||||
if options.doc:
|
||||
|
||||
@@ -13,7 +13,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
#import "DetailsPanel.h"
|
||||
#import "DirectoryPanel.h"
|
||||
#import "IgnoreListDialog.h"
|
||||
#import "HSFairwareAboutBox.h"
|
||||
#import "HSAboutBox.h"
|
||||
#import "HSRecentFiles.h"
|
||||
#import "HSProgressWindow.h"
|
||||
|
||||
@@ -30,7 +30,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
IgnoreListDialog *_ignoreListDialog;
|
||||
HSProgressWindow *_progressWindow;
|
||||
NSWindowController *_preferencesPanel;
|
||||
HSFairwareAboutBox *_aboutBox;
|
||||
HSAboutBox *_aboutBox;
|
||||
HSRecentFiles *_recentResults;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,4 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
/* model --> view */
|
||||
- (void)showMessage:(NSString *)msg;
|
||||
- (void)setupAsRegistered;
|
||||
- (void)showDemoNagWithPrompt:(NSString *)prompt;
|
||||
@end
|
||||
|
||||
@@ -8,7 +8,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
#import "AppDelegateBase.h"
|
||||
#import "ProgressController.h"
|
||||
#import "HSFairwareReminder.h"
|
||||
#import "HSPyUtil.h"
|
||||
#import "Consts.h"
|
||||
#import "Dialogs.h"
|
||||
@@ -140,7 +139,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
|
||||
[op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
|
||||
if ([op runModal] == NSOKButton) {
|
||||
NSString *filename = [[op filenames] objectAtIndex:0];
|
||||
NSString *filename = [[[op URLs] objectAtIndex:0] path];
|
||||
[model loadResultsFrom:filename];
|
||||
[[self recentResults] addFile:filename];
|
||||
}
|
||||
@@ -162,7 +161,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
- (void)showAboutBox
|
||||
{
|
||||
if (_aboutBox == nil) {
|
||||
_aboutBox = [[HSFairwareAboutBox alloc] initWithApp:model];
|
||||
_aboutBox = [[HSAboutBox alloc] initWithApp:model];
|
||||
}
|
||||
[[_aboutBox window] makeKeyAndOrderFront:nil];
|
||||
}
|
||||
@@ -199,7 +198,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
/* Delegate */
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||
{
|
||||
[model initialRegistrationSetup];
|
||||
[model loadSession];
|
||||
}
|
||||
|
||||
@@ -261,16 +259,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[[self resultWindow] showProblemDialog];
|
||||
}
|
||||
|
||||
- (void)setupAsRegistered
|
||||
{
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
- (void)showDemoNagWithPrompt:(NSString *)prompt
|
||||
{
|
||||
[HSFairwareReminder showDemoNagWithApp:[self model] prompt:prompt];
|
||||
}
|
||||
|
||||
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
|
||||
{
|
||||
NSOpenPanel *op = [NSOpenPanel openPanel];
|
||||
@@ -280,7 +268,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[op setAllowsMultipleSelection:NO];
|
||||
[op setTitle:prompt];
|
||||
if ([op runModal] == NSOKButton) {
|
||||
return [[op filenames] objectAtIndex:0];
|
||||
return [[[op URLs] objectAtIndex:0] path];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
@@ -294,7 +282,7 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
|
||||
[sp setTitle:prompt];
|
||||
if ([sp runModal] == NSOKButton) {
|
||||
return [sp filename];
|
||||
return [[sp URL] path];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
|
||||
@@ -64,4 +64,9 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[[self window] close];
|
||||
return r == NSOKButton;
|
||||
}
|
||||
|
||||
- (void)setHardlinkOptionEnabled:(BOOL)enabled
|
||||
{
|
||||
[linkTypeRadio setEnabled:enabled];
|
||||
}
|
||||
@end
|
||||
@@ -16,4 +16,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
@interface DirectoryOutline : HSOutline {}
|
||||
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView;
|
||||
- (PyDirectoryOutline *)model;
|
||||
|
||||
- (void)selectAll;
|
||||
@end;
|
||||
@@ -22,6 +22,12 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
return (PyDirectoryOutline *)model;
|
||||
}
|
||||
|
||||
/* Public */
|
||||
- (void)selectAll
|
||||
{
|
||||
[[self model] selectAll];
|
||||
}
|
||||
|
||||
/* Delegate */
|
||||
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
|
||||
{
|
||||
|
||||
@@ -46,4 +46,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
- (void)addDirectory:(NSString *)directory;
|
||||
- (void)refreshRemoveButtonText;
|
||||
- (void)markAll;
|
||||
|
||||
@end
|
||||
|
||||
@@ -91,8 +91,8 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")];
|
||||
[op setDelegate:self];
|
||||
if ([op runModal] == NSOKButton) {
|
||||
for (NSString *directory in [op filenames]) {
|
||||
[self addDirectory:directory];
|
||||
for (NSURL *directoryURL in [op URLs]) {
|
||||
[self addDirectory:[directoryURL path]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,14 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
}
|
||||
}
|
||||
|
||||
- (void)markAll
|
||||
{
|
||||
/* markAll isn't very descriptive of what we do, but since we re-use the Mark All button from
|
||||
the result window, we don't have much choice.
|
||||
*/
|
||||
[outline selectAll];
|
||||
}
|
||||
|
||||
/* Delegate */
|
||||
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path
|
||||
{
|
||||
@@ -171,6 +179,14 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[self addDirectory:path];
|
||||
}
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)item
|
||||
{
|
||||
if ([item action] == @selector(markAll)) {
|
||||
[item setTitle:NSLocalizedString(@"Select All", @"")];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
|
||||
- (void)directorySelectionChanged:(NSNotification *)aNotification
|
||||
|
||||
@@ -258,8 +258,8 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
|
||||
[sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")];
|
||||
if ([sp runModal] == NSOKButton) {
|
||||
[model saveResultsAs:[sp filename]];
|
||||
[[app recentResults] addFile:[sp filename]];
|
||||
[model saveResultsAs:[[sp URL] path]];
|
||||
[[app recentResults] addFile:[[sp URL] path]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +344,9 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)item
|
||||
{
|
||||
if ([item action] == @selector(markAll)) {
|
||||
[item setTitle:NSLocalizedString(@"Mark All", @"")];
|
||||
}
|
||||
return ![[ProgressController mainProgressController] isShown];
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
"%@ Results" = "%@ Results";
|
||||
"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.";
|
||||
"About dupeGuru" = "About dupeGuru";
|
||||
"Action" = "Action";
|
||||
"Actions" = "Actions";
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import logging
|
||||
|
||||
from objp.util import pyref, dontwrap
|
||||
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer, proxy
|
||||
from cocoa.inter import PyFairware, FairwareView
|
||||
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
|
||||
from cocoa.inter import PyBaseApp, BaseAppView
|
||||
|
||||
class DupeGuruView(FairwareView):
|
||||
class DupeGuruView(BaseAppView):
|
||||
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
|
||||
def showProblemDialog(self): pass
|
||||
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
|
||||
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
|
||||
|
||||
class PyDupeGuruBase(PyFairware):
|
||||
class PyDupeGuruBase(PyBaseApp):
|
||||
@dontwrap
|
||||
def _init(self, modelclass):
|
||||
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
|
||||
install_exception_hook()
|
||||
install_cocoa_logger()
|
||||
patch_threaded_job_performer()
|
||||
appdata = proxy.getAppdataPath()
|
||||
self.model = modelclass(self, appdata)
|
||||
self.model = modelclass(self)
|
||||
|
||||
#---Sub-proxies
|
||||
def detailsPanel(self) -> pyref:
|
||||
@@ -144,14 +143,6 @@ class PyDupeGuruBase(PyFairware):
|
||||
self.model.options['copymove_dest_type'] = copymove_dest_type
|
||||
|
||||
#--- model --> view
|
||||
@dontwrap
|
||||
def open_path(self, path):
|
||||
proxy.openPath_(str(path))
|
||||
|
||||
@dontwrap
|
||||
def reveal_path(self, path):
|
||||
proxy.revealPath_(str(path))
|
||||
|
||||
@dontwrap
|
||||
def ask_yes_no(self, prompt):
|
||||
return self.callback.askYesNoWithPrompt_(prompt)
|
||||
|
||||
@@ -143,16 +143,13 @@ class Directories(directories.Directories):
|
||||
|
||||
|
||||
class DupeGuruME(DupeGuruBase):
|
||||
def __init__(self, view, appdata):
|
||||
appdata = op.join(appdata, 'dupeGuru Music Edition')
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
# Use fileclasses set in DupeGuruBase.__init__()
|
||||
self.directories = Directories(fileclasses=self.directories.fileclasses)
|
||||
self.dead_tracks = []
|
||||
|
||||
def _do_delete(self, j, *args):
|
||||
# XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so
|
||||
# we'll be able to replace "op" below with DupeGuruBase._do_delete.op.
|
||||
def op(dupe):
|
||||
j.add_progress()
|
||||
return self._do_delete_dupe(dupe, *args)
|
||||
@@ -174,7 +171,7 @@ class DupeGuruME(DupeGuruBase):
|
||||
DupeGuruBase._do_delete_dupe(self, dupe, *args)
|
||||
|
||||
def _create_file(self, path):
|
||||
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]):
|
||||
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath.parent()):
|
||||
if not hasattr(self, 'itunes_songs'):
|
||||
songs = get_itunes_songs(self.directories.itunes_libpath)
|
||||
self.itunes_songs = {song.path: song for song in songs}
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
import os.path as op
|
||||
import plistlib
|
||||
import logging
|
||||
import re
|
||||
|
||||
from appscript import app, its, k, CommandError, ApplicationNotFoundError
|
||||
|
||||
from hscommon import io
|
||||
from hscommon.util import remove_invalid_xml, first
|
||||
from hscommon.path import Path
|
||||
from hscommon.path import Path, pathify
|
||||
from hscommon.trans import trget
|
||||
from cocoa import proxy
|
||||
|
||||
@@ -48,6 +46,16 @@ class Photo(PhotoBase):
|
||||
raise IOError('The picture %s could not be read' % str(self.path))
|
||||
return blocks
|
||||
|
||||
def _get_exif_timestamp(self):
|
||||
exifdata = proxy.readExifData_(str(self.path))
|
||||
if exifdata:
|
||||
try:
|
||||
return exifdata['{Exif}']['DateTimeOriginal']
|
||||
except KeyError:
|
||||
return ''
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
class IPhoto(Photo):
|
||||
def __init__(self, path, db_id):
|
||||
@@ -67,11 +75,12 @@ class AperturePhoto(Photo):
|
||||
def display_folder_path(self):
|
||||
return APERTURE_PATH
|
||||
|
||||
def get_iphoto_or_aperture_pictures(plistpath, photo_class):
|
||||
@pathify
|
||||
def get_iphoto_or_aperture_pictures(plistpath: Path, photo_class):
|
||||
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
|
||||
if not io.exists(plistpath):
|
||||
if not plistpath.exists():
|
||||
return []
|
||||
s = io.open(plistpath, 'rt', encoding='utf-8').read()
|
||||
s = plistpath.open('rt', encoding='utf-8').read()
|
||||
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
|
||||
s = remove_invalid_xml(s, replace_with='')
|
||||
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
|
||||
@@ -114,12 +123,12 @@ class Directories(directories.Directories):
|
||||
directories.Directories.__init__(self, fileclasses=[Photo])
|
||||
try:
|
||||
self.iphoto_libpath = get_iphoto_database_path()
|
||||
self.set_state(self.iphoto_libpath[:-1], directories.DirectoryState.Excluded)
|
||||
self.set_state(self.iphoto_libpath.parent(), directories.DirectoryState.Excluded)
|
||||
except directories.InvalidPathError:
|
||||
self.iphoto_libpath = None
|
||||
try:
|
||||
self.aperture_libpath = get_aperture_database_path()
|
||||
self.set_state(self.aperture_libpath[:-1], directories.DirectoryState.Excluded)
|
||||
self.set_state(self.aperture_libpath.parent(), directories.DirectoryState.Excluded)
|
||||
except directories.InvalidPathError:
|
||||
self.aperture_libpath = None
|
||||
|
||||
@@ -171,9 +180,8 @@ class Directories(directories.Directories):
|
||||
|
||||
|
||||
class DupeGuruPE(DupeGuruBase):
|
||||
def __init__(self, view, appdata):
|
||||
appdata = op.join(appdata, 'dupeGuru Picture Edition')
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.directories = Directories()
|
||||
|
||||
def _do_delete(self, j, *args):
|
||||
@@ -247,12 +255,12 @@ class DupeGuruPE(DupeGuruBase):
|
||||
DupeGuruBase._do_delete_dupe(self, dupe, *args)
|
||||
|
||||
def _create_file(self, path):
|
||||
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):
|
||||
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath.parent()):
|
||||
if not hasattr(self, 'path2iphoto'):
|
||||
photos = get_iphoto_pictures(self.directories.iphoto_libpath)
|
||||
self.path2iphoto = {p.path: p for p in photos}
|
||||
return self.path2iphoto.get(path)
|
||||
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath[:-1]):
|
||||
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath.parent()):
|
||||
if not hasattr(self, 'path2aperture'):
|
||||
photos = get_aperture_pictures(self.directories.aperture_libpath)
|
||||
self.path2aperture = {p.path: p for p in photos}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
import logging
|
||||
import os.path as op
|
||||
|
||||
from hscommon import io
|
||||
from hscommon.path import Path
|
||||
from hscommon.path import Path, pathify
|
||||
from cocoa import proxy
|
||||
|
||||
from core.scanner import ScanType
|
||||
@@ -27,8 +26,9 @@ def is_bundle(str_path):
|
||||
|
||||
class Bundle(fs.Folder):
|
||||
@classmethod
|
||||
def can_handle(cls, path):
|
||||
return not io.islink(path) and io.isdir(path) and is_bundle(str(path))
|
||||
@pathify
|
||||
def can_handle(cls, path: Path):
|
||||
return not path.islink() and path.isdir() and is_bundle(str(path))
|
||||
|
||||
|
||||
class Directories(DirectoriesBase):
|
||||
@@ -68,9 +68,10 @@ class Directories(DirectoriesBase):
|
||||
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
def __init__(self, view, appdata):
|
||||
appdata = op.join(appdata, 'dupeGuru')
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
# appdata = op.join(appdata, 'dupeGuru')
|
||||
# print(repr(appdata))
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.directories = Directories()
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from cocoa.inter import PyGUIObject, GUIObjectView
|
||||
class DeletionOptionsView(GUIObjectView):
|
||||
def updateMsg_(self, msg: str): pass
|
||||
def show(self) -> bool: pass
|
||||
def setHardlinkOptionEnabled_(self, enabled: bool): pass
|
||||
|
||||
class PyDeletionOptions(PyGUIObject):
|
||||
def setLinkDeleted_(self, link_deleted: bool):
|
||||
@@ -31,3 +32,6 @@ class PyDeletionOptions(PyGUIObject):
|
||||
def show(self):
|
||||
return self.callback.show()
|
||||
|
||||
@dontwrap
|
||||
def set_hardlink_option_enabled(self, enabled):
|
||||
self.callback.setHardlinkOptionEnabled_(enabled)
|
||||
|
||||
@@ -11,6 +11,9 @@ class PyDirectoryOutline(PyOutline):
|
||||
def removeSelectedDirectory(self):
|
||||
self.model.remove_selected()
|
||||
|
||||
def selectAll(self):
|
||||
self.model.select_all()
|
||||
|
||||
# python --> cocoa
|
||||
@dontwrap
|
||||
def refresh_states(self):
|
||||
|
||||
@@ -31,7 +31,7 @@ def configure(conf):
|
||||
os.symlink('../build/Python', versioned_dylib_path)
|
||||
# The rest is standard WAF code that you can find the the python and macapp demos.
|
||||
conf.load('compiler_c python')
|
||||
conf.check_python_version((3,2,0))
|
||||
conf.check_python_version((3,3,0))
|
||||
conf.check_python_headers()
|
||||
conf.env.FRAMEWORK_COCOA = 'Cocoa'
|
||||
conf.env.ARCH_COCOA = ['x86_64']
|
||||
@@ -44,7 +44,7 @@ def build(ctx):
|
||||
cocoalib_node = ctx.srcnode.find_dir('..').find_dir('cocoalib')
|
||||
cocoalib_folders = ['controllers', 'views']
|
||||
cocoalib_includes = [cocoalib_node] + [cocoalib_node.find_dir(folder) for folder in cocoalib_folders]
|
||||
cocoalib_uses = ['NSEventAdditions', 'Dialogs', 'HSFairwareAboutBox', 'HSFairwareReminder', 'Utils',
|
||||
cocoalib_uses = ['NSEventAdditions', 'Dialogs', 'HSAboutBox', 'Utils',
|
||||
'HSPyUtil', 'ProgressController', 'HSRecentFiles', 'HSQuicklook', 'ValueTransformers',
|
||||
'NSImageAdditions', 'NSNotificationAdditions',
|
||||
'views/HSTableView', 'views/HSOutlineView', 'views/NSIndexPathAdditions',
|
||||
|
||||
@@ -14,8 +14,6 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
NSTextField *titleTextField;
|
||||
NSTextField *versionTextField;
|
||||
NSTextField *copyrightTextField;
|
||||
NSTextField *registeredTextField;
|
||||
NSButton *registerButton;
|
||||
|
||||
PyBaseApp *app;
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "HSFairwareProtocol.h"
|
||||
|
||||
@interface HSFairware : NSObject <HSFairwareProtocol>
|
||||
{
|
||||
NSInteger appId;
|
||||
NSString *name;
|
||||
BOOL registered;
|
||||
}
|
||||
- (id)initWithAppId:(NSInteger)aAppId name:(NSString *)aName;
|
||||
@end
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import "HSFairware.h"
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import "HSFairwareReminder.h"
|
||||
#import "Dialogs.h"
|
||||
#import "Utils.h"
|
||||
|
||||
NSString* md5str(NSString *source)
|
||||
{
|
||||
const char *cSource = [source UTF8String];
|
||||
unsigned char result[16];
|
||||
CC_MD5(cSource, strlen(cSource), result);
|
||||
return fmt(@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
|
||||
result[8], result[9], result[10], result[11], result[12], result[13], result[14], result[15]
|
||||
);
|
||||
}
|
||||
|
||||
BOOL validateCode(NSString *code, NSString *email, NSInteger appId)
|
||||
{
|
||||
if ([code length] != 32) {
|
||||
return NO;
|
||||
}
|
||||
NSInteger i;
|
||||
for (i=0; i<=100; i++) {
|
||||
NSString *blob = fmt(@"%i%@%iaybabtu", appId, email, i);
|
||||
if ([md5str(blob) isEqualTo:code]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString* normalizeString(NSString *str)
|
||||
{
|
||||
return [[str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
||||
}
|
||||
|
||||
@implementation HSFairware
|
||||
- (id)initWithAppId:(NSInteger)aAppId name:(NSString *)aName;
|
||||
{
|
||||
self = [super init];
|
||||
appId = aAppId;
|
||||
name = [aName retain];
|
||||
registered = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[name release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
/* Private */
|
||||
- (void)setRegistrationCode:(NSString *)aCode email:(NSString *)aEmail
|
||||
{
|
||||
registered = validateCode(aCode, aEmail, appId);
|
||||
}
|
||||
|
||||
/* Public */
|
||||
- (void)initialRegistrationSetup
|
||||
{
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
NSString *code = [ud stringForKey:@"RegistrationCode"];
|
||||
NSString *email = [ud stringForKey:@"RegistrationEmail"];
|
||||
if (code && email) {
|
||||
[self setRegistrationCode:code email:email];
|
||||
}
|
||||
if (!registered) {
|
||||
BOOL fairwareMode = [ud boolForKey:@"FairwareMode"];
|
||||
if (!fairwareMode) {
|
||||
NSString *prompt = @"%@ is fairware, which means \"open source software developed "
|
||||
"with expectation of fair contributions from users\". It's a very interesting "
|
||||
"concept, but one year of fairware has shown that most people just want to know "
|
||||
"how much it costs and not be bothered with theories about intellectual property."
|
||||
"\n\n"
|
||||
"So I won't bother you and will be very straightforward: You can try %@ for "
|
||||
"free but you have to buy it in order to use it without limitations. In demo mode, "
|
||||
"%@ will show this dialog on startup."
|
||||
"\n\n"
|
||||
"So it's as simple as this. If you're curious about fairware, however, I encourage "
|
||||
"you to read more about it by clicking on the \"Fairware?\" button.";
|
||||
[HSFairwareReminder showDemoNagWithApp:self prompt:fmt(prompt, name, name, name)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)appName
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
- (NSString *)appLongName
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
- (BOOL)isRegistered
|
||||
{
|
||||
return registered;
|
||||
}
|
||||
|
||||
- (BOOL)setRegisteredCode:(NSString *)code andEmail:(NSString *)email
|
||||
{
|
||||
code = normalizeString(code);
|
||||
email = normalizeString(email);
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
if (([code isEqualTo:@"fairware"]) || ([email isEqualTo:@"fairware"])) {
|
||||
[ud setBool:YES forKey:@"FairwareMode"];
|
||||
[Dialogs showMessage:@"Fairware mode enabled."];
|
||||
return YES;
|
||||
}
|
||||
[self setRegistrationCode:code email:email];
|
||||
if (registered) {
|
||||
[ud setObject:code forKey:@"RegistrationCode"];
|
||||
[ud setObject:email forKey:@"RegistrationEmail"];
|
||||
[Dialogs showMessage:@"Your code is valid, thanks!"];
|
||||
return YES;
|
||||
}
|
||||
else {
|
||||
[Dialogs showMessage:@"Your code is invalid. Make sure that you wrote the good code. Also "
|
||||
"make sure that the e-mail you gave is the same as the e-mail you used for your purchase."];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)contribute
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://open.hardcoded.net/contribute/"]];
|
||||
}
|
||||
|
||||
- (void)buy
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/purchase.htm"]];
|
||||
}
|
||||
|
||||
- (void)aboutFairware
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://open.hardcoded.net/about/"]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "PyFairware.h"
|
||||
|
||||
@interface HSFairwareAboutBox : NSWindowController
|
||||
{
|
||||
NSTextField *titleTextField;
|
||||
NSTextField *versionTextField;
|
||||
NSTextField *copyrightTextField;
|
||||
NSTextField *registeredTextField;
|
||||
NSButton *registerButton;
|
||||
|
||||
PyFairware *app;
|
||||
}
|
||||
|
||||
@property (readwrite, retain) NSTextField *titleTextField;
|
||||
@property (readwrite, retain) NSTextField *versionTextField;
|
||||
@property (readwrite, retain) NSTextField *copyrightTextField;
|
||||
@property (readwrite, retain) NSTextField *registeredTextField;
|
||||
@property (readwrite, retain) NSButton *registerButton;
|
||||
|
||||
- (id)initWithApp:(PyFairware *)app;
|
||||
- (void)updateFields;
|
||||
|
||||
- (void)showRegisterDialog;
|
||||
@end
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import "HSFairwareAboutBox.h"
|
||||
#import "HSFairwareAboutBox_UI.h"
|
||||
#import "HSFairwareReminder.h"
|
||||
|
||||
@implementation HSFairwareAboutBox
|
||||
|
||||
@synthesize titleTextField;
|
||||
@synthesize versionTextField;
|
||||
@synthesize copyrightTextField;
|
||||
@synthesize registeredTextField;
|
||||
@synthesize registerButton;
|
||||
|
||||
- (id)initWithApp:(PyFairware *)aApp
|
||||
{
|
||||
self = [super initWithWindow:nil];
|
||||
[self setWindow:createHSFairwareAboutBox_UI(self)];
|
||||
app = [aApp retain];
|
||||
[self updateFields];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[app release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)updateFields
|
||||
{
|
||||
[titleTextField setStringValue:[app appLongName]];
|
||||
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
[versionTextField setStringValue:[NSString stringWithFormat:@"Version: %@",version]];
|
||||
NSString *copyright = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSHumanReadableCopyright"];
|
||||
[copyrightTextField setStringValue:copyright];
|
||||
if ([app isRegistered]) {
|
||||
[registeredTextField setHidden:NO];
|
||||
[registerButton setHidden:YES];
|
||||
}
|
||||
else {
|
||||
[registeredTextField setHidden:YES];
|
||||
[registerButton setHidden:NO];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showRegisterDialog
|
||||
{
|
||||
HSFairwareReminder *fr = [[HSFairwareReminder alloc] initWithApp:app];
|
||||
[fr enterCode];
|
||||
[fr release];
|
||||
[self updateFields];
|
||||
}
|
||||
@end
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@protocol HSFairwareProtocol
|
||||
- (void)initialRegistrationSetup;
|
||||
- (NSString *)appName;
|
||||
- (NSString *)appLongName;
|
||||
- (BOOL)isRegistered;
|
||||
- (BOOL)setRegisteredCode:(NSString *)code andEmail:(NSString *)email;
|
||||
- (void)contribute;
|
||||
- (void)buy;
|
||||
- (void)aboutFairware;
|
||||
@end
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "HSFairwareProtocol.h"
|
||||
|
||||
@interface HSFairwareReminder : NSObject
|
||||
{
|
||||
NSWindow *codePanel;
|
||||
NSTextField *codePromptTextField;
|
||||
NSTextField *codeTextField;
|
||||
NSTextField *emailTextField;
|
||||
NSWindow *demoNagPanel;
|
||||
NSTextField *demoPromptTextField;
|
||||
|
||||
id <HSFairwareProtocol> app;
|
||||
}
|
||||
|
||||
@property (readwrite, retain) NSWindow *codePanel;
|
||||
@property (readwrite, retain) NSTextField *codePromptTextField;
|
||||
@property (readwrite, retain) NSTextField *codeTextField;
|
||||
@property (readwrite, retain) NSTextField *emailTextField;
|
||||
@property (readwrite, retain) NSWindow *demoNagPanel;
|
||||
@property (readwrite, retain) NSTextField *demoPromptTextField;
|
||||
|
||||
//Show nag only if needed
|
||||
+ (BOOL)showDemoNagWithApp:(id <HSFairwareProtocol>)app prompt:(NSString *)prompt;
|
||||
- (id)initWithApp:(id <HSFairwareProtocol>)app;
|
||||
|
||||
- (void)contribute;
|
||||
- (void)buy;
|
||||
- (void)moreInfo;
|
||||
- (void)cancelCode;
|
||||
- (void)showEnterCode;
|
||||
- (void)submitCode;
|
||||
- (void)closeDialog;
|
||||
|
||||
- (BOOL)showNagPanel:(NSWindow *)panel; //YES: The code has been sucessfully submitted NO: The use wan't to try the demo.
|
||||
- (BOOL)showDemoNagPanelWithPrompt:(NSString *)prompt;
|
||||
- (NSInteger)enterCode; //returns the modal code.
|
||||
@end
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
which should be included with this package. The terms are also available at
|
||||
http://www.hardcoded.net/licenses/bsd_license
|
||||
*/
|
||||
|
||||
#import "HSFairwareReminder.h"
|
||||
#import "HSDemoReminder_UI.h"
|
||||
#import "HSEnterCode_UI.h"
|
||||
#import "Dialogs.h"
|
||||
#import "Utils.h"
|
||||
|
||||
@implementation HSFairwareReminder
|
||||
|
||||
@synthesize codePanel;
|
||||
@synthesize codePromptTextField;
|
||||
@synthesize codeTextField;
|
||||
@synthesize emailTextField;
|
||||
@synthesize demoNagPanel;
|
||||
@synthesize demoPromptTextField;
|
||||
|
||||
+ (BOOL)showDemoNagWithApp:(id <HSFairwareProtocol>)app prompt:(NSString *)prompt
|
||||
{
|
||||
HSFairwareReminder *fr = [[HSFairwareReminder alloc] initWithApp:app];
|
||||
BOOL r = [fr showDemoNagPanelWithPrompt:prompt];
|
||||
[fr release];
|
||||
return r;
|
||||
}
|
||||
|
||||
- (id)initWithApp:(id <HSFairwareProtocol>)aApp
|
||||
{
|
||||
self = [super init];
|
||||
app = aApp;
|
||||
[self setDemoNagPanel:createHSDemoReminder_UI(self)];
|
||||
[self setCodePanel:createHSEnterCode_UI(self)];
|
||||
[codePanel update];
|
||||
[codePromptTextField setStringValue:fmt([codePromptTextField stringValue],[app appName])];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)contribute
|
||||
{
|
||||
[app contribute];
|
||||
}
|
||||
|
||||
- (void)buy
|
||||
{
|
||||
[app buy];
|
||||
}
|
||||
|
||||
- (void)moreInfo
|
||||
{
|
||||
[app aboutFairware];
|
||||
}
|
||||
|
||||
- (void)cancelCode
|
||||
{
|
||||
[codePanel close];
|
||||
[NSApp stopModalWithCode:NSCancelButton];
|
||||
}
|
||||
|
||||
- (void)showEnterCode
|
||||
{
|
||||
[demoNagPanel close];
|
||||
[NSApp stopModalWithCode:NSOKButton];
|
||||
}
|
||||
|
||||
- (void)submitCode
|
||||
{
|
||||
NSString *code = [codeTextField stringValue];
|
||||
NSString *email = [emailTextField stringValue];
|
||||
if ([app setRegisteredCode:code andEmail:email]) {
|
||||
[codePanel close];
|
||||
[NSApp stopModalWithCode:NSOKButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeDialog
|
||||
{
|
||||
[demoNagPanel close];
|
||||
[NSApp stopModalWithCode:NSCancelButton];
|
||||
}
|
||||
|
||||
- (BOOL)showNagPanel:(NSWindow *)panel;
|
||||
{
|
||||
NSInteger r;
|
||||
while (YES) {
|
||||
r = [NSApp runModalForWindow:panel];
|
||||
if (r == NSOKButton) {
|
||||
r = [self enterCode];
|
||||
if (r == NSOKButton) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)showDemoNagPanelWithPrompt:(NSString *)prompt
|
||||
{
|
||||
[demoNagPanel setTitle:fmt([demoNagPanel title],[app appName])];
|
||||
[demoPromptTextField setStringValue:prompt];
|
||||
return [self showNagPanel:demoNagPanel];
|
||||
}
|
||||
|
||||
- (NSInteger)enterCode
|
||||
{
|
||||
return [NSApp runModalForWindow:codePanel];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -20,6 +20,7 @@
|
||||
- (NSString *)bundleIdentifier;
|
||||
- (NSString *)appVersion;
|
||||
- (NSString *)osxVersion;
|
||||
- (NSString *)bundleInfo:(NSString *)key;
|
||||
- (void)postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo;
|
||||
- (id)prefValue:(NSString *)prefname;
|
||||
- (void)setPrefValue:(NSString *)prefname value:(id)value;
|
||||
@@ -29,4 +30,5 @@
|
||||
- (void)destroyPool;
|
||||
- (void)reportCrash:(NSString *)crashReport;
|
||||
- (void)log:(NSString *)s;
|
||||
- (NSDictionary *)readExifData:(NSString *)imagePath;
|
||||
@end
|
||||
@@ -1,5 +1,4 @@
|
||||
#import "CocoaProxy.h"
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import "HSErrorReportWindow.h"
|
||||
|
||||
@implementation CocoaProxy
|
||||
@@ -92,13 +91,14 @@
|
||||
return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
}
|
||||
|
||||
- (NSString *)bundleInfo:(NSString *)key
|
||||
{
|
||||
return [[NSBundle mainBundle] objectForInfoDictionaryKey:key];
|
||||
}
|
||||
|
||||
- (NSString *)osxVersion
|
||||
{
|
||||
SInt32 major, minor, bugfix;
|
||||
Gestalt(gestaltSystemVersionMajor, &major);
|
||||
Gestalt(gestaltSystemVersionMinor, &minor);
|
||||
Gestalt(gestaltSystemVersionBugFix, &bugfix);
|
||||
return [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix];
|
||||
return [[NSProcessInfo processInfo] operatingSystemVersionString];
|
||||
}
|
||||
|
||||
- (void)postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo
|
||||
@@ -152,4 +152,20 @@
|
||||
{
|
||||
NSLog(@"%@", s);
|
||||
}
|
||||
|
||||
- (NSDictionary *)readExifData:(NSString *)imagePath
|
||||
{
|
||||
NSDictionary *result = nil;
|
||||
NSURL* url = [NSURL fileURLWithPath:imagePath];
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, nil);
|
||||
if (source != nil) {
|
||||
CFDictionaryRef metadataRef = CGImageSourceCopyPropertiesAtIndex (source, 0, nil);
|
||||
if (metadataRef != nil) {
|
||||
result = [NSDictionary dictionaryWithDictionary:(NSDictionary *)metadataRef];
|
||||
CFRelease(metadataRef);
|
||||
}
|
||||
CFRelease(source);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@end
|
||||
@@ -294,45 +294,7 @@ class PyBaseApp(PyGUIObject):
|
||||
def set_default(self, key_name, value):
|
||||
proxy.setPrefValue_value_(key_name, value)
|
||||
|
||||
@dontwrap
|
||||
def open_url(self, url):
|
||||
proxy.openURL_(url)
|
||||
|
||||
@dontwrap
|
||||
def show_message(self, msg):
|
||||
self.callback.showMessage_(msg)
|
||||
|
||||
class FairwareView(BaseAppView):
|
||||
def setupAsRegistered(self): pass
|
||||
def showDemoNagWithPrompt_(self, prompt: str): pass
|
||||
|
||||
class PyFairware(PyBaseApp):
|
||||
FOLLOW_PROTOCOLS = ['HSFairwareProtocol']
|
||||
|
||||
def initialRegistrationSetup(self):
|
||||
self.model.initial_registration_setup()
|
||||
|
||||
def isRegistered(self) -> bool:
|
||||
return self.model.registered
|
||||
|
||||
def setRegisteredCode_andEmail_(self, code: str, email: str) -> bool:
|
||||
return self.model.set_registration(code, email, False)
|
||||
|
||||
def contribute(self):
|
||||
self.model.contribute()
|
||||
|
||||
def buy(self):
|
||||
self.model.buy()
|
||||
|
||||
def aboutFairware(self):
|
||||
self.model.about_fairware()
|
||||
|
||||
#--- Python --> Cocoa
|
||||
@dontwrap
|
||||
def setup_as_registered(self):
|
||||
self.callback.setupAsRegistered()
|
||||
|
||||
@dontwrap
|
||||
def show_demo_nag(self, prompt):
|
||||
self.callback.showDemoNagWithPrompt_(prompt)
|
||||
|
||||
|
||||
@@ -101,7 +101,13 @@ http://www.hardcoded.net/licenses/bsd_license
|
||||
[[self view] setDelegate:nil];
|
||||
[[self view] reloadData];
|
||||
[[self view] setDelegate:self];
|
||||
[oldRetainer release];
|
||||
/* Item retainer and releasing
|
||||
|
||||
In theory, [oldRetainer release] should work, but in practice, doing so causes occasional
|
||||
crashes during drag & drop, which I guess keep the reference of an item a bit longer than it
|
||||
should. This is why we autorelease here. See #354.
|
||||
*/
|
||||
[oldRetainer autorelease];
|
||||
[self updateSelection];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
|
||||
"%@ is Fairware" = "%@ is Fairware";
|
||||
"Although the application should continue to run after this error, it may be in an instable state, so it is recommended that you restart the application." = "Although the application should continue to run after this error, it may be in an instable state, so it is recommended that you restart the application.";
|
||||
"Buy" = "Buy";
|
||||
"Cancel" = "Cancel";
|
||||
"Clear List" = "Clear List";
|
||||
"Contribute" = "Contribute";
|
||||
"Don't Send" = "Don't Send";
|
||||
"Enter Key" = "Enter Key";
|
||||
"Enter your key" = "Enter your key";
|
||||
"Error Report" = "Error Report";
|
||||
"Fairware?" = "Fairware?";
|
||||
"No" = "No";
|
||||
"OK" = "OK";
|
||||
"Please wait..." = "Please wait...";
|
||||
"Register" = "Register";
|
||||
"Registration e-mail:" = "Registration e-mail:";
|
||||
"Registration key:" = "Registration key:";
|
||||
"Send" = "Send";
|
||||
"Something went wrong. Would you like to send the error report to Hardcoded Software?" = "Something went wrong. Would you like to send the error report to Hardcoded Software?";
|
||||
"Status: Working..." = "Status: Working...";
|
||||
"Submit" = "Submit";
|
||||
"This app is registered, thanks!" = "This app is registered, thanks!";
|
||||
"Try" = "Try";
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase." = "Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase.";
|
||||
"Work in progress, please wait." = "Work in progress, please wait.";
|
||||
"Work in progress..." = "Work in progress...";
|
||||
"Yes" = "Yes";
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: utf-8\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
@@ -14,10 +8,6 @@ msgid ""
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -26,30 +16,14 @@ msgstr ""
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -62,18 +36,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr ""
|
||||
@@ -88,24 +50,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ is Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Buy"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
@@ -31,30 +23,14 @@ msgstr "Cancel"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Contribute"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Don't Send"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Enter Key"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Enter your key"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Register"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Registration key:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Send"
|
||||
@@ -95,26 +59,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Submit"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "This app is registered, thanks!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Try"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ is Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Buy"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
@@ -31,30 +23,14 @@ msgstr "Abbrechen"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Spenden"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Don't Send"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Registrieren"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Schlüssel eingeben"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Register"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Registrierungsschlüssel:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Send"
|
||||
@@ -95,26 +59,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Abschicken"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "This app is registered, thanks!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Try"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Geben Sie den empfangenen Schlüssel und die E-Mail-Adresse als Referenz für "
|
||||
"den Kauf an, wenn Sie für %@ gespendet haben."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,10 +9,6 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ es Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
@@ -21,10 +17,6 @@ msgstr ""
|
||||
"Aunque la aplicación debería continuar funcionado tras el fallo, sin embargo"
|
||||
" podría volverse inestable. Se recomienda reiniciar la aplicación."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Comprar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -33,30 +25,14 @@ msgstr "Cancelar"
|
||||
msgid "Clear List"
|
||||
msgstr "Limpiar lista"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Donar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "No envíar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Introducir clave"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Introduzca su clave"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr "Informe de error"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "¿Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
@@ -69,18 +45,6 @@ msgstr "Aceptar"
|
||||
msgid "Please wait..."
|
||||
msgstr "Por favor, espere..."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Registrar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr "Correo electrónico de registro:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Clave de registro"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Enviar"
|
||||
@@ -97,26 +61,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr "Estado: procesando..."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Enviar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "La aplicación está registrada. ¡Gracias!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Probar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Escriba la clave que recibió al donar a %@, así como el correo electrónico "
|
||||
"que usó en el proceso."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr "En proceso, por favor, espere."
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ est Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Acheter"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
@@ -31,30 +23,14 @@ msgstr "Annuler"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Contribuer"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Ignorer"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Entrez votre clé"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Clé d'enregistrement:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Envoyer"
|
||||
@@ -94,26 +58,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Soumettre"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "L'application est enregistrée, merci!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Essayer"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Entrez la clé que vous avez reçue en contribuant à %@, ainsi que le courriel"
|
||||
" utilisé pour la contribution."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: hy\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "$appname-ը Fairware է"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Գնել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Չեղարկել"
|
||||
@@ -31,30 +23,14 @@ msgstr "Չեղարկել"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Մասնակցել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Չուղարկել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Գրել բանալին"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Շարունակել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware է՞"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Գրանցել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Գրանցման բանալին."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Ուղարկել"
|
||||
@@ -94,26 +58,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Հաստատել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "Այս ծրագիրը գրանցված է, շնորհակալություն!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Փորձել"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Մուտքագրեք այն բանալին, որը ստացել եք %@-ին աջակցելիս, քանզի Ձեր էլ. հասցեն "
|
||||
"օգտագործվել է գնման ժամանակ:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ è Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Acquista"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
@@ -31,30 +23,14 @@ msgstr "Annulla"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Contribuisci"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Non inviare"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Inserisci Codice"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Inserisci il tuo codice"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Registra"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Codice di registrazione:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Invia"
|
||||
@@ -95,26 +59,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Invia"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "Questa applicazione è registrata, grazie!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Prova"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Inserisci il codice che hai ricevuto quando hai contribuito a %@, così come "
|
||||
"l'email di riferimento utilizzata per l'acquisto."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: nl\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -31,30 +23,14 @@ msgstr ""
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr ""
|
||||
@@ -93,24 +57,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "This app is registered, thanks!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,10 +9,6 @@ msgstr ""
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ é Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
@@ -21,10 +17,6 @@ msgstr ""
|
||||
"Embora o aplicativo continue a funcionar após este erro, ele pode estar "
|
||||
"instável. É recomendável reiniciá-lo."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Comprar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
@@ -33,30 +25,14 @@ msgstr "Cancelar"
|
||||
msgid "Clear List"
|
||||
msgstr "Limpar Lista"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Contribuir"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Não Enviar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Entrar Chave"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Entre sua chave"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr "Não"
|
||||
@@ -69,18 +45,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr "Aguarde..."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Registrar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Chave de registro:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Enviar"
|
||||
@@ -96,26 +60,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Enviar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "O app está registrado, obrigado!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Testar"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Digite a chave que você recebeu ao contribuir com o %@, assim como o e-mail "
|
||||
"usado para a compra."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: ru\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ является Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Купить"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Отменить"
|
||||
@@ -31,30 +23,14 @@ msgstr "Отменить"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Способствовайте"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Не отправлять"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Видите ключ"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Видите Ваш ключ"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Регистрация"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Регистрационный ключ:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Отправить"
|
||||
@@ -94,26 +58,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Передать"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "Это приложение зарегистрировано, спасибо!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Пробовать"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Тип ключа, который вы получили при способствовала %@, а также электронной "
|
||||
"почты используется в качестве ссылки для покупки."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,10 +9,6 @@ msgstr ""
|
||||
"Language: uk\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ це Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
@@ -21,10 +17,6 @@ msgstr ""
|
||||
"Хоча програма має продовжувати роботу після цієї помилки, вона може "
|
||||
"перебувати у нестабільному стані, тож рекомендується перезапустити програму."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Купити"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "Відмінити"
|
||||
@@ -33,30 +25,14 @@ msgstr "Відмінити"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "Зробити внесок"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Не надсилати"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "Введіть ключ"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Введіть Ваш ключ"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -69,18 +45,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Зареєструвати"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "Реєстраційний ключ:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Надіслати"
|
||||
@@ -96,26 +60,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "Надіслати"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "Програму зареєстровано, дякуємо!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Спробувати"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
"Введіть ключ, який Ви отримали зробивши внесок за %@, а також адресу "
|
||||
"електронної пошти, яка була вказана під час покупки."
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -10,20 +10,12 @@ msgstr ""
|
||||
"Language: vi\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
@@ -32,30 +24,14 @@ msgstr ""
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -68,18 +44,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr ""
|
||||
@@ -94,24 +58,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -9,20 +9,12 @@ msgstr ""
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "%@ is Fairware"
|
||||
msgstr "%@ is Fairware"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Although the application should continue to run after this error, it may be "
|
||||
"in an instable state, so it is recommended that you restart the application."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Buy"
|
||||
msgstr "Buy"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
@@ -31,30 +23,14 @@ msgstr "取消"
|
||||
msgid "Clear List"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Contribute"
|
||||
msgstr "捐助"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Don't Send"
|
||||
msgstr "Don't Send"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter Key"
|
||||
msgstr "输入密钥"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Enter your key"
|
||||
msgstr "Enter your key"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Error Report"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Fairware?"
|
||||
msgstr "Fairware?"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -67,18 +43,6 @@ msgstr ""
|
||||
msgid "Please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Register"
|
||||
msgstr "Register"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration e-mail:"
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Registration key:"
|
||||
msgstr "密钥:"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Send"
|
||||
msgstr "Send"
|
||||
@@ -95,24 +59,6 @@ msgstr ""
|
||||
msgid "Status: Working..."
|
||||
msgstr ""
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Submit"
|
||||
msgstr "提交"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "This app is registered, thanks!"
|
||||
msgstr "This app is registered, thanks!"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Try"
|
||||
msgstr "Try"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid ""
|
||||
"Type the key you received when you contributed to %@, as well as the e-mail "
|
||||
"used as a reference for the purchase."
|
||||
msgstr "当您捐助 %@ 后,请输入收到的注册密钥以及电子邮件,这将作为购买凭证。"
|
||||
|
||||
#: cocoalib/en.lproj/cocoalib.strings:0
|
||||
msgid "Work in progress, please wait."
|
||||
msgstr ""
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
ownerclass = 'HSFairwareReminder'
|
||||
ownerimport = 'HSFairwareReminder.h'
|
||||
|
||||
result = Window(528, 253, "%@ is Fairware")
|
||||
result.canClose = False
|
||||
result.canResize = False
|
||||
result.canMinimize = False
|
||||
demoPromptLabel = Label(result, NLSTR("<demo prompt>"))
|
||||
tryButton = Button(result, "Try")
|
||||
enterKeyButton = Button(result, "Enter Key")
|
||||
buyButton = Button(result, "Buy")
|
||||
fairwareButton = Button(result, "Fairware?")
|
||||
|
||||
owner.demoPromptTextField = demoPromptLabel
|
||||
result.initialFirstResponder = tryButton
|
||||
demoPromptLabel.font = Font(FontFamily.Label, FontSize.SmallControl)
|
||||
tryButton.action = Action(owner, 'closeDialog')
|
||||
tryButton.keyEquivalent = "\\r"
|
||||
enterKeyButton.action = Action(owner, 'showEnterCode')
|
||||
buyButton.action = Action(owner, 'buy')
|
||||
fairwareButton.action = Action(owner, 'moreInfo')
|
||||
|
||||
for button in (tryButton, enterKeyButton, buyButton, fairwareButton):
|
||||
button.width = 113
|
||||
demoPromptLabel.height = 185
|
||||
|
||||
demoPromptLabel.packToCorner(Pack.UpperLeft)
|
||||
demoPromptLabel.fill(Pack.Right)
|
||||
tryButton.packRelativeTo(demoPromptLabel, Pack.Below, Pack.Left)
|
||||
enterKeyButton.packRelativeTo(tryButton, Pack.Right, Pack.Middle)
|
||||
buyButton.packRelativeTo(enterKeyButton, Pack.Right, Pack.Middle)
|
||||
fairwareButton.packRelativeTo(buyButton, Pack.Right, Pack.Middle)
|
||||
@@ -1,52 +0,0 @@
|
||||
ownerclass = 'HSFairwareReminder'
|
||||
ownerimport = 'HSFairwareReminder.h'
|
||||
|
||||
result = Window(450, 185, "Enter Key")
|
||||
result.canClose = False
|
||||
result.canResize = False
|
||||
result.canMinimize = False
|
||||
titleLabel = Label(result, "Enter your key")
|
||||
promptLabel = Label(result, "Type the key you received when you contributed to %@, as well as the e-mail used as a reference for the purchase.")
|
||||
regkeyLabel = Label(result, "Registration key:")
|
||||
regkeyField = TextField(result, "")
|
||||
regemailLabel = Label(result, "Registration e-mail:")
|
||||
regemailField = TextField(result, "")
|
||||
contributeButton = Button(result, "Contribute")
|
||||
cancelButton = Button(result, "Cancel")
|
||||
submitButton = Button(result, "Submit")
|
||||
|
||||
owner.codePromptTextField = promptLabel
|
||||
owner.codeTextField = regkeyField
|
||||
owner.emailTextField = regemailField
|
||||
result.initialFirstResponder = regkeyField
|
||||
|
||||
titleLabel.font = Font(FontFamily.Label, FontSize.RegularControl, traits=[FontTrait.Bold])
|
||||
smallerFont = Font(FontFamily.Label, FontSize.SmallControl)
|
||||
for control in (promptLabel, regkeyLabel, regemailLabel):
|
||||
control.font = smallerFont
|
||||
regkeyField.usesSingleLineMode = regemailField.usesSingleLineMode = True
|
||||
contributeButton.action = Action(owner, 'contribute')
|
||||
cancelButton.action = Action(owner, 'cancelCode')
|
||||
cancelButton.keyEquivalent = "\\E"
|
||||
submitButton.action = Action(owner, 'submitCode')
|
||||
submitButton.keyEquivalent = "\\r"
|
||||
|
||||
for button in (contributeButton, cancelButton, submitButton):
|
||||
button.width = 100
|
||||
regkeyLabel.width = 128
|
||||
regemailLabel.width = 128
|
||||
promptLabel.height = 32
|
||||
|
||||
titleLabel.packToCorner(Pack.UpperLeft)
|
||||
titleLabel.fill(Pack.Right)
|
||||
promptLabel.packRelativeTo(titleLabel, Pack.Below, Pack.Left)
|
||||
promptLabel.fill(Pack.Right)
|
||||
regkeyField.packRelativeTo(promptLabel, Pack.Below, Pack.Right)
|
||||
regkeyLabel.packRelativeTo(regkeyField, Pack.Left, Pack.Middle)
|
||||
regkeyField.fill(Pack.Left)
|
||||
regemailField.packRelativeTo(regkeyField, Pack.Below, Pack.Right)
|
||||
regemailLabel.packRelativeTo(regemailField, Pack.Left, Pack.Middle)
|
||||
regemailField.fill(Pack.Left)
|
||||
contributeButton.packRelativeTo(regemailLabel, Pack.Below, Pack.Left)
|
||||
submitButton.packRelativeTo(regemailField, Pack.Below, Pack.Right)
|
||||
cancelButton.packRelativeTo(submitButton, Pack.Left, Pack.Middle)
|
||||
@@ -1,46 +0,0 @@
|
||||
ownerclass = 'HSFairwareAboutBox'
|
||||
ownerimport = 'HSFairwareAboutBox.h'
|
||||
|
||||
result = Window(259, 217, "")
|
||||
result.canResize = False
|
||||
result.canMinimize = False
|
||||
image = ImageView(result, "NSApplicationIcon")
|
||||
titleLabel = Label(result, NLSTR("AppTitle"))
|
||||
versionLabel = Label(result, NLSTR("AppVersion"))
|
||||
copyrightLabel = Label(result, NLSTR("AppCopyright"))
|
||||
registeredLabel = Label(result, "This app is registered, thanks!")
|
||||
registerButton = Button(result, "Register")
|
||||
|
||||
owner.window = result
|
||||
owner.titleTextField = titleLabel
|
||||
owner.versionTextField = versionLabel
|
||||
owner.copyrightTextField = copyrightLabel
|
||||
owner.registeredTextField = registeredLabel
|
||||
owner.registerButton = registerButton
|
||||
for label in (titleLabel, versionLabel, copyrightLabel, registeredLabel):
|
||||
label.alignment = const.NSCenterTextAlignment
|
||||
titleLabel.font = Font(FontFamily.Label, FontSize.RegularControl, traits=[FontTrait.Bold])
|
||||
for label in (versionLabel, copyrightLabel, registeredLabel):
|
||||
label.font = Font(FontFamily.Label, FontSize.SmallControl)
|
||||
label.height = 14
|
||||
registerButton.bezelStyle = const.NSRoundRectBezelStyle
|
||||
registerButton.action = Action(owner, 'showRegisterDialog')
|
||||
|
||||
image.height = 96
|
||||
image.packToCorner(Pack.UpperLeft)
|
||||
image.y = result.height - 10 - image.height
|
||||
image.fill(Pack.Right)
|
||||
image.setAnchor(Pack.UpperLeft, growX=True)
|
||||
titleLabel.packRelativeTo(image, Pack.Below, Pack.Left)
|
||||
titleLabel.fill(Pack.Right)
|
||||
titleLabel.setAnchor(Pack.UpperLeft, growX=True)
|
||||
versionLabel.packRelativeTo(titleLabel, Pack.Below, Pack.Left)
|
||||
versionLabel.fill(Pack.Right)
|
||||
versionLabel.setAnchor(Pack.UpperLeft, growX=True)
|
||||
copyrightLabel.packRelativeTo(versionLabel, Pack.Below, Pack.Left)
|
||||
copyrightLabel.fill(Pack.Right)
|
||||
copyrightLabel.setAnchor(Pack.UpperLeft, growX=True)
|
||||
registeredLabel.packRelativeTo(copyrightLabel, Pack.Below, Pack.Left)
|
||||
registeredLabel.fill(Pack.Right)
|
||||
registeredLabel.setAnchor(Pack.UpperLeft, growX=True)
|
||||
registerButton.packRelativeTo(copyrightLabel, Pack.Below, Pack.Middle)
|
||||
150
core/app.py
150
core/app.py
@@ -16,15 +16,14 @@ import shutil
|
||||
|
||||
from send2trash import send2trash
|
||||
from jobprogress import job
|
||||
from hscommon.reg import RegistrableApplication
|
||||
from hscommon.notify import Broadcaster
|
||||
from hscommon.path import Path
|
||||
from hscommon.conflict import smart_move, smart_copy
|
||||
from hscommon.gui.progress_window import ProgressWindow
|
||||
from hscommon.util import (delete_if_empty, first, escape, nonone, format_time_decimal, allsame,
|
||||
rem_file_ext)
|
||||
from hscommon.util import delete_if_empty, first, escape, nonone, format_time_decimal, allsame
|
||||
from hscommon.trans import tr
|
||||
from hscommon.plat import ISWINDOWS
|
||||
from hscommon import desktop
|
||||
|
||||
from . import directories, results, scanner, export, fs
|
||||
from .gui.deletion_options import DeletionOptions
|
||||
@@ -89,20 +88,38 @@ def format_dupe_count(c):
|
||||
return str(c) if c else '---'
|
||||
|
||||
def cmp_value(dupe, attrname):
|
||||
if attrname == 'name':
|
||||
value = rem_file_ext(dupe.name)
|
||||
else:
|
||||
value = getattr(dupe, attrname, '')
|
||||
return value.lower() if isinstance(value, str) else value
|
||||
|
||||
class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
def fix_surrogate_encoding(s, encoding='utf-8'):
|
||||
# ref #210. It's possible to end up with file paths that, while correct unicode strings, are
|
||||
# decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix
|
||||
# these strings here by trying to encode them and, if it fails, we do an encode/decode dance
|
||||
# to remove the problematic characters. This dance is *lossy* but there's not much we can do
|
||||
# because if we end up with this type of string, it means that we don't know the encoding of the
|
||||
# underlying filesystem that brought them. Don't use this for strings you're going to re-use in
|
||||
# fs-related functions because you're going to lose your path (it's going to change). Use this
|
||||
# if you need to export the path somewhere else, outside of the unicode realm.
|
||||
# See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/
|
||||
try:
|
||||
s.encode(encoding)
|
||||
except UnicodeEncodeError:
|
||||
return s.encode(encoding, 'replace').decode(encoding)
|
||||
else:
|
||||
return s
|
||||
|
||||
class DupeGuru(Broadcaster):
|
||||
"""Holds everything together.
|
||||
|
||||
Instantiated once per running application, it holds a reference to every high-level object
|
||||
whose reference needs to be held: :class:`Results`, :class:`Scanner`,
|
||||
whose reference needs to be held: :class:`~core.results.Results`, :class:`Scanner`,
|
||||
:class:`~core.directories.Directories`, :mod:`core.gui` instances, etc..
|
||||
|
||||
It also hosts high level methods and acts as a coordinator for all those elements.
|
||||
It also hosts high level methods and acts as a coordinator for all those elements. This is why
|
||||
some of its methods seem a bit shallow, like for example :meth:`mark_all` and
|
||||
:meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but
|
||||
they are also followed by a notification call which is very important if we want GUI elements
|
||||
to be correctly notified of a change in the data they're presenting.
|
||||
|
||||
.. attribute:: directories
|
||||
|
||||
@@ -123,6 +140,10 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
Instance of :mod:`meta-gui <core.gui>` table listing the results from :attr:`results`
|
||||
"""
|
||||
#--- View interface
|
||||
# get_default(key_name)
|
||||
# set_default(key_name, value)
|
||||
# show_message(msg)
|
||||
# open_url(url)
|
||||
# open_path(path)
|
||||
# reveal_path(path)
|
||||
# ask_yes_no(prompt) --> bool
|
||||
@@ -133,15 +154,14 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
|
||||
# in fairware prompts, we don't mention the edition, it's too long.
|
||||
PROMPT_NAME = "dupeGuru"
|
||||
DEMO_LIMITATION = tr("will only be able to delete, move or copy 10 duplicates at once")
|
||||
|
||||
def __init__(self, view, appdata):
|
||||
def __init__(self, view):
|
||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
logging.debug("Debug mode enabled")
|
||||
RegistrableApplication.__init__(self, view, appid=1)
|
||||
Broadcaster.__init__(self)
|
||||
self.appdata = appdata
|
||||
self.view = view
|
||||
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
|
||||
if not op.exists(self.appdata):
|
||||
os.makedirs(self.appdata)
|
||||
self.directories = directories.Directories()
|
||||
@@ -179,19 +199,17 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
return self.results.is_marked(dupe)
|
||||
if key == 'percentage':
|
||||
m = get_group().get_match_of(dupe)
|
||||
result = m.percentage
|
||||
return m.percentage
|
||||
elif key == 'dupe_count':
|
||||
result = 0
|
||||
return 0
|
||||
else:
|
||||
result = cmp_value(dupe, key)
|
||||
if delta:
|
||||
refval = getattr(get_group().ref, key)
|
||||
refval = cmp_value(get_group().ref, key)
|
||||
if key in self.result_table.DELTA_COLUMNS:
|
||||
result -= refval
|
||||
else:
|
||||
# We use directly getattr() because cmp_value() does thing that we don't want to do
|
||||
# when we want to determine whether two values are exactly the same.
|
||||
same = getattr(dupe, key) == refval
|
||||
same = cmp_value(dupe, key) == refval
|
||||
result = (same, result)
|
||||
return result
|
||||
|
||||
@@ -229,7 +247,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
ref = group.ref
|
||||
linkfunc = os.link if use_hardlinks else os.symlink
|
||||
linkfunc(str(ref.path), str_path)
|
||||
self.clean_empty_dirs(dupe.path[:-1])
|
||||
self.clean_empty_dirs(dupe.path.parent())
|
||||
|
||||
def _create_file(self, path):
|
||||
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
|
||||
@@ -254,7 +272,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
for group_id, group in enumerate(self.results.groups):
|
||||
for dupe in group:
|
||||
data = self.get_display_info(dupe, group)
|
||||
row = [data[col.name] for col in columns]
|
||||
row = [fix_surrogate_encoding(data[col.name]) for col in columns]
|
||||
row.insert(0, group_id)
|
||||
rows.append(row)
|
||||
return colnames, rows
|
||||
@@ -316,13 +334,6 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self.selected_dupes = dupes
|
||||
self.notify('dupes_selected')
|
||||
|
||||
def _check_demo(self):
|
||||
if self.should_apply_demo_limitation and self.results.mark_count > 10:
|
||||
msg = tr("You cannot delete, move or copy more than 10 duplicates at once in demo mode.")
|
||||
self.view.show_message(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
#--- Public
|
||||
def add_directory(self, d):
|
||||
"""Adds folder ``d`` to :attr:`directories`.
|
||||
@@ -372,7 +383,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
def clean_empty_dirs(self, path):
|
||||
if self.options['clean_empty_dirs']:
|
||||
while delete_if_empty(path, ['.DS_Store']):
|
||||
path = path[:-1]
|
||||
path = path.parent()
|
||||
|
||||
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
|
||||
source_path = dupe.path
|
||||
@@ -380,21 +391,21 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
dest_path = Path(destination)
|
||||
if dest_type in {DestType.Relative, DestType.Absolute}:
|
||||
# no filename, no windows drive letter
|
||||
source_base = source_path.remove_drive_letter()[:-1]
|
||||
source_base = source_path.remove_drive_letter().parent()
|
||||
if dest_type == DestType.Relative:
|
||||
source_base = source_base[location_path:]
|
||||
dest_path = dest_path + source_base
|
||||
dest_path = dest_path[source_base]
|
||||
if not dest_path.exists():
|
||||
dest_path.makedirs()
|
||||
# Add filename to dest_path. For file move/copy, it's not required, but for folders, yes.
|
||||
dest_path = dest_path + source_path[-1]
|
||||
dest_path = dest_path[source_path.name]
|
||||
logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path)
|
||||
# Raises an EnvironmentError if there's a problem
|
||||
if copy:
|
||||
smart_copy(source_path, dest_path)
|
||||
else:
|
||||
smart_move(source_path, dest_path)
|
||||
self.clean_empty_dirs(source_path[:-1])
|
||||
self.clean_empty_dirs(source_path.parent())
|
||||
|
||||
def copy_or_move_marked(self, copy):
|
||||
"""Start an async move (or copy) job on marked duplicates.
|
||||
@@ -409,8 +420,6 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
j.start_job(self.results.mark_count)
|
||||
self.results.perform_on_marked(op, not copy)
|
||||
|
||||
if not self._check_demo():
|
||||
return
|
||||
if not self.results.mark_count:
|
||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||
return
|
||||
@@ -425,8 +434,6 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
def delete_marked(self):
|
||||
"""Start an async job to send marked duplicates to the trash.
|
||||
"""
|
||||
if not self._check_demo():
|
||||
return
|
||||
if not self.results.mark_count:
|
||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||
return
|
||||
@@ -438,11 +445,22 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||
|
||||
def export_to_xhtml(self):
|
||||
"""Export current results to XHTML.
|
||||
|
||||
The configuration of the :attr:`result_table` (columns order and visibility) is used to
|
||||
determine how the data is presented in the export. In other words, the exported table in
|
||||
the resulting XHTML will look just like the results table.
|
||||
"""
|
||||
colnames, rows = self._get_export_data()
|
||||
export_path = export.export_to_xhtml(colnames, rows)
|
||||
self.view.open_path(export_path)
|
||||
desktop.open_path(export_path)
|
||||
|
||||
def export_to_csv(self):
|
||||
"""Export current results to CSV.
|
||||
|
||||
The columns and their order in the resulting CSV file is determined in the same way as in
|
||||
:meth:`export_to_xhtml`.
|
||||
"""
|
||||
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), 'csv')
|
||||
if dest_file:
|
||||
colnames, rows = self._get_export_data()
|
||||
@@ -490,6 +508,12 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
|
||||
def load(self):
|
||||
"""Load directory selection and ignore list from files in appdata.
|
||||
|
||||
This method is called during startup so that directory selection and ignore list, which
|
||||
is persistent data, is the same as when the last session was closed (when :meth:`save` was
|
||||
called).
|
||||
"""
|
||||
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
|
||||
self.notify('directories_changed')
|
||||
p = op.join(self.appdata, 'ignore_list.xml')
|
||||
@@ -506,6 +530,12 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self._start_job(JobType.Load, do)
|
||||
|
||||
def make_selected_reference(self):
|
||||
"""Promote :attr:`selected_dupes` to reference position within their respective groups.
|
||||
|
||||
Each selected dupe will become the :attr:`~core.engine.Group.ref` of its group. If there's
|
||||
more than one dupe selected for the same group, only the first (in the order currently shown
|
||||
in :attr:`result_table`) dupe will be promoted.
|
||||
"""
|
||||
dupes = self.without_ref(self.selected_dupes)
|
||||
changed_groups = set()
|
||||
for dupe in dupes:
|
||||
@@ -532,18 +562,30 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self.notify('results_changed_but_keep_selection')
|
||||
|
||||
def mark_all(self):
|
||||
"""Set all dupes in the results as marked.
|
||||
"""
|
||||
self.results.mark_all()
|
||||
self.notify('marking_changed')
|
||||
|
||||
def mark_none(self):
|
||||
"""Set all dupes in the results as unmarked.
|
||||
"""
|
||||
self.results.mark_none()
|
||||
self.notify('marking_changed')
|
||||
|
||||
def mark_invert(self):
|
||||
"""Invert the marked state of all dupes in the results.
|
||||
"""
|
||||
self.results.mark_invert()
|
||||
self.notify('marking_changed')
|
||||
|
||||
def mark_dupe(self, dupe, marked):
|
||||
"""Change marked status of ``dupe``.
|
||||
|
||||
:param dupe: dupe to mark/unmark
|
||||
:type dupe: :class:`~core.fs.File`
|
||||
:param bool marked: True = mark, False = unmark
|
||||
"""
|
||||
if marked:
|
||||
self.results.mark(dupe)
|
||||
else:
|
||||
@@ -557,13 +599,20 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
||||
return
|
||||
for dupe in self.selected_dupes:
|
||||
self.view.open_path(dupe.path)
|
||||
desktop.open_path(dupe.path)
|
||||
|
||||
def purge_ignore_list(self):
|
||||
"""Remove files that don't exist from :attr:`ignore_list`.
|
||||
"""
|
||||
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
|
||||
self.ignore_list_dialog.refresh()
|
||||
|
||||
def remove_directories(self, indexes):
|
||||
"""Remove root directories at ``indexes`` from :attr:`directories`.
|
||||
|
||||
:param indexes: Indexes of the directories to remove.
|
||||
:type indexes: list of int
|
||||
"""
|
||||
try:
|
||||
indexes = sorted(indexes, reverse=True)
|
||||
for index in indexes:
|
||||
@@ -573,6 +622,13 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
pass
|
||||
|
||||
def remove_duplicates(self, duplicates):
|
||||
"""Remove ``duplicates`` from :attr:`results`.
|
||||
|
||||
Calls :meth:`~core.results.Results.remove_duplicates` and send appropriate notifications.
|
||||
|
||||
:param duplicates: duplicates to remove.
|
||||
:type duplicates: list of :class:`~core.fs.File`
|
||||
"""
|
||||
self.results.remove_duplicates(self.without_ref(duplicates))
|
||||
self.notify('results_changed_but_keep_selection')
|
||||
|
||||
@@ -601,6 +657,12 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
self.remove_duplicates(dupes)
|
||||
|
||||
def rename_selected(self, newname):
|
||||
"""Renames the selected dupes's file to ``newname``.
|
||||
|
||||
If there's more than one selected dupes, the first one is used.
|
||||
|
||||
:param str newname: The filename to rename the dupe's file to.
|
||||
"""
|
||||
try:
|
||||
d = self.selected_dupes[0]
|
||||
d.rename(newname)
|
||||
@@ -610,6 +672,14 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
return False
|
||||
|
||||
def reprioritize_groups(self, sort_key):
|
||||
"""Sort dupes in each group (in :attr:`results`) according to ``sort_key``.
|
||||
|
||||
Called by the re-prioritize dialog. Calls :meth:`~core.engine.Group.prioritize` and, once
|
||||
the sorting is done, show a message that confirms the action.
|
||||
|
||||
:param sort_key: The key being sent to :meth:`~core.engine.Group.prioritize`
|
||||
:type sort_key: f(dupe)
|
||||
"""
|
||||
count = 0
|
||||
for group in self.results.groups:
|
||||
if group.prioritize(key_func=sort_key):
|
||||
@@ -620,7 +690,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
|
||||
|
||||
def reveal_selected(self):
|
||||
if self.selected_dupes:
|
||||
self.view.reveal_path(self.selected_dupes[0].path)
|
||||
desktop.reveal_path(self.selected_dupes[0].path)
|
||||
|
||||
def save(self):
|
||||
if not op.exists(self.appdata):
|
||||
|
||||
@@ -73,7 +73,7 @@ class Directories:
|
||||
#---Private
|
||||
def _default_state_for_path(self, path):
|
||||
# Override this in subclasses to specify the state of some special folders.
|
||||
if path[-1].startswith('.'): # hidden
|
||||
if path.name.startswith('.'): # hidden
|
||||
return DirectoryState.Excluded
|
||||
|
||||
def _get_files(self, from_path, j):
|
||||
@@ -94,9 +94,8 @@ class Directories:
|
||||
file.is_ref = state == DirectoryState.Reference
|
||||
filepaths.add(file.path)
|
||||
yield file
|
||||
subpaths = [from_path + name for name in from_path.listdir()]
|
||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
|
||||
subfolders = [p for p in subpaths if not p.islink() and p.isdir() and p not in filepaths]
|
||||
subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths]
|
||||
for subfolder in subfolders:
|
||||
for file in self._get_files(subfolder, j):
|
||||
yield file
|
||||
@@ -143,9 +142,9 @@ class Directories:
|
||||
:rtype: list of Path
|
||||
"""
|
||||
try:
|
||||
names = [name for name in path.listdir() if (path + name).isdir()]
|
||||
names.sort(key=lambda x:x.lower())
|
||||
return [path + name for name in names]
|
||||
subpaths = [p for p in path.listdir() if p.isdir()]
|
||||
subpaths.sort(key=lambda x:x.name.lower())
|
||||
return subpaths
|
||||
except EnvironmentError:
|
||||
return []
|
||||
|
||||
@@ -178,7 +177,7 @@ class Directories:
|
||||
default_state = self._default_state_for_path(path)
|
||||
if default_state is not None:
|
||||
return default_state
|
||||
parent = path[:-1]
|
||||
parent = path.parent()
|
||||
if parent in self:
|
||||
return self.get_state(parent)
|
||||
else:
|
||||
|
||||
@@ -157,8 +157,12 @@ def reduce_common_words(word_dict, threshold):
|
||||
else:
|
||||
del word_dict[word]
|
||||
|
||||
Match = namedtuple('Match', 'first second percentage')
|
||||
Match.__doc__ = """Represents a match between two :class:`~core.fs.File`.
|
||||
# Writing docstrings in a namedtuple is tricky. From Python 3.3, it's possible to set __doc__, but
|
||||
# some research allowed me to find a more elegant solution, which is what is done here. See
|
||||
# http://stackoverflow.com/questions/1606436/adding-docstrings-to-namedtuples-in-python
|
||||
|
||||
class Match(namedtuple('Match', 'first second percentage')):
|
||||
"""Represents a match between two :class:`~core.fs.File`.
|
||||
|
||||
Regarless of the matching method, when two files are determined to match, a Match pair is created,
|
||||
which holds, of course, the two matched files, but also their match "level".
|
||||
@@ -176,6 +180,7 @@ which holds, of course, the two matched files, but also their match "level".
|
||||
their match level according to the scan method which found the match. int from 1 to 100. For
|
||||
exact scan methods, such as Contents scans, this will always be 100.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def get_match(first, second, flags=()):
|
||||
#it is assumed here that first and second both have a "words" attribute
|
||||
|
||||
22
core/fs.py
22
core/fs.py
@@ -150,9 +150,9 @@ class File:
|
||||
def rename(self, newname):
|
||||
if newname == self.name:
|
||||
return
|
||||
destpath = self.path[:-1] + newname
|
||||
destpath = self.path.parent()[newname]
|
||||
if destpath.exists():
|
||||
raise AlreadyExistsError(newname, self.path[:-1])
|
||||
raise AlreadyExistsError(newname, self.path.parent())
|
||||
try:
|
||||
self.path.rename(destpath)
|
||||
except EnvironmentError:
|
||||
@@ -173,11 +173,11 @@ class File:
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.path[-1]
|
||||
return self.path.name
|
||||
|
||||
@property
|
||||
def folder_path(self):
|
||||
return self.path[:-1]
|
||||
return self.path.parent()
|
||||
|
||||
|
||||
class Folder(File):
|
||||
@@ -219,8 +219,7 @@ class Folder(File):
|
||||
@property
|
||||
def subfolders(self):
|
||||
if self._subfolders is None:
|
||||
subpaths = [self.path + name for name in self.path.listdir()]
|
||||
subfolders = [p for p in subpaths if not p.islink() and p.isdir()]
|
||||
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
||||
self._subfolders = [self.__class__(p) for p in subfolders]
|
||||
return self._subfolders
|
||||
|
||||
@@ -248,18 +247,9 @@ def get_files(path, fileclasses=[File]):
|
||||
:param fileclasses: List of candidate :class:`File` classes
|
||||
"""
|
||||
assert all(issubclass(fileclass, File) for fileclass in fileclasses)
|
||||
def combine_paths(p1, p2):
|
||||
try:
|
||||
return p1 + p2
|
||||
except Exception:
|
||||
# This is temporary debug logging for #84.
|
||||
logging.warning("Failed to combine %r and %r.", p1, p2)
|
||||
raise
|
||||
|
||||
try:
|
||||
paths = [combine_paths(path, name) for name in path.listdir()]
|
||||
result = []
|
||||
for path in paths:
|
||||
for path in path.listdir():
|
||||
file = get_file(path, fileclasses=fileclasses)
|
||||
if file is not None:
|
||||
result.append(file)
|
||||
|
||||
@@ -10,14 +10,60 @@ import os
|
||||
from hscommon.gui.base import GUIObject
|
||||
from hscommon.trans import tr
|
||||
|
||||
class DeletionOptionsView:
|
||||
"""Expected interface for :class:`DeletionOptions`'s view.
|
||||
|
||||
*Not actually used in the code. For documentation purposes only.*
|
||||
|
||||
Our view presents the user with an appropriate way (probably a mix of checkboxes and radio
|
||||
buttons) to set the different flags in :class:`DeletionOptions`. Note that
|
||||
:attr:`DeletionOptions.use_hardlinks` is only relevant if :attr:`DeletionOptions.link_deleted`
|
||||
is true. This is why we toggle the "enabled" state of that flag.
|
||||
|
||||
We expect the view to set :attr:`DeletionOptions.link_deleted` immediately as the user changes
|
||||
its value because it will toggle :meth:`set_hardlink_option_enabled`
|
||||
|
||||
Other than the flags, there's also a prompt message which has a dynamic content, defined by
|
||||
:meth:`update_msg`.
|
||||
"""
|
||||
def update_msg(self, msg: str):
|
||||
"""Update the dialog's prompt with ``str``.
|
||||
"""
|
||||
|
||||
def show(self):
|
||||
"""Show the dialog in a modal fashion.
|
||||
|
||||
Returns whether the dialog was "accepted" (the user pressed OK).
|
||||
"""
|
||||
|
||||
def set_hardlink_option_enabled(self, is_enabled: bool):
|
||||
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`.
|
||||
"""
|
||||
|
||||
class DeletionOptions(GUIObject):
|
||||
#--- View interface
|
||||
# update_msg(msg: str)
|
||||
# show()
|
||||
#
|
||||
"""Present the user with deletion options before proceeding.
|
||||
|
||||
When the user activates "Send to trash", we present him with a couple of options that changes
|
||||
the behavior of that deletion operation.
|
||||
"""
|
||||
def __init__(self):
|
||||
GUIObject.__init__(self)
|
||||
#: Whether symlinks or hardlinks are used when doing :attr:`link_deleted`.
|
||||
#: *bool*. *get/set*
|
||||
self.use_hardlinks = False
|
||||
#: Delete dupes directly and don't send to trash.
|
||||
#: *bool*. *get/set*
|
||||
self.direct = False
|
||||
|
||||
def show(self, mark_count):
|
||||
self.link_deleted = False
|
||||
"""Prompt the user with a modal dialog offering our deletion options.
|
||||
|
||||
:param int mark_count: Number of dupes marked for deletion.
|
||||
:rtype: bool
|
||||
:returns: Whether the user accepted the dialog (we cancel deletion if false).
|
||||
"""
|
||||
self._link_deleted = False
|
||||
self.view.set_hardlink_option_enabled(False)
|
||||
self.use_hardlinks = False
|
||||
self.direct = False
|
||||
msg = tr("You are sending {} file(s) to the Trash.").format(mark_count)
|
||||
@@ -25,6 +71,8 @@ class DeletionOptions(GUIObject):
|
||||
return self.view.show()
|
||||
|
||||
def supports_links(self):
|
||||
"""Returns whether our platform supports symlinks.
|
||||
"""
|
||||
# When on a platform that doesn't implement it, calling os.symlink() (with the wrong number
|
||||
# of arguments) raises NotImplementedError, which allows us to gracefully check for the
|
||||
# feature.
|
||||
@@ -40,3 +88,20 @@ class DeletionOptions(GUIObject):
|
||||
# wrong number of arguments
|
||||
return True
|
||||
|
||||
@property
|
||||
def link_deleted(self):
|
||||
"""Replace deleted dupes with symlinks (or hardlinks) to the dupe group reference.
|
||||
|
||||
*bool*. *get/set*
|
||||
|
||||
Whether the link is a symlink or hardlink is decided by :attr:`use_hardlinks`.
|
||||
"""
|
||||
return self._link_deleted
|
||||
|
||||
@link_deleted.setter
|
||||
def link_deleted(self, value):
|
||||
self._link_deleted = value
|
||||
hardlinks_enabled = value and self.supports_links()
|
||||
self.view.set_hardlink_option_enabled(hardlinks_enabled)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class DirectoryNode(Node):
|
||||
self.clear()
|
||||
subpaths = self._tree.app.directories.get_subfolders(self._directory_path)
|
||||
for path in subpaths:
|
||||
self.append(DirectoryNode(self._tree, path, path[-1]))
|
||||
self.append(DirectoryNode(self._tree, path, path.name))
|
||||
self._loaded = True
|
||||
|
||||
def update_all_states(self):
|
||||
@@ -91,6 +91,10 @@ class DirectoryTree(Tree, DupeGuruGUIObject):
|
||||
for node in nodes:
|
||||
node.state = newstate
|
||||
|
||||
def select_all(self):
|
||||
self.selected_nodes = list(self)
|
||||
self.view.refresh()
|
||||
|
||||
def update_all_states(self):
|
||||
for node in self:
|
||||
node.update_all_states()
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from hscommon import desktop
|
||||
|
||||
from .problem_table import ProblemTable
|
||||
|
||||
class ProblemDialog:
|
||||
@@ -20,7 +22,7 @@ class ProblemDialog:
|
||||
|
||||
def reveal_selected_dupe(self):
|
||||
if self._selected_dupe is not None:
|
||||
self.app.view.reveal_path(self._selected_dupe.path)
|
||||
desktop.reveal_path(self._selected_dupe.path)
|
||||
|
||||
def select_dupe(self, dupe):
|
||||
self._selected_dupe = dupe
|
||||
|
||||
@@ -42,7 +42,7 @@ class DupeRow(Row):
|
||||
dupe_info = self.data
|
||||
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
|
||||
for key, value in dupe_info.items():
|
||||
if ref_info[key] != value:
|
||||
if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()):
|
||||
self._delta_columns.add(key)
|
||||
return column_name in self._delta_columns
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import os.path as op
|
||||
import logging
|
||||
|
||||
from pytest import mark
|
||||
from hscommon import io
|
||||
from hscommon.path import Path
|
||||
import hscommon.conflict
|
||||
import hscommon.util
|
||||
@@ -57,7 +56,7 @@ class TestCaseDupeGuru:
|
||||
# for this unit is pathetic. What's done is done. My approach now is to add tests for
|
||||
# every change I want to make. The blowup was caused by a missing import.
|
||||
p = Path(str(tmpdir))
|
||||
io.open(p + 'foo', 'w').close()
|
||||
p['foo'].open('w').close()
|
||||
monkeypatch.setattr(hscommon.conflict, 'smart_copy', log_calls(lambda source_path, dest_path: None))
|
||||
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
||||
monkeypatch.setattr(app, 'smart_copy', hscommon.conflict.smart_copy)
|
||||
@@ -73,14 +72,14 @@ class TestCaseDupeGuru:
|
||||
|
||||
def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):
|
||||
tmppath = Path(str(tmpdir))
|
||||
sourcepath = tmppath + 'source'
|
||||
io.mkdir(sourcepath)
|
||||
io.open(sourcepath + 'myfile', 'w')
|
||||
sourcepath = tmppath['source']
|
||||
sourcepath.mkdir()
|
||||
sourcepath['myfile'].open('w')
|
||||
app = TestApp().app
|
||||
app.directories.add_path(tmppath)
|
||||
[myfile] = app.directories.get_files()
|
||||
monkeypatch.setattr(app, 'clean_empty_dirs', log_calls(lambda path: None))
|
||||
app.copy_or_move(myfile, False, tmppath + 'dest', 0)
|
||||
app.copy_or_move(myfile, False, tmppath['dest'], 0)
|
||||
calls = app.clean_empty_dirs.calls
|
||||
eq_(1, len(calls))
|
||||
eq_(sourcepath, calls[0]['path'])
|
||||
@@ -104,8 +103,8 @@ class TestCaseDupeGuru:
|
||||
# If the ignore_hardlink_matches option is set, don't match files hardlinking to the same
|
||||
# inode.
|
||||
tmppath = Path(str(tmpdir))
|
||||
io.open(tmppath + 'myfile', 'w').write('foo')
|
||||
os.link(str(tmppath + 'myfile'), str(tmppath + 'hardlink'))
|
||||
tmppath['myfile'].open('w').write('foo')
|
||||
os.link(str(tmppath['myfile']), str(tmppath['hardlink']))
|
||||
app = TestApp().app
|
||||
app.directories.add_path(tmppath)
|
||||
app.scanner.scan_type = ScanType.Contents
|
||||
@@ -171,8 +170,8 @@ class TestCaseDupeGuruWithResults:
|
||||
self.rtable.refresh()
|
||||
tmpdir = request.getfuncargvalue('tmpdir')
|
||||
tmppath = Path(str(tmpdir))
|
||||
io.mkdir(tmppath + 'foo')
|
||||
io.mkdir(tmppath + 'bar')
|
||||
tmppath['foo'].mkdir()
|
||||
tmppath['bar'].mkdir()
|
||||
self.app.directories.add_path(tmppath)
|
||||
|
||||
def test_GetObjects(self, do_setup):
|
||||
@@ -400,15 +399,28 @@ class TestCaseDupeGuruWithResults:
|
||||
eq_(len(self.rtable), 0)
|
||||
eq_(app.selected_dupes, [])
|
||||
|
||||
def test_dont_crash_on_delta_powermarker_dupecount_sort(self, do_setup):
|
||||
# Don't crash when sorting by dupe count or percentage while delta+powermarker are enabled.
|
||||
# Ref #238
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
self.rtable.delta_values = True
|
||||
self.rtable.power_marker = True
|
||||
self.rtable.sort('dupe_count', False)
|
||||
# don't crash
|
||||
self.rtable.sort('percentage', False)
|
||||
# don't crash
|
||||
|
||||
|
||||
class TestCaseDupeGuru_renameSelected:
|
||||
def pytest_funcarg__do_setup(self, request):
|
||||
tmpdir = request.getfuncargvalue('tmpdir')
|
||||
p = Path(str(tmpdir))
|
||||
fp = open(str(p + 'foo bar 1'),mode='w')
|
||||
fp = open(str(p['foo bar 1']),mode='w')
|
||||
fp.close()
|
||||
fp = open(str(p + 'foo bar 2'),mode='w')
|
||||
fp = open(str(p['foo bar 2']),mode='w')
|
||||
fp.close()
|
||||
fp = open(str(p + 'foo bar 3'),mode='w')
|
||||
fp = open(str(p['foo bar 3']),mode='w')
|
||||
fp.close()
|
||||
files = fs.get_files(p)
|
||||
for f in files:
|
||||
@@ -431,7 +443,7 @@ class TestCaseDupeGuru_renameSelected:
|
||||
g = self.groups[0]
|
||||
self.rtable.select([1])
|
||||
assert app.rename_selected('renamed')
|
||||
names = io.listdir(self.p)
|
||||
names = [p.name for p in self.p.listdir()]
|
||||
assert 'renamed' in names
|
||||
assert 'foo bar 2' not in names
|
||||
eq_(g.dupes[0].name, 'renamed')
|
||||
@@ -444,7 +456,7 @@ class TestCaseDupeGuru_renameSelected:
|
||||
assert not app.rename_selected('renamed')
|
||||
msg = logging.warning.calls[0]['msg']
|
||||
eq_('dupeGuru Warning: list index out of range', msg)
|
||||
names = io.listdir(self.p)
|
||||
names = [p.name for p in self.p.listdir()]
|
||||
assert 'renamed' not in names
|
||||
assert 'foo bar 2' in names
|
||||
eq_(g.dupes[0].name, 'foo bar 2')
|
||||
@@ -457,7 +469,7 @@ class TestCaseDupeGuru_renameSelected:
|
||||
assert not app.rename_selected('foo bar 1')
|
||||
msg = logging.warning.calls[0]['msg']
|
||||
assert msg.startswith('dupeGuru Warning: \'foo bar 1\' already exists in')
|
||||
names = io.listdir(self.p)
|
||||
names = [p.name for p in self.p.listdir()]
|
||||
assert 'foo bar 1' in names
|
||||
assert 'foo bar 2' in names
|
||||
eq_(g.dupes[0].name, 'foo bar 2')
|
||||
@@ -467,9 +479,9 @@ class TestAppWithDirectoriesInTree:
|
||||
def pytest_funcarg__do_setup(self, request):
|
||||
tmpdir = request.getfuncargvalue('tmpdir')
|
||||
p = Path(str(tmpdir))
|
||||
io.mkdir(p + 'sub1')
|
||||
io.mkdir(p + 'sub2')
|
||||
io.mkdir(p + 'sub3')
|
||||
p['sub1'].mkdir()
|
||||
p['sub2'].mkdir()
|
||||
p['sub3'].mkdir()
|
||||
app = TestApp()
|
||||
self.app = app.app
|
||||
self.dtree = app.dtree
|
||||
|
||||
@@ -57,10 +57,12 @@ class ResultTable(ResultTableBase):
|
||||
DELTA_COLUMNS = {'size', }
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
NAME = 'dupeGuru'
|
||||
METADATA_TO_READ = ['size']
|
||||
|
||||
def __init__(self):
|
||||
DupeGuruBase.__init__(self, DupeGuruView(), '/tmp')
|
||||
DupeGuruBase.__init__(self, DupeGuruView())
|
||||
self.appdata = '/tmp'
|
||||
|
||||
def _prioritization_categories(self):
|
||||
return prioritize.all_categories()
|
||||
@@ -100,11 +102,11 @@ class NamedObject:
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._folder + self.name
|
||||
return self._folder[self.name]
|
||||
|
||||
@property
|
||||
def folder_path(self):
|
||||
return self.path[:-1]
|
||||
return self.path.parent()
|
||||
|
||||
@property
|
||||
def extension(self):
|
||||
|
||||
@@ -12,7 +12,6 @@ import tempfile
|
||||
import shutil
|
||||
|
||||
from pytest import raises
|
||||
from hscommon import io
|
||||
from hscommon.path import Path
|
||||
from hscommon.testutil import eq_
|
||||
|
||||
@@ -20,27 +19,27 @@ from ..directories import *
|
||||
|
||||
def create_fake_fs(rootpath):
|
||||
# We have it as a separate function because other units are using it.
|
||||
rootpath = rootpath + 'fs'
|
||||
io.mkdir(rootpath)
|
||||
io.mkdir(rootpath + 'dir1')
|
||||
io.mkdir(rootpath + 'dir2')
|
||||
io.mkdir(rootpath + 'dir3')
|
||||
fp = io.open(rootpath + 'file1.test', 'w')
|
||||
rootpath = rootpath['fs']
|
||||
rootpath.mkdir()
|
||||
rootpath['dir1'].mkdir()
|
||||
rootpath['dir2'].mkdir()
|
||||
rootpath['dir3'].mkdir()
|
||||
fp = rootpath['file1.test'].open('w')
|
||||
fp.write('1')
|
||||
fp.close()
|
||||
fp = io.open(rootpath + 'file2.test', 'w')
|
||||
fp = rootpath['file2.test'].open('w')
|
||||
fp.write('12')
|
||||
fp.close()
|
||||
fp = io.open(rootpath + 'file3.test', 'w')
|
||||
fp = rootpath['file3.test'].open('w')
|
||||
fp.write('123')
|
||||
fp.close()
|
||||
fp = io.open(rootpath + ('dir1', 'file1.test'), 'w')
|
||||
fp = rootpath['dir1']['file1.test'].open('w')
|
||||
fp.write('1')
|
||||
fp.close()
|
||||
fp = io.open(rootpath + ('dir2', 'file2.test'), 'w')
|
||||
fp = rootpath['dir2']['file2.test'].open('w')
|
||||
fp.write('12')
|
||||
fp.close()
|
||||
fp = io.open(rootpath + ('dir3', 'file3.test'), 'w')
|
||||
fp = rootpath['dir3']['file3.test'].open('w')
|
||||
fp.write('123')
|
||||
fp.close()
|
||||
return rootpath
|
||||
@@ -50,9 +49,9 @@ def setup_module(module):
|
||||
# and another with a more complex structure.
|
||||
testpath = Path(tempfile.mkdtemp())
|
||||
module.testpath = testpath
|
||||
rootpath = testpath + 'onefile'
|
||||
io.mkdir(rootpath)
|
||||
fp = io.open(rootpath + 'test.txt', 'w')
|
||||
rootpath = testpath['onefile']
|
||||
rootpath.mkdir()
|
||||
fp = rootpath['test.txt'].open('w')
|
||||
fp.write('test_data')
|
||||
fp.close()
|
||||
create_fake_fs(testpath)
|
||||
@@ -67,30 +66,30 @@ def test_empty():
|
||||
|
||||
def test_add_path():
|
||||
d = Directories()
|
||||
p = testpath + 'onefile'
|
||||
p = testpath['onefile']
|
||||
d.add_path(p)
|
||||
eq_(1,len(d))
|
||||
assert p in d
|
||||
assert (p + 'foobar') in d
|
||||
assert p[:-1] not in d
|
||||
p = testpath + 'fs'
|
||||
assert (p['foobar']) in d
|
||||
assert p.parent() not in d
|
||||
p = testpath['fs']
|
||||
d.add_path(p)
|
||||
eq_(2,len(d))
|
||||
assert p in d
|
||||
|
||||
def test_AddPath_when_path_is_already_there():
|
||||
d = Directories()
|
||||
p = testpath + 'onefile'
|
||||
p = testpath['onefile']
|
||||
d.add_path(p)
|
||||
with raises(AlreadyThereError):
|
||||
d.add_path(p)
|
||||
with raises(AlreadyThereError):
|
||||
d.add_path(p + 'foobar')
|
||||
d.add_path(p['foobar'])
|
||||
eq_(1, len(d))
|
||||
|
||||
def test_add_path_containing_paths_already_there():
|
||||
d = Directories()
|
||||
d.add_path(testpath + 'onefile')
|
||||
d.add_path(testpath['onefile'])
|
||||
eq_(1, len(d))
|
||||
d.add_path(testpath)
|
||||
eq_(len(d), 1)
|
||||
@@ -98,7 +97,7 @@ def test_add_path_containing_paths_already_there():
|
||||
|
||||
def test_AddPath_non_latin(tmpdir):
|
||||
p = Path(str(tmpdir))
|
||||
to_add = p + 'unicode\u201a'
|
||||
to_add = p['unicode\u201a']
|
||||
os.mkdir(str(to_add))
|
||||
d = Directories()
|
||||
try:
|
||||
@@ -108,24 +107,24 @@ def test_AddPath_non_latin(tmpdir):
|
||||
|
||||
def test_del():
|
||||
d = Directories()
|
||||
d.add_path(testpath + 'onefile')
|
||||
d.add_path(testpath['onefile'])
|
||||
try:
|
||||
del d[1]
|
||||
assert False
|
||||
except IndexError:
|
||||
pass
|
||||
d.add_path(testpath + 'fs')
|
||||
d.add_path(testpath['fs'])
|
||||
del d[1]
|
||||
eq_(1, len(d))
|
||||
|
||||
def test_states():
|
||||
d = Directories()
|
||||
p = testpath + 'onefile'
|
||||
p = testpath['onefile']
|
||||
d.add_path(p)
|
||||
eq_(DirectoryState.Normal ,d.get_state(p))
|
||||
d.set_state(p, DirectoryState.Reference)
|
||||
eq_(DirectoryState.Reference ,d.get_state(p))
|
||||
eq_(DirectoryState.Reference ,d.get_state(p + 'dir1'))
|
||||
eq_(DirectoryState.Reference ,d.get_state(p['dir1']))
|
||||
eq_(1,len(d.states))
|
||||
eq_(p,list(d.states.keys())[0])
|
||||
eq_(DirectoryState.Reference ,d.states[p])
|
||||
@@ -133,67 +132,67 @@ def test_states():
|
||||
def test_get_state_with_path_not_there():
|
||||
# When the path's not there, just return DirectoryState.Normal
|
||||
d = Directories()
|
||||
d.add_path(testpath + 'onefile')
|
||||
d.add_path(testpath['onefile'])
|
||||
eq_(d.get_state(testpath), DirectoryState.Normal)
|
||||
|
||||
def test_states_remain_when_larger_directory_eat_smaller_ones():
|
||||
d = Directories()
|
||||
p = testpath + 'onefile'
|
||||
p = testpath['onefile']
|
||||
d.add_path(p)
|
||||
d.set_state(p, DirectoryState.Excluded)
|
||||
d.add_path(testpath)
|
||||
d.set_state(testpath, DirectoryState.Reference)
|
||||
eq_(DirectoryState.Excluded ,d.get_state(p))
|
||||
eq_(DirectoryState.Excluded ,d.get_state(p + 'dir1'))
|
||||
eq_(DirectoryState.Excluded ,d.get_state(p['dir1']))
|
||||
eq_(DirectoryState.Reference ,d.get_state(testpath))
|
||||
|
||||
def test_set_state_keep_state_dict_size_to_minimum():
|
||||
d = Directories()
|
||||
p = testpath + 'fs'
|
||||
p = testpath['fs']
|
||||
d.add_path(p)
|
||||
d.set_state(p, DirectoryState.Reference)
|
||||
d.set_state(p + 'dir1', DirectoryState.Reference)
|
||||
d.set_state(p['dir1'], DirectoryState.Reference)
|
||||
eq_(1,len(d.states))
|
||||
eq_(DirectoryState.Reference ,d.get_state(p + 'dir1'))
|
||||
d.set_state(p + 'dir1', DirectoryState.Normal)
|
||||
eq_(DirectoryState.Reference ,d.get_state(p['dir1']))
|
||||
d.set_state(p['dir1'], DirectoryState.Normal)
|
||||
eq_(2,len(d.states))
|
||||
eq_(DirectoryState.Normal ,d.get_state(p + 'dir1'))
|
||||
d.set_state(p + 'dir1', DirectoryState.Reference)
|
||||
eq_(DirectoryState.Normal ,d.get_state(p['dir1']))
|
||||
d.set_state(p['dir1'], DirectoryState.Reference)
|
||||
eq_(1,len(d.states))
|
||||
eq_(DirectoryState.Reference ,d.get_state(p + 'dir1'))
|
||||
eq_(DirectoryState.Reference ,d.get_state(p['dir1']))
|
||||
|
||||
def test_get_files():
|
||||
d = Directories()
|
||||
p = testpath + 'fs'
|
||||
p = testpath['fs']
|
||||
d.add_path(p)
|
||||
d.set_state(p + 'dir1', DirectoryState.Reference)
|
||||
d.set_state(p + 'dir2', DirectoryState.Excluded)
|
||||
d.set_state(p['dir1'], DirectoryState.Reference)
|
||||
d.set_state(p['dir2'], DirectoryState.Excluded)
|
||||
files = list(d.get_files())
|
||||
eq_(5, len(files))
|
||||
for f in files:
|
||||
if f.path[:-1] == p + 'dir1':
|
||||
if f.path.parent() == p['dir1']:
|
||||
assert f.is_ref
|
||||
else:
|
||||
assert not f.is_ref
|
||||
|
||||
def test_get_folders():
|
||||
d = Directories()
|
||||
p = testpath + 'fs'
|
||||
p = testpath['fs']
|
||||
d.add_path(p)
|
||||
d.set_state(p + 'dir1', DirectoryState.Reference)
|
||||
d.set_state(p + 'dir2', DirectoryState.Excluded)
|
||||
d.set_state(p['dir1'], DirectoryState.Reference)
|
||||
d.set_state(p['dir2'], DirectoryState.Excluded)
|
||||
folders = list(d.get_folders())
|
||||
eq_(len(folders), 3)
|
||||
ref = [f for f in folders if f.is_ref]
|
||||
not_ref = [f for f in folders if not f.is_ref]
|
||||
eq_(len(ref), 1)
|
||||
eq_(ref[0].path, p + 'dir1')
|
||||
eq_(ref[0].path, p['dir1'])
|
||||
eq_(len(not_ref), 2)
|
||||
eq_(ref[0].size, 1)
|
||||
|
||||
def test_get_files_with_inherited_exclusion():
|
||||
d = Directories()
|
||||
p = testpath + 'onefile'
|
||||
p = testpath['onefile']
|
||||
d.add_path(p)
|
||||
d.set_state(p, DirectoryState.Excluded)
|
||||
eq_([], list(d.get_files()))
|
||||
@@ -202,19 +201,19 @@ def test_save_and_load(tmpdir):
|
||||
d1 = Directories()
|
||||
d2 = Directories()
|
||||
p1 = Path(str(tmpdir.join('p1')))
|
||||
io.mkdir(p1)
|
||||
p1.mkdir()
|
||||
p2 = Path(str(tmpdir.join('p2')))
|
||||
io.mkdir(p2)
|
||||
p2.mkdir()
|
||||
d1.add_path(p1)
|
||||
d1.add_path(p2)
|
||||
d1.set_state(p1, DirectoryState.Reference)
|
||||
d1.set_state(p1 + 'dir1', DirectoryState.Excluded)
|
||||
d1.set_state(p1['dir1'], DirectoryState.Excluded)
|
||||
tmpxml = str(tmpdir.join('directories_testunit.xml'))
|
||||
d1.save_to_file(tmpxml)
|
||||
d2.load_from_file(tmpxml)
|
||||
eq_(2, len(d2))
|
||||
eq_(DirectoryState.Reference ,d2.get_state(p1))
|
||||
eq_(DirectoryState.Excluded ,d2.get_state(p1 + 'dir1'))
|
||||
eq_(DirectoryState.Excluded ,d2.get_state(p1['dir1']))
|
||||
|
||||
def test_invalid_path():
|
||||
d = Directories()
|
||||
@@ -234,12 +233,12 @@ def test_load_from_file_with_invalid_path(tmpdir):
|
||||
#This test simulates a load from file resulting in a
|
||||
#InvalidPath raise. Other directories must be loaded.
|
||||
d1 = Directories()
|
||||
d1.add_path(testpath + 'onefile')
|
||||
d1.add_path(testpath['onefile'])
|
||||
#Will raise InvalidPath upon loading
|
||||
p = Path(str(tmpdir.join('toremove')))
|
||||
io.mkdir(p)
|
||||
p.mkdir()
|
||||
d1.add_path(p)
|
||||
io.rmdir(p)
|
||||
p.rmdir()
|
||||
tmpxml = str(tmpdir.join('directories_testunit.xml'))
|
||||
d1.save_to_file(tmpxml)
|
||||
d2 = Directories()
|
||||
@@ -248,11 +247,11 @@ def test_load_from_file_with_invalid_path(tmpdir):
|
||||
|
||||
def test_unicode_save(tmpdir):
|
||||
d = Directories()
|
||||
p1 = Path(str(tmpdir)) + 'hello\xe9'
|
||||
io.mkdir(p1)
|
||||
io.mkdir(p1 + 'foo\xe9')
|
||||
p1 = Path(str(tmpdir))['hello\xe9']
|
||||
p1.mkdir()
|
||||
p1['foo\xe9'].mkdir()
|
||||
d.add_path(p1)
|
||||
d.set_state(p1 + 'foo\xe9', DirectoryState.Excluded)
|
||||
d.set_state(p1['foo\xe9'], DirectoryState.Excluded)
|
||||
tmpxml = str(tmpdir.join('directories_testunit.xml'))
|
||||
try:
|
||||
d.save_to_file(tmpxml)
|
||||
@@ -261,12 +260,12 @@ def test_unicode_save(tmpdir):
|
||||
|
||||
def test_get_files_refreshes_its_directories():
|
||||
d = Directories()
|
||||
p = testpath + 'fs'
|
||||
p = testpath['fs']
|
||||
d.add_path(p)
|
||||
files = d.get_files()
|
||||
eq_(6, len(list(files)))
|
||||
time.sleep(1)
|
||||
os.remove(str(p + ('dir1','file1.test')))
|
||||
os.remove(str(p['dir1']['file1.test']))
|
||||
files = d.get_files()
|
||||
eq_(5, len(list(files)))
|
||||
|
||||
@@ -274,14 +273,14 @@ def test_get_files_does_not_choke_on_non_existing_directories(tmpdir):
|
||||
d = Directories()
|
||||
p = Path(str(tmpdir))
|
||||
d.add_path(p)
|
||||
io.rmtree(p)
|
||||
p.rmtree()
|
||||
eq_([], list(d.get_files()))
|
||||
|
||||
def test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir):
|
||||
d = Directories()
|
||||
p = Path(str(tmpdir))
|
||||
hidden_dir_path = p + '.foo'
|
||||
io.mkdir(p + '.foo')
|
||||
hidden_dir_path = p['.foo']
|
||||
p['.foo'].mkdir()
|
||||
d.add_path(p)
|
||||
eq_(d.get_state(hidden_dir_path), DirectoryState.Excluded)
|
||||
# But it can be overriden
|
||||
@@ -297,16 +296,16 @@ def test_default_path_state_override(tmpdir):
|
||||
|
||||
d = MyDirectories()
|
||||
p1 = Path(str(tmpdir))
|
||||
io.mkdir(p1 + 'foobar')
|
||||
io.open(p1 + 'foobar/somefile', 'w').close()
|
||||
io.mkdir(p1 + 'foobaz')
|
||||
io.open(p1 + 'foobaz/somefile', 'w').close()
|
||||
p1['foobar'].mkdir()
|
||||
p1['foobar/somefile'].open('w').close()
|
||||
p1['foobaz'].mkdir()
|
||||
p1['foobaz/somefile'].open('w').close()
|
||||
d.add_path(p1)
|
||||
eq_(d.get_state(p1 + 'foobaz'), DirectoryState.Normal)
|
||||
eq_(d.get_state(p1 + 'foobar'), DirectoryState.Excluded)
|
||||
eq_(d.get_state(p1['foobaz']), DirectoryState.Normal)
|
||||
eq_(d.get_state(p1['foobar']), DirectoryState.Excluded)
|
||||
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
|
||||
# However, the default state can be changed
|
||||
d.set_state(p1 + 'foobar', DirectoryState.Normal)
|
||||
eq_(d.get_state(p1 + 'foobar'), DirectoryState.Normal)
|
||||
d.set_state(p1['foobar'], DirectoryState.Normal)
|
||||
eq_(d.get_state(p1['foobar']), DirectoryState.Normal)
|
||||
eq_(len(list(d.get_files())), 2)
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ def test_md5_aggregate_subfiles_sorted(tmpdir):
|
||||
#same order everytime.
|
||||
p = create_fake_fs(Path(str(tmpdir)))
|
||||
b = fs.Folder(p)
|
||||
md51 = fs.File(p + ('dir1', 'file1.test')).md5
|
||||
md52 = fs.File(p + ('dir2', 'file2.test')).md5
|
||||
md53 = fs.File(p + ('dir3', 'file3.test')).md5
|
||||
md54 = fs.File(p + 'file1.test').md5
|
||||
md55 = fs.File(p + 'file2.test').md5
|
||||
md56 = fs.File(p + 'file3.test').md5
|
||||
md51 = fs.File(p['dir1']['file1.test']).md5
|
||||
md52 = fs.File(p['dir2']['file2.test']).md5
|
||||
md53 = fs.File(p['dir3']['file3.test']).md5
|
||||
md54 = fs.File(p['file1.test']).md5
|
||||
md55 = fs.File(p['file2.test']).md5
|
||||
md56 = fs.File(p['file3.test']).md5
|
||||
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
|
||||
folder_md51 = hashlib.md5(md51).digest()
|
||||
folder_md52 = hashlib.md5(md52).digest()
|
||||
|
||||
@@ -44,3 +44,13 @@ def test_delta_flags_delta_mode_on_non_delta_columns():
|
||||
assert not app.rtable[3].is_cell_delta('name')
|
||||
# "ibabtu" == "ibabtu", flag off
|
||||
assert not app.rtable[4].is_cell_delta('name')
|
||||
|
||||
def test_delta_flags_delta_mode_on_non_delta_columns_case_insensitive():
|
||||
# Comparison that occurs for non-numeric columns to check whether they're delta is case
|
||||
# insensitive
|
||||
app = app_with_results()
|
||||
app.app.results.groups[1].ref.name = "ibAbtu"
|
||||
app.app.results.groups[1].dupes[0].name = "IBaBTU"
|
||||
app.rtable.delta_values = True
|
||||
# "ibAbtu" == "IBaBTU", flag off
|
||||
assert not app.rtable[4].is_cell_delta('name')
|
||||
|
||||
@@ -230,6 +230,23 @@ class TestCaseResultsWithSomeGroups:
|
||||
# also remove group ref
|
||||
assert self.results.get_group_of_duplicate(ref) is None
|
||||
|
||||
def test_dupe_list_sort_delta_values_nonnumeric(self):
|
||||
# When sorting dupes in delta mode on a non-numeric column, our first sort criteria is if
|
||||
# the string is the same as its ref.
|
||||
g1r, g1d1, g1d2, g2r, g2d1 = self.objects
|
||||
# "aaa" makes our dupe go first in alphabetical order, but since we have the same value as
|
||||
# ref, we're going last.
|
||||
g2r.name = g2d1.name = "aaa"
|
||||
self.results.sort_dupes('name', delta=True)
|
||||
eq_("aaa", self.results.dupes[2].name)
|
||||
|
||||
def test_dupe_list_sort_delta_values_nonnumeric_case_insensitive(self):
|
||||
# Non-numeric delta sorting comparison is case insensitive
|
||||
g1r, g1d1, g1d2, g2r, g2d1 = self.objects
|
||||
g2r.name = "AaA"
|
||||
g2d1.name = "aAa"
|
||||
self.results.sort_dupes('name', delta=True)
|
||||
eq_("aAa", self.results.dupes[2].name)
|
||||
|
||||
class TestCaseResultsWithSavedResults:
|
||||
def setup_method(self, method):
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from jobprogress import job
|
||||
from hscommon import io
|
||||
from hscommon.path import Path
|
||||
from hscommon.testutil import eq_
|
||||
|
||||
@@ -21,7 +20,7 @@ class NamedObject:
|
||||
if path is None:
|
||||
path = Path(name)
|
||||
else:
|
||||
path = Path(path) + name
|
||||
path = Path(path)[name]
|
||||
self.name = name
|
||||
self.size = size
|
||||
self.path = path
|
||||
@@ -37,7 +36,6 @@ def pytest_funcarg__fake_fileexists(request):
|
||||
# This is a hack to avoid invalidating all previous tests since the scanner started to test
|
||||
# for file existence before doing the match grouping.
|
||||
monkeypatch = request.getfuncargvalue('monkeypatch')
|
||||
monkeypatch.setattr(io, 'exists', lambda _: True)
|
||||
monkeypatch.setattr(Path, 'exists', lambda _: True)
|
||||
|
||||
def test_empty(fake_fileexists):
|
||||
@@ -471,11 +469,11 @@ def test_dont_group_files_that_dont_exist(tmpdir):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.Contents
|
||||
p = Path(str(tmpdir))
|
||||
io.open(p + 'file1', 'w').write('foo')
|
||||
io.open(p + 'file2', 'w').write('foo')
|
||||
p['file1'].open('w').write('foo')
|
||||
p['file2'].open('w').write('foo')
|
||||
file1, file2 = fs.get_files(p)
|
||||
def getmatches(*args, **kw):
|
||||
io.remove(file2.path)
|
||||
file2.path.remove()
|
||||
return [Match(file1, file2, 100)]
|
||||
s._getmatches = getmatches
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '6.6.0'
|
||||
__version__ = '6.7.0'
|
||||
__appname__ = 'dupeGuru Music Edition'
|
||||
@@ -16,8 +16,8 @@ class DupeGuru(DupeGuruBase):
|
||||
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||
'album', 'genre', 'year', 'track', 'comment']
|
||||
|
||||
def __init__(self, view, appdata):
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.scanner = scanner.ScannerME()
|
||||
self.directories.fileclasses = [fs.MusicFile]
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class MusicFile(fs.File):
|
||||
def can_handle(cls, path):
|
||||
if not fs.File.can_handle(path):
|
||||
return False
|
||||
return get_file_ext(path[-1]) in auto.EXT2CLASS
|
||||
return get_file_ext(path.name) in auto.EXT2CLASS
|
||||
|
||||
def get_display_info(self, group, delta):
|
||||
size = self.size
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '2.8.0'
|
||||
__version__ = '2.9.0'
|
||||
__appname__ = 'dupeGuru Picture Edition'
|
||||
@@ -18,8 +18,8 @@ class DupeGuru(DupeGuruBase):
|
||||
NAME = __appname__
|
||||
METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
|
||||
|
||||
def __init__(self, view, appdata):
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.scanner = ScannerPE()
|
||||
self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db')
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ MyCreateBitmapContext(int width, int height)
|
||||
}
|
||||
|
||||
context = CGBitmapContextCreate(bitmapData, width, height, 8, bitmapBytesPerRow, colorSpace,
|
||||
kCGImageAlphaNoneSkipLast);
|
||||
(CGBitmapInfo)kCGImageAlphaNoneSkipLast);
|
||||
if (context== NULL) {
|
||||
free(bitmapData);
|
||||
fprintf(stderr, "Context not created!");
|
||||
|
||||
@@ -49,9 +49,18 @@ class Photo(fs.File):
|
||||
self._cached_orientation = 0
|
||||
return self._cached_orientation
|
||||
|
||||
def _get_exif_timestamp(self):
|
||||
try:
|
||||
with self.path.open('rb') as fp:
|
||||
exifdata = exif.get_fields(fp)
|
||||
return exifdata['DateTimeOriginal']
|
||||
except Exception:
|
||||
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def can_handle(cls, path):
|
||||
return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS
|
||||
return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS
|
||||
|
||||
def get_display_info(self, group, delta):
|
||||
size = self.size
|
||||
@@ -89,12 +98,7 @@ class Photo(fs.File):
|
||||
if self._get_orientation() in {5, 6, 7, 8}:
|
||||
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
||||
elif field == 'exif_timestamp':
|
||||
try:
|
||||
with self.path.open('rb') as fp:
|
||||
exifdata = exif.get_fields(fp)
|
||||
self.exif_timestamp = exifdata['DateTimeOriginal']
|
||||
except Exception:
|
||||
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
||||
self.exif_timestamp = self._get_exif_timestamp()
|
||||
|
||||
def get_blocks(self, block_count_per_side):
|
||||
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = '3.7.1'
|
||||
__version__ = '3.8.0'
|
||||
__appname__ = 'dupeGuru'
|
||||
|
||||
@@ -14,8 +14,8 @@ class DupeGuru(DupeGuruBase):
|
||||
NAME = __appname__
|
||||
METADATA_TO_READ = ['size', 'mtime']
|
||||
|
||||
def __init__(self, view, appdata):
|
||||
DupeGuruBase.__init__(self, view, appdata)
|
||||
def __init__(self, view):
|
||||
DupeGuruBase.__init__(self, view)
|
||||
self.directories.fileclasses = [fs.File]
|
||||
self.directories.folderclass = fs.Folder
|
||||
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -8,5 +8,5 @@ Homepage: http://www.hardcoded.net
|
||||
|
||||
Package: {pkgname}
|
||||
Architecture: {arch}
|
||||
Depends: python3 (>=3.2), python3-pyqt4
|
||||
Depends: python3 (>=3.3), python3-pyqt4
|
||||
Description: {longname}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
=== 6.7.0 (2013-12-08)
|
||||
|
||||
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
||||
* Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228)
|
||||
* Make non-numeric delta comparison case insensitive. (#239)
|
||||
* Fix surrogate-related UnicodeEncodeError on CSV export. (#210)
|
||||
* Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238)
|
||||
* Improved documentation.
|
||||
* Important internal refactorings.
|
||||
* Dropped Ubuntu 12.04 and 12.10 support.
|
||||
* Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).
|
||||
|
||||
=== 6.6.0 (2013-08-18)
|
||||
|
||||
* Improved delta values to support non-numerical values. (#213)
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
=== 2.9.0 (2013-12-22)
|
||||
|
||||
* Read RAW pictures EXIF tags. [Mac] (#234)
|
||||
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
||||
* Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228)
|
||||
* Make non-numeric delta comparison case insensitive. (#239)
|
||||
* Fix surrogate-related UnicodeEncodeError on CSV export. (#210)
|
||||
* Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238)
|
||||
* Improved documentation.
|
||||
* Important internal refactorings.
|
||||
* Dropped Ubuntu 12.04 and 12.10 support.
|
||||
* Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).
|
||||
|
||||
=== 2.8.0 (2013-08-25)
|
||||
|
||||
* Improved delta values to support non-numerical values. (#213)
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
=== 3.8.0 (2013-12-07)
|
||||
|
||||
* Disable symlink/hardlink deletion option when not relevant. (#247)
|
||||
* Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228)
|
||||
* Make non-numeric delta comparison case insensitive. (#239)
|
||||
* Fix surrogate-related UnicodeEncodeError on CSV export. (#210)
|
||||
* Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238)
|
||||
* Improved documentation.
|
||||
* Important internal refactorings.
|
||||
* Dropped Ubuntu 12.04 and 12.10 support.
|
||||
* Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).
|
||||
|
||||
=== 3.7.1 (2013-08-19)
|
||||
|
||||
* Fixed folder scan type, which was broken in v3.7.0.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import re
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
@@ -20,6 +21,18 @@ import sys, os
|
||||
# for autodocs
|
||||
sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
|
||||
|
||||
# -- Misc fixes for autodoc
|
||||
|
||||
def fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation):
|
||||
if signature:
|
||||
signature = re.sub(r"<jobprogress.job.NullJob object at 0x[\da-f]+>", "nulljob", signature)
|
||||
return signature, return_annotation
|
||||
|
||||
def setup(app):
|
||||
app.connect('autodoc-process-signature', fix_nulljob_in_sig)
|
||||
|
||||
autodoc_member_order = 'groupwise'
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
@@ -27,7 +40,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc']
|
||||
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -147,10 +160,10 @@ html_theme = 'haiku'
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
html_domain_indices = False
|
||||
# html_domain_indices = False
|
||||
|
||||
# If false, no index is generated.
|
||||
html_use_index = False
|
||||
# html_use_index = False
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
core.gui
|
||||
========
|
||||
|
||||
.. automodule:: core.gui
|
||||
:members:
|
||||
5
help/en/developer/core/gui/deletion_options.rst
Normal file
5
help/en/developer/core/gui/deletion_options.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
core.gui.deletion_options
|
||||
=========================
|
||||
|
||||
.. automodule:: core.gui.deletion_options
|
||||
:members:
|
||||
10
help/en/developer/core/gui/index.rst
Normal file
10
help/en/developer/core/gui/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
core.gui
|
||||
========
|
||||
|
||||
.. automodule:: core.gui
|
||||
:members:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
deletion_options
|
||||
12
help/en/developer/core/index.rst
Normal file
12
help/en/developer/core/index.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
core
|
||||
====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
app
|
||||
fs
|
||||
engine
|
||||
directories
|
||||
results
|
||||
gui/index
|
||||
5
help/en/developer/hscommon/build.rst
Normal file
5
help/en/developer/hscommon/build.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.build
|
||||
==============
|
||||
|
||||
.. automodule:: hscommon.build
|
||||
:members:
|
||||
5
help/en/developer/hscommon/conflict.rst
Normal file
5
help/en/developer/hscommon/conflict.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.conflict
|
||||
=================
|
||||
|
||||
.. automodule:: hscommon.conflict
|
||||
:members:
|
||||
5
help/en/developer/hscommon/desktop.rst
Normal file
5
help/en/developer/hscommon/desktop.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.desktop
|
||||
================
|
||||
|
||||
.. automodule:: hscommon.desktop
|
||||
:members:
|
||||
12
help/en/developer/hscommon/gui/base.rst
Normal file
12
help/en/developer/hscommon/gui/base.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
hscommon.gui.base
|
||||
=================
|
||||
|
||||
.. automodule:: hscommon.gui.base
|
||||
|
||||
.. autosummary::
|
||||
|
||||
GUIObject
|
||||
|
||||
.. autoclass:: GUIObject
|
||||
:members:
|
||||
:private-members:
|
||||
25
help/en/developer/hscommon/gui/column.rst
Normal file
25
help/en/developer/hscommon/gui/column.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
hscommon.gui.column
|
||||
============================
|
||||
|
||||
.. automodule:: hscommon.gui.column
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Columns
|
||||
Column
|
||||
ColumnsView
|
||||
PrefAccessInterface
|
||||
|
||||
.. autoclass:: Columns
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: Column
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: ColumnsView
|
||||
:members:
|
||||
|
||||
.. autoclass:: PrefAccessInterface
|
||||
:members:
|
||||
18
help/en/developer/hscommon/gui/progress_window.rst
Normal file
18
help/en/developer/hscommon/gui/progress_window.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
hscommon.gui.progress_window
|
||||
============================
|
||||
|
||||
.. automodule:: hscommon.gui.progress_window
|
||||
|
||||
.. autosummary::
|
||||
|
||||
ProgressWindow
|
||||
ProgressWindowView
|
||||
|
||||
.. autoclass:: ProgressWindow
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: ProgressWindowView
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
26
help/en/developer/hscommon/gui/selectable_list.rst
Normal file
26
help/en/developer/hscommon/gui/selectable_list.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
hscommon.gui.selectable_list
|
||||
============================
|
||||
|
||||
.. automodule:: hscommon.gui.selectable_list
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Selectable
|
||||
SelectableList
|
||||
GUISelectableList
|
||||
GUISelectableListView
|
||||
|
||||
.. autoclass:: Selectable
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: SelectableList
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: GUISelectableList
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: GUISelectableListView
|
||||
:members:
|
||||
26
help/en/developer/hscommon/gui/table.rst
Normal file
26
help/en/developer/hscommon/gui/table.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
hscommon.gui.table
|
||||
==================
|
||||
|
||||
.. automodule:: hscommon.gui.table
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Table
|
||||
Row
|
||||
GUITable
|
||||
GUITableView
|
||||
|
||||
.. autoclass:: Table
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: Row
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: GUITable
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: GUITableView
|
||||
:members:
|
||||
16
help/en/developer/hscommon/gui/text_field.rst
Normal file
16
help/en/developer/hscommon/gui/text_field.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
hscommon.gui.text_field
|
||||
=======================
|
||||
|
||||
.. automodule:: hscommon.gui.text_field
|
||||
|
||||
.. autosummary::
|
||||
|
||||
TextField
|
||||
TextFieldView
|
||||
|
||||
.. autoclass:: TextField
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: TextFieldView
|
||||
:members:
|
||||
18
help/en/developer/hscommon/gui/tree.rst
Normal file
18
help/en/developer/hscommon/gui/tree.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
hscommon.gui.tree
|
||||
=================
|
||||
|
||||
.. automodule:: hscommon.gui.tree
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Tree
|
||||
Node
|
||||
|
||||
.. autoclass:: Tree
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
.. autoclass:: Node
|
||||
:members:
|
||||
:private-members:
|
||||
|
||||
19
help/en/developer/hscommon/index.rst
Normal file
19
help/en/developer/hscommon/index.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
hscommon
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
build
|
||||
conflict
|
||||
desktop
|
||||
notify
|
||||
path
|
||||
util
|
||||
gui/base
|
||||
gui/text_field
|
||||
gui/selectable_list
|
||||
gui/table
|
||||
gui/tree
|
||||
gui/column
|
||||
gui/progress_window
|
||||
5
help/en/developer/hscommon/notify.rst
Normal file
5
help/en/developer/hscommon/notify.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.notify
|
||||
===============
|
||||
|
||||
.. automodule:: hscommon.notify
|
||||
:members:
|
||||
5
help/en/developer/hscommon/path.rst
Normal file
5
help/en/developer/hscommon/path.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.path
|
||||
=============
|
||||
|
||||
.. automodule:: hscommon.path
|
||||
:members:
|
||||
5
help/en/developer/hscommon/util.rst
Normal file
5
help/en/developer/hscommon/util.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
hscommon.util
|
||||
=============
|
||||
|
||||
.. automodule:: hscommon.util
|
||||
:members:
|
||||
@@ -53,9 +53,5 @@ API
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
core/app
|
||||
core/fs
|
||||
core/engine
|
||||
core/directories
|
||||
core/results
|
||||
core/gui
|
||||
core/index
|
||||
hscommon/index
|
||||
|
||||
@@ -29,7 +29,7 @@ What makes it better than other duplicate scanners?
|
||||
---------------------------------------------------
|
||||
|
||||
The scanning engine is extremely flexible. You can tweak it to really get the kind of results you
|
||||
want. You can read more about dupeGuru tweaking option at the :doc:`Preferences page <preferences>`.
|
||||
want. You can read more about dupeGuru tweaking option in :doc:`scan`.
|
||||
|
||||
How safe is it to use dupeGuru?
|
||||
-------------------------------
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
Folder Selection
|
||||
================
|
||||
|
||||
The first window you see when you launch dupeGuru is the folder selection window. This windows contains the list of the folders that will be scanned when you click on **Scan**.
|
||||
The first window you see when you launch dupeGuru is the folder selection window. This windows
|
||||
contains the list of the folders that will be scanned when you click on **Scan**.
|
||||
|
||||
This window is quite straightforward to use. If you want to add a folder, click on the **+** button. If you added folder before, a popup menu with a list of recent folders you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Folder...**, you will be prompted for a folder to add. If you never added a folder, no menu will pop and you will directly be prompted for a new folder to add.
|
||||
This window is quite straightforward to use. If you want to add a folder, click on the **+** button.
|
||||
If you added folder before, a popup menu with a list of recent folders you added will pop. You can
|
||||
click on one of them to add it directly to your list. If you click on the first item of the popup
|
||||
menu, **Add New Folder...**, you will be prompted for a folder to add. If you never added a folder,
|
||||
no menu will pop and you will directly be prompted for a new folder to add.
|
||||
|
||||
An alternate way to add folders to the list is to drag them in the list.
|
||||
|
||||
To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when you click the button, the selected folder will be set to **excluded** state (see below) instead of being removed.
|
||||
To remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when
|
||||
you click the button, the selected folder will be set to **excluded** state (see below) instead of
|
||||
being removed.
|
||||
|
||||
Folder states
|
||||
-------------
|
||||
|
||||
Every folder can be in one of these 3 states:
|
||||
|
||||
* **Normal:** Duplicates found in this folder can be deleted.
|
||||
* **Reference:** Duplicates found in this folder **cannot** be deleted. Files from this folder can only end up in **reference** position in the dupe group. If more than one file from reference folders end up in the same dupe group, only one will be kept. The others will be removed from the group.
|
||||
* **Excluded:** Files in this directory will not be included in the scan.
|
||||
**Normal:**
|
||||
Duplicates found in this folder can be deleted.
|
||||
**Reference:**
|
||||
Duplicates found in this folder **cannot** be deleted. Files from this folder can
|
||||
only end up in **reference** position in the dupe group. If more than one file from reference
|
||||
folders end up in the same dupe group, only one will be kept. The others will be removed from
|
||||
the group.
|
||||
**Excluded:**
|
||||
Files in this directory will not be included in the scan.
|
||||
|
||||
The default state of a folder is, of course, **Normal**. You can use **Reference** state for a folder if you want to be sure that you won't delete any file from it.
|
||||
The default state of a folder is, of course, **Normal**. You can use **Reference** state for a
|
||||
folder if you want to be sure that you won't delete any file from it.
|
||||
|
||||
When you set the state of a directory, all subfolders of this folder automatically inherit this state unless you explicitly set a subfolder's state.
|
||||
When you set the state of a directory, all subfolders of this folder automatically inherit this
|
||||
state unless you explicitly set a subfolder's state.
|
||||
|
||||
.. only:: edition_pe
|
||||
.. _iphoto:
|
||||
|
||||
iPhoto and Aperture libraries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
-----------------------------
|
||||
|
||||
dupeGuru PE supports iPhoto and Aperture, which means that it knows how to read these libraries
|
||||
and how to communicate with iPhoto and Aperture to remove photos from them. To use this feature,
|
||||
use the special "Add iPhoto Library" and "Add Aperture Library" buttons in the menu that pops
|
||||
up when you click the "+" button. This will then add a special folder for those libraries.
|
||||
dupeGuru Picture Edition supports iPhoto and Aperture, which means that it knows how to read these
|
||||
libraries and how to communicate with iPhoto and Aperture to remove photos from them. To use this
|
||||
feature, use the special "Add iPhoto Library" and "Add Aperture Library" buttons in the menu that
|
||||
pops up when you click the "+" button. This will then add a special folder for those libraries.
|
||||
|
||||
When duplicates are deleted from an iPhoto library, it's sent to iPhoto's trash.
|
||||
When duplicates are deleted (sent to trash) from an iPhoto library, it's sent to iPhoto's
|
||||
trash.
|
||||
|
||||
When duplicates are deleted from an Aperture library, it unfortunately can't send it directly
|
||||
to trash, but it creates a special project called "dupeGuru Trash" in Aperture and send all
|
||||
photos in there. You can then send this project to the trash manually.
|
||||
|
||||
.. only:: edition_me
|
||||
When duplicates are deleted (sent to trash) from an Aperture library, it unfortunately can't
|
||||
send it directly to trash, but it creates a special project called "dupeGuru Trash" in Aperture
|
||||
and send all photos in there. You can then send this project to the trash manually.
|
||||
|
||||
iTunes library
|
||||
^^^^^^^^^^^^^^
|
||||
--------------
|
||||
|
||||
dupeGuru ME supports iTunes, which means that it knows how to read its libraries and how to
|
||||
communicate with iTunes to remove songs from it. To use this feature, use the special
|
||||
dupeGuru Music Edition supports iTunes, which means that it knows how to read its libraries and how
|
||||
to communicate with iTunes to remove songs from it. To use this feature, use the special
|
||||
"Add iTunes Library" button in the menu that pops up when you click the "+" button. This will
|
||||
then add a special folder for those libraries.
|
||||
|
||||
|
||||
@@ -51,9 +51,16 @@ Contents:
|
||||
quick_start
|
||||
folders
|
||||
preferences
|
||||
scan
|
||||
results
|
||||
reprioritize
|
||||
faq
|
||||
developer/index
|
||||
changelog
|
||||
credits
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -1,63 +1,87 @@
|
||||
Preferences
|
||||
===========
|
||||
|
||||
.. only:: edition_se
|
||||
|
||||
**Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. If you select **Filename**, dupeGuru will compare every filenames word-by-word and, depending on the other settings below, it will determine if enough words are matching to consider 2 files duplicates. If you select **Content**, only files with the exact same content will match.
|
||||
|
||||
The **Folders** scan type is a bit special. When you choose it, dupeGuru will scan for duplicate *folders* instead of duplicate files. To determine whether two folders are duplicates, all files contained in the folders will be scanned, and if the contents of **all** files in the folders match, the folders will be considered duplicates.
|
||||
|
||||
**Filter Hardness:** If you chose the **Filename** scan type, this option determines how similar two filenames must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words).
|
||||
**Scan Type:**
|
||||
Basic scan type to use. See :doc:`scan` for details.
|
||||
|
||||
.. only:: edition_me
|
||||
|
||||
**Scan Type:** This option determines what aspect of the files will be compared in the duplicate scan. The nature of the duplicate scan varies greatly depending on what you select for this option.
|
||||
|
||||
* **Filename:** Every song will have its filename split into words, and then every word will be compared to compute a matching percentage. If this percentage is higher or equal to the **Filter Hardness** (see below for more details), dupeGuru will consider the 2 songs duplicates.
|
||||
* **Filename - Fields:** Like **Filename**, except that once filename have been split into words, these words are then grouped into fields. The field separator is " - ". The final matching percentage will be the lowest matching percentage among the fields. Thus, "An Artist - The Title" and "An Artist - Other Title" would have a matching percentage of 50 (With a **Filename** scan, it would be 75).
|
||||
* **Filename - Fields (No Order):** Like **Filename - Fields**, except that field order doesn't matter. For example, "An Artist - The Title" and "The Title - An Artist" would have a matching percentage of 100 instead of 0.
|
||||
* **Tags:** This method reads the tag (metadata) of every song and compare their fields. This method, like the **Filename - Fields**, considers the lowest matching field as its final matching percentage.
|
||||
* **Content:** This scan method use the actual content of the songs to determine which are duplicates. For 2 songs to match with this method, they must have the **exact same content**.
|
||||
* **Audio Content:** Same as content, but only the audio content is compared (without metadata).
|
||||
|
||||
**Filter Hardness:** If you chose a filename or tag based scan type, this option determines how similar two filenames/tags must be for dupeGuru to consider them duplicates. If the filter hardness is, for example 80, it means that 80% of the words of two filenames must match. To determine the matching percentage, dupeGuru first counts the total number of words in **both** filenames, then count the number of words matching (every word matching count as 2), and then divide the number of words matching by the total number of words. If the result is higher or equal to the filter hardness, we have a duplicate match. For example, "a b c d" and "c d e" have a matching percentage of 57 (4 words matching, 7 total words).
|
||||
|
||||
**Tags to scan:** When using the **Tags** scan type, you can select the tags that will be used for comparison.
|
||||
**Tags to scan:**
|
||||
When using the **Tags** scan type, you can select the tags that will be used for comparison.
|
||||
|
||||
.. only:: edition_se or edition_me
|
||||
|
||||
**Word weighting:** If you chose the **Filename** scan type, this option slightly changes how matching percentage is calculated. With word weighting, instead of having a value of 1 in the duplicate count and total word count, every word have a value equal to the number of characters they have. With word weighting, "ab cde fghi" and "ab cde fghij" would have a matching percentage of 53% (19 total characters, 10 characters matching (4 for "ab" and 6 for "cde")).
|
||||
**Word weighting:**
|
||||
See :ref:`word-weighting`.
|
||||
|
||||
**Match similar words:** If you turn this option on, similar words will be counted as matches. For example "The White Stripes" and "The White Stripe" would have a match % of 100 instead of 66 with that option turned on. **Warning:** Use this option with caution. It is likely that you will get a lot of false positives in your results when turning it on. However, it will help you to find duplicates that you wouldn't have found otherwise. The scan process also is significantly slower with this option turned on.
|
||||
**Match similar words:**
|
||||
See :ref:`similarity-matching`.
|
||||
|
||||
.. only:: edition_pe
|
||||
|
||||
**Scan Type:** This option determines the type of scan that will be made on your pictures. The **Contents** scan type compares the actual contents of the pictures in a fuzzy way (making it possible to find not only exact duplicates, but also similar ones). The **EXIF Timestamp** scan type looks at the EXIF metadata of the picture (if it exists) and matches pictures that have the same one. It's much faster than the Contents scan. **Warning:** Modified pictures often keep the same EXIF timestamp, so watch out for false positives when you use that scan type.
|
||||
**Match pictures of different dimensions:**
|
||||
If you check this box, pictures of different dimensions will be allowed in the same
|
||||
duplicate group.
|
||||
|
||||
**Filter Hardness:** *Contents scan type only.* The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot.
|
||||
.. _filter-hardness:
|
||||
|
||||
**Match pictures of different dimensions:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group.
|
||||
**Filter Hardness:**
|
||||
The threshold needed for two files to be considered duplicates. A lower value means more
|
||||
duplicates. The meaning of the threshold depends on the scanning type (see :doc:`scan`).
|
||||
Only works for :ref:`worded <worded-scan>` and :ref:`picture blocks <picture-blocks-scan>`
|
||||
scans.
|
||||
|
||||
**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't!
|
||||
**Can mix file kind:**
|
||||
If you check this box, duplicate groups are allowed to have files with different extensions. If
|
||||
you don't check it, well, they aren't!
|
||||
|
||||
**Ignore duplicates hardlinking to the same file:** If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same `inode <http://en.wikipedia.org/wiki/Inode>`_. If they do, they will not be considered duplicates. (Only for OS X and Linux)
|
||||
**Ignore duplicates hardlinking to the same file:**
|
||||
If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same
|
||||
`inode`_. If they do, they will not be considered duplicates. (Only for OS X and Linux)
|
||||
|
||||
**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is `regular-expressions.info <http://www.regular-expressions.info>`_.
|
||||
**Use regular expressions when filtering:**
|
||||
If you check this box, the filtering feature will treat your filter query as a
|
||||
**regular expression**. Explaining them is beyond the scope of this document. A good place to
|
||||
start learning it is `regular-expressions.info`_.
|
||||
|
||||
**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty.
|
||||
**Remove empty folders after delete or move:**
|
||||
When this option is enabled, folders are deleted after a file is deleted or moved and the folder
|
||||
is empty.
|
||||
|
||||
**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave.
|
||||
**Copy and Move:**
|
||||
Determines how the Copy and Move operations (in the Action menu) will behave.
|
||||
|
||||
* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all.
|
||||
* **Recreate relative path:** The source file's path will be re-created in the destination folder up to the root selection in the Directories panel. For example, if you added ``/Users/foobar/SomeFolder`` to your Directories panel and you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in the final destination.).
|
||||
* **Recreate absolute path:** The source file's path will be re-created in the destination folder in it's entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination ``/Users/foobar/MyDestination``, the final destination for the file will be ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``.
|
||||
* **Right in destination:** All files will be sent directly in the selected destination, without
|
||||
trying to recreate the source path at all.
|
||||
* **Recreate relative path:** The source file's path will be re-created in the destination folder up
|
||||
to the root selection in the Directories panel. For example, if you added
|
||||
``/Users/foobar/SomeFolder`` to your Directories panel and you move
|
||||
``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination
|
||||
``/Users/foobar/MyDestination``, the final destination for the file will be
|
||||
``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in
|
||||
the final destination.).
|
||||
* **Recreate absolute path:** The source file's path will be re-created in the destination folder in
|
||||
its entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the
|
||||
destination ``/Users/foobar/MyDestination``, the final destination for the file will be
|
||||
``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``.
|
||||
|
||||
In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination.
|
||||
In all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination
|
||||
filename if the filename already exists in the destination.
|
||||
|
||||
**Custom Command:** This preference determines the command that will be invoked by the "Invoke Custom Command" action. You can invoke any external application through this action. This can be useful if, for example, you have a nice diffing application installed.
|
||||
**Custom Command:**
|
||||
This preference determines the command that will be invoked by the "Invoke Custom Command"
|
||||
action. You can invoke any external application through this action. This can be useful if,
|
||||
for example, you have a nice diffing application installed.
|
||||
|
||||
The format of the command is the same as what you would write in the command line, except that there are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the selected dupe (%d) and the path of the selected dupe's reference file (%r).
|
||||
The format of the command is the same as what you would write in the command line, except that there
|
||||
are 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the
|
||||
selected dupe (%d) and the path of the selected dupe's reference file (%r).
|
||||
|
||||
If the path to your executable contains space characters, you should enclose it in "" quotes. You should also enclose placeholders in quotes because it's very possible that paths to dupes and refs will contain spaces. Here's an example custom command::
|
||||
If the path to your executable contains space characters, you should enclose it in "" quotes. You
|
||||
should also enclose placeholders in quotes because it's very possible that paths to dupes and refs
|
||||
will contain spaces. Here's an example custom command::
|
||||
|
||||
"C:\Program Files\SuperDiffProg\SuperDiffProg.exe" "%d" "%r"
|
||||
|
||||
.. _inode: http://en.wikipedia.org/wiki/Inode
|
||||
.. _regular-expressions.info: http://www.regular-expressions.info
|
||||
@@ -1,6 +1,8 @@
|
||||
Results
|
||||
=======
|
||||
|
||||
.. contents::
|
||||
|
||||
When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list.
|
||||
|
||||
About duplicate groups
|
||||
@@ -118,42 +120,54 @@ filtered duplicates.
|
||||
Action Menu
|
||||
-----------
|
||||
|
||||
* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the
|
||||
**Clear Ignore List:**
|
||||
Remove all ignored matches you added. You have to start a new scan for the
|
||||
newly cleared ignore list to be effective.
|
||||
* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The
|
||||
**Export Results to XHTML:**
|
||||
Take the current results, and create an XHTML file out of it. The
|
||||
columns that are visible when you click on this button will be the columns present in the XHTML
|
||||
file. The file will automatically be opened in your default browser.
|
||||
* **Send Marked to Trash:** Send all marked duplicates to trash, obviously. Before proceeding,
|
||||
**Send Marked to Trash:**
|
||||
Send all marked duplicates to trash, obviously. Before proceeding,
|
||||
you'll be presented deletion options (see below).
|
||||
* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that
|
||||
**Move Marked to...:**
|
||||
Prompt you for a destination, and then move all marked files to that
|
||||
destination. Source file's path might be re-created in destination, depending on the
|
||||
"Copy and Move" preference.
|
||||
* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that
|
||||
**Copy Marked to...:**
|
||||
Prompt you for a destination, and then copy all marked files to that
|
||||
destination. Source file's path might be re-created in destination, depending on the
|
||||
"Copy and Move" preference.
|
||||
* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will
|
||||
**Remove Marked from Results:**
|
||||
Remove all marked duplicates from results. The actual files will
|
||||
not be touched and will stay where they are.
|
||||
* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all
|
||||
**Remove Selected from Results:**
|
||||
Remove all selected duplicates from results. Note that all
|
||||
selected reference files will be ignored, only duplicates can be removed with this action.
|
||||
* **Make Selected into Reference:** Promote all selected duplicates to reference. If a duplicate is
|
||||
**Make Selected into Reference:**
|
||||
Promote all selected duplicates to reference. If a duplicate is
|
||||
a part of a group having a reference file coming from a reference folder (in blue color), no
|
||||
action will be taken for this duplicate. If more than one duplicate among the same group are
|
||||
selected, only the first of each group will be promoted.
|
||||
* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and
|
||||
**Add Selected to Ignore List:**
|
||||
This first removes all selected duplicates from results, and
|
||||
then add the match of that duplicate and the current reference in the ignore list. This match
|
||||
will not come up again in further scan. The duplicate itself might come back, but it will be
|
||||
matched with another reference file. You can clear the ignore list with the Clear Ignore List
|
||||
command.
|
||||
* **Open Selected with Default Application:** Open the file with the application associated with
|
||||
selected file's type.
|
||||
* **Reveal Selected in Finder:** Open the folder containing selected file.
|
||||
* **Invoke Custom Command:** Invokes the external application you've set up in your preferences
|
||||
using the current selection as arguments in the invocation.
|
||||
* **Rename Selected:** Prompts you for a new name, and then rename the selected file.
|
||||
**Open Selected with Default Application:**
|
||||
Open the file with the application associated with selected file's type.
|
||||
**Reveal Selected in Finder:**
|
||||
Open the folder containing selected file.
|
||||
**Invoke Custom Command:**
|
||||
Invokes the external application you've set up in your preferences using the current selection
|
||||
as arguments in the invocation.
|
||||
**Rename Selected:**
|
||||
Prompts you for a new name, and then rename the selected file.
|
||||
|
||||
**Warning about moving files in iPhoto/iTunes:** When using the "Move Marked" action on duplicates
|
||||
that come from iPhoto or iTunes, files are copied, not moved. dupeGuru cannot use the Move action
|
||||
on those files.
|
||||
**Warning about moving files in iPhoto/iTunes/Aperture:** When using the "Move Marked" action on
|
||||
duplicates that come from iPhoto, Aperture or iTunes, files are copied, not moved. dupeGuru cannot
|
||||
use the Move action on those files.
|
||||
|
||||
Deletion Options
|
||||
----------------
|
||||
@@ -161,8 +175,9 @@ Deletion Options
|
||||
These options affect how duplicate deletion takes place. Most of the time, you don't need to enable
|
||||
any of them.
|
||||
|
||||
* **Link deleted files:** The deleted files are replaced by a link to the reference file. You have
|
||||
a choice of replacing it either with a `symlink`_ or a `hardlink`_. It's better to read the whole
|
||||
**Link deleted files:**
|
||||
The deleted files are replaced by a link to the reference file. You have a choice of replacing
|
||||
it either with a `symlink`_ or a `hardlink`_. It's better to read the whole
|
||||
wikipedia pages about them to make a informed choice, but in short, a symlink is a shortcut to
|
||||
the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a
|
||||
link to the file *itself*. That link is as good as a "real" file. Only when *all* hardlinks to a
|
||||
@@ -172,7 +187,8 @@ any of them.
|
||||
Windows XP doesn't support it, but Vista and up support it. However, for the feature to work,
|
||||
dupeGuru has to run with administrative privileges.
|
||||
|
||||
* **Directly delete files:** Instead of sending files to trash, directly delete them. This is used
|
||||
**Directly delete files:**
|
||||
Instead of sending files to trash, directly delete them. This is used
|
||||
for troubleshooting and you normally don't need to enable this unless dupeGuru has problems
|
||||
deleting files normally, something that can happens when you try to delete files on network
|
||||
storage (NAS).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user