1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-25 08:01:39 +00:00

Compare commits

..

273 Commits

Author SHA1 Message Date
Virgil Dupras
e69a1764a0 Fix cocoa build script
It wouldn't properly find python 3.5 dylib for linking.
2016-07-01 19:50:19 -04:00
Virgil Dupras
215307df93 Remove this dependency inclusion thing in src packages
It's pointless and wasteful.
2016-07-01 17:12:31 -04:00
Virgil Dupras
3aa99c396b Bump OS X requirements to 10.8 and update README
Because of Sparkle, it's now required to build dupeguru on 10.10+, but with MACOSX_DEPLOYMENT_TARGET, which we now properly set, the results properly runs on 10.8.

This requires a python that has also been compiled with  MACOSX_DEPLOYMENT_TARGET=10.8
2016-07-01 15:36:15 -04:00
Virgil Dupras
9f2c3e7732 Fix failing test on OS X / py35
A 100 recursion limit was too low in that environment.
2016-07-01 15:29:50 -04:00
Eugene San
d660cef245 Update packaging to conform with package unification and few fixes (#372)
* Rename package (dupeguru-se -> dupeguru)
* Update package name in .desktop files and scripts
* Add Ubuntu package building instructions
* Fix build_pe_modules.py
* Add description to package
* Add conflicts dependencies to replace previous versions
* Update python version
* Unify .json configs
* Few cosmetics changes (mainly missing end-lines and images permissions)
2016-06-28 22:39:23 -04:00
Virgil Dupras
bdd404ce0e Remove appscript from OS X requirements
It's not needed anymore.
2016-06-10 09:48:53 -04:00
Virgil Dupras
df9f72d9bf v4.0.0 2016-06-10 09:16:54 -04:00
Virgil Dupras
53bbc5901c Add xenial to the list of supported ubuntu distros 2016-06-10 09:15:20 -04:00
Virgil Dupras
0959f4581e Update to Sparkle 1.14 2016-06-08 13:28:52 -04:00
Virgil Dupras
b1ef3dc8fe Simplify progress report during scanning
We now get less progress feedback, but in exchange, our progress job is
simpler. Previously, our progress bar would often get wonky towards the
end of the scan and I didn't have the energy to debug that.

Besides, people don't care about that level of progress feedback.
2016-06-08 12:29:28 -04:00
Virgil Dupras
334f4dd2ae Increase md5 reading buffer to 1mb
This makes md5 computing faster without using too much memory.
2016-06-08 12:23:10 -04:00
Virgil Dupras
fbdd1d866e Simplify getmatches_by_contents() signature
partial and sizeattr attributes are not needed anymore.
2016-06-08 12:06:08 -04:00
Virgil Dupras
64e86c9ff9 Add ctags config 2016-06-07 21:36:25 -04:00
Virgil Dupras
80f659858c Fail with excplicit message when unable to load results file
Previously, we would simply show an empty results window. Not very
helpful.
2016-06-07 21:34:04 -04:00
Virgil Dupras
ef8f8f0e44 Fix broken tests 2016-06-07 21:32:30 -04:00
Virgil Dupras
b7a7282c2a Fix results loading
The merge operation broke it. It would try to access a result_table that
didn't exist yet.
2016-06-07 16:56:59 -04:00
Virgil Dupras
668821301c Update documentation 2016-06-06 20:48:26 -04:00
Virgil Dupras
13fb06a693 Remove ContentsAusio scan type
It had few uses and had a confusing name. People though it did fuzzy
audio data matching, which it does not.
2016-06-06 17:08:41 -04:00
Virgil Dupras
61b219ff43 flake8 fix 2016-06-06 17:05:48 -04:00
Virgil Dupras
c4aeda0bd0 Clean up README 2016-06-06 17:00:57 -04:00
Virgil Dupras
76f3332d36 Remove windows leftover file 2016-06-06 17:00:38 -04:00
Virgil Dupras
b47b1e11af cocoa: remove inter.app_se
The last remnant of the pre-merge era.
2016-06-06 11:20:45 -04:00
Virgil Dupras
168d94910b cocoa: fix image loading in picture mode details panel
I had broken it during the big merge.
2016-06-06 11:03:09 -04:00
Virgil Dupras
ca3172044f qt: move scan type and app mode selector to the top of the window 2016-06-06 10:29:02 -04:00
Virgil Dupras
f66849b09d Fix tox tests 2016-06-06 10:21:32 -04:00
Virgil Dupras
8c1078aa71 cocoa: merge se/me/pe into one single app
That merge has already been done in core and qt, we're following.

I broke picture scan details panel image loading. Will fix later.
2016-06-05 21:18:48 -04:00
Virgil Dupras
b780816e3c Merge commit 'a65077f871481ca98ce51810751e66f228cb096a'
# Conflicts:
#	build.py
#	core/pe/iphoto_plist.py
2016-06-05 13:18:33 -04:00
Virgil Dupras
fb8a384a6a cocoa: get rid of edition-specific ResultWindow overrides 2016-06-04 21:18:14 -04:00
Virgil Dupras
2be4ae8f65 cocoa: move scan type selector to directory window
Also, use dynamic scan type labels supplied by core.
2016-06-02 21:31:12 -04:00
Virgil Dupras
f8686ffb55 cocoa: remove iPhoto and Aperture support
These apps don't exist anymore.
2016-06-01 22:34:16 -04:00
Virgil Dupras
3093a42553 cocoa: remove iTunes support
It was an unmaintained feature that wasn't working well with recent OS X releases.
2016-06-01 22:12:27 -04:00
Virgil Dupras
83d934fd4f cocoa: make auto-update URLs HTTPS 2016-06-01 21:57:27 -04:00
Virgil Dupras
f3c09c7a8d cocoa: adjust to latest changes
...that is, scanner on-the-fly instantiation and fileclasses/folderclass config move.

We haven't moved the scan type selector in the UI yet.
2016-06-01 21:56:18 -04:00
Virgil Dupras
a65077f871 Merge core_{se,me,pe} into core.{se,me,pe} 2016-05-31 22:32:37 -04:00
Virgil Dupras
d4919054f9 qt: move qt.base units into qt root package 2016-05-31 21:59:31 -04:00
Virgil Dupras
773f6651e6 Merge core_se.app into core.app 2016-05-31 21:43:24 -04:00
Virgil Dupras
9a25670552 qt: merge se.app into base.app 2016-05-31 21:22:50 -04:00
Virgil Dupras
8c9ef3ea29 Re-add the Clear Picture Cache action 2016-05-31 20:55:32 -04:00
Virgil Dupras
7256adb4d4 qt: remove UI testapp 2016-05-31 20:23:09 -04:00
Virgil Dupras
ad45a6e16e Adapt build/package scripts to single-edition 2016-05-31 20:21:07 -04:00
Virgil Dupras
c865f84c16 Merge PE into SE 2016-05-30 22:27:59 -04:00
Virgil Dupras
7d749779f2 qt: merge ME edition into SE
(breaks PE temporarily)

Adds a Standard/Music Application Mode button to SE and thus adds the
ability to run ME scan types in SE. When in Music mode, the
Music-specific results window, details panel and preferences panel will
show up.

All preferences except scan_type become shared between app modes
(changing the pref in a mode changes it in the other mode).

Results Window and Details Panel are now re-created at each scan
operation because they could change their type between two runs.

Preferences panel is instantiated on the fly and discarded after close.

This is a very big merge operation and I'm trying to touch as little
code as possible, sometimes at the cost of elegance. I try to minimize
the breakage that this change brings.
2016-05-29 22:37:38 -04:00
Virgil Dupras
8b878b7b13 core_me: properly set scanner class
It was wrongly instatiating the scanner on startup (we now do it
on-the-fly).
2016-05-29 17:22:46 -04:00
Virgil Dupras
0056f696df refactoring: move fileclasses and folderclass options in app class
Previously, it was in `Directory`.

This will make our job easier for an upcoming SE/ME/PE merge.
2016-05-29 17:15:55 -04:00
Virgil Dupras
abd2f3a9d6 core_pe: fix missing scanner option refactoring 2016-05-29 17:08:54 -04:00
Virgil Dupras
5c57a2a8fc Instantiate Scanner on-the-fly
Previously, it would be instantiated on startup.

This will make our job easier for an upcoming SE/ME/PE merge.
2016-05-29 16:52:07 -04:00
Virgil Dupras
dc76f9744e Update qtlib subrepo 2016-05-29 16:48:44 -04:00
Virgil Dupras
130581db53 Apply flake8 checks to tests 2016-05-29 15:02:39 -04:00
Virgil Dupras
9ed4b7abf0 refactoring: take ignore_list out of Scanner class
It's now `DupeGuru` that holds it and passes it to `get_dupe_groups()`,
the only place where it's actually used in `Scanner`.

This will make the SE/ME/PE merge easier by allowing us to instantiate
the Scanner on-the-fly since it doesn't hold state anymore.
2016-05-29 14:13:19 -04:00
Virgil Dupras
a0a90e8ef8 Update qtlib subrepo 2016-05-28 22:20:42 -04:00
Virgil Dupras
197acbf5b3 qt: move scan_type preference to main window
It leads to better discoverability of dupeguru's options and will make
more sense after the big merge of all editions.
2016-05-28 21:54:25 -04:00
Virgil Dupras
09d5243648 Bump python requirement to v3.4 2016-05-27 19:28:19 -04:00
Virgil Dupras
10169bee9c Update qtlib
This updates `progress_window`, which fixes a bug where the progress
window would be mistakenly shown on starup.

fixes #357
2016-05-25 21:27:48 -04:00
Virgil Dupras
bb8a41f8c5 Do git submodule init/update in bootstrap script 2016-05-25 21:15:56 -04:00
Virgil Dupras
bb1f0f5be6 Convert hscommon, qtlib and cocoalib to submodules
... rather than subtrees. That also represents a small qtlib updates
which needed a code adjustment.
2016-05-25 21:07:30 -04:00
Virgil Dupras
4b6f8b45e2 Fix tox tests
Add new warning to ignores
2016-05-24 22:54:28 -04:00
Virgil Dupras
2ed1b82ecf Push edition-specific scan option listing down to the core
... rather than have each UI layer repeat them.

Did qt, but not cocoa yet.
2016-05-24 22:53:03 -04:00
Virgil Dupras
de9122c3cb Remove obsolete ABOUT_LICENSE
dupeGuru is GPL now
2016-05-24 22:36:37 -04:00
Yichi Zhang
632650b483 Fix some compiler warnings (Cocoa) 2016-04-04 22:08:58 -04:00
Virgil Dupras
c05f01853d Merge remote-tracking branch 'patrick/feature/tox-py35' 2016-01-05 17:21:44 -05:00
Virgil Dupras
15539eb3c5 flake8 fix 2016-01-05 17:16:39 -05:00
Virgil Dupras
b9874cc7ed Add tox instructions in README
Also, remove py33 from tox envlist
2016-01-05 17:14:05 -05:00
Patrick Atamaniuk
13a2868dd2 add py35 to tox environments 2016-01-05 22:15:42 +01:00
Virgil Dupras
abb1345c49 Update FAQ 2015-12-26 09:20:07 -05:00
Virgil Dupras
9c53b2218c Update waf to v1.8.17
This allows us to call it from Python 3.5 without the failures we previously had.
2015-12-25 10:26:56 -05:00
Virgil Dupras
4b3c1e2828 Add Windows/Mac maintainer notice in the README 2015-12-24 20:50:33 -05:00
Virgil Dupras
b64f9f5ec0 Add support for Python 3.5 and pyenv's pythons on OS X 2015-10-27 21:33:41 -04:00
Virgil Dupras
40d9a486e2 Add Spanish and Dutch localizations
Thanks Josep and Kees Duvekot!

Also, made the language selector sorted alphabetically. It was getting
confusing in there.
2015-07-20 13:18:14 -04:00
Virgil Dupras
6930e092e0 Document branching in the repo 2015-07-20 13:02:14 -04:00
Virgil Dupras
6b41223a22 Add missing pl_PL log in cocoalib/qtlib 2015-07-20 12:54:38 -04:00
Virgil Dupras
d15321a8e9 Update locales from transifex
Also, add missing Korean locales from cocoalib/qtlib, which prevented
proper build on OS X.
2015-07-20 12:50:58 -04:00
Virgil Dupras
d6533cbfa2 Update README 2015-07-03 19:41:10 -04:00
Virgil Dupras
43974f9ebd Update Russian localisation (from Igor Fokusov) 2015-04-14 19:07:08 -04:00
Virgil Dupras
0068e7b85a Add Korean localization (from woosuk park) 2015-04-12 22:22:00 -04:00
Virgil Dupras
23b29eb5c3 Add Polish localization (from mstefanski1987) 2015-04-12 21:53:45 -04:00
Virgil Dupras
dba231cf21 Fix broken link in README :( 2015-04-12 15:34:29 -04:00
Virgil Dupras
f25b1f9f46 Fix typo in README 2015-04-12 15:33:07 -04:00
Virgil Dupras
60dd73f634 Add Current Status section to README 2015-04-12 15:31:01 -04:00
Virgil Dupras
3b6fe992c0 Clarify documentation about results filtering
It wasn't clear that filtering was applied to whole paths.

ref #294
2015-04-05 16:19:03 -04:00
Virgil Dupras
6d263215ad Fix wrong use_regexp option propagation to core (qt)
We need to flip `use_regexp` before sending it down to
`escape_filter_regexp`!

fixes #295
2015-04-05 09:17:35 -04:00
Virgil Dupras
bba20f4218 Improve bootstrap script by working around some problems
... notably, Ubuntu 14.04's python and python v3.4.1, which is still the
newest python 3.4 on some systems.
2015-03-01 08:56:01 -05:00
Virgil Dupras
bb9908abb4 Change license from BSD to GPLv3
See http://www.hardcoded.net/archive2014#2014-12-28 for context
2015-01-04 09:59:08 -05:00
Virgil Dupras
e7076bc3bd Change license from BSD to GPLv3
See http://www.hardcoded.net/archive2014#2014-12-28 for context
2015-01-03 16:33:16 -05:00
Virgil Dupras
fc16ea8c49 Change copyright year to 2015 2015-01-03 16:30:57 -05:00
Virgil Dupras
0c07046ec4 cocoalib update 2015-01-03 16:29:36 -05:00
Virgil Dupras
943a6570d8 Added Utopic Unicorn to the list of supported Ubuntu dists 2014-10-26 12:18:49 -04:00
Virgil Dupras
854a253d9f me v6.8.1 2014-10-26 12:00:54 -04:00
Virgil Dupras
4e477104a6 Use --deep flag when code signing under OS X
It is now required in new versions of OS X that the embedded Python framework is signed separately.
2014-10-18 11:09:18 -04:00
Virgil Dupras
79800bc6ed Added --arch-pkg option to package.py
Otherwise, AUR packages don't work with Arch lookalikes like Manjaro.
2014-10-17 15:58:45 -04:00
Virgil Dupras
6e7b95b2cf se v3.9.1 2014-10-17 15:51:48 -04:00
Virgil Dupras
bf09c4ce8a Nicely wrap PermissionDenied errors on save
In fact, all `OSError`.

ref #266
2014-10-17 15:46:43 -04:00
Virgil Dupras
b4a73771c2 Fix iCCP: known incorrect sRGB profile warnings in stderr
I processed all images through `convert -strip`.

It's still possible, however, to get these error if PE tries to open an
image with an invalid profile.
2014-10-17 15:45:07 -04:00
Virgil Dupras
2166a0996c Added tox configuration
... and fixed pep8 warnings. There's a lot of them that are still
ignored, but that's because it's too much of a step to take at once.
2014-10-13 15:08:59 -04:00
Virgil Dupras
24643a9b5d Updated copyright year to 2014 in Cocoa about boxes
Better late than never.
2014-10-12 13:19:55 -04:00
Virgil Dupras
045051ce06 Fixed formatting in changelog_pe 2014-10-12 10:52:41 -04:00
Virgil Dupras
7c3728ca47 Converted hscommon.jobprogress.qt to Qt5 2014-10-12 10:52:21 -04:00
Virgil Dupras
91be1c7336 pe v2.10.1 2014-10-12 10:47:18 -04:00
Virgil Dupras
162378bb0a Updated hscommon 2014-10-12 10:39:21 -04:00
Virgil Dupras
4e3cad5702 Fixed minor typo 2014-10-12 10:15:07 -04:00
Virgil Dupras
321f8ab406 Catch MemoryError better in PE's block matching algo
fixes #264 (for good this time, hopefully)
2014-10-05 22:22:59 -04:00
Virgil Dupras
5b3d5f5d1c Tweaked the main dev help page to have actual reflinks 2014-10-05 20:12:38 -04:00
Virgil Dupras
372a682610 Catch MemoryError in PE's block matching algo
fixes #264 (hopefully)
2014-10-05 17:13:36 -04:00
Virgil Dupras
44266273bf Included hscommon.jobprogress in the devdocs 2014-10-05 17:12:10 -04:00
Virgil Dupras
ac32305532 Integrated the jobprogress library into hscommon
I have a fix to make in it and it's really silly to pretend that this
lib is of any use to anybody outside HS apps. Bringing it back here will
make things more simple.
2014-10-05 16:31:16 -04:00
Virgil Dupras
87c2fa2573 Updated README which was a bit outdated 2014-10-04 17:01:22 -04:00
Virgil Dupras
db63b63cfd Fix crash in PE when reading some EXIF tags
The crash was caused by ObjP, which crashed when converting `NSDictionary` containing unsupported types.

Updating ObjP to v1.3.1 does the trick.

fixes #263
fixes #265
2014-10-04 16:35:26 -04:00
Virgil Dupras
6725b2bf0f Updated German localisation, by Frank Weber 2014-09-28 13:40:09 -04:00
Virgil Dupras
990e73c383 Catch Spinx SystemExit when building help
In a recent Sphinx release, it started calling `sys.exit()` and that
caused our whole build process to exit prematurely.
2014-09-13 16:05:40 -04:00
Virgil Dupras
9e9e73aa6b qtlib: Fix broken SelectableList
It was still using `.reset()`, which disappeared in Qt5.

Fixes #254.
2014-07-01 08:30:56 -04:00
Virgil Dupras
8434befe1f me v6.8.0 2014-05-11 09:26:55 -04:00
Virgil Dupras
1114ac5613 Fixed debian packaging 2014-05-11 09:11:38 -04:00
Virgil Dupras
f5f29d775c Adapt IPhotoPlistParser to Python 3.4
This also means that Python 3.3 isn't supported anymore for that part.
Updated README accordingly.
2014-05-03 15:12:13 -04:00
Virgil Dupras
ebd7f1b4ce pe v2.10.0 2014-05-03 13:57:00 -04:00
Virgil Dupras
279b7ad10c Fix typo in README 2014-05-03 13:53:16 -04:00
Virgil Dupras
878205fc49 Fix empty ignore List dialog bug in PE
Re-instantiating a new scanner for PE  made the ignore list dialog
target the wrong ignore list. We now only instantiate a scanner once.

Fixes #253
2014-05-03 13:44:38 -04:00
Virgil Dupras
b16df32150 I'm giving PyCharm a try 2014-05-03 13:39:39 -04:00
Virgil Dupras
04b06f7704 Removed the setNativeMenuBar() call under Qt
I put it there to make the menu usable under Ubuntu 13.10, but since
14.04, this line actually brakes it.
2014-05-03 09:34:41 -04:00
Virgil Dupras
c6ea1c62d4 Fixed Windows packaging 2014-04-21 10:00:53 -04:00
Virgil Dupras
6ce0f66601 Fixed debian packaging 2014-04-19 18:32:11 -04:00
Virgil Dupras
ac3a9e3ba8 Removed Qt's "Check for updates"
It only worked on 32bit Windows, and it's gone now.
2014-04-19 18:21:56 -04:00
Virgil Dupras
903d2f9183 Improved arch packaging
No need to bundle a .desktop file with arch source packages anymore.
dupeGuru's source package takes care of that.
2014-04-19 17:50:40 -04:00
Virgil Dupras
ca709a60cf Updated copyright year to 2014 2014-04-19 12:19:11 -04:00
Virgil Dupras
a9b4ce5529 se v3.9.0 2014-04-19 12:17:26 -04:00
Virgil Dupras
9b82ceca67 Updated windows packaging for Qt5
We now only support 64bit Windows.
2014-04-18 13:22:04 -04:00
Virgil Dupras
4c7c279dd2 Avoid crashes on quit under Windows 2014-04-18 10:55:01 -04:00
Virgil Dupras
79db31685e Fixed crash on results double-click
Introduced by the Qt5 move. Looks like passing `None` to
`doubleClicked.emit()` doesn't cut it anymore.
2014-04-18 10:44:59 -04:00
Virgil Dupras
ba13b700b0 Fixed crashing save dialogs under Qt5 2014-03-30 15:57:07 -04:00
Virgil Dupras
640561a534 Updated F.A.Q.
Fixes #252
2014-03-30 13:43:29 -04:00
Virgil Dupras
e4f81cbf04 Update loc 2014-03-30 10:47:37 -04:00
Virgil Dupras
4be4825112 Bootstrapping: don't use system-site-packages under OS X 2014-03-30 10:26:09 -04:00
Virgil Dupras
7d107d8efa Moved Cocoa error reporting to Github mode. 2014-03-30 10:07:01 -04:00
Virgil Dupras
10d1363334 Changed the error report so it brings the user to Github directly
Making error reporting too easy results in too much context-less
tracebacks which demand attention and, in the end, aren't of much use.

Requiring the user to report errors on Github will reduce the number of
reports, but hopefully make these reports have better context.
2014-03-29 17:42:23 -04:00
Virgil Dupras
b76820ebde Fixed bootstrapping under Python 3.3 2014-03-28 16:27:45 -04:00
Virgil Dupras
72b3cfb364 Adapted bootstrapping procedure to Python 3.4 2014-03-28 16:21:05 -04:00
Virgil Dupras
8b83ed0e5c Removed needless PyQt signal overloading
After a PyQt5 update, dupeGuru wouldn't run anymore because it choked on
signal overloading that weren't necessary.
2014-03-27 19:09:10 -04:00
Virgil Dupras
781f13ae1a Overwrite subfolders' state when setting states in folder dialog
Fixes #248
2014-03-15 17:31:33 -04:00
Virgil Dupras
8193bbae6e Fixed broken tests in core_me 2014-03-15 14:09:36 -04:00
Virgil Dupras
4cafeaff91 Don't crash on malformed integer in iPhoto plist
Simply default to 0. Fixes #214.
2014-03-15 14:06:20 -04:00
Virgil Dupras
95c6a7d41f Add debugging data to iPhoto plist parsing
Fixes #233.
2014-03-15 13:59:15 -04:00
Virgil Dupras
a29e007475 cocoalib: Replaced the "Relevant Console log" mechanism
The old grepping method wasn't reliable and now, we simply keep the last
20 logs in memory to place in that section of error reporting.
2014-03-15 13:57:34 -04:00
Virgil Dupras
d924d7797a Qt: Don't use a native menubar for the Result Window
Having two native menu bars in the app made the result window all
glitchy under Ubuntu 13.10.
2014-02-15 21:02:38 -05:00
Virgil Dupras
33c217ecc8 Straightened out Qt window parenting chain 2014-02-15 15:05:46 -05:00
Virgil Dupras
c9035046ae Updated cocoalib 2014-02-01 17:54:30 -05:00
Virgil Dupras
ad31016825 Updated qtlib 2014-02-01 17:17:15 -05:00
Virgil Dupras
c809066a93 Updated hscommon 2014-02-01 16:18:00 -05:00
Virgil Dupras
60ca27b5e1 Make Cocoa use the new FTP report-sender 2014-01-26 15:27:02 -05:00
Virgil Dupras
1104e24408 Error reports are now dropped by FTP on drop.hardcoded.net 2014-01-26 15:03:24 -05:00
Virgil Dupras
f66db94ffd Merge branch 'master' into develop
Conflicts:
	bootstrap.sh
2014-01-26 09:49:57 -05:00
Virgil Dupras
d98b5b22da polib is now on PyPI 2014-01-26 09:48:30 -05:00
Virgil Dupras
937748e838 Improved source packaging and bootstrapping 2014-01-26 09:41:15 -05:00
Virgil Dupras
37ebf36cee Merge branch 'master' into develop
Conflicts:
	bootstrap.sh
2014-01-11 13:30:30 -05:00
Virgil Dupras
1c84bdd198 Fixed bootstrapping and README for pip 1.5 2014-01-11 13:27:31 -05:00
Virgil Dupras
4a2fa7cd2c Updated FAQ 2014-01-10 15:05:45 -05:00
Virgil Dupras
7d4110f6d3 Merge branch 'master' into develop
Conflicts:
	README.md
2014-01-10 15:00:02 -05:00
Virgil Dupras
8497343d7f Updated FAQ in docs 2014-01-05 21:45:57 -05:00
Virgil Dupras
235a2c2904 Added a "contribute" page to the docs. 2014-01-05 21:44:56 -05:00
Virgil Dupras
25169cfc20 Create an empty site.py in collect_stdlib_dependencies()
Since we have Python 3.3 as a minimum requirement, we don't need to
patch our site.py with copy_sysconfig_files_for_embed() anymore, but we
still need a site.py file on startup. We create it when we collect
stdlib deps.
2013-12-22 12:13:39 -05:00
Virgil Dupras
152f5f37ce pe v2.9.0 2013-12-22 10:23:54 -05:00
Virgil Dupras
3e42ad8469 Minimum Python version is now 3.3 2013-12-22 09:52:19 -05:00
Virgil Dupras
7ba2e38cd6 Package PyPI dependencies right into our source package 2013-12-21 12:13:26 -05:00
Virgil Dupras
c7c7a73384 me v6.7.0 2013-12-08 10:34:04 -05:00
Virgil Dupras
46f8984bdc Merge branch 'qt5' into develop
Conflicts:
	README.md
	qtlib/about_box.py
	qtlib/reg.py
	qtlib/reg_demo_dialog.py
	qtlib/reg_submit_dialog.py
2013-12-07 19:49:27 -05:00
Virgil Dupras
c7d306b7d5 Minimum Python version is now 3.3 2013-12-07 17:23:18 -05:00
Virgil Dupras
47e636e949 Merge branch 'develop' 2013-12-07 11:15:39 -05:00
Virgil Dupras
0562729d8b Improved build script's --clean. 2013-12-07 11:14:59 -05:00
Virgil Dupras
4a36227a18 v3.8.0 2013-12-07 10:57:30 -05:00
Virgil Dupras
28b8b2e415 Sync locs with Transifex 2013-12-07 10:26:01 -05:00
Virgil Dupras
fd82464564 Removed .tx config in hscommon (useless now) 2013-12-07 10:20:13 -05:00
Virgil Dupras
418acf6e5e Merge branch 'regless' into develop
Conflicts:
	cocoa/inter/app.py
	core/app.py
	hscommon/reg.py
	locale/cs/LC_MESSAGES/ui.po
	locale/de/LC_MESSAGES/ui.po
	locale/fr/LC_MESSAGES/ui.po
	locale/hy/LC_MESSAGES/ui.po
	locale/it/LC_MESSAGES/ui.po
	locale/pt_BR/LC_MESSAGES/ui.po
	locale/ru/LC_MESSAGES/ui.po
	locale/ui.pot
	locale/uk/LC_MESSAGES/ui.po
	locale/vi/LC_MESSAGES/ui.po
	locale/zh_CN/LC_MESSAGES/ui.po
	qt/base/app.py
2013-12-07 10:19:31 -05:00
Virgil Dupras
d14d076989 Disable symlink/hardlink option when not relevant (Cocoa)
Fixes #247.
2013-12-06 16:17:04 -05:00
Virgil Dupras
cb8bb5a70e Disable symlink/hardlink option when not relevant (Qt)
When the "Replace with links" option is not enabled, the choice of
symlink or hardlink is irrelevant and causes confusion. Implemented core
mechanism for controlling the enabled state of that option. Also
implemented the Qt interface for it. Cocoa-part is still to be done.

I used this opportunity to greatly enhance documentation of this part of
the code. I'm beginning to like documenting...

Ref #247.
2013-12-06 15:48:01 -05:00
Virgil Dupras
563c9aeff3 Updated README 2013-12-01 11:26:30 -05:00
Virgil Dupras
a0cc1f2e03 Fixed regless cocoa and updated locs 2013-11-30 18:23:42 -05:00
Virgil Dupras
01403a3f92 Removed fairware 2013-11-30 17:54:40 -05:00
Virgil Dupras
7116674663 Improved hscommon docs 2013-11-30 16:13:12 -05:00
Virgil Dupras
b6bc5de79c Improved hscommon docs
TIL sphinx is rather smart about partial class refrences (starting with
a ".")
2013-11-30 12:29:25 -05:00
Virgil Dupras
5a275db67d Improved hscommon doc
* Completed hscommon.gui.table's doc
* Use sphinx.ext.autosummary.
* Moved attribute docstrings directly into properties.
2013-11-30 12:15:03 -05:00
Virgil Dupras
31395d8794 Fix typos in docs 2013-11-28 22:49:26 -05:00
Virgil Dupras
3734bd6f6c Improved hscommon.gui docs
Added docs for Table and Row in hscommon.gui.table.
2013-11-28 22:38:07 -05:00
Virgil Dupras
da06ef8cad Improved hscommon.gui docs 2013-11-24 13:53:52 -05:00
Virgil Dupras
0b00171655 pygettext: explicitly open files as utf-8
When running it through SSH, I couldn't open files with non-ascii chars.
2013-11-24 10:22:05 -05:00
Virgil Dupras
c1cfa86ad1 Make Cmd+A select all folders in the Folder Selection dialog (Cocoa)
Fixes #228.
2013-11-24 10:12:47 -05:00
Virgil Dupras
c34c9562d3 Make non-numeric delta comparison case insensitive
Fixes #239.
2013-11-23 15:31:20 -05:00
Virgil Dupras
0e542577b0 Merge branch 'master' into develop 2013-11-23 12:39:59 -05:00
Virgil Dupras
42be49da83 Fix surrogate-related UnicodeEncodeError on CSV export
Fixes #210.
2013-11-23 12:38:55 -05:00
Virgil Dupras
398ac9b7c6 Greatly improved docs
Added a new scan.rst page, laying out in much more details than before
the inner workings of the scanning process.

Fixes #208, but does much more than that.
2013-11-17 12:03:48 -05:00
Virgil Dupras
508e9a5d94 Reorganized hscommon documentation
Removed hscommon's "docs" folder and moved all documentation directly
into docstrings. Then, in dupeGuru's developer documentation, added
autodoc references to relevant modules.

The result is a much more usable hscommon documentation.
2013-11-16 14:46:34 -05:00
Virgil Dupras
cc5ea1dbc1 Fixed qt5 migration for ME and PE 2013-11-16 13:38:07 -05:00
Virgil Dupras
3b8d355b9e Merge branch 'develop' into qt5
Conflicts:
	hscommon/desktop.py
2013-11-16 12:11:32 -05:00
Virgil Dupras
10dbfa9b38 Refactoring: Path API compatibility with pathlib
Refactored dupeGuru to make hscommon.path's API a bit close to pathlib's
API. It's not 100% compatible yet, but it's much better than before.

This is more of a hscommon refactoring than a dupeguru one, but since
duepGuru is the main user of Path, it was the driver behind the
refactoring.

This refactoring also see the introduction of @pathify, which ensure
Path arguments. Previously, we were often unsure of whether the caller
of a function was passing a Path or a str. This problem is now solved
and this allows us to remove hscommon.io, an ill-conceived attempt to
solve that same ambiguity problem.

Fixes #235.
2013-11-16 12:06:16 -05:00
Virgil Dupras
e8c42740cf Fixed tests which were broken 2013-11-10 12:54:35 -05:00
Virgil Dupras
4b6c4f048d Fixed compilation warnings on OS X 2013-11-10 12:41:10 -05:00
Virgil Dupras
7594cccf8c Fixed build on OS X which was broken 2013-11-10 12:39:02 -05:00
Virgil Dupras
1d9573cf6f On OS X, read Exif tags using Cocoa's built-in functionality
This allows for RAW files Exif reading. Fixes #234.
2013-11-10 12:00:16 -05:00
Virgil Dupras
76f45fb5a6 Fixed appdata logic which was broken on OS X. 2013-11-10 11:05:03 -05:00
Virgil Dupras
12cf9b800b Merge branch 'master' into develop 2013-11-09 16:21:59 -05:00
Virgil Dupras
ba7e6494c6 Fixed crash on Dupe Count sorting with Delta + Dupes Only
Fixes #238
2013-11-09 16:20:33 -05:00
Virgil Dupras
72d8160b28 Fix boken tests 2013-11-08 16:45:14 -05:00
Virgil Dupras
6d53511cee Merge branch 'master' into develop 2013-11-08 16:03:35 -05:00
Virgil Dupras
64d3c211e6 Updated README 2013-10-20 16:26:16 -04:00
Virgil Dupras
fad112f554 Merge branch 'develop' into qt5 2013-10-20 16:02:36 -04:00
Virgil Dupras
a563327723 Updated cocoalib 2013-10-20 16:01:59 -04:00
Virgil Dupras
096e2bb78a Updated hscommon 2013-10-20 16:01:27 -04:00
Virgil Dupras
5a8cb6f5e3 Implemented super() inheritance style suggested by PyQt5 2013-10-20 15:53:59 -04:00
Virgil Dupras
664d630b96 Fixed occasional core dumps on exit 2013-10-20 15:38:24 -04:00
Virgil Dupras
a4256d3d2b First Qt5 conversion commit
Replaced PyQt4 with PyQt5 and made all adjustments necessary to make
dupeGuru start up.
2013-10-20 15:15:09 -04:00
Virgil Dupras
8e65f15e1a Fixed core.engine.Match docstring
The way it was set made dupeGuru crash under Python 3.2
2013-10-20 13:33:27 -04:00
Virgil Dupras
9ea9f60e92 Added packaging support for ubuntu 13.10 2013-10-19 14:37:01 -04:00
Virgil Dupras
8efefaf0bf Improved API docs 2013-10-12 13:55:36 -04:00
Virgil Dupras
33d9569427 Refactoring: Created hscommon.desktop
This unit hosts previously awkward UI view methods which weren't related
to the view itself, but to the current desktop environment. These
functions are now at their appropriate place.
2013-10-12 13:54:13 -04:00
Virgil Dupras
2fdfacb34e Docs: Fix ugly nulljob repr in method signatures 2013-10-11 12:15:02 -04:00
Virgil Dupras
97fcf1ffa8 Fixed debian packaging
.so files were included in the source package, which messed up builds
under archs that weren't the same as the srcpkg creator (namely, i386
builds).
2013-09-22 09:38:52 -04:00
Virgil Dupras
350b2c64e0 Fixed nasty crash during PE's Cocoa block scanning
Using PyUnicode_GET_SIZE was obviously wrong, but I'm guessing that the str changes in py3.3 made that wrongness significant...
2013-08-26 07:17:02 -04:00
Virgil Dupras
dcc57a7afb Ah crap, another Cocoa fatal mistake 2013-08-25 17:10:26 -04:00
Virgil Dupras
8b510994ad pe v2.8.0 2013-08-25 10:53:08 -04:00
Virgil Dupras
4a4d1bbfcd Eased "Clear Picture Cache" triggering under Qt
Added a keybinding and added the action to the directories dialog's menu
(it was previously only in the results window's menu). Fixes #230.
2013-08-25 10:47:10 -04:00
Virgil Dupras
78c3c8ec2d Improved dev docs 2013-08-20 22:52:43 -04:00
Virgil Dupras
e99e2b18e0 Call sphinx-build from withing Python instead of a subprocess 2013-08-19 17:43:32 -04:00
Virgil Dupras
ae1283f2e1 se v3.7.1 2013-08-19 16:48:07 -04:00
Virgil Dupras
cc76f3ca87 Fixed SE folder scanning under Cocoa 2013-08-18 21:07:33 -04:00
Virgil Dupras
be8efea081 Fixed folder scanning in SE, which was completely broken
Oops
2013-08-18 20:50:31 -04:00
Virgil Dupras
7e8f9036d8 Began serious code documentation effort
Enabled the autodoc Sphinx extension and started adding docstrings to
classes, methods, etc.. It's quickly becoming quite interesting...
2013-08-18 18:36:09 -04:00
Virgil Dupras
8a8ac027f5 Fixed ME's cocoa interface file, which was broken (again)
The Remove Dead Tracks didn't use the new job system and appscript wasn't properly packaged.
2013-08-18 11:23:20 -04:00
Virgil Dupras
1d9d09fdf7 Fixed ME's cocoa interface file, which was broken
It tried to update JOBID2TITLE from inter.app, but it has moved to core.app.
2013-08-18 10:48:02 -04:00
Virgil Dupras
5dc956870d me v6.6.0 2013-08-18 10:16:39 -04:00
Virgil Dupras
d8f48cbd42 Fixed 32bit Windows packaging for Python 3.3
Python 3.3 is compiled with VS2010, and the old VS2008 pre-requisite
scheme doesn't work anymore. We now do like with 64bit, include the DLLs
directly in the package.
2013-08-17 14:48:36 -04:00
Virgil Dupras
39d24817f6 Added packaging for Ubuntu 13.04 2013-08-17 11:42:19 -04:00
Virgil Dupras
2364e44707 Tweaked bootstrap script so it works on Ubuntu
Ubuntu 13.04 doesn't have the pyvenv command. Instead, it's pyvenv-3.3.
Replaced pyvenv with python3 -m venv.
2013-08-17 11:32:49 -04:00
Virgil Dupras
3e2249bf89 se v3.7.0 2013-08-17 11:13:16 -04:00
Virgil Dupras
38acb6f91c Updated french doc 2013-08-17 10:41:41 -04:00
Virgil Dupras
9bcb28d5e2 Fixed inaccuracies in docs 2013-08-16 18:06:58 -04:00
Virgil Dupras
d0a3f081da Tweaked bootstrap script to work on OS X 2013-08-04 16:18:31 -04:00
Virgil Dupras
d11ec557e7 Added bootstrap script for easy build setup 2013-08-04 15:57:39 -04:00
Virgil Dupras
b9124a497c Added Phan Anh to the credits for the vietnamese loc 2013-08-04 10:19:05 -04:00
Virgil Dupras
502715cfd6 Updated locs from Transifex 2013-08-04 10:18:38 -04:00
Virgil Dupras
e1f532e2fd Fixed broken tests 2013-08-04 09:26:18 -04:00
Virgil Dupras
a71033d9d6 Added a splitter control to the Re-Prioritize dialog
Fixes #224
2013-08-04 09:20:08 -04:00
Virgil Dupras
a15a62f55c Fixed progress under Cocoa which always cancelled the job
Yeah, it's funny, same problem as with Qt, but for different reasons.
2013-08-04 09:11:19 -04:00
Virgil Dupras
2fe5cdcf02 Fixed progress under Qt which always cancelled the job 2013-08-03 21:28:02 -04:00
Virgil Dupras
21c64545e5 Fixed job UI cancellation
It was broken since the modernization.
2013-08-03 18:33:35 -04:00
Virgil Dupras
c93a88f8b0 Fix startup crash with PE
Fixes #232
2013-08-03 18:01:28 -04:00
Virgil Dupras
86a81eab4e Added the Vietnamese language 2013-08-03 17:36:53 -04:00
Virgil Dupras
1c779cb3ec Removed spurious debug code 2013-08-03 17:36:12 -04:00
Virgil Dupras
04949c853d Pulled all locs from Transifex
Vietnamese was added.

There's also updated to Russian and Brazilian.
2013-08-03 17:34:02 -04:00
Virgil Dupras
ff782a09f5 Added the --normpo build option
This build command normalizes all PO so that I stop getting
spurious diffs whenever I pull from Transifex.
2013-08-03 17:13:24 -04:00
Virgil Dupras
e5ce6680ca Modernized progress window GUI
Following the refactoring that has been initiated in pdfmasher's
"vala" branch, I pushed more progress window logic into the
core.

The UI code is now a bit dumber than it used to be, and the core
now directly decides when the progress window is shown and
hidden. The "job finished" notification is also directly sent by the
core. Job description update logic is handled by a core gui
textfield.

Job description contsants also moved to the core, triggering
a localisation migration from "ui" to "core".
2013-08-03 16:27:36 -04:00
Virgil Dupras
8e15d89a2e Updated cocoalib subtree. 2013-08-03 11:12:31 -04:00
Virgil Dupras
d874f26f06 Updated qtlib subtree 2013-08-03 11:06:58 -04:00
Virgil Dupras
80a99ff29e Updated hscommon subtree 2013-08-03 10:59:44 -04:00
Virgil Dupras
b11b97dd7c Improved delta values to support non-numerical values
Delta values now work for non-numerical values. Any column,
when its value differs from its ref, becomes orange.

A column that was already a "delta column" keeps its previous
behavior (dupe cells for these columns are always displayed in
orange).

Sorting behavior, when Dupes Only and Delta Values are enabled
at the same time, has also been extended to non-numerical
values, making it easy to mass-mark dupe rows with orange
values.

Documentation was updated, unit tests were added.

Fixes #213
2013-07-28 17:45:23 -04:00
Virgil Dupras
386a5f2c64 Fixed results display bug under Mac OS X
Since the latest refactoring, results wouldn't display properly.
2013-07-28 16:41:07 -04:00
Virgil Dupras
c13a2f207c Dropped i386 support under Mac OS X. 2013-07-28 16:39:53 -04:00
Virgil Dupras
d36710ef38 Docs: Changelog issue now point to Github 2013-07-28 15:11:39 -04:00
Virgil Dupras
bbc9b003c6 Docs: Changed theme to haiku 2013-07-28 15:09:35 -04:00
Virgil Dupras
3edba28f0b Docs: Added a Developer Guide page 2013-07-28 15:09:17 -04:00
Virgil Dupras
9304f42f69 Use Send2Trash instead of the newly deprecated Send2Trash3k 2013-07-28 11:58:49 -04:00
Virgil Dupras
375963ebfd Docs: Updated F.A.Q 2013-07-28 11:29:36 -04:00
Virgil Dupras
7891fb5396 Refactoring: Moved some code from app.DupeGuru to fs.File.
Moved DupeGuru._get_display_info() to File.get_display_info().
This method used none of the app's global state or methods
and had nothing to do there.
2013-07-14 17:43:58 -04:00
Virgil Dupras
bdd5f0a515 Updated help about symlinks and hardlinks
It now mentions that Windows is supported and under which
conditions it is. Ref #220.
2013-07-14 14:06:01 -04:00
Virgil Dupras
db0901b1de Handle OSError during symlink support check
Under a windows that supports symlinks (Vista+), we still need
proper privileges. If we don't have it, OSError is raised and we
need to correctly handle this case. Ref #220.
2013-07-14 13:59:03 -04:00
Virgil Dupras
9225697053 Added hardlink/symlink support for Windows Vista+.
Fixes #220.
2013-07-14 11:58:49 -04:00
Virgil Dupras
097b949763 Tweaked README 2013-06-24 16:20:05 -04:00
Virgil Dupras
60701c2a5c Fixed package.py --src-pkg
Make it use "git archive" instead of "hg archive".
2013-06-23 09:36:44 -04:00
Virgil Dupras
3ef1281450 Updated README to include clearer build instructions 2013-06-22 21:43:24 -04:00
Virgil Dupras
af4e74a130 Added qtlib repo as a subtree 2013-06-22 21:34:41 -04:00
Virgil Dupras
422fb2670d Added cocoalib as a subtree. 2013-06-22 21:32:48 -04:00
Virgil Dupras
94a469205a Added hscommon repo as a subtree 2013-06-22 21:32:23 -04:00
Virgil Dupras
95623f9b47 Removed submodules. 2013-06-22 21:22:32 -04:00
Virgil Dupras
a65c246a2e Updated README so it talks about git submodules. 2013-06-21 21:37:20 -04:00
Virgil Dupras
045d496a98 Converted repo to Git. 2013-06-21 21:00:52 -04:00
Virgil Dupras
5ed98b3d92 Fixed Cocoa build for ME. 2013-05-18 13:14:04 -04:00
Virgil Dupras
5799e3548b me v6.5.1 2013-05-18 12:19:03 -04:00
Virgil Dupras
b01ed1e9f3 Added tag pe2.7.1 for changeset 3cfae8ce9120 2013-05-05 12:32:49 -04:00
349 changed files with 14952 additions and 10749 deletions

5
.ctags Normal file
View File

@@ -0,0 +1,5 @@
-R
--exclude=build
--exclude=env
--exclude=.tox
--python-kinds=-i

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
.DS_Store
__pycache__
*.so
*.mo
*.waf*
.lock-waf*
.tox
/tags
build
dist
env
/deps
cocoa/autogen
/run.py
/cocoa/*/Info.plist
/cocoa/*/build
/qt/*_rc.py
/help/*/conf.py
/help/*/changelog.rst

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "qtlib"]
path = qtlib
url = https://github.com/hsoft/qtlib.git
[submodule "hscommon"]
path = hscommon
url = https://github.com/hsoft/hscommon.git
[submodule "cocoalib"]
path = cocoalib
url = https://github.com/hsoft/cocoalib.git
[submodule "cocoa/Sparkle"]
path = cocoa/Sparkle
url = https://github.com/sparkle-project/Sparkle.git

View File

@@ -1,22 +0,0 @@
syntax: glob
.DS_Store
run.py
*.pyc
*.so
*.mo
*.pyd
*.waf*
.lock-waf*
conf.json
build
dist
install
installer_tmp-cache
env
cocoa/autogen
cocoa/*/Info.plist
cocoa/*/build
qt/base/*_rc.py
help/*/conf.py
help/*/changelog.rst

87
.hgtags
View File

@@ -1,87 +0,0 @@
0ef0ca83b49ad009c896f55824189acc932bcf22 se2.8.2
0ef0ca83b49ad009c896f55824189acc932bcf22 me5.6.6
0ef0ca83b49ad009c896f55824189acc932bcf22 pe1.7.8
a8f232f880b6f9ada565d472996a627ebf69b6e9 before-tiger-drop
321d15e818cf9a3f1fc037543090bb2fca2cccd7 me5.7.0
adc73ccd14b1386cb04dee773c53a2d126800e31 se2.9.0
cbcf9c80fee4c908ef2efbf1c143c9e47676c9b2 pe1.8.0
61c4101851bdea3cb37dfb76f0d404c78c7c594c se2.9.1
0e923897a3389331d4ab3debbc40b8dd616199d9 pe1.8.1
2c454eca9ebe93b6cf34916068f828a6a39e3eaf me5.7.1
19e40bab20521d4256acf325dba9b32e95e135c5 pe1.8.2
7b7c5a66ebee4e4b8125330d24fe9ce1a070ff25 se2.9.2
1cef6d39855f85d4be728646bc78b860e6d4e398 pe1.8.3
90ed56ee602666db2f267f73eac6f824347039b5 me5.7.2
4c3cb1e671a333eabde1151c7c6ffb3609cab025 pe1.8.4
0a71306434bca51bea9a5d5ae54fe1bf0e4900d8 pe1.8.5
556baf4a410779e9bbf43129de133e4c4b26d679 pe1.8.6
9149024283959a50fe9a47a5f175b905d1672c19 se2.10.0
388a7e5aef6385e515189f4a15b4c4fed3ae2fcf me5.8.0
27501167e3b9262ecb60c967941294f36d77eb25 pe1.9.0
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
f71d405e62badcfdc1b037facaac043cece40ee5 se2.10.1
3742e83edd9eadf44e1a501859f5e2462b1ef6fd me5.8.1
724ff565dd785fb739774588c6ee652cfc0612d5 pe1.9.1
634b66415c6529f46ae4f837318027cc9d70c3b5 before-py3k
2b67955db2b0580a8b0854dc918b6ab0d1fa3b88 se2.11.0
b56fe4dd8c95bca270b078a09e86848df77e2b2d me5.9.0
618a7365457d56fdc6920c70843a244762e2ea00 pe1.10.0
95b3a4b564c6222b414f2b40182dde2bd6d0e8a4 me5.9.1
9735a5218d2b5b3b1e1dfe17f2f874177cf8f61c se2.11.1
dbfee3ee2fa5cbb9e7ab36570659c17cd5b8561f se2.12.0
d3fe0d0dcda1e0bf1100d02f117503d3bf6baacf me5.10.0
b07ac1398703dd358912c1f3d20bd995633db9fe pe1.11.0
96b6aee668398d663b04eafc8d5dae05e18500ee before-fairware
22239f94589baf2a9fad2123045b8a718dbd68f5 se2.12.2
f9cae82a0752191276b24ffb2cc4e4a8afb5d754 me5.10.2
154c8cb6f018d446d88fa099490c900906e86386 pe1.11.2
ca93352ce35184853ad9fcb881935a43a8b1e249 me5.10.3
44f6ff67066c083f79daa18a9d2f1ab909e0a62e me5.10.4
3f71a8f5bf8f6d0729748a27af9163e013723294 pe1.11.3
0056293b0dade8b8230f68c1fe6f0c2d1e0b74d8 se2.12.3
8d12cab3b12b723e3a86d02cf8002731a0f73f95 se3.0.0
778876a8a9787658aa6adf6944b53aebcb7faeea se3.0.1
f1d40b556c01f32c58f9ef9f9acac5b78e01ba7a pe2.0.0
2fd901a516f8cb6b4438491f63f2ebfd52a57c13 me6.0.0
ff43c6d9feb388f103b7857eaa6f7809185f78ec before-pluginbuilder
d274bcb98f2d02b86470a04cd62e718eff33b74f pe2.1.0
77e169f757195c11e9c1c5febeb2db8eb3589510 se3.0.2
97893f37d7d0767b5aedf1b4b40de57ee36d426b se3.1.0
e44d5127ed605daa7a17a01eee65d0a157de20c0 pe2.2.0
ecf9aaa568340e3d03e8854b7556edd5a3285107 pe2.2.1
db1f325c907ffa9808a49cb7bc2886b9fca7aee2 se3.1.1
e62183e907d6177cf0bac354e31afa9546c199e6 se3.1.2
28ba95706dc54ba32b1c0cf4e1e6350515d19ba3 me6.0.2
925847384dcef62a5c3518fc9e5ce42feab2b093 pe2.2.2
383b14d6e8555ed2c8d5552259bc7ce600dfb1d0 before-leopard-drop
a2f7b7302e178f08725a6404ddc28464409510b1 se3.2.0
5a5134a4fa9bb7ca80ae3e32010030f5bd7ba434 me6.1.0
0fd77be57ff716d5c93232e829dc02acec036d7c se3.2.1
3dd08060135b0b9cef70b6f5a81f191ea339c8d5 me6.1.1
4e6cbef6bcdfcc0e56ff9690fbfe1cac1f4b1b09 pe2.3.0
9ea9af1b886cd1adc4f42fd2276cc2b38376eab0 se3.3.0
6e3379be6821bb36d7f0877a17dd6c07aa037b0a se3.3.1
015ba7e2c10d09afb944f387c2a9c97f7eff7571 me6.2.0
8178bda48324461a17118c98634241952c074f29 pe2.4.0
2a96f2fb3ddb6f1e0ae87951586733fc3e4a28a0 se3.3.2
6a08c1205dfe5e537e5c2dc99d05e05d1d3928f6 me6.2.1
a619f313712e2923160b8f90d8250ee0e184c7b9 pe2.4.1
fad463ae749b7189dce92f1e42a57ac4ee03987d se3.3.3
236cf9b690a144392e7e86e7c9749fc834a8b271 me6.3.0
90318f1303858d9d01065d92d78d98b888b38ea0 se3.4.0
93ed33410df2d2f21229a77ae49c83ece2c50a55 pe2.5.0
c153aef25e5c9911f2197d13899591c50cf38ffc se3.4.1
71b7e18613f3790cea18cb0dd8c9c986ce237267 me6.3.1
c3d9f91dc9c9d60f370c72bc211f09be3e4fc18d se3.5.0
254bce83ad6e56c102d69fd603f6845e2324b470 me6.4.0
e772f1de86744999ffbbe5845554417965b1dfba me6.4.1
c8a9a4d355927e509f514308c82306192bc71f92 pe2.6.0
a618e954f01e4bbdbe9a03e5667a67d62be995a7 me6.4.2
0f18c4498a6c7529bf77207db70aed8a5ec96ee4 se3.6.0
8f478379ec62fd1329d527aafb1ab0f2410f3a79 me6.5.0
d773721e6c3260f8130f40b4ab10442edc9965ec pe2.7.0
6b42e0d5628b937aee8039ee34d4b329149718a5 se3.6.0-arch
df6e045b9e7679f2a1949a57060e5c1863904444 me6.5.0-arch
286ba6959cd0af059f245371a3afb52c1da91dee pe2.7.0-arch
810ab1e1324ed32dbd3b4db425e590dc0e344358 se3.6.1

View File

@@ -1,5 +1,5 @@
[main] [main]
host = https://www.transifex.net host = https://www.transifex.com
[dupeguru.core] [dupeguru.core]
file_filter = locale/<lang>/LC_MESSAGES/core.po file_filter = locale/<lang>/LC_MESSAGES/core.po

626
LICENSE
View File

@@ -1,10 +1,622 @@
Copyright 2013 Hardcoded Software Inc. (http://www.hardcoded.net) GNU GENERAL PUBLIC LICENSE
All rights reserved. Version 3, 29 June 2007
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Preamble
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
# dupeGuru
[dupeGuru][dupeguru] is a cross-platform (Linux and OS X) 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, it's written in Python and uses Qt5.
## Current status: People wanted
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
corrected.
The goal is to eventually have another active maintainer, but before we can get there, the project
needs more contributors. It is very much lacking on that side right now.
Whatever your skills, if you are remotely interestested in being a contributor, I'm interested in
mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue]
and let's get started.
### Slowed development
Until I manage to find contributors, I'm slowing the development pace of dupeGuru. I'm not much
interested in maintaining it alone, I personally have no use for this app (it's been a *loooong*,
time since I had dupe problems :) )
I don't want to let it die, however, so I will still do normal maintainership, that is, issue
triaging, code review, critical bugfixes, releases management.
But anything non-critical, I'm not going to implement it myself because I see every issue as a
contribution opportunity.
### Windows maintainer wanted
As [described on my website][nowindows], v3.9.x/6.8.x/2.10.x series of dupeGuru are the last ones
to support Windows unless someone steps up to maintain it. If you're a Windows developer and are
interested in taking this task, [don't hesitate to let me know][contrib-issue].
### OS X maintainer wanted
My Mac Mini is already a couple of years old and is likely to be my last Apple purchase. When it
dies, I will be unable maintain the OS X version of moneyGuru. I've already stopped paying for the
Mac Developer membership so I can't sign the apps anymore (in the "official way" I mean. The
download is still PGP signed) If you're a Mac developer and are interested in taking this task,
[don't hesitate to let me know][contrib-issue].
## 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.
* cocoa: UI code for the Cocoa toolkit. It's Objective-C code.
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
* images: Images used by the different UI codebases.
* pkg: Skeleton files required to create different packages
* help: Help document, written for Sphinx.
* locale: .po files for localisation.
There are also other sub-folder that comes from external repositories and are part of this repo as
git submodules:
* Sparkle: An auto-update library for the OS X version.
* hscommon: A collection of helpers used across HS applications.
* cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications.
* qtlib: A collection of helpers used across Qt UI codebases of HS applications.
## How to build dupeGuru from source
There's a bootstrap script that will make building very easy. There might be some things that you
need to install manually on your system, but the bootstrap script will tell you when what you need
to install. You can run the bootstrap with:
./bootstrap.sh
and follow instructions from the script.
### Prerequisites installation
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
to be installed manually.
* All systems: [Python 3.4+][python]
* Mac OS X: OS X 10.10+ with XCode command line tools.
* Linux: PyQt5
On Ubuntu (14.04+), the apt-get command to install all pre-requisites is:
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools python3-venv
### OS X and pyenv
[pyenv][pyenv] is a popular way to manage multiple python versions. However, be aware that dupeGuru
will not compile with a pyenv's python unless it's been built with `--enable-framework`. You can do
this with:
$ env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.4.3
### Setting up the virtual environment
*This is done automatically by the bootstrap script. This is a reference in case you need to do it
manually.*
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
Python-related dependencies. In that environment, we then install our requirements with pip.
For Linux (`--system-site-packages` is to be able to import PyQt):
$ pyvenv --system-site-packages env
$ source env/bin/activate
$ pip install -r requirements.txt
For OS X:
$ pyvenv env
$ source env/bin/activate
$ pip install -r requirements-osx.txt
### Actual building and running
With your virtualenv activated, you can build and run dupeGuru with these commands:
$ python build.py
$ python run.py
You can also package dupeGuru into an installable package with:
$ python package.py
### Generate Ubuntu packages
$ bash -c "pyvenv --system-site-packages env && source env/bin/activate && pip install -r requirements.txt && python3 build.py --clean && python3 package.py"
### Running tests
The complete test suite is ran with [Tox 1.7+][tox]. If you have it installed system-wide, you
don't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`.
If you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then
run `tox`.
You can also run automated tests without Tox. Extra requirements for running tests are in
`requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your
virtualenv and then `py.test core hscommon`
[dupeguru]: http://www.hardcoded.net/dupeguru/
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
[nowindows]: https://www.hardcoded.net/archive2015#2015-11-01
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
[python]: http://www.python.org/
[pyqt]: http://www.riverbankcomputing.com
[pyenv]: https://github.com/yyuu/pyenv
[tox]: https://tox.readthedocs.org/en/latest/

View File

@@ -1,117 +0,0 @@
Contents
========
This package contains the source for dupeGuru. To learn how to build it, refer to the
"Build dupeGuru" section. Below is the description of the various subfolders:
- core: Contains the core logic code for dupeGuru. It's Python code.
- core_*: Edition-specific-cross-toolkit code written in Python.
- cocoa: UI code for the Cocoa toolkit. It's Objective-C code.
- qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
- images: Images used by the different UI codebases.
- debian: Skeleton files required to create a .deb package
- help: Help document, written for Sphinx.
There are also other sub-folder that comes from external repositories (automatically checked out
as mercurial subrepos):
- hscommon: A collection of helpers used across HS applications.
- cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications.
- qtlib: A collection of helpers used across Qt UI codebases of HS applications.
dupeGuru Dependencies
=====================
Before being able to build dupeGuru, a few dependencies have to be installed. If you use pip, you
will not have to install them all manually (see "The easy way!" below):
General dependencies
--------------------
- Python 3.2 (http://www.python.org)
- Send2Trash3k (http://hg.hardcoded.net/send2trash)
- hsaudiotag3k 1.1.0 (for ME) (http://hg.hardcoded.net/hsaudiotag)
- jobprogress 1.0.3 (http://hg.hardcoded.net/jobprogress)
- Sphinx 1.1 (http://sphinx.pocoo.org/)
- polib 0.7.0 (http://bitbucket.org/izi/polib)
- pytest 2.0.0, to run unit tests. (http://pytest.org/)
OS X prerequisites
------------------
- XCode's command line tools
- objp 1.1.0 (http://bitbucket.org/hsoft/objp)
- appscript 1.0.0 for ME and PE (http://appscript.sourceforge.net/)
- xibless 0.4.0 (https://bitbucket.org/hsoft/xibless)
Windows prerequisites
---------------------
- Visual Studio 2008 (Express is enough) is needed to build C extensions. (http://www.microsoft.com/Express/)
- PyQt 4.7+ (http://www.riverbankcomputing.co.uk/news)
- cx_Freeze, if you want to build a exe. You don't need it if you just want to run dupeGuru. (http://cx-freeze.sourceforge.net/)
- Advanced Installer, if you want to build the installer file. (http://www.advancedinstaller.com/)
Linux prerequisites
-------------------
- PyQt 4.7+ (http://www.riverbankcomputing.co.uk/news)
The easy way!
-------------
There's an easy way to install the majority of the prerequisites above, and it's `pip <http://www.pip-installer.org/>`_ which has recently started to support Python 3. So install it and then run::
pip install -r requirements-[osx|win].txt
([osx|win] depends, of course, on your platform. On other platforms, just use requirements.txt).
Advanced Installer, having nothing to do with Python, needs to be installed manually.
PyQt isn't in the requirements file either (there's no package uploaded on PyPI) and you also have
to install it manually.
If you use a virtualenv (which you should), you have to enable the "site-packages" option because
dupeGuru will need the PyQt library which you'll have installed on your system.
Prerequisite gotchas
--------------------
Correctly installing the prerequisites is tricky. Make sure you have at least the version number
required for each prerequisite.
If you didn't use mercurial to download this source, you probably have an incomplete source folder!
External projects (hscommon, qtlib, cocoalib) need to be at the root of the dupeGuru project folder.
You'll have to download those separately. Or use mercurial, it's much easier.
Another one on OS X: I wouldn't use macports/fink/whatever. Whenever I tried using those, I always
ended up with problems.
Whenever you have a problem, always double-check that you're running the correct python version.
You'll probably have to tweak your $PATH.
To setup a build machine under Ubuntu 12.04 and up, install those packages: python3-dev, python3-pyqt4,
pyqt4-dev-tools, mercurial and then python3-setuptools. Once you've done that, install pip with
`easy_install`. Once you've done that, you can then perform "The easy way!" installation.
Building dupeGuru
=================
First, make sure you meet the dependencies listed in the section above. Then you need to configure
your build with::
python configure.py
If you want, you can specify a UI to use with the ``--ui`` option. So, if you want to build dupeGuru
with Qt on OS X, then you have to type ``python configure.py --ui=qt``. You can also use the
``--dev`` flag to indicate a dev build (mostly useful in OS X, where the python code in symlinked
so you don't have to repackage whenever you make a change in the python code).
Then, just build the thing and then run it with::
python build.py
python run.py
If you want to create ready-to-upload package, run::
python package.py

41
bootstrap.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
PYTHON=python3
ret=`$PYTHON -c "import sys; print(int(sys.version_info[:2] >= (3, 4)))"`
if [ $ret -ne 1 ]; then
echo "Python 3.4+ required. Aborting."
exit 1
fi
if [ -d ".git" ]; then
git submodule init
git submodule update
fi
if [ ! -d "env" ]; then
echo "No virtualenv. Creating one"
# We need a "system-site-packages" env to have PyQt, but we also need to ensure a local pip
# install. To achieve our latter goal, we start with a normal venv, which we later upgrade to
# a system-site-packages once pip is installed.
if ! $PYTHON -m venv env ; then
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
exit 1
fi
if [ "$(uname)" != "Darwin" ]; then
$PYTHON -m venv env --upgrade --system-site-packages
fi
fi
source env/bin/activate
echo "Installing pip requirements"
if [ "$(uname)" == "Darwin" ]; then
./env/bin/pip install -r requirements-osx.txt
else
./env/bin/python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
./env/bin/pip install -r requirements.txt
fi
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
echo ". env/bin/activate && python build.py && python run.py"

343
build.py
View File

@@ -1,65 +1,84 @@
# Created By: Virgil Dupras # Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
# Created On: 2009-12-30 #
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# # which should be included with this package. The terms are also available at
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # http://www.gnu.org/licenses/gpl-3.0.html
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys import sys
import os import os
import os.path as op import os.path as op
from optparse import OptionParser from optparse import OptionParser
import shutil import shutil
import json
import importlib
import compileall import compileall
from setuptools import setup, Extension from setuptools import setup, Extension
from hscommon import sphinxgen from hscommon import sphinxgen
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, filereplace, from hscommon.build import (
get_module_version, move_all, copy_sysconfig_files_for_embed, copy_all, OSXAppStructure, add_to_pythonpath, print_and_do, copy_packages, filereplace,
get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib, build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies, copy) collect_stdlib_dependencies
)
from hscommon import loc from hscommon import loc
from hscommon.plat import ISOSX, ISLINUX from hscommon.plat import ISOSX
from hscommon.util import ensure_folder, delete_files_with_pattern from hscommon.util import ensure_folder, delete_files_with_pattern
def parse_args(): def parse_args():
usage = "usage: %prog [options]" usage = "usage: %prog [options]"
parser = OptionParser(usage=usage) parser = OptionParser(usage=usage)
parser.add_option('--clean', action='store_true', dest='clean', parser.add_option(
help="Clean build folder before building") '--clean', action='store_true', dest='clean',
parser.add_option('--doc', action='store_true', dest='doc', help="Clean build folder before building"
help="Build only the help file") )
parser.add_option('--loc', action='store_true', dest='loc', parser.add_option(
help="Build only localization") '--doc', action='store_true', dest='doc',
parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext', help="Build only the help file"
help="Build only Cocoa extensions") )
parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile', parser.add_option(
help="Build only Cocoa executable") '--ui', dest='ui',
parser.add_option('--xibless', action='store_true', dest='xibless', help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system."
help="Build only xibless UIs") )
parser.add_option('--updatepot', action='store_true', dest='updatepot', parser.add_option(
help="Generate .pot files from source code.") '--dev', action='store_true', dest='dev', default=False,
parser.add_option('--mergepot', action='store_true', dest='mergepot', help="If this flag is set, will configure for dev builds."
help="Update all .po files based on .pot files.") )
parser.add_option(
'--loc', action='store_true', dest='loc',
help="Build only localization"
)
parser.add_option(
'--cocoa-ext', action='store_true', dest='cocoa_ext',
help="Build only Cocoa extensions"
)
parser.add_option(
'--cocoa-compile', action='store_true', dest='cocoa_compile',
help="Build only Cocoa executable"
)
parser.add_option(
'--xibless', action='store_true', dest='xibless',
help="Build only xibless UIs"
)
parser.add_option(
'--updatepot', action='store_true', dest='updatepot',
help="Generate .pot files from source code."
)
parser.add_option(
'--mergepot', action='store_true', dest='mergepot',
help="Update all .po files based on .pot files."
)
parser.add_option(
'--normpo', action='store_true', dest='normpo',
help="Normalize all PO files (do this before commit)."
)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
return options return options
def cocoa_compile_command(edition): def cocoa_app():
return '{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition) app_path = 'build/dupeGuru.app'
def cocoa_app(edition):
app_path = {
'se': 'build/dupeGuru.app',
'me': 'build/dupeGuru ME.app',
'pe': 'build/dupeGuru PE.app',
}[edition]
return OSXAppStructure(app_path) return OSXAppStructure(app_path)
def build_xibless(edition, dest='cocoa/autogen'): def build_xibless(dest='cocoa/autogen'):
import xibless import xibless
ensure_folder(dest) ensure_folder(dest)
FNPAIRS = [ FNPAIRS = [
@@ -70,58 +89,61 @@ def build_xibless(edition, dest='cocoa/autogen'):
('prioritize_dialog.py', 'PrioritizeDialog_UI'), ('prioritize_dialog.py', 'PrioritizeDialog_UI'),
('result_window.py', 'ResultWindow_UI'), ('result_window.py', 'ResultWindow_UI'),
('main_menu.py', 'MainMenu_UI'), ('main_menu.py', 'MainMenu_UI'),
('preferences_panel.py', 'PreferencesPanel_UI'), ('details_panel.py', 'DetailsPanel_UI'),
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
] ]
for srcname, dstname in FNPAIRS: for srcname, dstname in FNPAIRS:
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname), xibless.generate(
localizationTable='Localizable', args={'edition': edition}) op.join('cocoa', 'ui', srcname), op.join(dest, dstname),
if edition == 'pe': localizationTable='Localizable'
xibless.generate('cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable') )
else: for appmode in ('standard', 'music', 'picture'):
xibless.generate('cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable') xibless.generate(
op.join('cocoa', 'ui', 'preferences_panel.py'),
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()),
localizationTable='Localizable',
args={'appmode': appmode},
)
def build_cocoa(edition, dev): def build_cocoa(dev):
sparkle_framework_path = op.join('cocoa', 'Sparkle', 'build', 'Release', 'Sparkle.framework')
if not op.exists(sparkle_framework_path):
print("Building Sparkle")
os.chdir(op.join('cocoa', 'Sparkle'))
print_and_do('make build')
os.chdir(op.join('..', '..'))
print("Creating OS X app structure") print("Creating OS X app structure")
ed = lambda s: s.format(edition) app = cocoa_app()
app = cocoa_app(edition) app_version = get_module_version('core')
app_version = get_module_version(ed('core_{}')) cocoa_project_path = 'cocoa'
cocoa_project_path = ed('cocoa/{}')
filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version) filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version)
app.create(op.join('build', 'Info.plist')) app.create(op.join('build', 'Info.plist'))
print("Building localizations") print("Building localizations")
build_localizations('cocoa', edition) build_localizations('cocoa')
print("Building xibless UIs") print("Building xibless UIs")
build_cocoalib_xibless() build_cocoalib_xibless()
build_xibless(edition) build_xibless()
print("Building Python extensions") print("Building Python extensions")
build_cocoa_proxy_module() build_cocoa_proxy_module()
build_cocoa_bridging_interfaces(edition) build_cocoa_bridging_interfaces()
print("Building the cocoa layer") print("Building the cocoa layer")
copy_embeddable_python_dylib('build') copy_embeddable_python_dylib('build')
pydep_folder = op.join(app.resources, 'py') pydep_folder = op.join(app.resources, 'py')
if not op.exists(pydep_folder): if not op.exists(pydep_folder):
os.mkdir(pydep_folder) os.mkdir(pydep_folder)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build') shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
appscript_pkgs = ['appscript', 'aem', 'mactypes'] tocopy = [
specific_packages = { 'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
'se': ['core_se'], ]
'me': ['core_me'] + appscript_pkgs,
'pe': ['core_pe'] + appscript_pkgs,
}[edition]
tocopy = ['core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'jobprogress', 'objp',
'send2trash'] + specific_packages
copy_packages(tocopy, pydep_folder, create_links=dev) copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build') sys.path.insert(0, 'build')
extra_deps = None # ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
if edition == 'pe': # to manually specify it.
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have extra_deps = ['multiprocessing']
# to manually specify it.
extra_deps=['multiprocessing']
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps) collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
del sys.path[0] del sys.path[0]
# Views are not referenced by python code, so they're not found by the collector. # 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_all('build/inter/*.so', op.join(pydep_folder, 'inter'))
copy_sysconfig_files_for_embed(pydep_folder)
if not dev: if not dev:
# Important: Don't ever run delete_files_with_pattern('*.py') on dev builds because you'll # Important: Don't ever run delete_files_with_pattern('*.py') on dev builds because you'll
# be deleting all py files in symlinked folders. # be deleting all py files in symlinked folders.
@@ -130,56 +152,51 @@ def build_cocoa(edition, dev):
delete_files_with_pattern(pydep_folder, '__pycache__') delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF") print("Compiling with WAF")
os.chdir('cocoa') os.chdir('cocoa')
print_and_do(cocoa_compile_command(edition)) print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..') os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru') app.copy_executable('cocoa/build/dupeGuru')
build_help()
print("Copying resources and frameworks") print("Copying resources and frameworks")
image_path = ed('cocoa/{}/dupeguru.icns') image_path = 'cocoa/dupeguru.icns'
resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help'] resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev) app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python', 'cocoalib/Sparkle.framework') app.copy_frameworks('build/Python', sparkle_framework_path)
print("Creating the run.py file") print("Creating the run.py file")
tmpl = open('cocoa/run_template.py', 'rt').read() tmpl = open('cocoa/run_template.py', 'rt').read()
run_contents = tmpl.replace('{{app_path}}', app.dest) run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents) open('run.py', 'wt').write(run_contents)
def build_qt(edition, dev, conf): def build_qt(dev):
print("Building localizations") print("Building localizations")
build_localizations('qt', edition) build_localizations('qt')
print("Building Qt stuff") 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'))) print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'dg.qrc'), op.join('qt', 'dg_rc.py')))
fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py')) fix_qt_resource_file(op.join('qt', 'dg_rc.py'))
build_help()
print("Creating the run.py file") print("Creating the run.py file")
filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition) shutil.copy(op.join('qt', 'run_template.py'), 'run.py')
def build_help(edition): def build_help():
print("Generating Help") print("Generating Help")
current_path = op.abspath('.') current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en') help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help'.format(edition)) help_destpath = op.join(current_path, 'build', 'help')
changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition)) changelog_path = op.join(current_path, 'help', 'changelog')
tixurl = "https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}" tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
appname = {'se': 'dupeGuru', 'me': 'dupeGuru Music Edition', 'pe': 'dupeGuru Picture Edition'}[edition] confrepl = {'language': 'en'}
homepage = 'http://www.hardcoded.net/dupeguru{}/'.format('_' + edition if edition != 'se' else '')
confrepl = {'edition': edition, 'appname': appname, 'homepage': homepage, 'language': 'en'}
changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl') changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl')
conftmpl = op.join(current_path, 'help', 'conf.tmpl') conftmpl = op.join(current_path, 'help', 'conf.tmpl')
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl) 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(): def build_qt_localizations():
loc.compile_all_po(op.join('qtlib', 'locale')) loc.compile_all_po(op.join('qtlib', 'locale'))
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale') loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
def build_localizations(ui, edition): def build_localizations(ui):
build_base_localizations() loc.compile_all_po('locale')
if ui == 'cocoa': if ui == 'cocoa':
app = cocoa_app(edition) app = cocoa_app()
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings')) loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale') locale_dest = op.join(app.resources, 'locale')
elif ui == 'qt': elif ui == 'qt':
build_qt_localizations() build_qt_localizations()
@@ -187,39 +204,24 @@ def build_localizations(ui, edition):
if op.exists(locale_dest): if op.exists(locale_dest):
shutil.rmtree(locale_dest) shutil.rmtree(locale_dest)
shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot')) shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot'))
if ui == 'qt' and not ISLINUX:
print("Copying qt_*.qm files into the 'locale' folder")
from PyQt4.QtCore import QLibraryInfo
trfolder = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
for lang in loc.get_langs('locale'):
qmname = 'qt_%s.qm' % lang
src = op.join(trfolder, qmname)
if op.exists(src):
copy(src, op.join('build', 'locale', qmname))
def build_updatepot(): def build_updatepot():
if ISOSX: if ISOSX:
print("Updating Cocoa strings file.") print("Updating Cocoa strings file.")
# We need to have strings from *all* editions in here, so we'll call xibless for all editions
# in dummy subfolders.
build_cocoalib_xibless('cocoalib/autogen') build_cocoalib_xibless('cocoalib/autogen')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj') loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
for edition in ('se', 'me', 'pe'): build_xibless('se', op.join('cocoa', 'autogen', 'se'))
build_xibless(edition, op.join('cocoa', 'autogen', edition)) loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj')
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/base/en.lproj')
print("Building .pot files from source files") print("Building .pot files from source files")
print("Building core.pot") print("Building core.pot")
all_cores = ['core', 'core_se', 'core_me', 'core_pe'] loc.generate_pot(['core'], op.join('locale', 'core.pot'), ['tr'])
loc.generate_pot(all_cores, op.join('locale', 'core.pot'), ['tr'])
print("Building columns.pot") print("Building columns.pot")
loc.generate_pot(all_cores, op.join('locale', 'columns.pot'), ['coltr']) loc.generate_pot(['core'], op.join('locale', 'columns.pot'), ['coltr'])
print("Building ui.pot") print("Building ui.pot")
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs # When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
# We want to merge the generated pot with the old pot in the most preserving way possible. # We want to merge the generated pot with the old pot in the most preserving way possible.
ui_packages = ['qt', op.join('cocoa', 'inter')] ui_packages = ['qt', op.join('cocoa', 'inter')]
loc.generate_pot(ui_packages, op.join('locale', 'ui.pot'), ['tr'], merge=(not ISOSX)) 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") print("Building qtlib.pot")
loc.generate_pot(['qtlib'], op.join('qtlib', 'locale', 'qtlib.pot'), ['tr']) loc.generate_pot(['qtlib'], op.join('qtlib', 'locale', 'qtlib.pot'), ['tr'])
if ISOSX: if ISOSX:
@@ -228,34 +230,47 @@ def build_updatepot():
os.remove(cocoalib_pot) os.remove(cocoalib_pot)
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot) loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
print("Enhancing ui.pot with Cocoa's strings files") print("Enhancing ui.pot with Cocoa's strings files")
loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'), loc.strings2pot(
op.join('locale', 'ui.pot')) op.join('cocoa', 'en.lproj', 'Localizable.strings'),
op.join('locale', 'ui.pot')
)
def build_mergepot(): def build_mergepot():
print("Updating .po files using .pot files") print("Updating .po files using .pot files")
loc.merge_pots_into_pos('locale') 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('qtlib', 'locale'))
loc.merge_pots_into_pos(op.join('cocoalib', 'locale')) loc.merge_pots_into_pos(op.join('cocoalib', 'locale'))
def build_normpo():
loc.normalize_all_pos('locale')
loc.normalize_all_pos(op.join('qtlib', 'locale'))
loc.normalize_all_pos(op.join('cocoalib', 'locale'))
def build_cocoa_proxy_module(): def build_cocoa_proxy_module():
print("Building Cocoa Proxy") print("Building Cocoa Proxy")
import objp.p2o import objp.p2o
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m') objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa', build_cocoa_ext(
['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m', "CocoaProxy", 'cocoalib/cocoa',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'], [
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
],
['AppKit', 'CoreServices'], ['AppKit', 'CoreServices'],
['cocoalib', 'cocoa/autogen']) ['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces(edition): def build_cocoa_bridging_interfaces():
print("Building Cocoa Bridging Interfaces") print("Building Cocoa Bridging Interfaces")
import objp.o2p import objp.o2p
import objp.p2o import objp.p2o
add_to_pythonpath('cocoa') add_to_pythonpath('cocoa')
add_to_pythonpath('cocoalib') add_to_pythonpath('cocoalib')
from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline, from cocoa.inter import (
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp, PyFairware) PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
PyTextField, ProgressWindowView, PyProgressWindow
)
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
from inter.details_panel import PyDetailsPanel, DetailsPanelView from inter.details_panel import PyDetailsPanel, DetailsPanelView
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
@@ -265,17 +280,21 @@ def build_cocoa_bridging_interfaces(edition):
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
from inter.result_table import PyResultTable, ResultTableView from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuruBase, DupeGuruView from inter.app import PyDupeGuru, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition)) allclasses = [
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyFairware, PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog, PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase, PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru,
appmod.PyDupeGuru] PyTextField, PyProgressWindow
]
for class_ in allclasses: for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True) objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
allclasses = [GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView, allclasses = [
GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView, DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView, DupeGuruView] IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
ProgressWindowView, DupeGuruView
]
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses] clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m') objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m']) build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
@@ -283,73 +302,81 @@ def build_cocoa_bridging_interfaces(edition):
def build_pe_modules(ui): def build_pe_modules(ui):
print("Building PE Modules") print("Building PE Modules")
exts = [ exts = [
Extension("_block", [op.join('core_pe', 'modules', 'block.c'), op.join('core_pe', 'modules', 'common.c')]), Extension(
Extension("_cache", [op.join('core_pe', 'modules', 'cache.c'), op.join('core_pe', 'modules', 'common.c')]), "_block",
[op.join('core', 'pe', 'modules', 'block.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
Extension(
"_cache",
[op.join('core', 'pe', 'modules', 'cache.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
] ]
if ui == 'qt': if ui == 'qt':
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')])) exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
elif ui == 'cocoa': elif ui == 'cocoa':
exts.append(Extension( exts.append(Extension(
"_block_osx", [op.join('core_pe', 'modules', 'block_osx.m'), op.join('core_pe', 'modules', 'common.c')], "_block_osx",
[op.join('core', 'pe', 'modules', 'block_osx.m'), op.join('core', 'pe', 'modules', 'common.c')],
extra_link_args=[ extra_link_args=[
"-framework", "CoreFoundation", "-framework", "CoreFoundation",
"-framework", "Foundation", "-framework", "Foundation",
"-framework", "ApplicationServices",] "-framework", "ApplicationServices",
]
)) ))
setup( setup(
script_args = ['build_ext', '--inplace'], script_args=['build_ext', '--inplace'],
ext_modules = exts, ext_modules=exts,
) )
move_all('_block_qt*', op.join('qt', 'pe')) move_all('_block_qt*', op.join('qt', 'pe'))
move_all('_block*', 'core_pe') move_all('_block*', op.join('core', 'pe'))
move_all('_cache*', 'core_pe') move_all('_cache*', op.join('core', 'pe'))
def build_normal(edition, ui, dev, conf): def build_normal(ui, dev):
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui)) print("Building dupeGuru with UI {}".format(ui))
add_to_pythonpath('.') add_to_pythonpath('.')
build_help(edition)
print("Building dupeGuru") print("Building dupeGuru")
if edition == 'pe': build_pe_modules(ui)
build_pe_modules(ui)
if ui == 'cocoa': if ui == 'cocoa':
build_cocoa(edition, dev) build_cocoa(dev)
elif ui == 'qt': elif ui == 'qt':
build_qt(edition, dev, conf) build_qt(dev)
def main(): def main():
options = parse_args() options = parse_args()
conf = json.load(open('conf.json')) ui = options.ui
edition = conf['edition'] if ui not in ('cocoa', 'qt'):
ui = conf['ui'] ui = 'cocoa' if ISOSX else 'qt'
dev = conf['dev'] if options.dev:
if dev:
print("Building in Dev mode") print("Building in Dev mode")
if options.clean: if options.clean:
if op.exists('build'): for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
shutil.rmtree('build') if op.exists(path):
shutil.rmtree(path)
if not op.exists('build'): if not op.exists('build'):
os.mkdir('build') os.mkdir('build')
if options.doc: if options.doc:
build_help(edition) build_help()
elif options.loc: elif options.loc:
build_localizations(ui, edition) build_localizations(ui)
elif options.updatepot: elif options.updatepot:
build_updatepot() build_updatepot()
elif options.mergepot: elif options.mergepot:
build_mergepot() build_mergepot()
elif options.normpo:
build_normpo()
elif options.cocoa_ext: elif options.cocoa_ext:
build_cocoa_proxy_module() build_cocoa_proxy_module()
build_cocoa_bridging_interfaces(edition) build_cocoa_bridging_interfaces()
elif options.cocoa_compile: elif options.cocoa_compile:
os.chdir('cocoa') os.chdir('cocoa')
print_and_do(cocoa_compile_command(edition)) print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..') os.chdir('..')
cocoa_app(edition).copy_executable('cocoa/build/dupeGuru') cocoa_app().copy_executable('cocoa/build/dupeGuru')
elif options.xibless: elif options.xibless:
build_cocoalib_xibless() build_cocoalib_xibless()
build_xibless(edition) build_xibless()
else: else:
build_normal(edition, ui, dev, conf) build_normal(ui, options.dev)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -1,34 +1,41 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <Sparkle/SUUpdater.h> #import <Sparkle/SUUpdater.h>
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
#import "ResultWindow.h" #import "ResultWindow.h"
#import "ResultTable.h"
#import "DetailsPanel.h" #import "DetailsPanel.h"
#import "DirectoryPanel.h" #import "DirectoryPanel.h"
#import "IgnoreListDialog.h" #import "IgnoreListDialog.h"
#import "HSFairwareAboutBox.h" #import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSAboutBox.h"
#import "HSRecentFiles.h" #import "HSRecentFiles.h"
#import "HSProgressWindow.h"
@interface AppDelegateBase : NSObject @interface AppDelegate : NSObject <NSFileManagerDelegate>
{ {
NSMenu *recentResultsMenu; NSMenu *recentResultsMenu;
NSMenu *columnsMenu; NSMenu *columnsMenu;
SUUpdater *updater; SUUpdater *updater;
PyDupeGuru *model; PyDupeGuru *model;
ResultWindowBase *_resultWindow; ResultWindow *_resultWindow;
DirectoryPanel *_directoryPanel; DirectoryPanel *_directoryPanel;
DetailsPanel *_detailsPanel; DetailsPanel *_detailsPanel;
IgnoreListDialog *_ignoreListDialog; IgnoreListDialog *_ignoreListDialog;
ProblemDialog *_problemDialog;
DeletionOptions *_deletionOptions;
HSProgressWindow *_progressWindow;
NSWindowController *_preferencesPanel; NSWindowController *_preferencesPanel;
HSFairwareAboutBox *_aboutBox; HSAboutBox *_aboutBox;
HSRecentFiles *_recentResults; HSRecentFiles *_recentResults;
} }
@@ -39,17 +46,17 @@ http://www.hardcoded.net/licenses/bsd_license
/* Virtual */ /* Virtual */
+ (NSDictionary *)defaultPreferences; + (NSDictionary *)defaultPreferences;
- (PyDupeGuru *)model; - (PyDupeGuru *)model;
- (ResultWindowBase *)createResultWindow;
- (DirectoryPanel *)createDirectoryPanel;
- (DetailsPanel *)createDetailsPanel; - (DetailsPanel *)createDetailsPanel;
- (NSString *)homepageURL; - (void)setScanOptions;
/* Public */ /* Public */
- (void)finalizeInit; - (void)finalizeInit;
- (ResultWindowBase *)resultWindow; - (ResultWindow *)resultWindow;
- (DirectoryPanel *)directoryPanel; - (DirectoryPanel *)directoryPanel;
- (DetailsPanel *)detailsPanel; - (DetailsPanel *)detailsPanel;
- (HSRecentFiles *)recentResults; - (HSRecentFiles *)recentResults;
- (NSInteger)getAppMode;
- (void)setAppMode:(NSInteger)appMode;
/* Delegate */ /* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
@@ -59,6 +66,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)recentFileClicked:(NSString *)path; - (void)recentFileClicked:(NSString *)path;
/* Actions */ /* Actions */
- (void)clearPictureCache;
- (void)loadResults; - (void)loadResults;
- (void)openWebsite; - (void)openWebsite;
- (void)openHelp; - (void)openHelp;
@@ -71,6 +79,4 @@ http://www.hardcoded.net/licenses/bsd_license
/* model --> view */ /* model --> view */
- (void)showMessage:(NSString *)msg; - (void)showMessage:(NSString *)msg;
- (void)setupAsRegistered;
- (void)showDemoNagWithPrompt:(NSString *)prompt;
@end @end

View File

@@ -1,22 +1,24 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "AppDelegateBase.h" #import "AppDelegate.h"
#import "ProgressController.h" #import "ProgressController.h"
#import "HSFairwareReminder.h"
#import "HSPyUtil.h" #import "HSPyUtil.h"
#import "Consts.h" #import "Consts.h"
#import "Dialogs.h" #import "Dialogs.h"
#import "Utils.h" #import "Utils.h"
#import "ValueTransformers.h" #import "ValueTransformers.h"
#import "PreferencesPanel_UI.h" #import "DetailsPanelPicture.h"
#import "PreferencesPanelStandard_UI.h"
#import "PreferencesPanelMusic_UI.h"
#import "PreferencesPanelPicture_UI.h"
@implementation AppDelegateBase @implementation AppDelegate
@synthesize recentResultsMenu; @synthesize recentResultsMenu;
@synthesize columnsMenu; @synthesize columnsMenu;
@@ -25,6 +27,21 @@ http://www.hardcoded.net/licenses/bsd_license
+ (NSDictionary *)defaultPreferences + (NSDictionary *)defaultPreferences
{ {
NSMutableDictionary *d = [NSMutableDictionary dictionary]; NSMutableDictionary *d = [NSMutableDictionary dictionary];
[d setObject:i2n(1) forKey:@"scanTypeStandard"];
[d setObject:i2n(3) forKey:@"scanTypeMusic"];
[d setObject:i2n(0) forKey:@"scanTypePicture"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
[d setObject:b2n(YES) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
[d setObject:b2n(NO) forKey:@"scanTagYear"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
[d setObject:i2n(1) forKey:@"recreatePathType"]; [d setObject:i2n(1) forKey:@"recreatePathType"];
[d setObject:i2n(11) forKey:TableFontSize]; [d setObject:i2n(11) forKey:TableFontSize];
[d setObject:b2n(YES) forKey:@"mixFileKind"]; [d setObject:b2n(YES) forKey:@"mixFileKind"];
@@ -54,6 +71,19 @@ http://www.hardcoded.net/licenses/bsd_license
model = [[PyDupeGuru alloc] init]; model = [[PyDupeGuru alloc] init];
[model bindCallback:createCallback(@"DupeGuruView", self)]; [model bindCallback:createCallback(@"DupeGuruView", self)];
[self setUpdater:[SUUpdater sharedUpdater]]; [self setUpdater:[SUUpdater sharedUpdater]];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
i = [NSMutableIndexSet indexSetWithIndex:4];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
return self; return self;
} }
@@ -70,12 +100,17 @@ http://www.hardcoded.net/licenses/bsd_license
} }
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu]; _recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self]; [_recentResults setDelegate:self];
_resultWindow = [self createResultWindow]; _directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
_directoryPanel = [self createDirectoryPanel];
_detailsPanel = [self createDetailsPanel];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]]; _ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
_aboutBox = nil; // Lazily loaded _problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
_preferencesPanel = nil; // Lazily loaded _deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_directoryPanel window]];
// Lazily loaded
_aboutBox = nil;
_preferencesPanel = nil;
_resultWindow = nil;
_detailsPanel = nil;
[[[self directoryPanel] window] makeKeyAndOrderFront:self]; [[[self directoryPanel] window] makeKeyAndOrderFront:self];
} }
@@ -86,28 +121,51 @@ http://www.hardcoded.net/licenses/bsd_license
return model; return model;
} }
- (ResultWindowBase *)createResultWindow
{
return nil; // must be overriden by all editions
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanel alloc] initWithParentApp:self];
}
- (DetailsPanel *)createDetailsPanel - (DetailsPanel *)createDetailsPanel
{ {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]]; NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
return [[DetailsPanelPicture alloc] initWithApp:model];
}
else {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
} }
- (NSString *)homepageURL - (void)setScanOptions
{ {
return @""; // must be overriden by all editions NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *scanTypeOptionName;
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
scanTypeOptionName = @"scanTypePicture";
}
else if (appMode == AppModeMusic) {
scanTypeOptionName = @"scanTypeMusic";
}
else {
scanTypeOptionName = @"scanTypeStandard";
}
[model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[model setSizeThreshold:sizeThreshold];
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
} }
/* Public */ /* Public */
- (ResultWindowBase *)resultWindow - (ResultWindow *)resultWindow
{ {
return _resultWindow; return _resultWindow;
} }
@@ -127,7 +185,29 @@ http://www.hardcoded.net/licenses/bsd_license
return _recentResults; return _recentResults;
} }
- (NSInteger)getAppMode
{
return [model getAppMode];
}
- (void)setAppMode:(NSInteger)appMode
{
[model setAppMode:appMode];
if (_preferencesPanel != nil) {
[_preferencesPanel release];
_preferencesPanel = nil;
}
}
/* Actions */ /* Actions */
- (void)clearPictureCache
{
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[model clearPictureCache];
}
- (void)loadResults - (void)loadResults
{ {
NSOpenPanel *op = [NSOpenPanel openPanel]; NSOpenPanel *op = [NSOpenPanel openPanel];
@@ -138,7 +218,7 @@ http://www.hardcoded.net/licenses/bsd_license
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]]; [op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[op setTitle:NSLocalizedString(@"Select a results file to load", @"")]; [op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
if ([op runModal] == NSOKButton) { if ([op runModal] == NSOKButton) {
NSString *filename = [[op filenames] objectAtIndex:0]; NSString *filename = [[[op URLs] objectAtIndex:0] path];
[model loadResultsFrom:filename]; [model loadResultsFrom:filename];
[[self recentResults] addFile:filename]; [[self recentResults] addFile:filename];
} }
@@ -146,7 +226,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)openWebsite - (void)openWebsite
{ {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self homepageURL]]]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]];
} }
- (void)openHelp - (void)openHelp
@@ -160,7 +240,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)showAboutBox - (void)showAboutBox
{ {
if (_aboutBox == nil) { if (_aboutBox == nil) {
_aboutBox = [[HSFairwareAboutBox alloc] initWithApp:model]; _aboutBox = [[HSAboutBox alloc] initWithApp:model];
} }
[[_aboutBox window] makeKeyAndOrderFront:nil]; [[_aboutBox window] makeKeyAndOrderFront:nil];
} }
@@ -173,7 +253,18 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)showPreferencesPanel - (void)showPreferencesPanel
{ {
if (_preferencesPanel == nil) { if (_preferencesPanel == nil) {
_preferencesPanel = [[NSWindowController alloc] initWithWindow:createPreferencesPanel_UI(nil)]; NSWindow *window;
NSInteger appMode = [model getAppMode];
if (appMode == AppModePicture) {
window = createPreferencesPanelPicture_UI(nil);
}
else if (appMode == AppModeMusic) {
window = createPreferencesPanelMusic_UI(nil);
}
else {
window = createPreferencesPanelStandard_UI(nil);
}
_preferencesPanel = [[NSWindowController alloc] initWithWindow:window];
} }
[_preferencesPanel showWindow:nil]; [_preferencesPanel showWindow:nil];
} }
@@ -190,15 +281,13 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)startScanning - (void)startScanning
{ {
[[self resultWindow] startDuplicateScan]; [[self directoryPanel] startDuplicateScan];
} }
/* Delegate */ /* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{ {
[[ProgressController mainProgressController] setWorker:model];
[model initialRegistrationSetup];
[model loadSession]; [model loadSession];
} }
@@ -255,19 +344,25 @@ http://www.hardcoded.net/licenses/bsd_license
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn; return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
} }
- (void)createResultsWindow
{
if (_resultWindow != nil) {
[_resultWindow release];
}
if (_detailsPanel != nil) {
[_detailsPanel release];
}
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
_detailsPanel = [self createDetailsPanel];
}
- (void)showResultsWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showProblemDialog - (void)showProblemDialog
{ {
[[self resultWindow] showProblemDialog]; [_problemDialog showWindow:self];
}
- (void)setupAsRegistered
{
// Nothing to do.
}
- (void)showDemoNagWithPrompt:(NSString *)prompt
{
[HSFairwareReminder showDemoNagWithApp:[self model] prompt:prompt];
} }
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt - (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
@@ -279,7 +374,7 @@ http://www.hardcoded.net/licenses/bsd_license
[op setAllowsMultipleSelection:NO]; [op setAllowsMultipleSelection:NO];
[op setTitle:prompt]; [op setTitle:prompt];
if ([op runModal] == NSOKButton) { if ([op runModal] == NSOKButton) {
return [[op filenames] objectAtIndex:0]; return [[[op URLs] objectAtIndex:0] path];
} }
else { else {
return nil; return nil;
@@ -293,7 +388,7 @@ http://www.hardcoded.net/licenses/bsd_license
[sp setAllowedFileTypes:[NSArray arrayWithObject:extension]]; [sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
[sp setTitle:prompt]; [sp setTitle:prompt];
if ([sp runModal] == NSOKButton) { if ([sp runModal] == NSOKButton) {
return [sp filename]; return [[sp URL] path];
} }
else { else {
return nil; return nil;

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#define JobStarted @"JobStarted" #define JobStarted @"JobStarted"
@@ -17,3 +17,8 @@ http://www.hardcoded.net/licenses/bsd_license
#define jobDelete @"job_delete" #define jobDelete @"job_delete"
#define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType" #define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType"
#define ImageLoadedNotification @"ImageLoadedNotification"
#define AppModeStandard 0
#define AppModeMusic 1
#define AppModePicture 2

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "DeletionOptions.h" #import "DeletionOptions.h"
@@ -64,4 +64,9 @@ http://www.hardcoded.net/licenses/bsd_license
[[self window] close]; [[self window] close];
return r == NSOKButton; return r == NSOKButton;
} }
- (void)setHardlinkOptionEnabled:(BOOL)enabled
{
[linkTypeRadio setEnabled:enabled];
}
@end @end

View File

@@ -1,16 +1,16 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <Python.h> #import <Python.h>
#import "PyDetailsPanel.h" #import "PyDetailsPanel.h"
@interface DetailsPanelBase : NSWindowController <NSTableViewDataSource> @interface DetailsPanel : NSWindowController <NSTableViewDataSource>
{ {
NSTableView *detailsTable; NSTableView *detailsTable;

View File

@@ -1,15 +1,16 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "DetailsPanelBase.h" #import "DetailsPanel.h"
#import "HSPyUtil.h" #import "HSPyUtil.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanelBase @implementation DetailsPanel
@synthesize detailsTable; @synthesize detailsTable;
@@ -35,7 +36,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (NSWindow *)createWindow - (NSWindow *)createWindow
{ {
return nil; // Virtual return createDetailsPanel_UI(self);
} }
- (void)refreshDetails - (void)refreshDetails

View File

@@ -1,16 +1,16 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import "DetailsPanelBase.h" #import "DetailsPanel.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@interface DetailsPanel : DetailsPanelBase @interface DetailsPanelPicture : DetailsPanel
{ {
NSImageView *dupeImage; NSImageView *dupeImage;
NSProgressIndicator *dupeProgressIndicator; NSProgressIndicator *dupeProgressIndicator;

View File

@@ -1,20 +1,20 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "Utils.h" #import "Utils.h"
#import "NSNotificationAdditions.h" #import "NSNotificationAdditions.h"
#import "NSImageAdditions.h" #import "NSImageAdditions.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
#import "DetailsPanel.h" #import "DetailsPanelPicture.h"
#import "Consts.h" #import "Consts.h"
#import "DetailsPanel_UI.h" #import "DetailsPanelPicture_UI.h"
@implementation DetailsPanel @implementation DetailsPanelPicture
@synthesize dupeImage; @synthesize dupeImage;
@synthesize dupeProgressIndicator; @synthesize dupeProgressIndicator;
@@ -32,7 +32,7 @@ http://www.hardcoded.net/licenses/bsd_license
- (NSWindow *)createWindow - (NSWindow *)createWindow
{ {
return createDetailsPanel_UI(self); return createDetailsPanelPicture_UI(self);
} }
- (void)loadImageAsync:(NSString *)imagePath - (void)loadImageAsync:(NSString *)imagePath

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
@@ -16,4 +16,6 @@ http://www.hardcoded.net/licenses/bsd_license
@interface DirectoryOutline : HSOutline {} @interface DirectoryOutline : HSOutline {}
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView; - (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView;
- (PyDirectoryOutline *)model; - (PyDirectoryOutline *)model;
- (void)selectAll;
@end; @end;

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "DirectoryOutline.h" #import "DirectoryOutline.h"
@@ -22,6 +22,12 @@ http://www.hardcoded.net/licenses/bsd_license
return (PyDirectoryOutline *)model; return (PyDirectoryOutline *)model;
} }
/* Public */
- (void)selectAll
{
[[self model] selectAll];
}
/* Delegate */ /* Delegate */
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
{ {

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
@@ -12,15 +12,17 @@ http://www.hardcoded.net/licenses/bsd_license
#import "DirectoryOutline.h" #import "DirectoryOutline.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@class AppDelegateBase; @class AppDelegate;
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate> @interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
{ {
AppDelegateBase *_app; AppDelegate *_app;
PyDupeGuru *model; PyDupeGuru *model;
HSRecentFiles *_recentDirectories; HSRecentFiles *_recentDirectories;
DirectoryOutline *outline; DirectoryOutline *outline;
BOOL _alwaysShowPopUp; BOOL _alwaysShowPopUp;
NSSegmentedControl *appModeSelector;
NSPopUpButton *scanTypePopup;
NSPopUpButton *addButtonPopUp; NSPopUpButton *addButtonPopUp;
NSPopUpButton *loadRecentButtonPopUp; NSPopUpButton *loadRecentButtonPopUp;
HSOutlineView *outlineView; HSOutlineView *outlineView;
@@ -28,22 +30,28 @@ http://www.hardcoded.net/licenses/bsd_license
NSButton *loadResultsButton; NSButton *loadResultsButton;
} }
@property (readwrite, retain) NSSegmentedControl *appModeSelector;
@property (readwrite, retain) NSPopUpButton *scanTypePopup;
@property (readwrite, retain) NSPopUpButton *addButtonPopUp; @property (readwrite, retain) NSPopUpButton *addButtonPopUp;
@property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp; @property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp;
@property (readwrite, retain) HSOutlineView *outlineView; @property (readwrite, retain) HSOutlineView *outlineView;
@property (readwrite, retain) NSButton *removeButton; @property (readwrite, retain) NSButton *removeButton;
@property (readwrite, retain) NSButton *loadResultsButton; @property (readwrite, retain) NSButton *loadResultsButton;
- (id)initWithParentApp:(AppDelegateBase *)aParentApp; - (id)initWithParentApp:(AppDelegate *)aParentApp;
- (void)fillPopUpMenu; // Virtual - (void)fillPopUpMenu;
- (void)fillScanTypeMenu;
- (void)adjustUIToLocalization; - (void)adjustUIToLocalization;
- (void)askForDirectory; - (void)askForDirectory;
- (void)popupAddDirectoryMenu:(id)sender; - (void)popupAddDirectoryMenu:(id)sender;
- (void)popupLoadRecentMenu:(id)sender; - (void)popupLoadRecentMenu:(id)sender;
- (void)removeSelectedDirectory; - (void)removeSelectedDirectory;
- (void)startDuplicateScan;
- (void)addDirectory:(NSString *)directory; - (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText; - (void)refreshRemoveButtonText;
- (void)markAll;
@end @end

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "DirectoryPanel.h" #import "DirectoryPanel.h"
@@ -11,22 +11,27 @@ http://www.hardcoded.net/licenses/bsd_license
#import "Dialogs.h" #import "Dialogs.h"
#import "Utils.h" #import "Utils.h"
#import "AppDelegate.h" #import "AppDelegate.h"
#import "Consts.h"
@implementation DirectoryPanel @implementation DirectoryPanel
@synthesize appModeSelector;
@synthesize scanTypePopup;
@synthesize addButtonPopUp; @synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp; @synthesize loadRecentButtonPopUp;
@synthesize outlineView; @synthesize outlineView;
@synthesize removeButton; @synthesize removeButton;
@synthesize loadResultsButton; @synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegateBase *)aParentApp - (id)initWithParentApp:(AppDelegate *)aParentApp
{ {
self = [super initWithWindow:nil]; self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)]; [self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp; _app = aParentApp;
model = [_app model]; model = [_app model];
[[self window] setTitle:[model appName]]; [[self window] setTitle:[model appName]];
self.appModeSelector.selectedSegment = 0;
[self fillScanTypeMenu];
_alwaysShowPopUp = NO; _alwaysShowPopUp = NO;
[self fillPopUpMenu]; [self fillPopUpMenu];
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]]; _recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
@@ -59,6 +64,25 @@ http://www.hardcoded.net/licenses/bsd_license
[m addItem:[NSMenuItem separatorItem]]; [m addItem:[NSMenuItem separatorItem]];
} }
- (void)fillScanTypeMenu
{
[[self scanTypePopup] unbind:@"selectedIndex"];
[[self scanTypePopup] removeAllItems];
[[self scanTypePopup] addItemsWithTitles:[[_app model] getScanOptions]];
NSString *keypath;
NSInteger appMode = [_app getAppMode];
if (appMode == AppModePicture) {
keypath = @"values.scanTypePicture";
}
else if (appMode == AppModeMusic) {
keypath = @"values.scanTypeMusic";
}
else {
keypath = @"values.scanTypeStandard";
}
[[self scanTypePopup] bind:@"selectedIndex" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:keypath options:nil];
}
- (void)adjustUIToLocalization - (void)adjustUIToLocalization
{ {
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0]; NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
@@ -91,12 +115,29 @@ http://www.hardcoded.net/licenses/bsd_license
[op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")]; [op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")];
[op setDelegate:self]; [op setDelegate:self];
if ([op runModal] == NSOKButton) { if ([op runModal] == NSOKButton) {
for (NSString *directory in [op filenames]) { for (NSURL *directoryURL in [op URLs]) {
[self addDirectory:directory]; [self addDirectory:[directoryURL path]];
} }
} }
} }
- (void)changeAppMode:(id)sender
{
NSInteger appMode;
NSUInteger selectedSegment = self.appModeSelector.selectedSegment;
if (selectedSegment == 2) {
appMode = AppModePicture;
}
else if (selectedSegment == 1) {
appMode = AppModeMusic;
}
else {
appMode = AppModeStandard;
}
[_app setAppMode:appMode];
[self fillScanTypeMenu];
}
- (void)popupAddDirectoryMenu:(id)sender - (void)popupAddDirectoryMenu:(id)sender
{ {
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) { if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
@@ -134,6 +175,16 @@ http://www.hardcoded.net/licenses/bsd_license
[self refreshRemoveButtonText]; [self refreshRemoveButtonText];
} }
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[_app setScanOptions];
[model doScan];
}
/* Public */ /* Public */
- (void)addDirectory:(NSString *)directory - (void)addDirectory:(NSString *)directory
{ {
@@ -158,6 +209,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 */ /* Delegate */
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path
{ {
@@ -171,6 +230,14 @@ http://www.hardcoded.net/licenses/bsd_license
[self addDirectory:path]; [self addDirectory:path];
} }
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Select All", @"")];
}
return YES;
}
/* Notifications */ /* Notifications */
- (void)directorySelectionChanged:(NSNotification *)aNotification - (void)directorySelectionChanged:(NSNotification *)aNotification

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "IgnoreListDialog.h" #import "IgnoreListDialog.h"

View File

@@ -29,9 +29,9 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string> <string>© Hardcoded Software, 2016</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string> <string>https://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string> <string>dsa_pub.pem</string>
</dict> </dict>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "PrioritizeDialog.h" #import "PrioritizeDialog.h"

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "PrioritizeList.h" #import "PrioritizeList.h"

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "ProblemDialog.h" #import "ProblemDialog.h"

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
@@ -13,7 +13,6 @@ http://www.hardcoded.net/licenses/bsd_license
@interface ResultTable : HSTable <QLPreviewPanelDataSource, QLPreviewPanelDelegate> @interface ResultTable : HSTable <QLPreviewPanelDataSource, QLPreviewPanelDelegate>
{ {
NSSet *_deltaColumns;
} }
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView; - (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView;
- (PyResultTable *)model; - (PyResultTable *)model;

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "ResultTable.h" #import "ResultTable.h"
@@ -20,16 +20,9 @@ http://www.hardcoded.net/licenses/bsd_license
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView - (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView
{ {
self = [super initWithPyRef:aPyRef wrapperClass:[PyResultTable class] callbackClassName:@"ResultTableView" view:aTableView]; self = [super initWithPyRef:aPyRef wrapperClass:[PyResultTable class] callbackClassName:@"ResultTableView" view:aTableView];
_deltaColumns = [[NSSet setWithArray:[[self model] deltaColumns]] retain];
return self; return self;
} }
- (void)dealloc
{
[_deltaColumns release];
[super dealloc];
}
- (PyResultTable *)model - (PyResultTable *)model
{ {
return (PyResultTable *)model; return (PyResultTable *)model;
@@ -132,10 +125,8 @@ http://www.hardcoded.net/licenses/bsd_license
color = [NSColor selectedTextColor]; color = [NSColor selectedTextColor];
} }
else if (isMarkable) { else if (isMarkable) {
if ([self deltaValuesMode]) { if ([[self model] isDeltaAtRow:row column:[column identifier]]) {
if ([_deltaColumns containsObject:[column identifier]]) { color = [NSColor orangeColor];
color = [NSColor orangeColor];
}
} }
} }
else { else {

View File

@@ -1,23 +1,21 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h> #import <Quartz/Quartz.h>
#import "StatsLabel.h" #import "StatsLabel.h"
#import "ResultTable.h" #import "ResultTable.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSTableView.h" #import "HSTableView.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
@class AppDelegateBase; @class AppDelegate;
@interface ResultWindowBase : NSWindowController @interface ResultWindow : NSWindowController
{ {
@protected @protected
NSSegmentedControl *optionsSwitch; NSSegmentedControl *optionsSwitch;
@@ -26,12 +24,10 @@ http://www.hardcoded.net/licenses/bsd_license
NSTextField *stats; NSTextField *stats;
NSSearchField *filterField; NSSearchField *filterField;
AppDelegateBase *app; AppDelegate *app;
PyDupeGuru *model; PyDupeGuru *model;
ResultTable *table; ResultTable *table;
StatsLabel *statsLabel; StatsLabel *statsLabel;
ProblemDialog *problemDialog;
DeletionOptions *deletionOptions;
QLPreviewPanel* previewPanel; QLPreviewPanel* previewPanel;
} }
@@ -41,17 +37,13 @@ http://www.hardcoded.net/licenses/bsd_license
@property (readwrite, retain) NSTextField *stats; @property (readwrite, retain) NSTextField *stats;
@property (readwrite, retain) NSSearchField *filterField; @property (readwrite, retain) NSSearchField *filterField;
- (id)initWithParentApp:(AppDelegateBase *)app; - (id)initWithParentApp:(AppDelegate *)app;
/* Virtual */
- (void)initResultColumns;
- (void)setScanOptions;
/* Helpers */ /* Helpers */
- (void)fillColumnsMenu; - (void)fillColumnsMenu;
- (void)updateOptionSegments; - (void)updateOptionSegments;
- (void)showProblemDialog;
- (void)adjustUIToLocalization; - (void)adjustUIToLocalization;
- (void)initResultColumns:(ResultTable *)aTable;
/* Actions */ /* Actions */
- (void)changeOptions; - (void)changeOptions;
@@ -75,7 +67,6 @@ http://www.hardcoded.net/licenses/bsd_license
- (void)resetColumnsToDefault; - (void)resetColumnsToDefault;
- (void)revealSelected; - (void)revealSelected;
- (void)saveResults; - (void)saveResults;
- (void)startDuplicateScan;
- (void)switchSelected; - (void)switchSelected;
- (void)toggleColumn:(id)sender; - (void)toggleColumn:(id)sender;
- (void)toggleDelta; - (void)toggleDelta;

View File

@@ -1,12 +1,12 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "ResultWindowBase.h" #import "ResultWindow.h"
#import "ResultWindow_UI.h" #import "ResultWindow_UI.h"
#import "Dialogs.h" #import "Dialogs.h"
#import "ProgressController.h" #import "ProgressController.h"
@@ -15,7 +15,7 @@ http://www.hardcoded.net/licenses/bsd_license
#import "Consts.h" #import "Consts.h"
#import "PrioritizeDialog.h" #import "PrioritizeDialog.h"
@implementation ResultWindowBase @implementation ResultWindow
@synthesize optionsSwitch; @synthesize optionsSwitch;
@synthesize optionsToolbarItem; @synthesize optionsToolbarItem;
@@ -23,7 +23,7 @@ http://www.hardcoded.net/licenses/bsd_license
@synthesize stats; @synthesize stats;
@synthesize filterField; @synthesize filterField;
- (id)initWithParentApp:(AppDelegateBase *)aApp; - (id)initWithParentApp:(AppDelegate *)aApp;
{ {
self = [super initWithWindow:nil]; self = [super initWithWindow:nil];
app = aApp; app = aApp;
@@ -34,17 +34,12 @@ http://www.hardcoded.net/licenses/bsd_license
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge]; [[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches]; table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats]; statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]]; [self initResultColumns:table];
deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
[self initResultColumns];
[[table columns] setColumnsAsReadOnly]; [[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu]; [self fillColumnsMenu];
[matches setTarget:self]; [matches setTarget:self];
[matches setDoubleAction:@selector(openClicked)]; [matches setDoubleAction:@selector(openClicked)];
[self adjustUIToLocalization]; [self adjustUIToLocalization];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobInProgress:) name:JobInProgress object:nil];
return self; return self;
} }
@@ -52,19 +47,9 @@ http://www.hardcoded.net/licenses/bsd_license
{ {
[table release]; [table release];
[statsLabel release]; [statsLabel release];
[problemDialog release];
[super dealloc]; [super dealloc];
} }
/* Virtual */
- (void)initResultColumns
{
}
- (void)setScanOptions
{
}
/* Helpers */ /* Helpers */
- (void)fillColumnsMenu - (void)fillColumnsMenu
{ {
@@ -92,11 +77,6 @@ http://www.hardcoded.net/licenses/bsd_license
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2]; [optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
} }
- (void)showProblemDialog
{
[problemDialog showWindow:self];
}
- (void)adjustUIToLocalization - (void)adjustUIToLocalization
{ {
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0]; NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
@@ -121,6 +101,87 @@ http://www.hardcoded.net/licenses/bsd_license
} }
} }
- (void)initResultColumns:(ResultTable *)aTable
{
NSInteger appMode = [app getAppMode];
if (appMode == AppModePicture) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 162, 16, 0, YES, nil},
{@"folder_path", 142, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"dimensions", 73, 16, 0, YES, nil},
{@"exif_timestamp", 120, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 58, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else if (appMode == AppModeMusic) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 235, 16, 0, YES, nil},
{@"folder_path", 120, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"duration", 50, 16, 0, YES, nil},
{@"bitrate", 50, 16, 0, YES, nil},
{@"samplerate", 60, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"title", 120, 16, 0, YES, nil},
{@"artist", 120, 16, 0, YES, nil},
{@"album", 120, 16, 0, YES, nil},
{@"genre", 80, 16, 0, YES, nil},
{@"year", 40, 16, 0, YES, nil},
{@"track", 40, 16, 0, YES, nil},
{@"comment", 120, 16, 0, YES, nil},
{@"percentage", 57, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"duration"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"bitrate"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 195, 16, 0, YES, nil},
{@"folder_path", 183, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 60, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
[[aTable columns] restoreColumns];
}
/* Actions */ /* Actions */
- (void)changeOptions - (void)changeOptions
{ {
@@ -261,21 +322,11 @@ http://www.hardcoded.net/licenses/bsd_license
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]]; [sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")]; [sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")];
if ([sp runModal] == NSOKButton) { if ([sp runModal] == NSOKButton) {
[model saveResultsAs:[sp filename]]; [model saveResultsAs:[[sp URL] path]];
[[app recentResults] addFile:[sp filename]]; [[app recentResults] addFile:[[sp URL] path]];
} }
} }
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[self setScanOptions];
[model doScan];
}
- (void)switchSelected - (void)switchSelected
{ {
[model makeSelectedReference]; [model makeSelectedReference];
@@ -340,22 +391,6 @@ http://www.hardcoded.net/licenses/bsd_license
previewPanel = nil; previewPanel = nil;
} }
- (void)jobInProgress:(NSNotification *)aNotification
{
[Dialogs showMessage:NSLocalizedString(@"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.", @"")];
}
- (void)jobStarted:(NSNotification *)aNotification
{
[[self window] makeKeyAndOrderFront:nil];
NSDictionary *ui = [aNotification userInfo];
NSString *desc = [ui valueForKey:@"desc"];
[[ProgressController mainProgressController] setJobDesc:desc];
NSString *jobid = [ui valueForKey:@"jobid"];
[[ProgressController mainProgressController] setJobId:jobid];
[[ProgressController mainProgressController] showSheetForParent:[self window]];
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{ {
return ![[ProgressController mainProgressController] isShown]; return ![[ProgressController mainProgressController] isShown];
@@ -363,6 +398,9 @@ http://www.hardcoded.net/licenses/bsd_license
- (BOOL)validateMenuItem:(NSMenuItem *)item - (BOOL)validateMenuItem:(NSMenuItem *)item
{ {
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Mark All", @"")];
}
return ![[ProgressController mainProgressController] isShown]; return ![[ProgressController mainProgressController] isShown];
} }
@end @end

1
cocoa/Sparkle Submodule

Submodule cocoa/Sparkle added at 1c8d54166b

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "StatsLabel.h" #import "StatsLabel.h"

View File

@@ -1,8 +1,8 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# #
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.gnu.org/licenses/gpl-3.0.html
from hscommon.trans import install_gettext_trans_under_cocoa from hscommon.trans import install_gettext_trans_under_cocoa
install_gettext_trans_under_cocoa() install_gettext_trans_under_cocoa()
@@ -10,7 +10,7 @@ install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import * from inter.all import *
from inter.app_se import PyDupeGuru from inter.app import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it # When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module. # to see the module.

View File

@@ -1,6 +1,5 @@
"%@ Results" = "%@ Results"; "%@ 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"; "About dupeGuru" = "About dupeGuru";
"Action" = "Action"; "Action" = "Action";
"Actions" = "Actions"; "Actions" = "Actions";
@@ -127,6 +126,7 @@
"Select a file to save your results to" = "Select a file to save your results to"; "Select a file to save your results to" = "Select a file to save your results to";
"Select a folder to add to the scanning list" = "Select a folder to add to the scanning list"; "Select a folder to add to the scanning list" = "Select a folder to add to the scanning list";
"Select a results file to load" = "Select a results file to load"; "Select a results file to load" = "Select a results file to load";
"Select All" = "Select All";
"Select folders to scan and press \"Scan\"." = "Select folders to scan and press \"Scan\"."; "Select folders to scan and press \"Scan\"." = "Select folders to scan and press \"Scan\".";
"Selected" = "Selected"; "Selected" = "Selected";
"Send Marked to Trash..." = "Send Marked to Trash..."; "Send Marked to Trash..." = "Send Marked to Trash...";

View File

@@ -1,3 +1,4 @@
from cocoa.inter import PyTextField, PyProgressWindow
from .deletion_options import PyDeletionOptions from .deletion_options import PyDeletionOptions
from .details_panel import PyDetailsPanel from .details_panel import PyDetailsPanel
from .directory_outline import PyDirectoryOutline from .directory_outline import PyDirectoryOutline

View File

@@ -1,41 +1,56 @@
import logging import logging
from objp.util import pyref, dontwrap from objp.util import pyref, dontwrap
from jobprogress import job from hscommon.path import Path, pathify
import cocoa from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
from cocoa import install_exception_hook, install_cocoa_logger, proxy from cocoa.inter import PyBaseApp, BaseAppView
from cocoa.inter import PyFairware, FairwareView
from hscommon.trans import trget
from core.app import JobType import core.pe.photo
from core.app import DupeGuru as DupeGuruBase, AppMode
from .directories import Directories, Bundle
from .photo import Photo
tr = trget('ui') class DupeGuru(DupeGuruBase):
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.directories = Directories()
def selected_dupe_path(self):
if not self.selected_dupes:
return None
return self.selected_dupes[0].path
def selected_dupe_ref_path(self):
if not self.selected_dupes:
return None
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
return None
return ref.path
def _get_fileclasses(self):
result = DupeGuruBase._get_fileclasses(self)
if self.app_mode == AppMode.Standard:
result = [Bundle] + result
return result
JOBID2TITLE = { class DupeGuruView(BaseAppView):
JobType.Scan: tr("Scanning for duplicates"),
JobType.Load: tr("Loading"),
JobType.Move: tr("Moving"),
JobType.Copy: tr("Copying"),
JobType.Delete: tr("Sending to Trash"),
}
class DupeGuruView(FairwareView):
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
def createResultsWindow(self): pass
def showResultsWindow(self): pass
def showProblemDialog(self): pass def showProblemDialog(self): pass
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
class PyDupeGuruBase(PyFairware): class PyDupeGuru(PyBaseApp):
FOLLOW_PROTOCOLS = ['Worker']
@dontwrap @dontwrap
def _init(self, modelclass): def __init__(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s') logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
install_exception_hook() install_exception_hook('https://github.com/hsoft/dupeguru/issues')
install_cocoa_logger() install_cocoa_logger()
appdata = proxy.getAppdataPath() patch_threaded_job_performer()
self.model = modelclass(self, appdata) self.model = DupeGuru(self)
self.progress = cocoa.ThreadedJobPerformer()
#---Sub-proxies #---Sub-proxies
def detailsPanel(self) -> pyref: def detailsPanel(self) -> pyref:
@@ -56,6 +71,9 @@ class PyDupeGuruBase(PyFairware):
def ignoreListDialog(self) -> pyref: def ignoreListDialog(self) -> pyref:
return self.model.ignore_list_dialog return self.model.ignore_list_dialog
def progressWindow(self) -> pyref:
return self.model.progress_window
def deletionOptions(self) -> pyref: def deletionOptions(self) -> pyref:
return self.model.deletion_options return self.model.deletion_options
@@ -137,13 +155,62 @@ class PyDupeGuruBase(PyFairware):
def showIgnoreList(self): def showIgnoreList(self):
self.model.ignore_list_dialog.show() self.model.ignore_list_dialog.show()
def clearPictureCache(self):
self.model.clear_picture_cache()
#---Information #---Information
def getScanOptions(self) -> list:
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
def resultsAreModified(self) -> bool: def resultsAreModified(self) -> bool:
return self.model.results.is_modified return self.model.results.is_modified
def getSelectedDupePath(self) -> str:
return str(self.model.selected_dupe_path())
def getSelectedDupeRefPath(self) -> str:
return str(self.model.selected_dupe_ref_path())
#---Properties #---Properties
def getAppMode(self) -> int:
return self.model.app_mode
def setAppMode_(self, app_mode: int):
self.model.app_mode = app_mode
def setScanType_(self, scan_type_index: int):
scan_options = self.model.SCANNER_CLASS.get_scan_options()
try:
so = scan_options[scan_type_index]
self.model.options['scan_type'] = so.scan_type
except IndexError:
pass
def setMinMatchPercentage_(self, percentage: int):
self.model.options['min_match_percentage'] = int(percentage)
def setWordWeighting_(self, words_are_weighted: bool):
self.model.options['word_weighting'] = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.options['match_similar_words'] = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.options['size_threshold'] = size_threshold
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if 'scanned_tags' not in self.model.options:
self.model.options['scanned_tags'] = set()
if enable:
self.model.options['scanned_tags'].add(scan_tag)
else:
self.model.options['scanned_tags'].discard(scan_tag)
def setMatchScaled_(self, match_scaled: bool):
self.model.options['match_scaled'] = match_scaled
def setMixFileKind_(self, mix_file_kind: bool): def setMixFileKind_(self, mix_file_kind: bool):
self.model.scanner.mix_file_kind = mix_file_kind self.model.options['mix_file_kind'] = mix_file_kind
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool): def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
self.model.options['escape_filter_regexp'] = escape_filter_regexp self.model.options['escape_filter_regexp'] = escape_filter_regexp
@@ -157,62 +224,18 @@ class PyDupeGuruBase(PyFairware):
def setCopyMoveDestType_(self, copymove_dest_type: int): def setCopyMoveDestType_(self, copymove_dest_type: int):
self.model.options['copymove_dest_type'] = copymove_dest_type self.model.options['copymove_dest_type'] = copymove_dest_type
#---Worker
def getJobProgress(self) -> object: # NSNumber
try:
return self.progress.last_progress
except AttributeError:
# I have *no idea* why this can possible happen (last_progress is always set by
# create_job() *before* any threaded job notification, which shows the progress panel,
# is sent), but it happens anyway, so there we go. ref: #106
return -1
def getJobDesc(self) -> str:
try:
return self.progress.last_desc
except AttributeError:
# see getJobProgress
return ''
def cancelJob(self):
self.progress.job_cancelled = True
def jobCompleted_(self, jobid: str):
result = self.model._job_completed(jobid, self.progress.last_error)
if not result:
self.progress.reraise_if_error()
#--- model --> view #--- model --> view
@dontwrap
def open_path(self, path):
proxy.openPath_(str(path))
@dontwrap
def reveal_path(self, path):
proxy.revealPath_(str(path))
@dontwrap
def start_job(self, jobid, func, args=()):
try:
j = self.progress.create_job()
args = tuple([j] + list(args))
self.progress.run_threaded(func, args=args)
except job.JobInProgressError:
proxy.postNotification_userInfo_('JobInProgress', None)
else:
ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid}
proxy.postNotification_userInfo_('JobStarted', ud)
@dontwrap @dontwrap
def ask_yes_no(self, prompt): def ask_yes_no(self, prompt):
return self.callback.askYesNoWithPrompt_(prompt) return self.callback.askYesNoWithPrompt_(prompt)
@dontwrap
def create_results_window(self):
self.callback.createResultsWindow()
@dontwrap @dontwrap
def show_results_window(self): def show_results_window(self):
# Not needed yet because our progress dialog is shown as a sheet of the results window, self.callback.showResultsWindow()
# which causes it to be already visible when the scan/load ends.
# XXX Make progress sheet be a child of the folder selection window.
pass
@dontwrap @dontwrap
def show_problem_dialog(self): def show_problem_dialog(self):

View File

@@ -1,287 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/11/16
# 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 logging
import plistlib
import time
import os.path as op
from appscript import app, its, k, CommandError, ApplicationNotFoundError
from . import tunes
from cocoa import as_fetch, proxy
from hscommon.trans import trget
from hscommon.path import Path
from hscommon.util import remove_invalid_xml
from core import directories
from core.app import JobType
from core.scanner import ScanType
from core_me.app import DupeGuru as DupeGuruBase
from core_me import fs
from .app import JOBID2TITLE, PyDupeGuruBase
tr = trget('ui')
JobType.RemoveDeadTracks = 'jobRemoveDeadTracks'
JobType.ScanDeadTracks = 'jobScanDeadTracks'
JOBID2TITLE.update({
JobType.RemoveDeadTracks: tr("Removing dead tracks from your iTunes Library"),
JobType.ScanDeadTracks: tr("Scanning the iTunes Library"),
})
ITUNES = 'iTunes'
ITUNES_PATH = Path('iTunes Library')
def get_itunes_library(a):
try:
[source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library]
[library] = source.library_playlists(timeout=0)
return library
except ValueError:
logging.warning('Some unexpected iTunes configuration encountered')
return None
class ITunesSong(fs.MusicFile):
def __init__(self, song_data):
path = Path(proxy.url2path_(song_data['Location']))
fs.MusicFile.__init__(self, path)
self.id = song_data['Track ID']
def remove_from_library(self):
try:
a = app(ITUNES, terms=tunes)
library = get_itunes_library(a)
if library is None:
return
[song] = library.file_tracks[its.database_ID == self.id]()
a.delete(song, timeout=0)
except ValueError:
msg = "Could not find song '{}' (trackid: {}) in iTunes Library".format(str(self.path), self.id)
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
display_folder_path = ITUNES_PATH
def get_itunes_database_path():
plisturls = proxy.prefValue_inDomain_('iTunesRecentDatabases', 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_itunes_songs(plistpath):
if not plistpath.exists():
return []
s = plistpath.open('rt', encoding='utf-8').read()
# iTunes sometimes produces XML files with invalid characters in it.
s = remove_invalid_xml(s, replace_with='')
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for song_data in plist['Tracks'].values():
try:
if song_data['Track Type'] != 'File':
continue
song = ITunesSong(song_data)
except KeyError: # No "Track Type", "Location" or "Track ID" key in track
continue
if song.path.exists():
result.append(song)
return result
class Directories(directories.Directories):
def __init__(self, fileclasses):
directories.Directories.__init__(self, fileclasses)
try:
self.itunes_libpath = get_itunes_database_path()
except directories.InvalidPathError:
self.itunes_libpath = None
def _get_files(self, from_path, j):
if from_path == ITUNES_PATH:
if self.itunes_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
songs = get_itunes_songs(self.itunes_libpath)
for song in songs:
song.is_ref = is_ref
return songs
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path == ITUNES_PATH:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path == ITUNES_PATH:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_itunes_path(self):
return any(path == ITUNES_PATH for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iTunes library, we assume we have at least one file.
if self.has_itunes_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruME(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru Music Edition')
DupeGuruBase.__init__(self, view, appdata)
# 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)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, ITunesSong) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iTunes. Don't touch it!"))
try:
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, ITunesSong):
dupe.remove_from_library()
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 not hasattr(self, 'itunes_songs'):
songs = get_itunes_songs(self.directories.itunes_libpath)
self.itunes_songs = {song.path: song for song in songs}
if path in self.itunes_songs:
return self.itunes_songs[path]
else:
pass # We'll return the default file type, as per the last line of this method
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid, exc):
if (jobid in {JobType.RemoveDeadTracks, JobType.ScanDeadTracks}) and (exc is not None):
msg = tr("There were communication problems with iTunes. The operation couldn't be completed.")
self.view.show_message(msg)
return True
if jobid == JobType.ScanDeadTracks:
dead_tracks_count = len(self.dead_tracks)
if dead_tracks_count > 0:
msg = tr("Your iTunes Library contains %d dead tracks ready to be removed. Continue?")
if self.view.ask_yes_no(msg % dead_tracks_count):
self.remove_dead_tracks()
else:
msg = tr("You have no dead tracks in your iTunes Library")
self.view.show_message(msg)
if jobid == JobType.Load:
if hasattr(self, 'itunes_songs'):
# If we load another file, we want a refresh song list
del self.itunes_songs
DupeGuruBase._job_completed(self, jobid, exc)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, ITunesSong):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
def start_scanning(self):
if self.directories.has_itunes_path():
try:
app(ITUNES, terms=tunes)
except ApplicationNotFoundError:
self.view.show_message(tr("The iTunes application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
def remove_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
for index, track in enumerate(j.iter_with_progress(self.dead_tracks)):
if index % 100 == 0:
time.sleep(.1)
try:
track.delete(timeout=0)
except CommandError as e:
logging.warning('Error while trying to remove a track from iTunes: %s' % str(e))
self.view.start_job(JobType.RemoveDeadTracks, do)
def scan_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
library = get_itunes_library(a)
if library is None:
return
self.dead_tracks = []
tracks = as_fetch(library.file_tracks, k.file_track)
for index, track in enumerate(j.iter_with_progress(tracks)):
if index % 100 == 0:
time.sleep(.1)
if track.location(timeout=0) == k.missing_value:
self.dead_tracks.append(track)
logging.info('Found %d dead tracks' % len(self.dead_tracks))
self.view.start_job(JobType.ScanDeadTracks, do)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruME)
def scanDeadTracks(self):
self.model.scan_dead_tracks()
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = percentage
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Fields,
ScanType.FieldsNoOrder,
ScanType.Tag,
ScanType.Contents,
ScanType.ContentsAudio,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if enable:
self.model.scanner.scanned_tags.add(scan_tag)
else:
self.model.scanner.scanned_tags.discard(scan_tag)

View File

@@ -1,328 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/11/13
# 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 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.trans import trget
from cocoa import proxy
from core.scanner import ScanType
from core import directories
from core.app import JobType
from core_pe import _block_osx
from core_pe.photo import Photo as PhotoBase
from core_pe.app import DupeGuru as DupeGuruBase
from .app import PyDupeGuruBase
tr = trget('ui')
IPHOTO_PATH = Path('iPhoto Library')
APERTURE_PATH = Path('Aperture Library')
class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
HANDLED_EXTS.update({'psd', 'nef', 'cr2', 'orf'})
def _plat_get_dimensions(self):
return _block_osx.get_image_size(str(self.path))
def _plat_get_blocks(self, block_count_per_side, orientation):
try:
blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks:
raise IOError('The picture %s could not be read' % str(self.path))
return blocks
class IPhoto(Photo):
def __init__(self, path, db_id):
# In IPhoto, we don't care about the db_id, we find photos by path.
Photo.__init__(self, path)
@property
def display_folder_path(self):
return IPHOTO_PATH
class AperturePhoto(Photo):
def __init__(self, path, db_id):
Photo.__init__(self, path)
self.db_id = db_id
@property
def display_folder_path(self):
return APERTURE_PATH
def get_iphoto_or_aperture_pictures(plistpath, photo_class):
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
if not io.exists(plistpath):
return []
s = io.open(plistpath, '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
# any & char that is not a &-based entity (&amp;, &quot;, etc.). based on TextMate's XML
# bundle's regexp
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
if count:
logging.warning("%d invalid XML entities replacement made", count)
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for key, photo_data in plist['Master Image List'].items():
if photo_data['MediaType'] != 'Image':
continue
photo_path = Path(photo_data['ImagePath'])
photo = photo_class(photo_path, key)
result.append(photo)
return result
def get_iphoto_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, IPhoto)
def get_aperture_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, AperturePhoto)
def get_iapps_database_path(prefname):
plisturls = proxy.prefValue_inDomain_(prefname, 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_iphoto_database_path():
return get_iapps_database_path('iPhotoRecentDatabases')
def get_aperture_database_path():
return get_iapps_database_path('ApertureLibraries')
class Directories(directories.Directories):
def __init__(self):
directories.Directories.__init__(self, fileclasses=[Photo])
try:
self.iphoto_libpath = get_iphoto_database_path()
self.set_state(self.iphoto_libpath[:-1], 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)
except directories.InvalidPathError:
self.aperture_libpath = None
def _get_files(self, from_path, j):
if from_path == IPHOTO_PATH:
if self.iphoto_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_iphoto_pictures(self.iphoto_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
elif from_path == APERTURE_PATH:
if self.aperture_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_aperture_pictures(self.aperture_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_iphoto_path(self):
return any(path in {IPHOTO_PATH, APERTURE_PATH} for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iPhoto library, we assume we have at least one file.
if self.has_iphoto_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruPE(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru Picture Edition')
DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories()
def _do_delete(self, j, *args):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, *args)
self.deleted_aperture_photos = False
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, IPhoto) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iPhoto. Don't touch it!"))
try:
a = app('iPhoto')
a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
if any(isinstance(dupe, AperturePhoto) for dupe in marked):
self.deleted_aperture_photos = True
j.add_progress(0, desc=tr("Talking to Aperture. Don't touch it!"))
try:
a = app('Aperture')
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, IPhoto):
try:
a = app('iPhoto')
album = a.photo_library_album()
if album is None:
msg = "There are communication problems with iPhoto. Try opening iPhoto first, it might solve it."
raise EnvironmentError(msg)
[photo] = album.photos[its.image_path == str(dupe.path)]()
a.remove(photo, timeout=0)
except ValueError:
msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
if isinstance(dupe, AperturePhoto):
try:
a = app('Aperture')
# I'm flying blind here. In my own test library, all photos are in an album with the
# id "LibraryFolder", so I'm going to guess that it's the case at least most of the
# time. As a safeguard, if we don't find any library with that id, we'll use the
# first album.
# Now, about deleting: All attempts I've made at sending photos to trash failed,
# even with normal applescript. So, what we're going to do here is to create a
# "dupeGuru Trash" project and tell the user to manually send those photos to trash.
libraries = a.libraries()
library = first(l for l in libraries if l.id == 'LibraryFolder')
if library is None:
library = libraries[0]
trash_project = a.projects["dupeGuru Trash"]
if trash_project.exists():
trash_project = trash_project()
else:
trash_project = library.make(new=k.project, with_properties={k.name: "dupeGuru Trash"})
[photo] = library.image_versions[its.id == dupe.db_id]()
photo.move(to=trash_project)
except (IndexError, ValueError):
msg = "Could not find photo '{}' in Aperture Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else:
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 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 not hasattr(self, 'path2aperture'):
photos = get_aperture_pictures(self.directories.aperture_libpath)
self.path2aperture = {p.path: p for p in photos}
return self.path2aperture.get(path)
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid, exc):
DupeGuruBase._job_completed(self, jobid, exc)
if jobid == JobType.Load:
if hasattr(self, 'path2iphoto'):
del self.path2iphoto
if hasattr(self, 'path2aperture'):
del self.path2aperture
if jobid == JobType.Delete and self.deleted_aperture_photos:
msg = tr("Deleted Aperture photos were sent to a project called \"dupeGuru Trash\".")
self.view.show_message(msg)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, (IPhoto, AperturePhoto)):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
def selected_dupe_path(self):
if not self.selected_dupes:
return None
return self.selected_dupes[0].path
def selected_dupe_ref_path(self):
if not self.selected_dupes:
return None
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
return None
return ref.path
def start_scanning(self):
if self.directories.has_iphoto_path():
try:
app('iPhoto')
except ApplicationNotFoundError:
self.view.show_message(tr("The iPhoto application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruPE)
def clearPictureCache(self):
self.model.scanner.clear_picture_cache()
#---Information
def getSelectedDupePath(self) -> str:
return str(self.model.selected_dupe_path())
def getSelectedDupeRefPath(self) -> str:
return str(self.model.selected_dupe_ref_path())
#---Properties
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.FuzzyBlock,
ScanType.ExifTimestamp,
][scan_type]
except IndexError:
pass
def setMatchScaled_(self, match_scaled: bool):
self.model.scanner.match_scaled = match_scaled
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.threshold = percentage

View File

@@ -1,102 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-24
# 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 logging
import os.path as op
from hscommon import io
from hscommon.path import Path
from cocoa import proxy
from core.scanner import ScanType
from core import fs
from core.directories import Directories as DirectoriesBase, DirectoryState
from core_se.app import DupeGuru as DupeGuruBase
from .app import PyDupeGuruBase
def is_bundle(str_path):
uti = proxy.getUTI_(str_path)
if uti is None:
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return proxy.type_conformsToType_(uti, 'com.apple.bundle') or proxy.type_conformsToType_(uti, 'com.apple.package')
class Bundle(fs.Folder):
@classmethod
def can_handle(cls, path):
return not io.islink(path) and io.isdir(path) and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def __init__(self):
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if path in self.ROOT_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
def _get_folders(self, from_folder, j):
# We don't want to scan bundle's subfolder even in Folders mode. Bundle's integrity has to
# stay intact.
if is_bundle(str(from_folder.path)):
# just yield the current folder and bail
state = self.get_state(from_folder.path)
if state != DirectoryState.Excluded:
from_folder.is_ref = state == DirectoryState.Reference
yield from_folder
return
else:
for folder in DirectoriesBase._get_folders(self, from_folder, j):
yield folder
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))]
class DupeGuru(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru')
DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories()
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuru)
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = int(percentage)
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Contents,
ScanType.Folders,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.scanner.size_threshold = size_threshold

View File

@@ -1,9 +1,9 @@
# Created On: 2012-05-30 # Created On: 2012-05-30
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# #
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.gnu.org/licenses/gpl-3.0.html
from objp.util import dontwrap from objp.util import dontwrap
from cocoa.inter import PyGUIObject, GUIObjectView from cocoa.inter import PyGUIObject, GUIObjectView
@@ -11,6 +11,7 @@ from cocoa.inter import PyGUIObject, GUIObjectView
class DeletionOptionsView(GUIObjectView): class DeletionOptionsView(GUIObjectView):
def updateMsg_(self, msg: str): pass def updateMsg_(self, msg: str): pass
def show(self) -> bool: pass def show(self) -> bool: pass
def setHardlinkOptionEnabled_(self, enabled: bool): pass
class PyDeletionOptions(PyGUIObject): class PyDeletionOptions(PyGUIObject):
def setLinkDeleted_(self, link_deleted: bool): def setLinkDeleted_(self, link_deleted: bool):
@@ -31,3 +32,6 @@ class PyDeletionOptions(PyGUIObject):
def show(self): def show(self):
return self.callback.show() return self.callback.show()
@dontwrap
def set_hardlink_option_enabled(self, enabled):
self.callback.setHardlinkOptionEnabled_(enabled)

View File

@@ -0,0 +1,53 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from hscommon.path import Path, pathify
from core.se import fs
from core.directories import Directories as DirectoriesBase, DirectoryState
def is_bundle(str_path):
uti = proxy.getUTI_(str_path)
if uti is None:
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return proxy.type_conformsToType_(uti, 'com.apple.bundle') or proxy.type_conformsToType_(uti, 'com.apple.package')
class Bundle(fs.Folder):
@classmethod
@pathify
def can_handle(cls, path: Path):
return not path.islink() and path.isdir() and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if path in self.ROOT_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
def _get_folders(self, from_folder, j):
# We don't want to scan bundle's subfolder even in Folders mode. Bundle's integrity has to
# stay intact.
if is_bundle(str(from_folder.path)):
# just yield the current folder and bail
state = self.get_state(from_folder.path)
if state != DirectoryState.Excluded:
from_folder.is_ref = state == DirectoryState.Reference
yield from_folder
return
else:
yield from DirectoriesBase._get_folders(self, from_folder, j)
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))]

View File

@@ -11,6 +11,9 @@ class PyDirectoryOutline(PyOutline):
def removeSelectedDirectory(self): def removeSelectedDirectory(self):
self.model.remove_selected() self.model.remove_selected()
def selectAll(self):
self.model.select_all()
# python --> cocoa # python --> cocoa
@dontwrap @dontwrap
def refresh_states(self): def refresh_states(self):

35
cocoa/inter/photo.py Normal file
View File

@@ -0,0 +1,35 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from core.pe import _block_osx
from core.pe.photo import Photo as PhotoBase
class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
HANDLED_EXTS.update({'psd', 'nef', 'cr2', 'orf'})
def _plat_get_dimensions(self):
return _block_osx.get_image_size(str(self.path))
def _plat_get_blocks(self, block_count_per_side, orientation):
try:
blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks:
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 ''

View File

@@ -17,12 +17,13 @@ class PyResultTable(PyTable):
def setDeltaValuesMode_(self, value: bool): def setDeltaValuesMode_(self, value: bool):
self.model.delta_values = value self.model.delta_values = value
def deltaColumns(self) -> list:
return list(self.model.DELTA_COLUMNS)
def valueForRow_column_(self, row_index: int, column: str) -> object: def valueForRow_column_(self, row_index: int, column: str) -> object:
return self.model.get_row_value(row_index, column) return self.model.get_row_value(row_index, column)
def isDeltaAtRow_column_(self, row_index: int, column: str) -> bool:
row = self.model[row_index]
return row.is_cell_delta(column)
def renameSelected_(self, newname: str) -> bool: def renameSelected_(self, newname: str) -> bool:
return self.model.rename_selected(newname) return self.model.rename_selected(newname)

View File

@@ -1,282 +0,0 @@
# Taken from https://github.com/abarnert/itunesterms
version = 1.1
path = '/Applications/iTunes.app'
classes = \
[('print_settings', b'pset'),
('application', b'capp'),
('artwork', b'cArt'),
('audio_CD_playlist', b'cCDP'),
('audio_CD_track', b'cCDT'),
('browser_window', b'cBrW'),
('device_playlist', b'cDvP'),
('device_track', b'cDvT'),
('encoder', b'cEnc'),
('EQ_preset', b'cEQP'),
('EQ_window', b'cEQW'),
('file_track', b'cFlT'),
('folder_playlist', b'cFoP'),
('item', b'cobj'),
('library_playlist', b'cLiP'),
('playlist', b'cPly'),
('playlist_window', b'cPlW'),
('radio_tuner_playlist', b'cRTP'),
('shared_track', b'cShT'),
('source', b'cSrc'),
('track', b'cTrk'),
('URL_track', b'cURT'),
('user_playlist', b'cUsP'),
('visual', b'cVis'),
('window', b'cwin')]
enums = \
[('track_listing', b'kTrk'),
('album_listing', b'kAlb'),
('cd_insert', b'kCDi'),
('standard', b'lwst'),
('detailed', b'lwdt'),
('stopped', b'kPSS'),
('playing', b'kPSP'),
('paused', b'kPSp'),
('fast_forwarding', b'kPSF'),
('rewinding', b'kPSR'),
('off', b'kRpO'),
('one', b'kRp1'),
('all', b'kAll'),
('small', b'kVSS'),
('medium', b'kVSM'),
('large', b'kVSL'),
('library', b'kLib'),
('iPod', b'kPod'),
('audio_CD', b'kACD'),
('MP3_CD', b'kMCD'),
('device', b'kDev'),
('radio_tuner', b'kTun'),
('shared_library', b'kShd'),
('unknown', b'kUnk'),
('albums', b'kSrL'),
('artists', b'kSrR'),
('composers', b'kSrC'),
('displayed', b'kSrV'),
('songs', b'kSrS'),
('none', b'kNon'),
('Books', b'kSpA'),
('folder', b'kSpF'),
('Genius', b'kSpG'),
('iTunes_U', b'kSpU'),
('Library', b'kSpL'),
('Movies', b'kSpI'),
('Music', b'kSpZ'),
('Party_Shuffle', b'kSpS'),
('Podcasts', b'kSpP'),
('Purchased_Music', b'kSpM'),
('TV_Shows', b'kSpT'),
('movie', b'kVdM'),
('music_video', b'kVdV'),
('TV_show', b'kVdT'),
('user', b'kRtU'),
('computed', b'kRtC')]
properties = \
[('copies', b'lwcp'),
('collating', b'lwcl'),
('starting_page', b'lwfp'),
('ending_page', b'lwlp'),
('pages_across', b'lwla'),
('pages_down', b'lwld'),
('error_handling', b'lweh'),
('requested_print_time', b'lwqt'),
('printer_features', b'lwpf'),
('fax_number', b'faxn'),
('target_printer', b'trpr'),
('current_encoder', b'pEnc'),
('current_EQ_preset', b'pEQP'),
('current_playlist', b'pPla'),
('current_stream_title', b'pStT'),
('current_stream_URL', b'pStU'),
('current_track', b'pTrk'),
('current_visual', b'pVis'),
('EQ_enabled', b'pEQ '),
('fixed_indexing', b'pFix'),
('frontmost', b'pisf'),
('full_screen', b'pFSc'),
('name', b'pnam'),
('mute', b'pMut'),
('player_position', b'pPos'),
('player_state', b'pPlS'),
('selection', b'sele'),
('sound_volume', b'pVol'),
('version', b'vers'),
('visuals_enabled', b'pVsE'),
('visual_size', b'pVSz'),
('data', b'pPCT'),
('description', b'pDes'),
('downloaded', b'pDlA'),
('format', b'pFmt'),
('kind', b'pKnd'),
('raw_data', b'pRaw'),
('artist', b'pArt'),
('compilation', b'pAnt'),
('composer', b'pCmp'),
('disc_count', b'pDsC'),
('disc_number', b'pDsN'),
('genre', b'pGen'),
('year', b'pYr '),
('location', b'pLoc'),
('minimized', b'pMin'),
('view', b'pPly'),
('band_1', b'pEQ1'),
('band_2', b'pEQ2'),
('band_3', b'pEQ3'),
('band_4', b'pEQ4'),
('band_5', b'pEQ5'),
('band_6', b'pEQ6'),
('band_7', b'pEQ7'),
('band_8', b'pEQ8'),
('band_9', b'pEQ9'),
('band_10', b'pEQ0'),
('modifiable', b'pMod'),
('preamp', b'pEQA'),
('update_tracks', b'pUTC'),
('container', b'ctnr'),
('id', b'ID '),
('index', b'pidx'),
('persistent_ID', b'pPIS'),
('duration', b'pDur'),
('parent', b'pPlP'),
('shuffle', b'pShf'),
('size', b'pSiz'),
('song_repeat', b'pRpt'),
('special_kind', b'pSpK'),
('time', b'pTim'),
('visible', b'pvis'),
('capacity', b'capa'),
('free_space', b'frsp'),
('album', b'pAlb'),
('album_artist', b'pAlA'),
('album_rating', b'pAlR'),
('album_rating_kind', b'pARk'),
('bit_rate', b'pBRt'),
('bookmark', b'pBkt'),
('bookmarkable', b'pBkm'),
('bpm', b'pBPM'),
('category', b'pCat'),
('comment', b'pCmt'),
('database_ID', b'pDID'),
('date_added', b'pAdd'),
('enabled', b'enbl'),
('episode_ID', b'pEpD'),
('episode_number', b'pEpN'),
('EQ', b'pEQp'),
('finish', b'pStp'),
('gapless', b'pGpl'),
('grouping', b'pGrp'),
('long_description', b'pLds'),
('lyrics', b'pLyr'),
('modification_date', b'asmo'),
('played_count', b'pPlC'),
('played_date', b'pPlD'),
('podcast', b'pTPc'),
('rating', b'pRte'),
('rating_kind', b'pRtk'),
('release_date', b'pRlD'),
('sample_rate', b'pSRt'),
('season_number', b'pSeN'),
('shufflable', b'pSfa'),
('skipped_count', b'pSkC'),
('skipped_date', b'pSkD'),
('show', b'pShw'),
('sort_album', b'pSAl'),
('sort_artist', b'pSAr'),
('sort_album_artist', b'pSAA'),
('sort_name', b'pSNm'),
('sort_composer', b'pSCm'),
('sort_show', b'pSSN'),
('start', b'pStr'),
('track_count', b'pTrC'),
('track_number', b'pTrN'),
('unplayed', b'pUnp'),
('video_kind', b'pVdK'),
('volume_adjustment', b'pAdj'),
('address', b'pURL'),
('shared', b'pShr'),
('smart', b'pSmt'),
('bounds', b'pbnd'),
('closeable', b'hclb'),
('collapseable', b'pWSh'),
('collapsed', b'wshd'),
('position', b'ppos'),
('resizable', b'prsz'),
('zoomable', b'iszm'),
('zoomed', b'pzum')]
elements = \
[('artworks', b'cArt'),
('audio_CD_playlists', b'cCDP'),
('audio_CD_tracks', b'cCDT'),
('browser_windows', b'cBrW'),
('device_playlists', b'cDvP'),
('device_tracks', b'cDvT'),
('encoders', b'cEnc'),
('EQ_presets', b'cEQP'),
('EQ_windows', b'cEQW'),
('file_tracks', b'cFlT'),
('folder_playlists', b'cFoP'),
('items', b'cobj'),
('library_playlists', b'cLiP'),
('playlists', b'cPly'),
('playlist_windows', b'cPlW'),
('radio_tuner_playlists', b'cRTP'),
('shared_tracks', b'cShT'),
('sources', b'cSrc'),
('tracks', b'cTrk'),
('URL_tracks', b'cURT'),
('user_playlists', b'cUsP'),
('visuals', b'cVis'),
('windows', b'cwin'),
('application', b'capp'),
('print_settings', b'pset')]
commands = \
[('set', b'coresetd', [('to', b'data')]),
('exists', b'coredoex', []),
('move', b'coremove', [('to', b'insh')]),
('subscribe', b'hookpSub', []),
('playpause', b'hookPlPs', []),
('download', b'hookDwnl', []),
('close', b'coreclos', []),
('open', b'aevtodoc', []),
('open_location', b'GURLGURL', []),
('quit', b'aevtquit', []),
('pause', b'hookPaus', []),
('make',
'corecrel',
[('new', b'kocl'), ('at', b'insh'), ('with_properties', b'prdt')]),
('duplicate', b'coreclon', [('to', b'insh')]),
('print_',
'aevtpdoc',
[('print_dialog', b'pdlg'),
('with_properties', b'prdt'),
('kind', b'pKnd'),
('theme', b'pThm')]),
('add', b'hookAdd ', [('to', b'insh')]),
('rewind', b'hookRwnd', []),
('play', b'hookPlay', [('once', b'POne')]),
('run', b'aevtoapp', []),
('resume', b'hookResu', []),
('updatePodcast', b'hookUpd1', []),
('next_track', b'hookNext', []),
('stop', b'hookStop', []),
('search', b'hookSrch', [('for_', b'pTrm'), ('only', b'pAre')]),
('updateAllPodcasts', b'hookUpdp', []),
('update', b'hookUpdt', []),
('previous_track', b'hookPrev', []),
('fast_forward', b'hookFast', []),
('count', b'corecnte', [('each', b'kocl')]),
('reveal', b'hookRevl', []),
('convert', b'hookConv', []),
('eject', b'hookEjct', []),
('back_track', b'hookBack', []),
('refresh', b'hookRfrs', []),
('delete', b'coredelo', [])]

View File

@@ -1,9 +1,9 @@
/* /*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>

View File

@@ -1,16 +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 "AppDelegateBase.h"
#import "ResultWindow.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase {}
- (void)removeDeadTracks;
@end

View File

@@ -1,68 +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 "AppDelegate.h"
#import "ProgressController.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "Dialogs.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
#import "Consts.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(3) forKey:@"scanType"];
[d setObject:i2n(80) forKey:@"minMatchPercentage"];
[d setObject:b2n(NO) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
[d setObject:b2n(NO) forKey:@"scanTagYear"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:4];
[i addIndex:5];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
_directoryPanel = nil;
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru_me/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanelME alloc] initWithParentApp:self];
}
- (void)removeDeadTracks
{
[(ResultWindow *)[self resultWindow] removeDeadTracks];
}
@end

View File

@@ -1,11 +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 "../base/Consts.h"
#define jobScanDeadTracks @"jobScanDeadTracks"

View File

@@ -1,13 +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 "DetailsPanelBase.h"
@interface DetailsPanel : DetailsPanelBase
@end

View File

@@ -1,17 +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 "DetailsPanel.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
@end

View File

@@ -1,16 +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 "../base/DirectoryPanel.h"
@interface DirectoryPanelME : DirectoryPanel
{
}
- (IBAction)addiTunes:(id)sender;
@end

View File

@@ -1,32 +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 "DirectoryPanel.h"
@implementation DirectoryPanelME
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithParentApp:aParentApp];
_alwaysShowPopUp = YES;
return self;
}
- (void)fillPopUpMenu
{
[super fillPopUpMenu];
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m insertItemWithTitle:NSLocalizedString(@"Add iTunes Library", @"") action:@selector(addiTunes:)
keyEquivalent:@"" atIndex:1];
[mi setTarget:self];
}
- (IBAction)addiTunes:(id)sender
{
[self addDirectory:@"iTunes Library"];
}
@end

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>dupeGuru</string>
<key>CFBundleHelpBookFolder</key>
<string>dupeguru_me_help</string>
<key>CFBundleHelpBookName</key>
<string>dupeGuru ME Help</string>
<key>CFBundleIconFile</key>
<string>dupeguru</string>
<key>CFBundleIdentifier</key>
<string>com.hardcoded-software.dupeguru-me</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>dupeGuru ME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleShortVersionString</key>
<string>{version}</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</dict>
</plist>

View File

@@ -1,14 +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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
- (void)removeDeadTracks;
@end

View File

@@ -1,77 +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 "ResultWindow.h"
#import "Dialogs.h"
#import "Utils.h"
#import "PyDupeGuru.h"
#import "Consts.h"
#import "ProgressController.h"
@implementation ResultWindow
/* Override */
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
}
- (void)initResultColumns
{
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 235, 16, 0, YES, nil},
{@"folder_path", 120, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"duration", 50, 16, 0, YES, nil},
{@"bitrate", 50, 16, 0, YES, nil},
{@"samplerate", 60, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"title", 120, 16, 0, YES, nil},
{@"artist", 120, 16, 0, YES, nil},
{@"album", 120, 16, 0, YES, nil},
{@"genre", 80, 16, 0, YES, nil},
{@"year", 40, 16, 0, YES, nil},
{@"track", 40, 16, 0, YES, nil},
{@"comment", 120, 16, 0, YES, nil},
{@"percentage", 57, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [matches tableColumnWithIdentifier:@"duration"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [matches tableColumnWithIdentifier:@"bitrate"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
/* Actions */
- (void)removeDeadTracks
{
[model scanDeadTracks];
}
@end

View File

@@ -1,17 +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
from hscommon.trans import install_gettext_trans_under_cocoa
install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import *
from inter.app_me import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module.
import distutils.sysconfig

Binary file not shown.

View File

@@ -1,14 +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 "AppDelegateBase.h"
@interface AppDelegate : AppDelegateBase {}
- (void)clearPictureCache;
@end

View File

@@ -1,61 +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 "AppDelegate.h"
#import "ProgressController.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "Consts.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(0) forKey:@"scanType"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru_pe/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanelPE alloc] initWithParentApp:self];
}
- (DetailsPanel *)createDetailsPanel
{
return [[DetailsPanel alloc] initWithApp:model];
}
- (void)clearPictureCache
{
[(ResultWindow *)[self resultWindow] clearPictureCache];
}
@end

View File

@@ -1,11 +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 "../base/Consts.h"
#define ImageLoadedNotification @"ImageLoadedNotification"

View File

@@ -1,17 +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 "../base/DirectoryPanel.h"
@interface DirectoryPanelPE : DirectoryPanel
{
}
- (IBAction)addiPhoto:(id)sender;
- (IBAction)addAperture:(id)sender;
@end

View File

@@ -1,40 +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 "DirectoryPanel.h"
@implementation DirectoryPanelPE
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithParentApp:aParentApp];
_alwaysShowPopUp = YES;
return self;
}
- (void)fillPopUpMenu
{
[super fillPopUpMenu];
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m insertItemWithTitle:NSLocalizedString(@"Add iPhoto Library", @"") action:@selector(addiPhoto:)
keyEquivalent:@"" atIndex:1];
[mi setTarget:self];
mi = [m insertItemWithTitle:NSLocalizedString(@"Add Aperture Library", @"") action:@selector(addAperture:)
keyEquivalent:@"" atIndex:2];
[mi setTarget:self];
}
- (IBAction)addiPhoto:(id)sender
{
[self addDirectory:@"iPhoto Library"];
}
- (IBAction)addAperture:(id)sender
{
[self addDirectory:@"Aperture Library"];
}
@end

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>dupeGuru</string>
<key>CFBundleHelpBookFolder</key>
<string>dupeguru_pe_help</string>
<key>CFBundleHelpBookName</key>
<string>dupeGuru PE Help</string>
<key>CFBundleIconFile</key>
<string>dupeguru</string>
<key>CFBundleIdentifier</key>
<string>com.hardcoded-software.dupeguru-pe</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>dupeGuru PE</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleShortVersionString</key>
<string>{version}</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2013</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
</dict>
</plist>

View File

@@ -1,14 +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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
- (void)clearPictureCache;
@end

View File

@@ -1,58 +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 "ResultWindow.h"
#import "Dialogs.h"
#import "Utils.h"
#import "PyDupeGuru.h"
@implementation ResultWindow
/* Override */
- (void)initResultColumns
{
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 162, 16, 0, YES, nil},
{@"folder_path", 142, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"dimensions", 73, 16, 0, YES, nil},
{@"exif_timestamp", 120, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 58, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
}
/* Actions */
- (void)clearPictureCache
{
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[model clearPictureCache];
}
@end

View File

@@ -1,17 +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
from hscommon.trans import install_gettext_trans_under_cocoa
install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import *
from inter.app_pe import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module.
import distutils.sysconfig

Binary file not shown.

View File

@@ -1,14 +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 "AppDelegateBase.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase {}
@end

View File

@@ -1,52 +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 "AppDelegate.h"
#import "ProgressController.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
#import "Consts.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(1) forKey:@"scanType"];
[d setObject:i2n(80) forKey:@"minMatchPercentage"];
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
[d setObject:b2n(YES) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
_directoryPanel = nil;
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
@end

View File

@@ -1,13 +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 "DetailsPanelBase.h"
@interface DetailsPanel : DetailsPanelBase
@end

View File

@@ -1,17 +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 "DetailsPanel.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
@end

View File

@@ -1,13 +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 "ResultWindowBase.h"
@interface ResultWindow : ResultWindowBase {}
@end

View File

@@ -1,52 +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 "ResultWindow.h"
#import "Utils.h"
#import "Consts.h"
#import "PyDupeGuru.h"
@implementation ResultWindow
/* Override */
- (void)initResultColumns
{
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 195, 16, 0, YES, nil},
{@"folder_path", 183, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 60, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[table columns] initializeColumns:defs];
NSTableColumn *c = [matches tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [matches tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
[[table columns] restoreColumns];
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setScanType:n2i([ud objectForKey:@"scanType"])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[model setSizeThreshold:sizeThreshold];
}
@end

View File

@@ -1,5 +1,5 @@
ownerclass = 'DetailsPanel' ownerclass = 'DetailsPanelPicture'
ownerimport = 'DetailsPanel.h' ownerimport = 'DetailsPanelPicture.h'
result = Panel(593, 398, "Details of Selected File") result = Panel(593, 398, "Details of Selected File")
table = TableView(result) table = TableView(result)

View File

@@ -5,6 +5,10 @@ result = Window(425, 300, "dupeGuru")
promptLabel = Label(result, "Select folders to scan and press \"Scan\".") promptLabel = Label(result, "Select folders to scan and press \"Scan\".")
directoryOutline = OutlineView(result) directoryOutline = OutlineView(result)
directoryOutline.OBJC_CLASS = 'HSOutlineView' directoryOutline.OBJC_CLASS = 'HSOutlineView'
appModeSelector = SegmentedControl(result)
appModeLabel = Label(result, "Application Mode:")
scanTypePopup = Popup(result)
scanTypeLabel = Label(result, "Scan Type:")
addButton = Button(result, "") addButton = Button(result, "")
removeButton = Button(result, "") removeButton = Button(result, "")
loadResultsButton = Button(result, "Load Results") loadResultsButton = Button(result, "Load Results")
@@ -13,6 +17,8 @@ addPopup = Popup(None)
loadRecentPopup = Popup(None) loadRecentPopup = Popup(None)
owner.outlineView = directoryOutline owner.outlineView = directoryOutline
owner.appModeSelector = appModeSelector
owner.scanTypePopup = scanTypePopup
owner.removeButton = removeButton owner.removeButton = removeButton
owner.loadResultsButton = loadResultsButton owner.loadResultsButton = loadResultsButton
owner.addButtonPopUp = addPopup owner.addButtonPopUp = addPopup
@@ -20,7 +26,9 @@ owner.loadRecentButtonPopUp = loadRecentPopup
result.autosaveName = 'DirectoryPanel' result.autosaveName = 'DirectoryPanel'
result.canMinimize = False result.canMinimize = False
result.minSize = Size(370, 270) result.minSize = Size(400, 270)
for label in ["Standard", "Music", "Picture"]:
appModeSelector.addSegment(label, 80)
addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle
addButton.image = 'NSAddTemplate' addButton.image = 'NSAddTemplate'
removeButton.image = 'NSRemoveTemplate' removeButton.image = 'NSRemoveTemplate'
@@ -28,6 +36,7 @@ for button in (addButton, removeButton):
button.style = const.NSTexturedRoundedBezelStyle button.style = const.NSTexturedRoundedBezelStyle
button.imagePosition = const.NSImageOnly button.imagePosition = const.NSImageOnly
scanButton.keyEquivalent = '\\r' scanButton.keyEquivalent = '\\r'
appModeSelector.action = Action(owner, 'changeAppMode:')
addButton.action = Action(owner, 'popupAddDirectoryMenu:') addButton.action = Action(owner, 'popupAddDirectoryMenu:')
removeButton.action = Action(owner, 'removeSelectedDirectory') removeButton.action = Action(owner, 'removeSelectedDirectory')
loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:') loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:')
@@ -46,18 +55,21 @@ directoryOutline.allowsColumnReordering = False
directoryOutline.allowsColumnSelection = False directoryOutline.allowsColumnSelection = False
directoryOutline.allowsMultipleSelection = True directoryOutline.allowsMultipleSelection = True
appModeLabel.width = scanTypeLabel.width = 110
scanTypePopup.width = 248
appModeLayout = HLayout([appModeLabel, appModeSelector])
scanTypeLayout = HLayout([scanTypeLabel, scanTypePopup])
for button in (addButton, removeButton): for button in (addButton, removeButton):
button.width = 28 button.width = 28
for button in (loadResultsButton, scanButton): for button in (loadResultsButton, scanButton):
button.width = 118 button.width = 118
buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton]) buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton])
promptLabel.packToCorner(Pack.UpperLeft) mainLayout = VLayout([appModeLayout, scanTypeLayout, promptLabel, directoryOutline, buttonLayout], filler=directoryOutline)
promptLabel.fill(Pack.Right) mainLayout.packToCorner(Pack.UpperLeft)
mainLayout.fill(Pack.LowerRight)
directoryOutline.packRelativeTo(promptLabel, Pack.Below) directoryOutline.packRelativeTo(promptLabel, Pack.Below)
buttonLayout.packRelativeTo(directoryOutline, Pack.Below, margin=8)
directoryOutline.fill(Pack.LowerRight)
buttonLayout.fill(Pack.Right)
promptLabel.setAnchor(Pack.UpperLeft, growX=True) promptLabel.setAnchor(Pack.UpperLeft, growX=True)
directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True) directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@@ -1,6 +1,5 @@
ownerclass = 'AppDelegateBase' ownerclass = 'AppDelegate'
ownerimport = 'AppDelegateBase.h' ownerimport = 'AppDelegate.h'
edition = args.get('edition', 'se')
result = Menu("") result = Menu("")
appMenu = result.addMenu("dupeGuru") appMenu = result.addMenu("dupeGuru")
@@ -30,10 +29,7 @@ owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s') fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s')
fileMenu.addItem("Export Results to XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e') fileMenu.addItem("Export Results to XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e')
fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV')) fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV'))
if edition == 'pe': fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
elif edition == 'me':
fileMenu.addItem("Remove Dead Tracks in iTunes", Action(owner, 'removeDeadTracks'))
editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a') editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a')
editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a') editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a')

View File

@@ -1,26 +1,14 @@
edition = args.get('edition', 'se') appmode = args.get('appmode', 'standard')
dialogTitles = {
'se': "dupeGuru Preferences",
'me': "dupeGuru ME Preferences",
'pe': "dupeGuru PE Preferences",
}
dialogHeights = { dialogHeights = {
'se': 345, 'standard': 325,
'me': 365, 'music': 345,
'pe': 275, 'picture': 255,
}
scanTypeNames = {
'se': ["Filename", "Content", "Folders"],
'me': ["Filename", "Filename - Fields", "Filename - Fields (No Order)", "Tags", "Content", "Audio Content"],
'pe': ["Contents", "EXIF Timestamp"],
} }
result = Window(410, dialogHeights[edition], dialogTitles[edition]) result = Window(410, dialogHeights[appmode], "dupeGuru Preferences")
tabView = TabView(result) tabView = TabView(result)
basicTab = tabView.addTab("Basic") basicTab = tabView.addTab("Basic")
advancedTab = tabView.addTab("Advanced") advancedTab = tabView.addTab("Advanced")
scanTypePopup = Popup(basicTab.view, scanTypeNames[edition])
scanTypeLabel = Label(basicTab.view, "Scan Type:")
thresholdSlider = Slider(basicTab.view, 1, 100, 80) thresholdSlider = Slider(basicTab.view, 1, 100, 80)
thresholdLabel = Label(basicTab.view, "Filter hardness:") thresholdLabel = Label(basicTab.view, "Filter hardness:")
moreResultsLabel = Label(basicTab.view, "More results") moreResultsLabel = Label(basicTab.view, "More results")
@@ -28,19 +16,19 @@ fewerResultsLabel = Label(basicTab.view, "Fewer results")
thresholdValueLabel = Label(basicTab.view, "") thresholdValueLabel = Label(basicTab.view, "")
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"]) fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
fontSizeLabel = Label(basicTab.view, "Font Size:") fontSizeLabel = Label(basicTab.view, "Font Size:")
if edition in ('se', 'me'): if appmode in ('standard', 'music'):
wordWeightingBox = Checkbox(basicTab.view, "Word weighting") wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words") matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
elif edition == 'pe': elif appmode == 'picture':
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions") matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
mixKindBox = Checkbox(basicTab.view, "Can mix file kind") mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move") removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates") checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
if edition == 'se': if appmode == 'standard':
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:") ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
smallFilesThresholdText = TextField(basicTab.view, "") smallFilesThresholdText = TextField(basicTab.view, "")
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB") smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
elif edition == 'me': elif appmode == 'music':
tagsToScanLabel = Label(basicTab.view, "Tags to scan:") tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
trackBox = Checkbox(basicTab.view, "Track") trackBox = Checkbox(basicTab.view, "Track")
artistBox = Checkbox(basicTab.view, "Artist") artistBox = Checkbox(basicTab.view, "Artist")
@@ -59,8 +47,6 @@ copyMoveLabel = Label(advancedTab.view, "Copy and Move:")
copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"]) copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"])
resetToDefaultsButton = Button(result, "Reset To Defaults") resetToDefaultsButton = Button(result, "Reset To Defaults")
scanTypePopup.bind('selectedIndex', defaults, 'values.scanType')
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage') thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage') thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
fontSizeCombo.bind('value', defaults, 'values.TableFontSize') fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
@@ -72,59 +58,61 @@ ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
debugModeCheckbox.bind('value', defaults, 'values.DebugMode') debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
customCommandText.bind('value', defaults, 'values.CustomCommand') customCommandText.bind('value', defaults, 'values.CustomCommand')
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType') copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
if edition in ('se', 'me'): if appmode in ('standard', 'music'):
wordWeightingBox.bind('value', defaults, 'values.wordWeighting') wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords') matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox] disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
for control in disableWhenContentScan: for control in disableWhenContentScan:
control.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsNotContent') vtname = 'vtScanTypeMusicIsNotContent' if appmode == 'music' else 'vtScanTypeIsNotContent'
if edition == 'se': prefname = 'values.scanTypeMusic' if appmode == 'music' else 'values.scanTypeStandard'
control.bind('enabled', defaults, prefname, valueTransformer=vtname)
if appmode == 'standard':
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles') ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold') smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
elif edition == 'me': elif appmode == 'music':
for box in tagBoxes: for box in tagBoxes:
box.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsTag') box.bind('enabled', defaults, 'values.scanTypeMusic', valueTransformer='vtScanTypeIsTag')
trackBox.bind('value', defaults, 'values.scanTagTrack') trackBox.bind('value', defaults, 'values.scanTagTrack')
artistBox.bind('value', defaults, 'values.scanTagArtist') artistBox.bind('value', defaults, 'values.scanTagArtist')
albumBox.bind('value', defaults, 'values.scanTagAlbum') albumBox.bind('value', defaults, 'values.scanTagAlbum')
titleBox.bind('value', defaults, 'values.scanTagTitle') titleBox.bind('value', defaults, 'values.scanTagTitle')
genreBox.bind('value', defaults, 'values.scanTagGenre') genreBox.bind('value', defaults, 'values.scanTagGenre')
yearBox.bind('value', defaults, 'values.scanTagYear') yearBox.bind('value', defaults, 'values.scanTagYear')
elif edition == 'pe': elif appmode == 'picture':
matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled') matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled')
thresholdSlider.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsFuzzy') thresholdSlider.bind('enabled', defaults, 'values.scanTypePicture', valueTransformer='vtScanTypeIsFuzzy')
result.canResize = False result.canResize = False
result.canMinimize = False result.canMinimize = False
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal) thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
thresholdValueLabel.formatter.maximumFractionDigits = 0 thresholdValueLabel.formatter.maximumFractionDigits = 0
allLabels = [scanTypeLabel, thresholdValueLabel, moreResultsLabel, fewerResultsLabel, allLabels = [thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel] thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox, allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
ignoreHardlinksBox, debugModeCheckbox] ignoreHardlinksBox, debugModeCheckbox]
if edition == 'se': if appmode == 'standard':
allLabels += [smallFilesThresholdSuffixLabel] allLabels += [smallFilesThresholdSuffixLabel]
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox] allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
elif edition == 'me': elif appmode == 'music':
allLabels += [tagsToScanLabel] allLabels += [tagsToScanLabel]
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox] allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
elif edition == 'pe': elif appmode == 'picture':
allCheckboxes += [matchDifferentDimensionsBox] allCheckboxes += [matchDifferentDimensionsBox]
for label in allLabels: for label in allLabels:
label.controlSize = ControlSize.Small label.controlSize = ControlSize.Small
fewerResultsLabel.alignment = TextAlignment.Right fewerResultsLabel.alignment = TextAlignment.Right
for checkbox in allCheckboxes: for checkbox in allCheckboxes:
checkbox.font = scanTypeLabel.font checkbox.font = thresholdValueLabel.font
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:') resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
scanTypeLabel.width = thresholdLabel.width = fontSizeLabel.width = 94 thresholdLabel.width = fontSizeLabel.width = 94
fontSizeCombo.width = 66 fontSizeCombo.width = 66
thresholdValueLabel.width = 25 thresholdValueLabel.width = 25
resetToDefaultsButton.width = 136 resetToDefaultsButton.width = 136
if edition == 'se': if appmode == 'standard':
smallFilesThresholdText.width = 60 smallFilesThresholdText.width = 60
smallFilesThresholdSuffixLabel.width = 40 smallFilesThresholdSuffixLabel.width = 40
elif edition == 'me': elif appmode == 'music':
for box in tagBoxes: for box in tagBoxes:
box.width = 70 box.width = 70
@@ -133,22 +121,18 @@ tabView.fill(Pack.Right)
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right) resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
tabView.fill(Pack.Below, margin=14) tabView.fill(Pack.Below, margin=14)
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True) tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
scanTypePopup.packToCorner(Pack.UpperRight) thresholdLayout = HLayout([thresholdLabel, thresholdSlider, thresholdValueLabel], filler=thresholdSlider)
scanTypeLabel.packRelativeTo(scanTypePopup, Pack.Left) thresholdLayout.packToCorner(Pack.UpperLeft)
scanTypePopup.fill(Pack.Left) thresholdLayout.fill(Pack.Right)
thresholdSlider.packRelativeTo(scanTypePopup, Pack.Below)
thresholdValueLabel.packRelativeTo(thresholdSlider, Pack.Right)
thresholdSlider.fill(Pack.Right)
# We want to give the labels as much space as possible, and we only "know" how much is available # We want to give the labels as much space as possible, and we only "know" how much is available
# after the slider's fill operation. # after the slider's fill operation.
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2 moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6) moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6) fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
thresholdLabel.packRelativeTo(thresholdSlider, Pack.Left)
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below) fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left) fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
if edition == 'me': if appmode == 'music':
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below) tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
tagsToScanLabel.fill(Pack.Left) tagsToScanLabel.fill(Pack.Left)
tagsToScanLabel.fill(Pack.Right) tagsToScanLabel.fill(Pack.Right)
@@ -163,13 +147,13 @@ if edition == 'me':
else: else:
viewToPackCheckboxesUnder = fontSizeCombo viewToPackCheckboxesUnder = fontSizeCombo
if edition == 'se': if appmode == 'standard':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
ignoreSmallFilesBox] ignoreSmallFilesBox]
elif edition == 'me': elif appmode == 'music':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox] checkForUpdatesBox]
elif edition == 'pe': elif appmode == 'picture':
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox, checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox] checkForUpdatesBox]
checkboxLayout = VLayout(checkboxesToLayout) checkboxLayout = VLayout(checkboxesToLayout)
@@ -177,7 +161,7 @@ checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
checkboxLayout.fill(Pack.Left) checkboxLayout.fill(Pack.Left)
checkboxLayout.fill(Pack.Right) checkboxLayout.fill(Pack.Right)
if edition == 'se': if appmode == 'standard':
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4) smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4) checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
checkForUpdatesBox.fill(Pack.Right) checkForUpdatesBox.fill(Pack.Right)

View File

@@ -5,11 +5,12 @@ result = Window(610, 400, "Re-Prioritize duplicates")
promptLabel = Label(result, "Add criteria to the right box and click OK to send the dupes that " promptLabel = Label(result, "Add criteria to the right box and click OK to send the dupes that "
"correspond the best to these criteria to their respective group's reference position. Read " "correspond the best to these criteria to their respective group's reference position. Read "
"the help file for more information.") "the help file for more information.")
categoryPopup = Popup(result) split = SplitView(result, 2, vertical=True)
criteriaTable = ListView(result) categoryPopup = Popup(split.subviews[0])
prioritizationTable = ListView(result) criteriaTable = ListView(split.subviews[0])
addButton = Button(result, NLSTR("-->")) prioritizationTable = ListView(split.subviews[1])
removeButton = Button(result, NLSTR("<--")) addButton = Button(split.subviews[1], NLSTR("-->"))
removeButton = Button(split.subviews[1], NLSTR("<--"))
okButton = Button(result, "Ok") okButton = Button(result, "Ok")
cancelButton = Button(result, "Cancel") cancelButton = Button(result, "Cancel")
@@ -27,24 +28,38 @@ cancelButton.action = Action(owner, 'cancel')
okButton.keyEquivalent = '\\r' okButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e' cancelButton.keyEquivalent = '\\e'
# For layouts to correctly work, subviews need to have the dimensions they'll approximately have
# at runtime.
split.subviews[0].width = 260
split.subviews[0].height = 260
split.subviews[1].width = 340
split.subviews[1].height = 260
promptLabel.height *= 3 # 3 lines promptLabel.height *= 3 # 3 lines
leftLayout = VLayout([categoryPopup, criteriaTable], width=262, filler=criteriaTable) leftLayout = VLayout([categoryPopup, criteriaTable], filler=criteriaTable)
middleLayout = VLayout([addButton, removeButton], width=41) middleLayout = VLayout([addButton, removeButton], width=41)
buttonLayout = HLayout([None, cancelButton, okButton]) buttonLayout = HLayout([None, cancelButton, okButton])
#pack split subview 0
leftLayout.fillAll()
#pack split subview 1
prioritizationTable.fillAll()
prioritizationTable.width -= 48
prioritizationTable.moveTo(Pack.Right)
middleLayout.moveNextTo(prioritizationTable, Pack.Left, align=Pack.Middle)
# Main layout
promptLabel.packToCorner(Pack.UpperLeft) promptLabel.packToCorner(Pack.UpperLeft)
promptLabel.fill(Pack.Right) promptLabel.fill(Pack.Right)
leftLayout.packRelativeTo(promptLabel, Pack.Below) split.moveNextTo(promptLabel, Pack.Below)
middleLayout.packRelativeTo(leftLayout, Pack.Right, align=Pack.Above) buttonLayout.moveNextTo(split, Pack.Below)
prioritizationTable.packRelativeTo(middleLayout, Pack.Right, align=Pack.Above)
buttonLayout.packRelativeTo(leftLayout, Pack.Below)
buttonLayout.fill(Pack.Right) buttonLayout.fill(Pack.Right)
leftLayout.fill(Pack.Below) split.fill(Pack.LowerRight)
middleLayout.packRelativeTo(leftLayout, Pack.Right, align=Pack.Middle)
prioritizationTable.fill(Pack.Below, goal=leftLayout.y)
prioritizationTable.fill(Pack.Right)
promptLabel.setAnchor(Pack.UpperLeft, growX=True) promptLabel.setAnchor(Pack.UpperLeft, growX=True)
prioritizationTable.setAnchor(Pack.UpperLeft, growX=True, growY=True) prioritizationTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
categoryPopup.setAnchor(Pack.UpperLeft, growX=True)
criteriaTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
split.setAnchor(Pack.UpperLeft, growX=True, growY=True)
buttonLayout.setAnchor(Pack.Below) buttonLayout.setAnchor(Pack.Below)

View File

@@ -1,5 +1,5 @@
ownerclass = 'ResultWindowBase' ownerclass = 'ResultWindow'
ownerimport = 'ResultWindowBase.h' ownerimport = 'ResultWindow.h'
result = Window(557, 400, "dupeGuru Results") result = Window(557, 400, "dupeGuru Results")
toolbar = result.createToolbar('ResultsToolbar') toolbar = result.createToolbar('ResultsToolbar')

BIN
cocoa/waf vendored

Binary file not shown.

View File

@@ -9,13 +9,8 @@ out = 'build'
def options(opt): def options(opt):
opt.load('compiler_c python') opt.load('compiler_c python')
opt.add_option('--edition', default='se', help="dupeGuru edition to build (se, me pe)")
def configure(conf): def configure(conf):
if conf.options.edition not in ('se', 'me', 'pe'):
conf.options.edition = 'se'
print("Building dupeGuru {}".format(conf.options.edition.upper()))
conf.env.DGEDITION = conf.options.edition
# We use clang to compile our app # We use clang to compile our app
conf.env.CC = 'clang' conf.env.CC = 'clang'
# WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking # WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking
@@ -26,34 +21,35 @@ def configure(conf):
# I did a lot of fiddling-around, but I didn't find how to tell WAF the Python library name # I did a lot of fiddling-around, but I didn't find how to tell WAF the Python library name
# to look for without making the whole compilation process fail, so I just create a symlink # to look for without making the whole compilation process fail, so I just create a symlink
# with the name WAF is looking for. # with the name WAF is looking for.
versioned_dylib_path = '../build/libpython{}.dylib'.format(sys.version[:3]) versioned_dylib_path = '../build/libpython{}m.dylib'.format(sys.version[:3])
if not op.exists(versioned_dylib_path): if not op.exists(versioned_dylib_path):
os.symlink('../build/Python', versioned_dylib_path) os.symlink('../build/Python', versioned_dylib_path)
# The rest is standard WAF code that you can find the the python and macapp demos. # The rest is standard WAF code that you can find the the python and macapp demos.
conf.load('compiler_c python') conf.load('compiler_c python')
conf.check_python_version((3,2,0)) conf.check_python_version((3,4,0))
conf.check_python_headers() conf.check_python_headers()
conf.env.FRAMEWORK_COCOA = 'Cocoa' conf.env.FRAMEWORK_COCOA = 'Cocoa'
conf.env.ARCH_COCOA = ['i386', 'x86_64'] conf.env.ARCH_COCOA = ['x86_64']
# Add cocoalib dir to the framework search path so we can find Sparkle. conf.env.MACOSX_DEPLOYMENT_TARGET = '10.8'
conf.env.CFLAGS = ['-F'+op.abspath('../cocoalib')] conf.env.CFLAGS = ['-F'+op.abspath('Sparkle/build/Release')]
conf.env.LINKFLAGS = ['-F'+op.abspath('../cocoalib')] conf.env.LINKFLAGS = ['-F'+op.abspath('Sparkle/build/Release')]
def build(ctx): def build(ctx):
# What do we compile? # What do we compile?
cocoalib_node = ctx.srcnode.find_dir('..').find_dir('cocoalib') cocoalib_node = ctx.srcnode.find_dir('..').find_dir('cocoalib')
cocoalib_folders = ['controllers', 'views'] cocoalib_folders = ['controllers', 'views']
cocoalib_includes = [cocoalib_node] + [cocoalib_node.find_dir(folder) for folder in cocoalib_folders] 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', 'HSPyUtil', 'ProgressController', 'HSRecentFiles', 'HSQuicklook', 'ValueTransformers',
'NSImageAdditions', 'NSNotificationAdditions', 'NSImageAdditions', 'NSNotificationAdditions',
'views/HSTableView', 'views/HSOutlineView', 'views/NSIndexPathAdditions', 'views/HSTableView', 'views/HSOutlineView', 'views/NSIndexPathAdditions',
'views/NSTableViewAdditions', 'views/NSTableViewAdditions',
'controllers/HSColumns', 'controllers/HSGUIController', 'controllers/HSTable', 'controllers/HSColumns', 'controllers/HSGUIController', 'controllers/HSTable',
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList'] 'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
'controllers/HSTextField', 'controllers/HSProgressWindow']
cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m') cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m')
project_folders = ['autogen', 'base', ctx.env.DGEDITION] project_folders = [ctx.srcnode, ctx.srcnode.find_dir('autogen')]
project_src = sum([ctx.srcnode.ant_glob('%s/*.m' % folder) for folder in project_folders], []) project_src = ctx.srcnode.ant_glob('autogen/*.m') + ctx.srcnode.ant_glob('*.m')
# Compile # Compile
ctx.program( ctx.program(

1
cocoalib Submodule

Submodule cocoalib added at bb41785aaa

View File

@@ -1,39 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-12-30
# 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 sys
from optparse import OptionParser
import json
from hscommon.plat import ISOSX
def main(options):
if options.edition not in {'se', 'me', 'pe'}:
options.edition = 'se'
if options.ui not in {'cocoa', 'qt'}:
options.ui = 'cocoa' if ISOSX else 'qt'
build_type = 'Dev' if options.dev else 'Release'
print("Configuring dupeGuru {0} for UI {1} ({2})".format(options.edition.upper(), options.ui, build_type))
conf = {
'edition': options.edition,
'ui': options.ui,
'dev': options.dev,
}
json.dump(conf, open('conf.json', 'w'))
if __name__ == '__main__':
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option('--edition', dest='edition',
help="dupeGuru edition to build (se, me or pe). Default is se.")
parser.add_option('--ui', dest='ui',
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system.")
parser.add_option('--dev', action='store_true', dest='dev', default=False,
help="If this flag is set, will configure for dev builds.")
(options, args) = parser.parse_args()
main(options)

View File

@@ -1 +1,3 @@
__version__ = '4.0.0'
__appname__ = 'dupeGuru'

View File

@@ -1,29 +1,32 @@
# Created By: Virgil Dupras # Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
# Created On: 2006/11/11 #
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# # which should be included with this package. The terms are also available at
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # http://www.gnu.org/licenses/gpl-3.0.html
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import os import os
import os.path as op import os.path as op
import logging import logging
import subprocess import subprocess
import re import re
import time
import shutil import shutil
from send2trash import send2trash from send2trash import send2trash
from hscommon.reg import RegistrableApplication from hscommon.jobprogress import job
from hscommon.notify import Broadcaster from hscommon.notify import Broadcaster
from hscommon.path import Path from hscommon.path import Path
from hscommon.conflict import smart_move, smart_copy from hscommon.conflict import smart_move, smart_copy
from hscommon.util import (delete_if_empty, first, escape, nonone, format_time_decimal, allsame, from hscommon.gui.progress_window import ProgressWindow
rem_file_ext) from hscommon.util import delete_if_empty, first, escape, nonone, allsame
from hscommon.trans import tr from hscommon.trans import tr
from hscommon import desktop
from . import directories, results, scanner, export, fs from . import se, me, pe
from .pe.photo import get_delta_dimensions
from .util import cmp_value, fix_surrogate_encoding
from . import directories, results, export, fs, prioritize
from .ignore import IgnoreList
from .scanner import ScanType
from .gui.deletion_options import DeletionOptions from .gui.deletion_options import DeletionOptions
from .gui.details_panel import DetailsPanel from .gui.details_panel import DetailsPanel
from .gui.directory_tree import DirectoryTree from .gui.directory_tree import DirectoryTree
@@ -36,8 +39,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.") MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.") MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.")
MSG_MANY_FILES_TO_OPEN = tr("You're about to open many files at once. Depending on what those " MSG_MANY_FILES_TO_OPEN = tr(
"files are opened with, doing so can create quite a mess. Continue?") "You're about to open many files at once. Depending on what those "
"files are opened with, doing so can create quite a mess. Continue?"
)
class DestType: class DestType:
Direct = 0 Direct = 0
@@ -51,69 +56,89 @@ class JobType:
Copy = 'job_copy' Copy = 'job_copy'
Delete = 'job_delete' Delete = 'job_delete'
def format_timestamp(t, delta): class AppMode:
if delta: Standard = 0
return format_time_decimal(t) Music = 1
else: Picture = 2
if t > 0:
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
else:
return '---'
def format_words(w): JOBID2TITLE = {
def do_format(w): JobType.Scan: tr("Scanning for duplicates"),
if isinstance(w, list): JobType.Load: tr("Loading"),
return '(%s)' % ', '.join(do_format(item) for item in w) JobType.Move: tr("Moving"),
else: JobType.Copy: tr("Copying"),
return w.replace('\n', ' ') JobType.Delete: tr("Sending to Trash"),
}
return ', '.join(do_format(item) for item in w)
def format_perc(p): class DupeGuru(Broadcaster):
return "%0.0f" % p """Holds everything together.
def format_dupe_count(c): Instantiated once per running application, it holds a reference to every high-level object
return str(c) if c else '---' whose reference needs to be held: :class:`~core.results.Results`,
:class:`~core.directories.Directories`, :mod:`core.gui` instances, etc..
def cmp_value(dupe, attrname): It also hosts high level methods and acts as a coordinator for all those elements. This is why
if attrname == 'name': some of its methods seem a bit shallow, like for example :meth:`mark_all` and
value = rem_file_ext(dupe.name) :meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but
else: they are also followed by a notification call which is very important if we want GUI elements
value = getattr(dupe, attrname, '') to be correctly notified of a change in the data they're presenting.
return value.lower() if isinstance(value, str) else value
class DupeGuru(RegistrableApplication, Broadcaster): .. attribute:: directories
Instance of :class:`~core.directories.Directories`. It holds the current folder selection.
.. attribute:: results
Instance of :class:`core.results.Results`. Holds the results of the latest scan.
.. attribute:: selected_dupes
List of currently selected dupes from our :attr:`results`. Whenever the user changes its
selection at the UI level, :attr:`result_table` takes care of updating this attribute, so
you can trust that it's always up-to-date.
.. attribute:: result_table
Instance of :mod:`meta-gui <core.gui>` table listing the results from :attr:`results`
"""
#--- View interface #--- View interface
# get_default(key_name)
# set_default(key_name, value)
# show_message(msg)
# open_url(url)
# open_path(path) # open_path(path)
# reveal_path(path) # reveal_path(path)
# start_job(jobid, func, args=()) ( func(j, *args) )
# ask_yes_no(prompt) --> bool # ask_yes_no(prompt) --> bool
# create_results_window()
# show_results_window() # show_results_window()
# show_problem_dialog() # show_problem_dialog()
# select_dest_folder(prompt: str) --> str # select_dest_folder(prompt: str) --> str
# select_dest_file(prompt: str, ext: str) --> str # select_dest_file(prompt: str, ext: str) --> str
# in fairware prompts, we don't mention the edition, it's too long. NAME = PROMPT_NAME = "dupeGuru"
PROMPT_NAME = "dupeGuru"
DEMO_LIMITATION = tr("will only be able to delete, move or copy 10 duplicates at once") def __init__(self, view):
def __init__(self, view, appdata):
if view.get_default(DEBUG_MODE_PREFERENCE): if view.get_default(DEBUG_MODE_PREFERENCE):
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
logging.debug("Debug mode enabled") logging.debug("Debug mode enabled")
RegistrableApplication.__init__(self, view, appid=1)
Broadcaster.__init__(self) 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): if not op.exists(self.appdata):
os.makedirs(self.appdata) os.makedirs(self.appdata)
self.app_mode = AppMode.Standard
self.discarded_file_count = 0
self.directories = directories.Directories() self.directories = directories.Directories()
self.results = results.Results(self) self.results = results.Results(self)
self.scanner = scanner.Scanner() self.ignore_list = IgnoreList()
# In addition to "app-level" options, this dictionary also holds options that will be
# sent to the scanner. They don't have default values because those defaults values are
# defined in the scanner class.
self.options = { self.options = {
'escape_filter_regexp': True, 'escape_filter_regexp': True,
'clean_empty_dirs': False, 'clean_empty_dirs': False,
'ignore_hardlink_matches': False, 'ignore_hardlink_matches': False,
'copymove_dest_type': DestType.Relative, 'copymove_dest_type': DestType.Relative,
'cache_path': op.join(self.appdata, 'cached_pictures.db'),
} }
self.selected_dupes = [] self.selected_dupes = []
self.details_panel = DetailsPanel(self) self.details_panel = DetailsPanel(self)
@@ -121,37 +146,59 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self.problem_dialog = ProblemDialog(self) self.problem_dialog = ProblemDialog(self)
self.ignore_list_dialog = IgnoreListDialog(self) self.ignore_list_dialog = IgnoreListDialog(self)
self.stats_label = StatsLabel(self) self.stats_label = StatsLabel(self)
self.result_table = self._create_result_table() self.result_table = None
self.deletion_options = DeletionOptions() self.deletion_options = DeletionOptions()
children = [self.result_table, self.directory_tree, self.stats_label, self.details_panel] self.progress_window = ProgressWindow(self._job_completed, self._job_error)
children = [self.directory_tree, self.stats_label, self.details_panel]
for child in children: for child in children:
child.connect() child.connect()
#--- Virtual
def _get_display_info(self, dupe, group, delta):
raise NotImplementedError()
def _prioritization_categories(self):
raise NotImplementedError()
def _create_result_table(self):
raise NotImplementedError()
#--- Private #--- Private
def _recreate_result_table(self):
if self.result_table is not None:
self.result_table.disconnect()
if self.app_mode == AppMode.Picture:
self.result_table = pe.result_table.ResultTable(self)
elif self.app_mode == AppMode.Music:
self.result_table = me.result_table.ResultTable(self)
else:
self.result_table = se.result_table.ResultTable(self)
self.result_table.connect()
self.view.create_results_window()
def _get_dupe_sort_key(self, dupe, get_group, key, delta): def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
if self.app_mode == AppMode.Picture:
if delta and key == 'dimensions':
r = cmp_value(dupe, key)
ref_value = cmp_value(get_group().ref, key)
return get_delta_dimensions(r, ref_value)
if key == 'marked':
return self.results.is_marked(dupe)
if key == 'percentage': if key == 'percentage':
m = get_group().get_match_of(dupe) m = get_group().get_match_of(dupe)
return m.percentage return m.percentage
if key == 'dupe_count': elif key == 'dupe_count':
return 0 return 0
if key == 'marked': else:
return self.results.is_marked(dupe) result = cmp_value(dupe, key)
r = cmp_value(dupe, key) if delta:
if delta and (key in self.result_table.DELTA_COLUMNS): refval = cmp_value(get_group().ref, key)
r -= cmp_value(get_group().ref, key) if key in self.result_table.DELTA_COLUMNS:
return r result -= refval
else:
same = cmp_value(dupe, key) == refval
result = (same, result)
return result
def _get_group_sort_key(self, group, key): def _get_group_sort_key(self, group, key):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
if key == 'percentage': if key == 'percentage':
return group.percentage return group.percentage
if key == 'dupe_count': if key == 'dupe_count':
@@ -159,15 +206,15 @@ class DupeGuru(RegistrableApplication, Broadcaster):
if key == 'marked': if key == 'marked':
return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)]) return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])
return cmp_value(group.ref, key) return cmp_value(group.ref, key)
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion): def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion) return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
j.start_job(self.results.mark_count) j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion): def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):
if not dupe.path.exists(): if not dupe.path.exists():
return return
@@ -185,12 +232,12 @@ class DupeGuru(RegistrableApplication, Broadcaster):
ref = group.ref ref = group.ref
linkfunc = os.link if use_hardlinks else os.symlink linkfunc = os.link if use_hardlinks else os.symlink
linkfunc(str(ref.path), str_path) 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): def _create_file(self, path):
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths. # We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
return fs.get_file(path, self.directories.fileclasses + [fs.Folder]) return fs.get_file(path, self.fileclasses + [fs.Folder])
def _get_file(self, str_path): def _get_file(self, str_path):
path = Path(str_path) path = Path(str_path)
f = self._create_file(path) f = self._create_file(path)
@@ -201,40 +248,52 @@ class DupeGuru(RegistrableApplication, Broadcaster):
return f return f
except EnvironmentError: except EnvironmentError:
return None return None
def _get_export_data(self): def _get_export_data(self):
columns = [col for col in self.result_table.columns.ordered_columns columns = [
if col.visible and col.name != 'marked'] col for col in self.result_table.columns.ordered_columns
if col.visible and col.name != 'marked'
]
colnames = [col.display for col in columns] colnames = [col.display for col in columns]
rows = [] rows = []
for group_id, group in enumerate(self.results.groups): for group_id, group in enumerate(self.results.groups):
for dupe in group: for dupe in group:
data = self.get_display_info(dupe, 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) row.insert(0, group_id)
rows.append(row) rows.append(row)
return colnames, rows return colnames, rows
def _results_changed(self): def _results_changed(self):
self.selected_dupes = [d for d in self.selected_dupes self.selected_dupes = [
if self.results.get_group_of_duplicate(d) is not None] d for d in self.selected_dupes
if self.results.get_group_of_duplicate(d) is not None
]
self.notify('results_changed') self.notify('results_changed')
def _job_completed(self, jobid, exc): def _start_job(self, jobid, func, args=()):
# Must be called by subclasses when they detect that an async job is completed. If an title = JOBID2TITLE[jobid]
# exception was raised during the job, `exc` will be set. Return True when the error was try:
# handled. If we return False when exc is set, a the exception will be re-raised. self.progress_window.run(jobid, title, func, args=args)
if exc is not None: except job.JobInProgressError:
return False # We don't handle any exception in here msg = tr(
"A previous action is still hanging in there. You can't start a new one yet. Wait "
"a few seconds, then try again."
)
self.view.show_message(msg)
def _job_completed(self, jobid):
if jobid == JobType.Scan: if jobid == JobType.Scan:
self._results_changed() self._results_changed()
if not self.results.groups: if not self.results.groups:
self.view.show_message(tr("No duplicates found.")) self.view.show_message(tr("No duplicates found."))
else: else:
self.view.show_results_window() self.view.show_results_window()
if jobid in {JobType.Load, JobType.Move, JobType.Delete}: if jobid in {JobType.Move, JobType.Delete}:
self._results_changed() self._results_changed()
if jobid == JobType.Load: if jobid == JobType.Load:
self._recreate_result_table()
self._results_changed()
self.view.show_results_window() self.view.show_results_window()
if jobid in {JobType.Copy, JobType.Move, JobType.Delete}: if jobid in {JobType.Copy, JobType.Move, JobType.Delete}:
if self.results.problems: if self.results.problems:
@@ -247,7 +306,15 @@ class DupeGuru(RegistrableApplication, Broadcaster):
JobType.Delete: tr("All marked files were successfully sent to Trash."), JobType.Delete: tr("All marked files were successfully sent to Trash."),
}[jobid] }[jobid]
self.view.show_message(msg) self.view.show_message(msg)
def _job_error(self, jobid, err):
if jobid == JobType.Load:
msg = tr("Could not load file: {}").format(err)
self.view.show_message(msg)
return False
else:
raise err
@staticmethod @staticmethod
def _remove_hardlink_dupes(files): def _remove_hardlink_dupes(files):
seen_inodes = set() seen_inodes = set()
@@ -262,22 +329,38 @@ class DupeGuru(RegistrableApplication, Broadcaster):
seen_inodes.add(inode) seen_inodes.add(inode)
result.append(file) result.append(file)
return result return result
def _select_dupes(self, dupes): def _select_dupes(self, dupes):
if dupes == self.selected_dupes: if dupes == self.selected_dupes:
return return
self.selected_dupes = dupes self.selected_dupes = dupes
self.notify('dupes_selected') self.notify('dupes_selected')
def _check_demo(self): #--- Protected
if self.should_apply_demo_limitation and self.results.mark_count > 10: def _get_fileclasses(self):
msg = tr("You cannot delete, move or copy more than 10 duplicates at once in demo mode.") if self.app_mode == AppMode.Picture:
self.view.show_message(msg) return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
return False elif self.app_mode == AppMode.Music:
return True return [me.fs.MusicFile]
else:
return [se.fs.File]
def _prioritization_categories(self):
if self.app_mode == AppMode.Picture:
return pe.prioritize.all_categories()
elif self.app_mode == AppMode.Music:
return me.prioritize.all_categories()
else:
return prioritize.all_categories()
#--- Public #--- Public
def add_directory(self, d): def add_directory(self, d):
"""Adds folder ``d`` to :attr:`directories`.
Shows an error message dialog if something bad happens.
:param str d: path of folder to add
"""
try: try:
self.directories.add_path(Path(d)) self.directories.add_path(Path(d))
self.notify('directories_changed') self.notify('directories_changed')
@@ -285,8 +368,10 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self.view.show_message(tr("'{}' already is in the list.").format(d)) self.view.show_message(tr("'{}' already is in the list.").format(d))
except directories.InvalidPathError: except directories.InvalidPathError:
self.view.show_message(tr("'{}' does not exist.").format(d)) self.view.show_message(tr("'{}' does not exist.").format(d))
def add_selected_to_ignore_list(self): def add_selected_to_ignore_list(self):
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`.
"""
dupes = self.without_ref(self.selected_dupes) dupes = self.without_ref(self.selected_dupes)
if not dupes: if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES) self.view.show_message(MSG_NO_SELECTED_DUPES)
@@ -298,56 +383,67 @@ class DupeGuru(RegistrableApplication, Broadcaster):
g = self.results.get_group_of_duplicate(dupe) g = self.results.get_group_of_duplicate(dupe)
for other in g: for other in g:
if other is not dupe: if other is not dupe:
self.scanner.ignore_list.Ignore(str(other.path), str(dupe.path)) self.ignore_list.Ignore(str(other.path), str(dupe.path))
self.remove_duplicates(dupes) self.remove_duplicates(dupes)
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
def apply_filter(self, filter): def apply_filter(self, filter):
"""Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.
:param str filter: filter to apply
"""
self.results.apply_filter(None) self.results.apply_filter(None)
if self.options['escape_filter_regexp']: if self.options['escape_filter_regexp']:
filter = escape(filter, set('()[]\\.|+?^')) filter = escape(filter, set('()[]\\.|+?^'))
filter = escape(filter, '*', '.') filter = escape(filter, '*', '.')
self.results.apply_filter(filter) self.results.apply_filter(filter)
self._results_changed() self._results_changed()
def clean_empty_dirs(self, path): def clean_empty_dirs(self, path):
if self.options['clean_empty_dirs']: if self.options['clean_empty_dirs']:
while delete_if_empty(path, ['.DS_Store']): while delete_if_empty(path, ['.DS_Store']):
path = path[:-1] path = path.parent()
def clear_picture_cache(self):
cache = pe.cache.Cache(self.options['cache_path'])
cache.clear()
cache.close()
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType): def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
source_path = dupe.path source_path = dupe.path
location_path = first(p for p in self.directories if dupe.path in p) location_path = first(p for p in self.directories if dupe.path in p)
dest_path = Path(destination) dest_path = Path(destination)
if dest_type in {DestType.Relative, DestType.Absolute}: if dest_type in {DestType.Relative, DestType.Absolute}:
# no filename, no windows drive letter # 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: if dest_type == DestType.Relative:
source_base = source_base[location_path:] source_base = source_base[location_path:]
dest_path = dest_path + source_base dest_path = dest_path[source_base]
if not dest_path.exists(): if not dest_path.exists():
dest_path.makedirs() dest_path.makedirs()
# Add filename to dest_path. For file move/copy, it's not required, but for folders, yes. # 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) logging.debug("Copy/Move operation from '%s' to '%s'", source_path, dest_path)
# Raises an EnvironmentError if there's a problem # Raises an EnvironmentError if there's a problem
if copy: if copy:
smart_copy(source_path, dest_path) smart_copy(source_path, dest_path)
else: else:
smart_move(source_path, dest_path) 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): def copy_or_move_marked(self, copy):
"""Start an async move (or copy) job on marked duplicates.
:param bool copy: If True, duplicates will be copied instead of moved
"""
def do(j): def do(j):
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
self.copy_or_move(dupe, copy, destination, desttype) self.copy_or_move(dupe, copy, destination, desttype)
j.start_job(self.results.mark_count) j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, not copy) self.results.perform_on_marked(op, not copy)
if not self._check_demo():
return
if not self.results.mark_count: if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
@@ -357,49 +453,65 @@ class DupeGuru(RegistrableApplication, Broadcaster):
if destination: if destination:
desttype = self.options['copymove_dest_type'] desttype = self.options['copymove_dest_type']
jobid = JobType.Copy if copy else JobType.Move jobid = JobType.Copy if copy else JobType.Move
self.view.start_job(jobid, do) self._start_job(jobid, do)
def delete_marked(self): def delete_marked(self):
if not self._check_demo(): """Start an async job to send marked duplicates to the trash.
return """
if not self.results.mark_count: if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
if not self.deletion_options.show(self.results.mark_count): if not self.deletion_options.show(self.results.mark_count):
return return
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks, args = [
self.deletion_options.direct] self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
self.deletion_options.direct
]
logging.debug("Starting deletion job with args %r", args) logging.debug("Starting deletion job with args %r", args)
self.view.start_job(JobType.Delete, self._do_delete, args=args) self._start_job(JobType.Delete, self._do_delete, args=args)
def export_to_xhtml(self): 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() colnames, rows = self._get_export_data()
export_path = export.export_to_xhtml(colnames, rows) export_path = export.export_to_xhtml(colnames, rows)
self.view.open_path(export_path) desktop.open_path(export_path)
def export_to_csv(self): 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') dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), 'csv')
if dest_file: if dest_file:
colnames, rows = self._get_export_data() colnames, rows = self._get_export_data()
export.export_to_csv(dest_file, colnames, rows) try:
export.export_to_csv(dest_file, colnames, rows)
except OSError as e:
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
def get_display_info(self, dupe, group, delta=False): def get_display_info(self, dupe, group, delta=False):
def empty_data(): def empty_data():
return {c.name: '---' for c in self.result_table.COLUMNS[1:]} return {c.name: '---' for c in self.result_table.COLUMNS[1:]}
if (dupe is None) or (group is None): if (dupe is None) or (group is None):
return empty_data() return empty_data()
try: try:
return self._get_display_info(dupe, group, delta) return dupe.get_display_info(group, delta)
except Exception as e: except Exception as e:
logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e)) logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e))
return empty_data() return empty_data()
def invoke_custom_command(self): def invoke_custom_command(self):
"""Calls command in 'CustomCommand' pref with %d and %r placeholders replaced. """Calls command in ``CustomCommand`` pref with ``%d`` and ``%r`` placeholders replaced.
Using the current selection, %d is replaced with the currently selected dupe and %r is Using the current selection, ``%d`` is replaced with the currently selected dupe and ``%r``
replaced with that dupe's ref file. If there's no selection, the command is not invoked. is replaced with that dupe's ref file. If there's no selection, the command is not invoked.
If the dupe is a ref, %d and %r will be the same. If the dupe is a ref, ``%d`` and ``%r`` will be the same.
""" """
cmd = self.view.get_default('CustomCommand') cmd = self.view.get_default('CustomCommand')
if not cmd: if not cmd:
@@ -423,20 +535,36 @@ class DupeGuru(RegistrableApplication, Broadcaster):
subprocess.Popen(exename + args, shell=True, cwd=path) subprocess.Popen(exename + args, shell=True, cwd=path)
else: else:
subprocess.Popen(cmd, shell=True) subprocess.Popen(cmd, shell=True)
def load(self): 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.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
self.notify('directories_changed') self.notify('directories_changed')
p = op.join(self.appdata, 'ignore_list.xml') p = op.join(self.appdata, 'ignore_list.xml')
self.scanner.ignore_list.load_from_xml(p) self.ignore_list.load_from_xml(p)
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
def load_from(self, filename): def load_from(self, filename):
"""Start an async job to load results from ``filename``.
:param str filename: path of the XML file (created with :meth:`save_as`) to load
"""
def do(j): def do(j):
self.results.load_from_xml(filename, self._get_file, j) self.results.load_from_xml(filename, self._get_file, j)
self.view.start_job(JobType.Load, do) self._start_job(JobType.Load, do)
def make_selected_reference(self): 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) dupes = self.without_ref(self.selected_dupes)
changed_groups = set() changed_groups = set()
for dupe in dupes: for dupe in dupes:
@@ -451,8 +579,10 @@ class DupeGuru(RegistrableApplication, Broadcaster):
# If no group was changed, however, we don't touch the selection. # If no group was changed, however, we don't touch the selection.
if not self.result_table.power_marker: if not self.result_table.power_marker:
if changed_groups: if changed_groups:
self.selected_dupes = [d for d in self.selected_dupes self.selected_dupes = [
if self.results.get_group_of_duplicate(d).ref is d] d for d in self.selected_dupes
if self.results.get_group_of_duplicate(d).ref is d
]
self.notify('results_changed') self.notify('results_changed')
else: else:
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit # If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
@@ -461,38 +591,59 @@ class DupeGuru(RegistrableApplication, Broadcaster):
# do is to keep our selection index-wise (different dupe selection, but same index # do is to keep our selection index-wise (different dupe selection, but same index
# selection). # selection).
self.notify('results_changed_but_keep_selection') self.notify('results_changed_but_keep_selection')
def mark_all(self): def mark_all(self):
"""Set all dupes in the results as marked.
"""
self.results.mark_all() self.results.mark_all()
self.notify('marking_changed') self.notify('marking_changed')
def mark_none(self): def mark_none(self):
"""Set all dupes in the results as unmarked.
"""
self.results.mark_none() self.results.mark_none()
self.notify('marking_changed') self.notify('marking_changed')
def mark_invert(self): def mark_invert(self):
"""Invert the marked state of all dupes in the results.
"""
self.results.mark_invert() self.results.mark_invert()
self.notify('marking_changed') self.notify('marking_changed')
def mark_dupe(self, dupe, marked): 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: if marked:
self.results.mark(dupe) self.results.mark(dupe)
else: else:
self.results.unmark(dupe) self.results.unmark(dupe)
self.notify('marking_changed') self.notify('marking_changed')
def open_selected(self): def open_selected(self):
"""Open :attr:`selected_dupes` with their associated application.
"""
if len(self.selected_dupes) > 10: if len(self.selected_dupes) > 10:
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN): if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
return return
for dupe in self.selected_dupes: for dupe in self.selected_dupes:
self.view.open_path(dupe.path) desktop.open_path(dupe.path)
def purge_ignore_list(self): def purge_ignore_list(self):
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s)) """Remove files that don't exist from :attr:`ignore_list`.
"""
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
def remove_directories(self, indexes): 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: try:
indexes = sorted(indexes, reverse=True) indexes = sorted(indexes, reverse=True)
for index in indexes: for index in indexes:
@@ -500,32 +651,49 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self.notify('directories_changed') self.notify('directories_changed')
except IndexError: except IndexError:
pass pass
def remove_duplicates(self, duplicates): 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.results.remove_duplicates(self.without_ref(duplicates))
self.notify('results_changed_but_keep_selection') self.notify('results_changed_but_keep_selection')
def remove_marked(self): def remove_marked(self):
"""Removed marked duplicates from the results (without touching the files themselves).
"""
if not self.results.mark_count: if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
msg = tr("You are about to remove %d files from results. Continue?") msg = tr("You are about to remove %d files from results. Continue?")
if not self.view.ask_yes_no(msg % self.results.mark_count): if not self.view.ask_yes_no(msg % self.results.mark_count):
return return
self.results.perform_on_marked(lambda x:None, True) self.results.perform_on_marked(lambda x: None, True)
self._results_changed() self._results_changed()
def remove_selected(self): def remove_selected(self):
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves).
"""
dupes = self.without_ref(self.selected_dupes) dupes = self.without_ref(self.selected_dupes)
if not dupes: if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES) self.view.show_message(MSG_NO_SELECTED_DUPES)
return return
msg = tr("You are about to remove %d files from results. Continue?") msg = tr("You are about to remove %d files from results. Continue?")
if not self.view.ask_yes_no(msg % len(dupes)): if not self.view.ask_yes_no(msg % len(dupes)):
return return
self.remove_duplicates(dupes) self.remove_duplicates(dupes)
def rename_selected(self, newname): 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: try:
d = self.selected_dupes[0] d = self.selected_dupes[0]
d.rename(newname) d.rename(newname)
@@ -533,8 +701,16 @@ class DupeGuru(RegistrableApplication, Broadcaster):
except (IndexError, fs.FSError) as e: except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % str(e)) logging.warning("dupeGuru Warning: %s" % str(e))
return False return False
def reprioritize_groups(self, sort_key): 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 count = 0
for group in self.results.groups: for group in self.results.groups:
if group.prioritize(key_func=sort_key): if group.prioritize(key_func=sort_key):
@@ -542,41 +718,60 @@ class DupeGuru(RegistrableApplication, Broadcaster):
self._results_changed() self._results_changed()
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count) msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
self.view.show_message(msg) self.view.show_message(msg)
def reveal_selected(self): def reveal_selected(self):
if self.selected_dupes: if self.selected_dupes:
self.view.reveal_path(self.selected_dupes[0].path) desktop.reveal_path(self.selected_dupes[0].path)
def save(self): def save(self):
if not op.exists(self.appdata): if not op.exists(self.appdata):
os.makedirs(self.appdata) os.makedirs(self.appdata)
self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml')) self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml'))
p = op.join(self.appdata, 'ignore_list.xml') p = op.join(self.appdata, 'ignore_list.xml')
self.scanner.ignore_list.save_to_xml(p) self.ignore_list.save_to_xml(p)
self.notify('save_session') self.notify('save_session')
def save_as(self, filename): def save_as(self, filename):
self.results.save_to_xml(filename) """Save results in ``filename``.
:param str filename: path of the file to save results (as XML) to.
"""
try:
self.results.save_to_xml(filename)
except OSError as e:
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
def start_scanning(self): def start_scanning(self):
def do(j): """Starts an async job to scan for duplicates.
j.set_progress(0, tr("Collecting files to scan"))
if self.scanner.scan_type == scanner.ScanType.Folders: Scans folders selected in :attr:`directories` and put the results in :attr:`results`
files = list(self.directories.get_folders(j)) """
else: scanner = self.SCANNER_CLASS()
files = list(self.directories.get_files(j))
if self.options['ignore_hardlink_matches']:
files = self._remove_hardlink_dupes(files)
logging.info('Scanning %d files' % len(files))
self.results.groups = self.scanner.get_dupe_groups(files, j)
if not self.directories.has_any_file(): if not self.directories.has_any_file():
self.view.show_message(tr("The selected directories contain no scannable file.")) self.view.show_message(tr("The selected directories contain no scannable file."))
return return
# Send relevant options down to the scanner instance
for k, v in self.options.items():
if hasattr(scanner, k):
setattr(scanner, k, v)
self.results.groups = [] self.results.groups = []
self._recreate_result_table()
self._results_changed() self._results_changed()
self.view.start_job(JobType.Scan, do)
def do(j):
j.set_progress(0, tr("Collecting files to scan"))
if scanner.scan_type == ScanType.Folders:
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
else:
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
if self.options['ignore_hardlink_matches']:
files = self._remove_hardlink_dupes(files)
logging.info('Scanning %d files' % len(files))
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
self.discarded_file_count = scanner.discarded_file_count
self._start_job(JobType.Scan, do)
def toggle_selected_mark_state(self): def toggle_selected_mark_state(self):
selected = self.without_ref(self.selected_dupes) selected = self.without_ref(self.selected_dupes)
if not selected: if not selected:
@@ -588,10 +783,12 @@ class DupeGuru(RegistrableApplication, Broadcaster):
for dupe in selected: for dupe in selected:
markfunc(dupe) markfunc(dupe)
self.notify('marking_changed') self.notify('marking_changed')
def without_ref(self, dupes): def without_ref(self, dupes):
"""Returns ``dupes`` with all reference elements removed.
"""
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe] return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
def get_default(self, key, fallback_value=None): def get_default(self, key, fallback_value=None):
result = nonone(self.view.get_default(key), fallback_value) result = nonone(self.view.get_default(key), fallback_value)
if fallback_value is not None and not isinstance(result, type(fallback_value)): if fallback_value is not None and not isinstance(result, type(fallback_value)):
@@ -601,15 +798,40 @@ class DupeGuru(RegistrableApplication, Broadcaster):
except Exception: except Exception:
result = fallback_value result = fallback_value
return result return result
def set_default(self, key, value): def set_default(self, key, value):
self.view.set_default(key, value) self.view.set_default(key, value)
#--- Properties #--- Properties
@property @property
def stat_line(self): def stat_line(self):
result = self.results.stat_line result = self.results.stat_line
if self.scanner.discarded_file_count: if self.discarded_file_count:
result = tr("%s (%d discarded)") % (result, self.scanner.discarded_file_count) result = tr("%s (%d discarded)") % (result, self.discarded_file_count)
return result return result
@property
def fileclasses(self):
return self._get_fileclasses()
@property
def SCANNER_CLASS(self):
if self.app_mode == AppMode.Picture:
return pe.scanner.ScannerPE
elif self.app_mode == AppMode.Music:
return me.scanner.ScannerME
else:
return se.scanner.ScannerSE
@property
def METADATA_TO_READ(self):
if self.app_mode == AppMode.Picture:
return ['size', 'mtime', 'dimensions', 'exif_timestamp']
elif self.app_mode == AppMode.Music:
return [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
else:
return ['size', 'mtime']

Some files were not shown because too many files have changed in this diff Show More