mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c20e5c770 | ||
|
|
edcff588e2 | ||
|
|
26aad6e948 | ||
|
|
c303a490ef | ||
|
|
6ed4499a97 | ||
|
|
aa7499aa12 | ||
|
|
63558d647a | ||
|
|
eb3f7d65de | ||
|
|
ac8a336c4a | ||
|
|
0206f2fd15 | ||
|
|
b41d3f7efc | ||
|
|
c43d37582e | ||
|
|
30a278719b | ||
|
|
87ef46ca15 | ||
|
|
9f3ec065ed | ||
|
|
e19056048c | ||
|
|
76e5817ff3 | ||
|
|
20dc2d63fd | ||
|
|
28d2aa8197 | ||
|
|
5be9d537a5 | ||
|
|
b97e89d4d8 | ||
|
|
0f4992de47 | ||
|
|
55ad9ef33a | ||
|
|
e69a1764a0 | ||
|
|
215307df93 | ||
|
|
3aa99c396b | ||
|
|
9f2c3e7732 | ||
|
|
d660cef245 | ||
|
|
bdd404ce0e | ||
|
|
df9f72d9bf | ||
|
|
53bbc5901c | ||
|
|
0959f4581e | ||
|
|
b1ef3dc8fe | ||
|
|
334f4dd2ae | ||
|
|
fbdd1d866e | ||
|
|
64e86c9ff9 | ||
|
|
80f659858c | ||
|
|
ef8f8f0e44 | ||
|
|
b7a7282c2a | ||
|
|
668821301c | ||
|
|
13fb06a693 | ||
|
|
61b219ff43 | ||
|
|
c4aeda0bd0 | ||
|
|
76f3332d36 | ||
|
|
b47b1e11af | ||
|
|
168d94910b | ||
|
|
ca3172044f | ||
|
|
f66849b09d | ||
|
|
8c1078aa71 | ||
|
|
b780816e3c | ||
|
|
fb8a384a6a | ||
|
|
2be4ae8f65 | ||
|
|
f8686ffb55 | ||
|
|
3093a42553 | ||
|
|
83d934fd4f | ||
|
|
f3c09c7a8d | ||
|
|
a65077f871 | ||
|
|
d4919054f9 | ||
|
|
773f6651e6 | ||
|
|
9a25670552 | ||
|
|
8c9ef3ea29 | ||
|
|
7256adb4d4 | ||
|
|
ad45a6e16e | ||
|
|
c865f84c16 | ||
|
|
7d749779f2 | ||
|
|
8b878b7b13 | ||
|
|
0056f696df | ||
|
|
abd2f3a9d6 | ||
|
|
5c57a2a8fc | ||
|
|
dc76f9744e | ||
|
|
130581db53 | ||
|
|
9ed4b7abf0 | ||
|
|
a0a90e8ef8 | ||
|
|
197acbf5b3 | ||
|
|
09d5243648 | ||
|
|
10169bee9c | ||
|
|
bb8a41f8c5 | ||
|
|
bb1f0f5be6 | ||
|
|
4b6f8b45e2 | ||
|
|
2ed1b82ecf | ||
|
|
de9122c3cb | ||
|
|
632650b483 | ||
|
|
c05f01853d | ||
|
|
15539eb3c5 | ||
|
|
b9874cc7ed | ||
|
|
13a2868dd2 | ||
|
|
abb1345c49 | ||
|
|
9c53b2218c | ||
|
|
4b3c1e2828 | ||
|
|
b64f9f5ec0 | ||
|
|
40d9a486e2 | ||
|
|
6930e092e0 | ||
|
|
6b41223a22 | ||
|
|
d15321a8e9 | ||
|
|
d6533cbfa2 |
5
.ctags
Normal file
5
.ctags
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-R
|
||||||
|
--exclude=build
|
||||||
|
--exclude=env
|
||||||
|
--exclude=.tox
|
||||||
|
--python-kinds=-i
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,25 +1,21 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.pyc
|
__pycache__
|
||||||
*.so
|
*.so
|
||||||
*.mo
|
*.mo
|
||||||
*.pyd
|
|
||||||
*.waf*
|
*.waf*
|
||||||
.lock-waf*
|
.lock-waf*
|
||||||
.idea
|
|
||||||
.tox
|
.tox
|
||||||
|
/tags
|
||||||
|
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
install
|
|
||||||
installer_tmp-cache
|
|
||||||
env
|
env
|
||||||
/deps
|
/deps
|
||||||
cocoa/autogen
|
cocoa/autogen
|
||||||
|
|
||||||
/run.py
|
/run.py
|
||||||
/conf.json
|
|
||||||
/cocoa/*/Info.plist
|
/cocoa/*/Info.plist
|
||||||
/cocoa/*/build
|
/cocoa/*/build
|
||||||
/qt/base/*_rc.py
|
/qt/*_rc.py
|
||||||
/help/*/conf.py
|
/help/*/conf.py
|
||||||
/help/*/changelog.rst
|
/help/*/changelog.rst
|
||||||
|
|||||||
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal 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
|
||||||
19
CREDITS
Normal file
19
CREDITS
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
To know who contributed to dupeGuru, you can look at the commit log, but not all contributions
|
||||||
|
result in a commit. This file lists contributors who don't necessarily appear in the commit log.
|
||||||
|
|
||||||
|
* Jérôme Cantin, Main icon
|
||||||
|
* Gregor Tätzner, German localization
|
||||||
|
* Frank Weber, German localization
|
||||||
|
* Eric Dee, Chinese localization
|
||||||
|
* Aleš Nehyba, Czech localization
|
||||||
|
* Paolo Rossi, Italian localization
|
||||||
|
* Hrant Ohanyan, Armenian localization
|
||||||
|
* Igor Pavlov, Russian localization
|
||||||
|
* Kyrill Detinov, Russian localization
|
||||||
|
* Yuri Petrashko, Ukrainian localization
|
||||||
|
* Nickolas Pohilets, Ukrainian localization
|
||||||
|
* Victor Figueiredo, Brazilian localization
|
||||||
|
* Phan Anh, Vietnamese localization
|
||||||
|
* Gabriel Koutilellis, Greek localization
|
||||||
|
|
||||||
|
Thanks!
|
||||||
111
Makefile
Normal file
111
Makefile
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
PYTHON ?= python3
|
||||||
|
REQ_MINOR_VERSION = 4
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
|
||||||
|
# If you're installing into a path that is not going to be the final path prefix (such as a
|
||||||
|
# sandbox), set DESTDIR to that path.
|
||||||
|
|
||||||
|
# Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we
|
||||||
|
# use one of each file to act as a representative, a target, of these groups.
|
||||||
|
submodules_target = hscommon/__init__.py
|
||||||
|
|
||||||
|
packages = hscommon qtlib core qt
|
||||||
|
localedirs = $(wildcard locale/*/LC_MESSAGES)
|
||||||
|
pofiles = $(wildcard locale/*/LC_MESSAGES/*.po)
|
||||||
|
mofiles = $(patsubst %.po,%.mo,$(pofiles))
|
||||||
|
|
||||||
|
vpath %.po $(localedirs)
|
||||||
|
vpath %.mo $(localedirs)
|
||||||
|
|
||||||
|
all : | run.py
|
||||||
|
@echo "Build complete! You can run dupeGuru with 'make run'"
|
||||||
|
|
||||||
|
run.py : | env i18n modules qt/dg_rc.py
|
||||||
|
cp qt/run_template.py run.py
|
||||||
|
|
||||||
|
run: | run.py
|
||||||
|
./env/bin/python run.py
|
||||||
|
|
||||||
|
pyc:
|
||||||
|
${PYTHON} -m compileall ${packages}
|
||||||
|
|
||||||
|
reqs :
|
||||||
|
@ret=`${PYTHON} -c "import sys; print(int(sys.version_info[:2] >= (3, ${REQ_MINOR_VERSION})))"`; \
|
||||||
|
if [ $${ret} -ne 1 ]; then \
|
||||||
|
echo "Python 3.${REQ_MINOR_VERSION}+ required. Aborting."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@${PYTHON} -m venv -h > /dev/null || \
|
||||||
|
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
|
||||||
|
@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \
|
||||||
|
{ echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
|
||||||
|
|
||||||
|
# Ensure that submodules are initialized
|
||||||
|
$(submodules_target) :
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
|
||||||
|
env : | $(submodules_target) reqs
|
||||||
|
@echo "Creating our virtualenv"
|
||||||
|
${PYTHON} -m venv env --system-site-packages
|
||||||
|
./env/bin/python -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
build/help : | env
|
||||||
|
./env/bin/python build.py --doc
|
||||||
|
|
||||||
|
qt/dg_rc.py : qt/dg.qrc
|
||||||
|
pyrcc5 qt/dg.qrc > qt/dg_rc.py
|
||||||
|
|
||||||
|
i18n: $(mofiles)
|
||||||
|
|
||||||
|
%.mo : %.po
|
||||||
|
msgfmt -o $@ $<
|
||||||
|
|
||||||
|
core/pe/_block.*.so : core/pe/modules/block.c core/pe/modules/common.c | env
|
||||||
|
./env/bin/python hscommon/build_ext.py $^ _block
|
||||||
|
mv _block.*.so core/pe
|
||||||
|
|
||||||
|
core/pe/_cache.*.so : core/pe/modules/cache.c core/pe/modules/common.c | env
|
||||||
|
./env/bin/python hscommon/build_ext.py $^ _cache
|
||||||
|
mv _cache.*.so core/pe
|
||||||
|
|
||||||
|
qt/pe/_block_qt.*.so : qt/pe/modules/block.c | env
|
||||||
|
./env/bin/python hscommon/build_ext.py $^ _block_qt
|
||||||
|
mv _block_qt.*.so qt/pe
|
||||||
|
|
||||||
|
modules : core/pe/_block.*.so core/pe/_cache.*.so qt/pe/_block_qt.*.so
|
||||||
|
|
||||||
|
mergepot :
|
||||||
|
./env/bin/python build.py --mergepot
|
||||||
|
|
||||||
|
normpo :
|
||||||
|
./env/bin/python build.py --normpo
|
||||||
|
|
||||||
|
srcpkg :
|
||||||
|
./scripts/srcpkg.sh
|
||||||
|
|
||||||
|
install: build/help | all pyc
|
||||||
|
mkdir -p ${DESTDIR}${PREFIX}/share/dupeguru
|
||||||
|
cp -rf ${packages} locale build/help ${DESTDIR}${PREFIX}/share/dupeguru
|
||||||
|
cp -f run.py ${DESTDIR}${PREFIX}/share/dupeguru/run.py
|
||||||
|
chmod 755 ${DESTDIR}${PREFIX}/share/dupeguru/run.py
|
||||||
|
mkdir -p ${DESTDIR}${PREFIX}/bin
|
||||||
|
ln -sf ${PREFIX}/share/dupeguru/run.py ${DESTDIR}${PREFIX}/bin/dupeguru
|
||||||
|
mkdir -p ${DESTDIR}${PREFIX}/share/applications
|
||||||
|
cp -f pkg/dupeguru.desktop ${DESTDIR}${PREFIX}/share/applications
|
||||||
|
mkdir -p ${DESTDIR}${PREFIX}/share/pixmaps
|
||||||
|
cp -f images/dgse_logo_128.png ${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png
|
||||||
|
|
||||||
|
uninstall :
|
||||||
|
rm -rf "${DESTDIR}${PREFIX}/share/dupeguru"
|
||||||
|
rm -f "${DESTDIR}${PREFIX}/bin/dupeguru"
|
||||||
|
rm -f "${DESTDIR}${PREFIX}/share/applications/dupeguru.desktop"
|
||||||
|
rm -f "${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm run.py
|
||||||
|
-rm -rf build
|
||||||
|
-rm locale/*/LC_MESSAGES/*.mo
|
||||||
|
-rm core/pe/*.so qt/pe/*.so
|
||||||
|
|
||||||
|
.PHONY : clean srcpkg normpo mergepot modules i18n reqs run pyc install uninstall all
|
||||||
135
README.md
135
README.md
@@ -1,14 +1,11 @@
|
|||||||
# dupeGuru
|
# dupeGuru
|
||||||
|
|
||||||
[dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in
|
[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
|
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
|
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
|
||||||
is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt5.
|
is written in Objective-C and uses Cocoa. On Linux, it's written in Python and uses Qt5.
|
||||||
|
|
||||||
dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same
|
## Current status: People wanted
|
||||||
source tree. You choose the edition you want to build in a `configure.py` flag.
|
|
||||||
|
|
||||||
# Current status: People wanted
|
|
||||||
|
|
||||||
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
|
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
|
||||||
corrected.
|
corrected.
|
||||||
@@ -20,13 +17,39 @@ Whatever your skills, if you are remotely interestested in being a contributor,
|
|||||||
mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue]
|
mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue]
|
||||||
and let's get started.
|
and let's get started.
|
||||||
|
|
||||||
# Contents of this folder
|
### 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], dupeGuru v4.0 dropped Windows support
|
||||||
|
because there isn't anyone to bear the burden of Windows maintenance. 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
|
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:
|
[available online][documentation] in its built form. Here's how this source tree is organised:
|
||||||
|
|
||||||
* core: Contains the core logic code for dupeGuru. It's Python code.
|
* core: 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.
|
* 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.
|
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
|
||||||
* images: Images used by the different UI codebases.
|
* images: Images used by the different UI codebases.
|
||||||
@@ -35,64 +58,83 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu
|
|||||||
* locale: .po files for localisation.
|
* locale: .po files for localisation.
|
||||||
|
|
||||||
There are also other sub-folder that comes from external repositories and are part of this repo as
|
There are also other sub-folder that comes from external repositories and are part of this repo as
|
||||||
git subtrees:
|
git submodules:
|
||||||
|
|
||||||
|
* Sparkle: An auto-update library for the OS X version.
|
||||||
* hscommon: A collection of helpers used across HS applications.
|
* hscommon: A collection of helpers used across HS applications.
|
||||||
* cocoalib: A collection of helpers used across Cocoa UI codebases of 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.
|
* qtlib: A collection of helpers used across Qt UI codebases of HS applications.
|
||||||
|
|
||||||
# How to build dupeGuru from source
|
## How to build dupeGuru from source
|
||||||
|
|
||||||
## The very, very, very easy way
|
### make
|
||||||
|
|
||||||
If you're on Linux or Mac, there's a bootstrap script that will make building very, very easy. There
|
If you're on linux, you can build the ap for local development with `make`:
|
||||||
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
|
$ make
|
||||||
|
$ make run
|
||||||
|
|
||||||
and follow instructions from the script. You can then ignore the rest of the build documentation.
|
The `Makefile` is a recent addition, however. You might have to fallback to the legacy build
|
||||||
|
scripts.
|
||||||
|
|
||||||
## Prerequisites installation
|
### Legacy build
|
||||||
|
|
||||||
|
If you're on OS X or that if the `make` method didn't work, you can build dupeGuru with the
|
||||||
|
legacy scripts.
|
||||||
|
|
||||||
|
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
|
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
|
||||||
to be installed manually.
|
to be installed manually.
|
||||||
|
|
||||||
* All systems: [Python 3.3+][python] and [setuptools][setuptools]
|
* All systems: [Python 3.4+][python]
|
||||||
* Mac OS X: The last XCode to have the 10.7 SDK included. Python 3.4+.
|
* Mac OS X: OS X 10.10+ with XCode command line tools.
|
||||||
* Windows: Visual Studio 2010, [PyQt 5.0+][pyqt], [cx_Freeze][cxfreeze] and
|
* Linux: PyQt5
|
||||||
[Advanced Installer][advinst] (you only need the last two if you want to create an installer)
|
|
||||||
|
|
||||||
On Ubuntu (14.04+), the apt-get command to install all pre-requisites is:
|
On Ubuntu (14.04+), the apt-get command to install all pre-requisites is:
|
||||||
|
|
||||||
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools
|
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools python3-venv
|
||||||
|
|
||||||
On Arch, it's:
|
### OS X and pyenv
|
||||||
|
|
||||||
$ pacman -S python-pyqt5
|
[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:
|
||||||
|
|
||||||
## Setting up the virtual environment
|
$ 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.
|
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
|
||||||
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
|
Python-related dependencies. In that environment, we then install our requirements with pip.
|
||||||
it doesn't install setuptools and pip (unless you use Python 3.4+), so it has to be installed
|
|
||||||
manually:
|
For Linux (`--system-site-packages` is to be able to import PyQt):
|
||||||
|
|
||||||
$ pyvenv --system-site-packages env
|
$ pyvenv --system-site-packages env
|
||||||
$ source env/bin/activate
|
$ source env/bin/activate
|
||||||
$ python get-pip.py
|
$ pip install -r requirements.txt
|
||||||
|
|
||||||
Then, you can install pip requirements in your virtualenv:
|
For OS X:
|
||||||
|
|
||||||
$ pip install -r requirements-[osx|win].txt
|
$ pyvenv env
|
||||||
|
$ source env/bin/activate
|
||||||
|
$ pip install -r requirements-osx.txt
|
||||||
|
|
||||||
([osx|win] depends, of course, on your platform. On other platforms, just use requirements.txt).
|
### Actual building and running
|
||||||
|
|
||||||
## Actual building and running
|
|
||||||
|
|
||||||
With your virtualenv activated, you can build and run dupeGuru with these commands:
|
With your virtualenv activated, you can build and run dupeGuru with these commands:
|
||||||
|
|
||||||
$ python configure.py
|
|
||||||
$ python build.py
|
$ python build.py
|
||||||
$ python run.py
|
$ python run.py
|
||||||
|
|
||||||
@@ -100,13 +142,30 @@ You can also package dupeGuru into an installable package with:
|
|||||||
|
|
||||||
$ python package.py
|
$ 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/
|
[dupeguru]: http://www.hardcoded.net/dupeguru/
|
||||||
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
|
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||||
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
|
[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/
|
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
|
||||||
[python]: http://www.python.org/
|
[python]: http://www.python.org/
|
||||||
[setuptools]: https://pypi.python.org/pypi/setuptools
|
|
||||||
[pyqt]: http://www.riverbankcomputing.com
|
[pyqt]: http://www.riverbankcomputing.com
|
||||||
[cxfreeze]: http://cx-freeze.sourceforge.net/
|
[pyenv]: https://github.com/yyuu/pyenv
|
||||||
[advinst]: http://www.advancedinstaller.com
|
[tox]: https://tox.readthedocs.org/en/latest/
|
||||||
|
|
||||||
|
|||||||
42
bootstrap.sh
42
bootstrap.sh
@@ -1,12 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PYTHON=python3
|
PYTHON=python3
|
||||||
command -v $PYTHON -m venv >/dev/null 2>&1 || { echo >&2 "Python 3.3 required. Install it and try again. Aborting"; exit 1; }
|
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 "deps" ]; then
|
|
||||||
# We have a collection of dependencies in our source package. We might as well use it instead
|
if [ -d ".git" ]; then
|
||||||
# of downloading it from PyPI.
|
git submodule init
|
||||||
PIPARGS="--no-index --find-links=deps"
|
git submodule update
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d "env" ]; then
|
if [ ! -d "env" ]; then
|
||||||
@@ -15,39 +19,23 @@ if [ ! -d "env" ]; then
|
|||||||
# install. To achieve our latter goal, we start with a normal venv, which we later upgrade to
|
# 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.
|
# a system-site-packages once pip is installed.
|
||||||
if ! $PYTHON -m venv env ; then
|
if ! $PYTHON -m venv env ; then
|
||||||
# We're probably under braindead Ubuntu 14.04 which completely messed up ensurepip.
|
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
|
||||||
# Work around it :(
|
exit 1
|
||||||
echo "Ubuntu 14.04's version of Python 3.4 is braindead stupid, but we work around it anyway..."
|
|
||||||
$PYTHON -m venv --without-pip env
|
|
||||||
fi
|
fi
|
||||||
source env/bin/activate
|
|
||||||
if python -m ensurepip; then
|
|
||||||
echo "We're under Python 3.4+, no need to try to install pip!"
|
|
||||||
else
|
|
||||||
python get-pip.py $PIPARGS --force-reinstall
|
|
||||||
fi
|
|
||||||
deactivate
|
|
||||||
if [ "$(uname)" != "Darwin" ]; then
|
if [ "$(uname)" != "Darwin" ]; then
|
||||||
# We only need system site packages for PyQt, so under OS X, we don't enable it
|
|
||||||
if ! $PYTHON -m venv env --upgrade --system-site-packages ; then
|
|
||||||
# We're probably under v3.4.1 and experiencing http://bugs.python.org/issue21643
|
|
||||||
# Work around it.
|
|
||||||
echo "Oops, can't upgrade our venv. Trying to work around it."
|
|
||||||
rm env/lib64
|
|
||||||
$PYTHON -m venv env --upgrade --system-site-packages
|
$PYTHON -m venv env --upgrade --system-site-packages
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
|
|
||||||
echo "Installing pip requirements"
|
echo "Installing pip requirements"
|
||||||
if [ "$(uname)" == "Darwin" ]; then
|
if [ "$(uname)" == "Darwin" ]; then
|
||||||
pip install $PIPARGS -r requirements-osx.txt
|
./env/bin/pip install -r requirements-osx.txt
|
||||||
else
|
else
|
||||||
python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.1+ required. Install it and try again. Aborting"; exit 1; }
|
./env/bin/python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
|
||||||
pip install $PIPARGS -r requirements.txt
|
./env/bin/pip install -r requirements.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
|
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
|
||||||
echo ". env/bin/activate && python configure.py && python build.py && python run.py"
|
echo ". env/bin/activate && python build.py && python run.py"
|
||||||
|
|||||||
201
build.py
201
build.py
@@ -1,6 +1,4 @@
|
|||||||
# Created By: Virgil Dupras
|
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||||
# Created On: 2009-12-30
|
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
#
|
#
|
||||||
# This software is licensed under the "GPLv3" 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
|
||||||
@@ -11,8 +9,6 @@ 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
|
||||||
@@ -22,10 +18,10 @@ from hscommon.build import (
|
|||||||
add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||||
get_module_version, move_all, copy_all, OSXAppStructure,
|
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
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():
|
||||||
@@ -39,6 +35,14 @@ def parse_args():
|
|||||||
'--doc', action='store_true', dest='doc',
|
'--doc', action='store_true', dest='doc',
|
||||||
help="Build only the help file"
|
help="Build only the help file"
|
||||||
)
|
)
|
||||||
|
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."
|
||||||
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--loc', action='store_true', dest='loc',
|
'--loc', action='store_true', dest='loc',
|
||||||
help="Build only localization"
|
help="Build only localization"
|
||||||
@@ -70,18 +74,11 @@ def parse_args():
|
|||||||
(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 = [
|
||||||
@@ -92,59 +89,54 @@ 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(
|
xibless.generate(
|
||||||
op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
op.join('cocoa', 'ui', srcname), op.join(dest, dstname),
|
||||||
localizationTable='Localizable', args={'edition': edition}
|
|
||||||
)
|
|
||||||
if edition == 'pe':
|
|
||||||
xibless.generate(
|
|
||||||
'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
|
||||||
localizationTable='Localizable'
|
localizationTable='Localizable'
|
||||||
)
|
)
|
||||||
else:
|
for appmode in ('standard', 'music', 'picture'):
|
||||||
xibless.generate(
|
xibless.generate(
|
||||||
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
op.join('cocoa', 'ui', 'preferences_panel.py'),
|
||||||
localizationTable='Localizable'
|
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', 'osax']
|
|
||||||
specific_packages = {
|
|
||||||
'se': ['core_se'],
|
|
||||||
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
|
|
||||||
'pe': ['core_pe'] + appscript_pkgs,
|
|
||||||
}[edition]
|
|
||||||
tocopy = [
|
tocopy = [
|
||||||
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
|
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
|
||||||
] + 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
|
|
||||||
if edition == 'pe':
|
|
||||||
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
||||||
# to manually specify it.
|
# to manually specify it.
|
||||||
extra_deps = ['multiprocessing']
|
extra_deps = ['multiprocessing']
|
||||||
@@ -160,40 +152,38 @@ 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(edition)
|
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("pyrcc5 {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(edition)
|
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://github.com/hsoft/dupeguru/issues/{}"
|
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)
|
||||||
@@ -202,11 +192,11 @@ 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):
|
||||||
loc.compile_all_po('locale')
|
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()
|
||||||
@@ -214,32 +204,19 @@ 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 PyQt5.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()
|
||||||
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.
|
||||||
@@ -254,7 +231,7 @@ def build_updatepot():
|
|||||||
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(
|
loc.strings2pot(
|
||||||
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
op.join('cocoa', 'en.lproj', 'Localizable.strings'),
|
||||||
op.join('locale', 'ui.pot')
|
op.join('locale', 'ui.pot')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,7 +260,7 @@ def build_cocoa_proxy_module():
|
|||||||
['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
|
||||||
@@ -303,13 +280,12 @@ 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,
|
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,
|
||||||
PyTextField, PyProgressWindow, 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)
|
||||||
@@ -326,14 +302,21 @@ 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",
|
||||||
@@ -345,27 +328,25 @@ def build_pe_modules(ui):
|
|||||||
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('.')
|
||||||
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:
|
||||||
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
|
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
|
||||||
@@ -374,9 +355,9 @@ def main():
|
|||||||
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:
|
||||||
@@ -385,17 +366,17 @@ def main():
|
|||||||
build_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()
|
||||||
|
|||||||
@@ -10,24 +10,29 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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 "ProblemDialog.h"
|
||||||
|
#import "DeletionOptions.h"
|
||||||
#import "HSAboutBox.h"
|
#import "HSAboutBox.h"
|
||||||
#import "HSRecentFiles.h"
|
#import "HSRecentFiles.h"
|
||||||
#import "HSProgressWindow.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;
|
HSProgressWindow *_progressWindow;
|
||||||
NSWindowController *_preferencesPanel;
|
NSWindowController *_preferencesPanel;
|
||||||
HSAboutBox *_aboutBox;
|
HSAboutBox *_aboutBox;
|
||||||
@@ -41,17 +46,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
/* 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;
|
||||||
@@ -61,6 +66,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
- (void)recentFileClicked:(NSString *)path;
|
- (void)recentFileClicked:(NSString *)path;
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
- (void)clearPictureCache;
|
||||||
- (void)loadResults;
|
- (void)loadResults;
|
||||||
- (void)openWebsite;
|
- (void)openWebsite;
|
||||||
- (void)openHelp;
|
- (void)openHelp;
|
||||||
@@ -6,16 +6,19 @@ which should be included with this package. The terms are also available at
|
|||||||
http://www.gnu.org/licenses/gpl-3.0.html
|
http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "AppDelegateBase.h"
|
#import "AppDelegate.h"
|
||||||
#import "ProgressController.h"
|
#import "ProgressController.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;
|
||||||
@@ -24,6 +27,21 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
+ (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"];
|
||||||
@@ -53,6 +71,19 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +100,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
}
|
}
|
||||||
_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]];
|
||||||
|
_problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
|
||||||
|
_deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
|
||||||
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
|
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
|
||||||
[_progressWindow setParentWindow:[_resultWindow window]];
|
[_progressWindow setParentWindow:[_directoryPanel window]];
|
||||||
_aboutBox = nil; // Lazily loaded
|
// Lazily loaded
|
||||||
_preferencesPanel = nil; // Lazily loaded
|
_aboutBox = nil;
|
||||||
|
_preferencesPanel = nil;
|
||||||
|
_resultWindow = nil;
|
||||||
|
_detailsPanel = nil;
|
||||||
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
|
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,28 +121,51 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
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
|
||||||
{
|
{
|
||||||
|
NSInteger appMode = [self getAppMode];
|
||||||
|
if (appMode == AppModePicture) {
|
||||||
|
return [[DetailsPanelPicture alloc] initWithApp:model];
|
||||||
|
}
|
||||||
|
else {
|
||||||
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
|
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;
|
||||||
}
|
}
|
||||||
@@ -128,7 +185,29 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
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];
|
||||||
@@ -147,7 +226,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
|
|
||||||
- (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
|
||||||
@@ -174,7 +253,18 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
- (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];
|
||||||
}
|
}
|
||||||
@@ -191,7 +281,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
|
|
||||||
- (void)startScanning
|
- (void)startScanning
|
||||||
{
|
{
|
||||||
[[self resultWindow] startDuplicateScan];
|
[[self directoryPanel] startDuplicateScan];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -254,9 +344,29 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
|
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)createResultsWindow
|
||||||
|
{
|
||||||
|
if (_resultWindow != nil) {
|
||||||
|
[_resultWindow release];
|
||||||
|
}
|
||||||
|
if (_detailsPanel != nil) {
|
||||||
|
[_detailsPanel release];
|
||||||
|
}
|
||||||
|
// Warning: creation order is important
|
||||||
|
// If the details panel is not created first and that there are some results in the model
|
||||||
|
// (happens if we load results), a dupe selection event triggers a details refresh in the
|
||||||
|
// core before we have the chance to initialize it, and then we crash.
|
||||||
|
_detailsPanel = [self createDetailsPanel];
|
||||||
|
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
|
||||||
|
}
|
||||||
|
- (void)showResultsWindow
|
||||||
|
{
|
||||||
|
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)showProblemDialog
|
- (void)showProblemDialog
|
||||||
{
|
{
|
||||||
[[self resultWindow] showProblemDialog];
|
[_problemDialog showWindow:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
|
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
|
||||||
@@ -17,3 +17,8 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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
|
||||||
@@ -10,7 +10,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#import <Python.h>
|
#import <Python.h>
|
||||||
#import "PyDetailsPanel.h"
|
#import "PyDetailsPanel.h"
|
||||||
|
|
||||||
@interface DetailsPanelBase : NSWindowController <NSTableViewDataSource>
|
@interface DetailsPanel : NSWindowController <NSTableViewDataSource>
|
||||||
{
|
{
|
||||||
NSTableView *detailsTable;
|
NSTableView *detailsTable;
|
||||||
|
|
||||||
@@ -6,10 +6,11 @@ which should be included with this package. The terms are also available at
|
|||||||
http://www.gnu.org/licenses/gpl-3.0.html
|
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.gnu.org/licenses/gpl-3.0.html
|
|||||||
|
|
||||||
- (NSWindow *)createWindow
|
- (NSWindow *)createWindow
|
||||||
{
|
{
|
||||||
return nil; // Virtual
|
return createDetailsPanel_UI(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)refreshDetails
|
- (void)refreshDetails
|
||||||
@@ -7,10 +7,10 @@ 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;
|
||||||
@@ -10,11 +10,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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.gnu.org/licenses/gpl-3.0.html
|
|||||||
|
|
||||||
- (NSWindow *)createWindow
|
- (NSWindow *)createWindow
|
||||||
{
|
{
|
||||||
return createDetailsPanel_UI(self);
|
return createDetailsPanelPicture_UI(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)loadImageAsync:(NSString *)imagePath
|
- (void)loadImageAsync:(NSString *)imagePath
|
||||||
@@ -12,15 +12,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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,21 +30,25 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
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;
|
||||||
@@ -11,22 +11,27 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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.gnu.org/licenses/gpl-3.0.html
|
|||||||
[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];
|
||||||
@@ -97,6 +121,23 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (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.gnu.org/licenses/gpl-3.0.html
|
|||||||
[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
|
||||||
{
|
{
|
||||||
@@ -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, 2014</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>
|
||||||
@@ -10,14 +10,12 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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.gnu.org/licenses/gpl-3.0.html
|
|||||||
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.gnu.org/licenses/gpl-3.0.html
|
|||||||
@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.gnu.org/licenses/gpl-3.0.html
|
|||||||
- (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;
|
||||||
@@ -6,7 +6,7 @@ which should be included with this package. The terms are also available at
|
|||||||
http://www.gnu.org/licenses/gpl-3.0.html
|
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.gnu.org/licenses/gpl-3.0.html
|
|||||||
#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.gnu.org/licenses/gpl-3.0.html
|
|||||||
@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,9 +34,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
[[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];
|
||||||
@@ -49,19 +47,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
{
|
{
|
||||||
[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
|
||||||
{
|
{
|
||||||
@@ -89,11 +77,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
[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];
|
||||||
@@ -118,6 +101,87 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (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
|
||||||
{
|
{
|
||||||
@@ -263,16 +327,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (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];
|
||||||
1
cocoa/Sparkle
Submodule
1
cocoa/Sparkle
Submodule
Submodule cocoa/Sparkle added at 1c8d54166b
@@ -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_me 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.
|
||||||
@@ -3,18 +3,15 @@
|
|||||||
"About dupeGuru" = "About dupeGuru";
|
"About dupeGuru" = "About dupeGuru";
|
||||||
"Action" = "Action";
|
"Action" = "Action";
|
||||||
"Actions" = "Actions";
|
"Actions" = "Actions";
|
||||||
"Add Aperture Library" = "Add Aperture Library";
|
|
||||||
"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 the help file for more information." = "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 the help file for more information.";
|
"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 the help file for more information." = "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 the help file for more information.";
|
||||||
"Add iPhoto Library" = "Add iPhoto Library";
|
|
||||||
"Add iTunes Library" = "Add iTunes Library";
|
|
||||||
"Add New Folder..." = "Add New Folder...";
|
"Add New Folder..." = "Add New Folder...";
|
||||||
"Add Selected to Ignore List" = "Add Selected to Ignore List";
|
"Add Selected to Ignore List" = "Add Selected to Ignore List";
|
||||||
"Advanced" = "Advanced";
|
"Advanced" = "Advanced";
|
||||||
"After having deleted a duplicate, place a link targeting the reference file to replace the deleted file." = "After having deleted a duplicate, place a link targeting the reference file to replace the deleted file.";
|
"After having deleted a duplicate, place a link targeting the reference file to replace the deleted file." = "After having deleted a duplicate, place a link targeting the reference file to replace the deleted file.";
|
||||||
"Album" = "Album";
|
"Album" = "Album";
|
||||||
|
"Application Mode:" = "Application Mode:";
|
||||||
"Artist" = "Artist";
|
"Artist" = "Artist";
|
||||||
"Attribute" = "Attribute";
|
"Attribute" = "Attribute";
|
||||||
"Audio Content" = "Audio Content";
|
|
||||||
"Automatically check for updates" = "Automatically check for updates";
|
"Automatically check for updates" = "Automatically check for updates";
|
||||||
"Basic" = "Basic";
|
"Basic" = "Basic";
|
||||||
"Bring All to Front" = "Bring All to Front";
|
"Bring All to Front" = "Bring All to Front";
|
||||||
@@ -26,8 +23,6 @@
|
|||||||
"Close" = "Close";
|
"Close" = "Close";
|
||||||
"Close Window" = "Close Window";
|
"Close Window" = "Close Window";
|
||||||
"Columns" = "Columns";
|
"Columns" = "Columns";
|
||||||
"Content" = "Content";
|
|
||||||
"Contents" = "Contents";
|
|
||||||
"Copy" = "Copy";
|
"Copy" = "Copy";
|
||||||
"Copy and Move:" = "Copy and Move:";
|
"Copy and Move:" = "Copy and Move:";
|
||||||
"Copy Marked to..." = "Copy Marked to...";
|
"Copy Marked to..." = "Copy Marked to...";
|
||||||
@@ -44,27 +39,20 @@
|
|||||||
"Do you really want to remove all your cached picture analysis?" = "Do you really want to remove all your cached picture analysis?";
|
"Do you really want to remove all your cached picture analysis?" = "Do you really want to remove all your cached picture analysis?";
|
||||||
"dupeGuru" = "dupeGuru";
|
"dupeGuru" = "dupeGuru";
|
||||||
"dupeGuru Help" = "dupeGuru Help";
|
"dupeGuru Help" = "dupeGuru Help";
|
||||||
"dupeGuru ME Preferences" = "dupeGuru ME Preferences";
|
|
||||||
"dupeGuru PE Preferences" = "dupeGuru PE Preferences";
|
|
||||||
"dupeGuru Preferences" = "dupeGuru Preferences";
|
"dupeGuru Preferences" = "dupeGuru Preferences";
|
||||||
"dupeGuru Results" = "dupeGuru Results";
|
"dupeGuru Results" = "dupeGuru Results";
|
||||||
"dupeGuru Website" = "dupeGuru Website";
|
"dupeGuru Website" = "dupeGuru Website";
|
||||||
"Dupes Only" = "Dupes Only";
|
"Dupes Only" = "Dupes Only";
|
||||||
"Edit" = "Edit";
|
"Edit" = "Edit";
|
||||||
"Excluded" = "Excluded";
|
"Excluded" = "Excluded";
|
||||||
"EXIF Timestamp" = "EXIF Timestamp";
|
|
||||||
"Export Results to CSV" = "Export Results to CSV";
|
"Export Results to CSV" = "Export Results to CSV";
|
||||||
"Export Results to XHTML" = "Export Results to XHTML";
|
"Export Results to XHTML" = "Export Results to XHTML";
|
||||||
"Fewer results" = "Fewer results";
|
"Fewer results" = "Fewer results";
|
||||||
"File" = "File";
|
"File" = "File";
|
||||||
"Filename" = "Filename";
|
|
||||||
"Filename - Fields" = "Filename - Fields";
|
|
||||||
"Filename - Fields (No Order)" = "Filename - Fields (No Order)";
|
|
||||||
"Filter" = "Filter";
|
"Filter" = "Filter";
|
||||||
"Filter hardness:" = "Filter hardness:";
|
"Filter hardness:" = "Filter hardness:";
|
||||||
"Filter Results..." = "Filter Results...";
|
"Filter Results..." = "Filter Results...";
|
||||||
"Folder Selection Window" = "Folder Selection Window";
|
"Folder Selection Window" = "Folder Selection Window";
|
||||||
"Folders" = "Folders";
|
|
||||||
"Font Size:" = "Font Size:";
|
"Font Size:" = "Font Size:";
|
||||||
"Genre" = "Genre";
|
"Genre" = "Genre";
|
||||||
"Help" = "Help";
|
"Help" = "Help";
|
||||||
@@ -92,12 +80,14 @@
|
|||||||
"Mode" = "Mode";
|
"Mode" = "Mode";
|
||||||
"More results" = "More results";
|
"More results" = "More results";
|
||||||
"Move Marked to..." = "Move Marked to...";
|
"Move Marked to..." = "Move Marked to...";
|
||||||
|
"Music" = "Music";
|
||||||
"Name" = "Name";
|
"Name" = "Name";
|
||||||
"Normal" = "Normal";
|
"Normal" = "Normal";
|
||||||
"Ok" = "Ok";
|
"Ok" = "Ok";
|
||||||
"Open Selected with Default Application" = "Open Selected with Default Application";
|
"Open Selected with Default Application" = "Open Selected with Default Application";
|
||||||
"Options" = "Options";
|
"Options" = "Options";
|
||||||
"Paste" = "Paste";
|
"Paste" = "Paste";
|
||||||
|
"Picture" = "Picture";
|
||||||
"Preferences..." = "Preferences...";
|
"Preferences..." = "Preferences...";
|
||||||
"Problems!" = "Problems!";
|
"Problems!" = "Problems!";
|
||||||
"Proceed" = "Proceed";
|
"Proceed" = "Proceed";
|
||||||
@@ -108,7 +98,6 @@
|
|||||||
"Recreate absolute path" = "Recreate absolute path";
|
"Recreate absolute path" = "Recreate absolute path";
|
||||||
"Recreate relative path" = "Recreate relative path";
|
"Recreate relative path" = "Recreate relative path";
|
||||||
"Reference" = "Reference";
|
"Reference" = "Reference";
|
||||||
"Remove Dead Tracks in iTunes" = "Remove Dead Tracks in iTunes";
|
|
||||||
"Remove empty folders on delete or move" = "Remove empty folders on delete or move";
|
"Remove empty folders on delete or move" = "Remove empty folders on delete or move";
|
||||||
"Remove Marked from Results" = "Remove Marked from Results";
|
"Remove Marked from Results" = "Remove Marked from Results";
|
||||||
"Remove Selected" = "Remove Selected";
|
"Remove Selected" = "Remove Selected";
|
||||||
@@ -134,9 +123,9 @@
|
|||||||
"Show All" = "Show All";
|
"Show All" = "Show All";
|
||||||
"Show Delta Values" = "Show Delta Values";
|
"Show Delta Values" = "Show Delta Values";
|
||||||
"Show Dupes Only" = "Show Dupes Only";
|
"Show Dupes Only" = "Show Dupes Only";
|
||||||
|
"Standard" = "Standard";
|
||||||
"Start Duplicate Scan" = "Start Duplicate Scan";
|
"Start Duplicate Scan" = "Start Duplicate Scan";
|
||||||
"State" = "State";
|
"State" = "State";
|
||||||
"Tags" = "Tags";
|
|
||||||
"Tags to scan:" = "Tags to scan:";
|
"Tags to scan:" = "Tags to scan:";
|
||||||
"The name '%@' already exists." = "The name '%@' already exists.";
|
"The name '%@' already exists." = "The name '%@' already exists.";
|
||||||
"There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results." = "There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results.";
|
"There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results." = "There were problems processing some (or all) of the files. The cause of these problems are described in the table below. Those files were not removed from your results.";
|
||||||
@@ -1,23 +1,56 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from objp.util import pyref, dontwrap
|
from objp.util import pyref, dontwrap
|
||||||
|
from hscommon.path import Path, pathify
|
||||||
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
|
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
|
||||||
from cocoa.inter import PyBaseApp, BaseAppView
|
from cocoa.inter import PyBaseApp, BaseAppView
|
||||||
|
|
||||||
|
import core.pe.photo
|
||||||
|
from core.app import DupeGuru as DupeGuruBase, AppMode
|
||||||
|
from .directories import Directories, Bundle
|
||||||
|
from .photo import Photo
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
class DupeGuruView(BaseAppView):
|
class DupeGuruView(BaseAppView):
|
||||||
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(PyBaseApp):
|
class PyDupeGuru(PyBaseApp):
|
||||||
@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('https://github.com/hsoft/dupeguru/issues')
|
install_exception_hook('https://github.com/hsoft/dupeguru/issues')
|
||||||
install_cocoa_logger()
|
install_cocoa_logger()
|
||||||
patch_threaded_job_performer()
|
patch_threaded_job_performer()
|
||||||
self.model = modelclass(self)
|
self.model = DupeGuru(self)
|
||||||
|
|
||||||
#---Sub-proxies
|
#---Sub-proxies
|
||||||
def detailsPanel(self) -> pyref:
|
def detailsPanel(self) -> pyref:
|
||||||
@@ -122,13 +155,62 @@ class PyDupeGuruBase(PyBaseApp):
|
|||||||
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
|
||||||
@@ -147,12 +229,13 @@ class PyDupeGuruBase(PyBaseApp):
|
|||||||
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):
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2006/11/16
|
|
||||||
# Copyright 2015 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
|
|
||||||
|
|
||||||
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, JOBID2TITLE
|
|
||||||
from core.scanner import ScanType
|
|
||||||
from core_me.app import DupeGuru as DupeGuruBase
|
|
||||||
from core_me import fs
|
|
||||||
from .app import 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):
|
|
||||||
DupeGuruBase.__init__(self, view)
|
|
||||||
# Use fileclasses set in DupeGuruBase.__init__()
|
|
||||||
self.directories = Directories(fileclasses=self.directories.fileclasses)
|
|
||||||
self.dead_tracks = []
|
|
||||||
|
|
||||||
def _do_delete(self, j, *args):
|
|
||||||
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.parent()):
|
|
||||||
if not hasattr(self, 'itunes_songs'):
|
|
||||||
songs = get_itunes_songs(self.directories.itunes_libpath)
|
|
||||||
self.itunes_songs = {song.path: song for song in songs}
|
|
||||||
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):
|
|
||||||
# XXX Just before release, I'm realizing that this piece of code below is why I was passing
|
|
||||||
# job exception as an argument to _job_completed(). I have to comment it for now. It's not
|
|
||||||
# the end of the world, but I should find an elegant solution to this at some point.
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
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._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._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)
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2006/11/13
|
|
||||||
# Copyright 2015 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
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import io
|
|
||||||
|
|
||||||
from appscript import app, its, k, CommandError, ApplicationNotFoundError
|
|
||||||
|
|
||||||
from hscommon.util import remove_invalid_xml, first
|
|
||||||
from hscommon.path import Path, pathify
|
|
||||||
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 core_pe.iphoto_plist import IPhotoPlistParser
|
|
||||||
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
|
|
||||||
|
|
||||||
def _get_exif_timestamp(self):
|
|
||||||
exifdata = proxy.readExifData_(str(self.path))
|
|
||||||
if exifdata:
|
|
||||||
try:
|
|
||||||
return exifdata['{Exif}']['DateTimeOriginal']
|
|
||||||
except KeyError:
|
|
||||||
return ''
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
class IPhoto(Photo):
|
|
||||||
def __init__(self, path, db_id):
|
|
||||||
# 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
|
|
||||||
|
|
||||||
@pathify
|
|
||||||
def get_iphoto_or_aperture_pictures(plistpath: Path, photo_class):
|
|
||||||
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
|
|
||||||
if not plistpath.exists():
|
|
||||||
return []
|
|
||||||
s = plistpath.open('rt', encoding='utf-8').read()
|
|
||||||
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
|
|
||||||
s = remove_invalid_xml(s, replace_with='')
|
|
||||||
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
|
|
||||||
# any & char that is not a &-based entity (&, ", 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)
|
|
||||||
parser = IPhotoPlistParser()
|
|
||||||
try:
|
|
||||||
plist = parser.parse(io.BytesIO(s.encode('utf-8')))
|
|
||||||
except Exception:
|
|
||||||
logging.warning("iPhoto plist parsing choked on data: %r", parser.lastdata)
|
|
||||||
raise
|
|
||||||
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.parent(), directories.DirectoryState.Excluded)
|
|
||||||
except directories.InvalidPathError:
|
|
||||||
self.iphoto_libpath = None
|
|
||||||
try:
|
|
||||||
self.aperture_libpath = get_aperture_database_path()
|
|
||||||
self.set_state(self.aperture_libpath.parent(), 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):
|
|
||||||
DupeGuruBase.__init__(self, view)
|
|
||||||
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.parent()):
|
|
||||||
if not hasattr(self, 'path2iphoto'):
|
|
||||||
photos = get_iphoto_pictures(self.directories.iphoto_libpath)
|
|
||||||
self.path2iphoto = {p.path: p for p in photos}
|
|
||||||
return self.path2iphoto.get(path)
|
|
||||||
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath.parent()):
|
|
||||||
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):
|
|
||||||
DupeGuruBase._job_completed(self, jobid)
|
|
||||||
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
|
|
||||||
@@ -1,22 +1,13 @@
|
|||||||
# Created By: Virgil Dupras
|
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||||
# Created On: 2009-05-24
|
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
#
|
#
|
||||||
# This software is licensed under the "GPLv3" 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.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import logging
|
|
||||||
import os.path as op
|
|
||||||
|
|
||||||
from hscommon.path import Path, pathify
|
|
||||||
from cocoa import proxy
|
from cocoa import proxy
|
||||||
|
from hscommon.path import Path, pathify
|
||||||
from core.scanner import ScanType
|
from core.se import fs
|
||||||
from core.directories import Directories as DirectoriesBase, DirectoryState
|
from core.directories import Directories as DirectoriesBase, DirectoryState
|
||||||
from core_se.app import DupeGuru as DupeGuruBase
|
|
||||||
from core_se import fs
|
|
||||||
from .app import PyDupeGuruBase
|
|
||||||
|
|
||||||
def is_bundle(str_path):
|
def is_bundle(str_path):
|
||||||
uti = proxy.getUTI_(str_path)
|
uti = proxy.getUTI_(str_path)
|
||||||
@@ -30,13 +21,9 @@ class Bundle(fs.Folder):
|
|||||||
def can_handle(cls, path: Path):
|
def can_handle(cls, path: Path):
|
||||||
return not path.islink() and path.isdir() and is_bundle(str(path))
|
return not path.islink() and path.isdir() and is_bundle(str(path))
|
||||||
|
|
||||||
|
|
||||||
class Directories(DirectoriesBase):
|
class Directories(DirectoriesBase):
|
||||||
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
|
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
|
||||||
HOME_PATH_TO_EXCLUDE = [Path('Library')]
|
HOME_PATH_TO_EXCLUDE = [Path('Library')]
|
||||||
def __init__(self):
|
|
||||||
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])
|
|
||||||
self.folderclass = fs.Folder
|
|
||||||
|
|
||||||
def _default_state_for_path(self, path):
|
def _default_state_for_path(self, path):
|
||||||
result = DirectoriesBase._default_state_for_path(self, path)
|
result = DirectoriesBase._default_state_for_path(self, path)
|
||||||
@@ -58,47 +45,9 @@ class Directories(DirectoriesBase):
|
|||||||
yield from_folder
|
yield from_folder
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
for folder in DirectoriesBase._get_folders(self, from_folder, j):
|
yield from DirectoriesBase._get_folders(self, from_folder, j)
|
||||||
yield folder
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_subfolders(path):
|
def get_subfolders(path):
|
||||||
result = DirectoriesBase.get_subfolders(path)
|
result = DirectoriesBase.get_subfolders(path)
|
||||||
return [p for p in result if not is_bundle(str(p))]
|
return [p for p in result if not is_bundle(str(p))]
|
||||||
|
|
||||||
|
|
||||||
class DupeGuru(DupeGuruBase):
|
|
||||||
def __init__(self, view):
|
|
||||||
# appdata = op.join(appdata, 'dupeGuru')
|
|
||||||
# print(repr(appdata))
|
|
||||||
DupeGuruBase.__init__(self, view)
|
|
||||||
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
|
|
||||||
|
|
||||||
35
cocoa/inter/photo.py
Normal file
35
cocoa/inter/photo.py
Normal 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 ''
|
||||||
@@ -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', [])]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "AppDelegateBase.h"
|
|
||||||
#import "ResultWindow.h"
|
|
||||||
#import "PyDupeGuru.h"
|
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase {}
|
|
||||||
- (void)removeDeadTracks;
|
|
||||||
@end
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "../base/Consts.h"
|
|
||||||
|
|
||||||
#define jobScanDeadTracks @"jobScanDeadTracks"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "DetailsPanelBase.h"
|
|
||||||
|
|
||||||
@interface DetailsPanel : DetailsPanelBase
|
|
||||||
@end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "DetailsPanel.h"
|
|
||||||
#import "DetailsPanel_UI.h"
|
|
||||||
|
|
||||||
@implementation DetailsPanel
|
|
||||||
- (NSWindow *)createWindow
|
|
||||||
{
|
|
||||||
return createDetailsPanel_UI(self);
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "../base/DirectoryPanel.h"
|
|
||||||
|
|
||||||
@interface DirectoryPanelME : DirectoryPanel
|
|
||||||
{
|
|
||||||
}
|
|
||||||
- (IBAction)addiTunes:(id)sender;
|
|
||||||
@end
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -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, 2014</string>
|
|
||||||
<key>SUFeedURL</key>
|
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
|
||||||
<key>SUPublicDSAKeyFile</key>
|
|
||||||
<string>dsa_pub.pem</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "ResultWindowBase.h"
|
|
||||||
|
|
||||||
@interface ResultWindow : ResultWindowBase {}
|
|
||||||
- (void)removeDeadTracks;
|
|
||||||
@end
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "AppDelegateBase.h"
|
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase {}
|
|
||||||
- (void)clearPictureCache;
|
|
||||||
@end
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "../base/Consts.h"
|
|
||||||
|
|
||||||
#define ImageLoadedNotification @"ImageLoadedNotification"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "../base/DirectoryPanel.h"
|
|
||||||
|
|
||||||
@interface DirectoryPanelPE : DirectoryPanel
|
|
||||||
{
|
|
||||||
}
|
|
||||||
- (IBAction)addiPhoto:(id)sender;
|
|
||||||
- (IBAction)addAperture:(id)sender;
|
|
||||||
@end
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -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, 2014</string>
|
|
||||||
<key>SUFeedURL</key>
|
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
|
||||||
<key>SUPublicDSAKeyFile</key>
|
|
||||||
<string>dsa_pub.pem</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "ResultWindowBase.h"
|
|
||||||
|
|
||||||
@interface ResultWindow : ResultWindowBase {}
|
|
||||||
- (void)clearPictureCache;
|
|
||||||
@end
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Copyright 2015 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 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.
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "AppDelegateBase.h"
|
|
||||||
#import "PyDupeGuru.h"
|
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase {}
|
|
||||||
@end
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "DetailsPanelBase.h"
|
|
||||||
|
|
||||||
@interface DetailsPanel : DetailsPanelBase
|
|
||||||
@end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "DetailsPanel.h"
|
|
||||||
#import "DetailsPanel_UI.h"
|
|
||||||
|
|
||||||
@implementation DetailsPanel
|
|
||||||
- (NSWindow *)createWindow
|
|
||||||
{
|
|
||||||
return createDetailsPanel_UI(self);
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "ResultWindowBase.h"
|
|
||||||
|
|
||||||
@interface ResultWindow : ResultWindowBase {}
|
|
||||||
@end
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Copyright 2015 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 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_se 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
|
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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')
|
||||||
@@ -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)
|
||||||
@@ -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')
|
||||||
@@ -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,18 +21,18 @@ 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,3,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 = ['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?
|
||||||
@@ -53,8 +48,8 @@ def build(ctx):
|
|||||||
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
|
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
|
||||||
'controllers/HSTextField', 'controllers/HSProgressWindow']
|
'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
1
cocoalib
Submodule
Submodule cocoalib added at d059aa9b79
15
cocoalib/.gitignore
vendored
15
cocoalib/.gitignore
vendored
@@ -1,15 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
__pycache__
|
|
||||||
autogen
|
|
||||||
*.pyc
|
|
||||||
*.so
|
|
||||||
nl.lproj
|
|
||||||
cs.lproj
|
|
||||||
de.lproj
|
|
||||||
fr.lproj
|
|
||||||
it.lproj
|
|
||||||
hy.lproj
|
|
||||||
ru.lproj
|
|
||||||
uk.lproj
|
|
||||||
zh_CN.lproj
|
|
||||||
pt_BR.lproj
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[main]
|
|
||||||
host = https://www.transifex.com
|
|
||||||
|
|
||||||
[hscommon.cocoalib]
|
|
||||||
file_filter = locale/<lang>/LC_MESSAGES/cocoalib.po
|
|
||||||
source_file = locale/cocoalib.pot
|
|
||||||
source_lang = en
|
|
||||||
type = PO
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
@interface Dialogs : NSObject
|
|
||||||
+ (void)showMessage:(NSString *)message;
|
|
||||||
+ (NSInteger)askYesNo:(NSString *)message;
|
|
||||||
@end
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "Dialogs.h"
|
|
||||||
|
|
||||||
@implementation Dialogs
|
|
||||||
+ (void)showMessage:(NSString *)message
|
|
||||||
{
|
|
||||||
NSAlert *a = [[NSAlert alloc] init];
|
|
||||||
[a addButtonWithTitle:NSLocalizedStringFromTable(@"OK", @"cocoalib", @"")];
|
|
||||||
[a setMessageText:message];
|
|
||||||
[a runModal];
|
|
||||||
[a release];
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (NSInteger)askYesNo:(NSString *)message
|
|
||||||
{
|
|
||||||
NSAlert *a = [[NSAlert alloc] init];
|
|
||||||
[a addButtonWithTitle:NSLocalizedStringFromTable(@"Yes", @"cocoalib", @"")];
|
|
||||||
[[a addButtonWithTitle:NSLocalizedStringFromTable(@"No", @"cocoalib", @"")] setKeyEquivalent:@"\E"];
|
|
||||||
[a setMessageText:message];
|
|
||||||
NSInteger r = [a runModal];
|
|
||||||
[a release];
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import "PyBaseApp.h"
|
|
||||||
|
|
||||||
@interface HSAboutBox : NSWindowController
|
|
||||||
{
|
|
||||||
NSTextField *titleTextField;
|
|
||||||
NSTextField *versionTextField;
|
|
||||||
NSTextField *copyrightTextField;
|
|
||||||
|
|
||||||
PyBaseApp *app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property (readwrite, retain) NSTextField *titleTextField;
|
|
||||||
@property (readwrite, retain) NSTextField *versionTextField;
|
|
||||||
@property (readwrite, retain) NSTextField *copyrightTextField;
|
|
||||||
|
|
||||||
- (id)initWithApp:(PyBaseApp *)app;
|
|
||||||
- (void)updateFields;
|
|
||||||
@end
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "HSAboutBox.h"
|
|
||||||
#import "HSAboutBox_UI.h"
|
|
||||||
|
|
||||||
@implementation HSAboutBox
|
|
||||||
|
|
||||||
@synthesize titleTextField;
|
|
||||||
@synthesize versionTextField;
|
|
||||||
@synthesize copyrightTextField;
|
|
||||||
|
|
||||||
- (id)initWithApp:(PyBaseApp *)aApp
|
|
||||||
{
|
|
||||||
self = [super initWithWindow:nil];
|
|
||||||
[self setWindow:createHSAboutBox_UI(self)];
|
|
||||||
app = [aApp retain];
|
|
||||||
[self updateFields];
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dealloc
|
|
||||||
{
|
|
||||||
[app release];
|
|
||||||
[super dealloc];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateFields
|
|
||||||
{
|
|
||||||
[titleTextField setStringValue:[app appLongName]];
|
|
||||||
NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
|
|
||||||
[versionTextField setStringValue:[NSString stringWithFormat:@"Version: %@",version]];
|
|
||||||
NSString *copyright = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSHumanReadableCopyright"];
|
|
||||||
[copyrightTextField setStringValue:copyright];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
@interface HSErrorReportWindow : NSWindowController
|
|
||||||
{
|
|
||||||
NSTextView *contentTextView;
|
|
||||||
NSString *githubUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property (readwrite, retain) NSTextView *contentTextView;
|
|
||||||
@property (readwrite, retain) NSString *githubUrl;
|
|
||||||
|
|
||||||
// True if the user wants to send the report
|
|
||||||
+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl;
|
|
||||||
- (id)initWithContent:(NSString *)content githubUrl:(NSString *)githubUrl;
|
|
||||||
|
|
||||||
- (void)goToGithub;
|
|
||||||
- (void)close;
|
|
||||||
@end
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "HSErrorReportWindow.h"
|
|
||||||
#import "HSErrorReportWindow_UI.h"
|
|
||||||
|
|
||||||
@implementation HSErrorReportWindow
|
|
||||||
|
|
||||||
@synthesize contentTextView;
|
|
||||||
@synthesize githubUrl;
|
|
||||||
|
|
||||||
+ (void)showErrorReportWithContent:(NSString *)content githubUrl:(NSString *)githubUrl
|
|
||||||
{
|
|
||||||
HSErrorReportWindow *report = [[HSErrorReportWindow alloc] initWithContent:content githubUrl:githubUrl];
|
|
||||||
[NSApp runModalForWindow:[report window]];
|
|
||||||
[report release];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)initWithContent:(NSString *)content githubUrl:(NSString *)aGithubUrl
|
|
||||||
{
|
|
||||||
self = [super initWithWindow:nil];
|
|
||||||
[self setWindow:createHSErrorReportWindow_UI(self)];
|
|
||||||
[contentTextView alignLeft:nil];
|
|
||||||
[[[contentTextView textStorage] mutableString] setString:content];
|
|
||||||
self.githubUrl = aGithubUrl;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)goToGithub
|
|
||||||
{
|
|
||||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:self.githubUrl]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)close
|
|
||||||
{
|
|
||||||
[[self window] orderOut:self];
|
|
||||||
[NSApp stopModalWithCode:NSOKButton];
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <math.h>
|
|
||||||
|
|
||||||
CGFloat deg2rad(CGFloat deg);
|
|
||||||
CGFloat distance(NSPoint p1, NSPoint p2);
|
|
||||||
NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle);
|
|
||||||
CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "HSGeometry.h"
|
|
||||||
|
|
||||||
CGFloat deg2rad(CGFloat deg)
|
|
||||||
{
|
|
||||||
return deg * M_PI / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat distance(NSPoint p1, NSPoint p2)
|
|
||||||
{
|
|
||||||
CGFloat dX = p1.x - p2.x;
|
|
||||||
CGFloat dY = p1.y - p2.y;
|
|
||||||
return sqrt(dX * dX + dY * dY);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSPoint pointInCircle(NSPoint center, CGFloat radius, CGFloat angle)
|
|
||||||
{
|
|
||||||
// a/sin(A) = b/sin(B) = c/sin(C) = 2R
|
|
||||||
// the start point it (center.x + radius, center.y) and goes counterclockwise
|
|
||||||
angle = fmod(angle, M_PI*2);
|
|
||||||
CGFloat C = M_PI/2;
|
|
||||||
CGFloat A = fmod(angle, M_PI/2);
|
|
||||||
CGFloat B = C - A;
|
|
||||||
CGFloat c = radius;
|
|
||||||
CGFloat ratio = c / sin(C);
|
|
||||||
CGFloat b = ratio * sin(B);
|
|
||||||
CGFloat a = ratio * sin(A);
|
|
||||||
if (angle >= M_PI * 1.5)
|
|
||||||
return NSMakePoint(center.x + a, center.y - b);
|
|
||||||
else if (angle >= M_PI)
|
|
||||||
return NSMakePoint(center.x - b, center.y - a);
|
|
||||||
else if (angle >= M_PI/2)
|
|
||||||
return NSMakePoint(center.x - a, center.y + b);
|
|
||||||
else
|
|
||||||
return NSMakePoint(center.x + b, center.y + a);
|
|
||||||
}
|
|
||||||
|
|
||||||
CGFloat angleFromPoints(NSPoint pt1, NSPoint pt2)
|
|
||||||
{
|
|
||||||
// Returns the angle (radian) formed by the line pt1-pt2. The angle follows the same logic
|
|
||||||
// as in pointInCircle.
|
|
||||||
// What we do here is that we take the line and reduce it to fit a "unit circle" (circle with
|
|
||||||
// a radius of 1). Then, either asin(adjusted_dy) or acos(adjusted_dx) will give us our angle.
|
|
||||||
// We'll use asin(adjusted_dy).
|
|
||||||
CGFloat length = distance(pt1, pt2);
|
|
||||||
CGFloat dx = pt2.x - pt1.x;
|
|
||||||
CGFloat dy = pt2.y - pt1.y;
|
|
||||||
CGFloat ajdusted_dy = ABS(dy) / length;
|
|
||||||
CGFloat angle = asin(ajdusted_dy);
|
|
||||||
|
|
||||||
if ((dx < 0) && (dy >= 0)) {
|
|
||||||
// top-left quadrant
|
|
||||||
angle = M_PI - angle;
|
|
||||||
}
|
|
||||||
else if ((dx < 0) && (dy < 0)) {
|
|
||||||
// bottom-left quadrant
|
|
||||||
angle = M_PI + angle;
|
|
||||||
}
|
|
||||||
else if ((dx >= 0) && (dy < 0)) {
|
|
||||||
// bottom-right quadrant
|
|
||||||
angle = (2 * M_PI) - angle;
|
|
||||||
}
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <Python.h>
|
|
||||||
|
|
||||||
void setCocoaViewsModuleName(NSString *moduleName);
|
|
||||||
PyObject* createCallback(NSString *aViewClassName, id aViewRef);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user