mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-24 23:51:38 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29fe4c756 | ||
|
|
942e5a3c31 | ||
|
|
6be927ed8b |
5
.ctags
5
.ctags
@@ -1,5 +0,0 @@
|
||||
-R
|
||||
--exclude=build
|
||||
--exclude=env
|
||||
--exclude=.tox
|
||||
--python-kinds=-i
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,21 +1,25 @@
|
||||
.DS_Store
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.so
|
||||
*.mo
|
||||
*.pyd
|
||||
*.waf*
|
||||
.lock-waf*
|
||||
.idea
|
||||
.tox
|
||||
/tags
|
||||
|
||||
build
|
||||
dist
|
||||
install
|
||||
installer_tmp-cache
|
||||
env
|
||||
/deps
|
||||
cocoa/autogen
|
||||
|
||||
/run.py
|
||||
/conf.json
|
||||
/cocoa/*/Info.plist
|
||||
/cocoa/*/build
|
||||
/qt/*_rc.py
|
||||
/qt/base/*_rc.py
|
||||
/help/*/conf.py
|
||||
/help/*/changelog.rst
|
||||
|
||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,12 +0,0 @@
|
||||
[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
|
||||
128
README.md
128
README.md
@@ -1,11 +1,21 @@
|
||||
# About the "rust" branch
|
||||
|
||||
I'm really excited about Rust and I'm trying out a Rust implementation of dupeGuru's bottlenecks
|
||||
(PE's image comparison).
|
||||
|
||||
For fun.
|
||||
|
||||
# dupeGuru
|
||||
|
||||
[dupeGuru][dupeguru] is a cross-platform (Linux and OS X) GUI tool to find duplicate files in
|
||||
[dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in
|
||||
a system. It's written mostly in Python 3 and has the peculiarity of using
|
||||
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
|
||||
is written in Objective-C and uses Cocoa. On Linux, it's written in Python and uses Qt5.
|
||||
is written in Objective-C and uses Cocoa. On Linux and Windows, it's written in Python and uses Qt5.
|
||||
|
||||
## Current status: People wanted
|
||||
dupeGuru comes in 3 editions (standard, music and picture) which are all buildable from this same
|
||||
source tree. You choose the edition you want to build in a `configure.py` flag.
|
||||
|
||||
# Current status: People wanted
|
||||
|
||||
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
|
||||
corrected.
|
||||
@@ -17,38 +27,13 @@ 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]
|
||||
and let's get started.
|
||||
|
||||
### Slowed development
|
||||
|
||||
Until I manage to find contributors, I'm slowing the development pace of dupeGuru. I'm not much
|
||||
interested in maintaining it alone, I personally have no use for this app (it's been a *loooong*,
|
||||
time since I had dupe problems :) )
|
||||
|
||||
I don't want to let it die, however, so I will still do normal maintainership, that is, issue
|
||||
triaging, code review, critical bugfixes, releases management.
|
||||
|
||||
But anything non-critical, I'm not going to implement it myself because I see every issue as a
|
||||
contribution opportunity.
|
||||
|
||||
### Windows maintainer wanted
|
||||
|
||||
As [described on my website][nowindows], v3.9.x/6.8.x/2.10.x series of dupeGuru are the last ones
|
||||
to support Windows unless someone steps up to maintain it. If you're a Windows developer and are
|
||||
interested in taking this task, [don't hesitate to let me know][contrib-issue].
|
||||
|
||||
### OS X maintainer wanted
|
||||
|
||||
My Mac Mini is already a couple of years old and is likely to be my last Apple purchase. When it
|
||||
dies, I will be unable maintain the OS X version of moneyGuru. I've already stopped paying for the
|
||||
Mac Developer membership so I can't sign the apps anymore (in the "official way" I mean. The
|
||||
download is still PGP signed) If you're a Mac developer and are interested in taking this task,
|
||||
[don't hesitate to let me know][contrib-issue].
|
||||
|
||||
## Contents of this folder
|
||||
# Contents of this folder
|
||||
|
||||
This folder contains the source for dupeGuru. Its documentation is in `help`, but is also
|
||||
[available online][documentation] in its built form. Here's how this source tree is organised:
|
||||
|
||||
* core: Contains the core logic code for dupeGuru. It's Python code.
|
||||
* core_*: Edition-specific-cross-toolkit code written in Python.
|
||||
* cocoa: UI code for the Cocoa toolkit. It's Objective-C code.
|
||||
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
|
||||
* images: Images used by the different UI codebases.
|
||||
@@ -57,99 +42,78 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu
|
||||
* locale: .po files for localisation.
|
||||
|
||||
There are also other sub-folder that comes from external repositories and are part of this repo as
|
||||
git submodules:
|
||||
git subtrees:
|
||||
|
||||
* Sparkle: An auto-update library for the OS X version.
|
||||
* hscommon: A collection of helpers used across HS applications.
|
||||
* cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications.
|
||||
* qtlib: A collection of helpers used across Qt UI codebases of HS applications.
|
||||
|
||||
## How to build dupeGuru from source
|
||||
# How to build dupeGuru from source
|
||||
|
||||
There's a bootstrap script that will make building very easy. There might be some things that you
|
||||
need to install manually on your system, but the bootstrap script will tell you when what you need
|
||||
to install. You can run the bootstrap with:
|
||||
## The very, very, very easy way
|
||||
|
||||
If you're on Linux or Mac, there's a bootstrap script that will make building very, 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.
|
||||
and follow instructions from the script. You can then ignore the rest of the build documentation.
|
||||
|
||||
### Prerequisites installation
|
||||
## Prerequisites installation
|
||||
|
||||
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
|
||||
to be installed manually.
|
||||
|
||||
* All systems: [Python 3.4+][python]
|
||||
* Mac OS X: OS X 10.10+ with XCode command line tools.
|
||||
* Linux: PyQt5
|
||||
* All systems: [Python 3.3+][python] and [setuptools][setuptools]
|
||||
* Mac OS X: The last XCode to have the 10.7 SDK included. Python 3.4+.
|
||||
* Windows: Visual Studio 2010, [PyQt 5.0+][pyqt], [cx_Freeze][cxfreeze] and
|
||||
[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:
|
||||
|
||||
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools python3-venv
|
||||
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools
|
||||
|
||||
### OS X and pyenv
|
||||
On Arch, it's:
|
||||
|
||||
[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:
|
||||
$ pacman -S python-pyqt5
|
||||
|
||||
$ 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.*
|
||||
## Setting up the virtual environment
|
||||
|
||||
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
|
||||
Python-related dependencies. In that environment, we then install our requirements with pip.
|
||||
|
||||
For Linux (`--system-site-packages` is to be able to import PyQt):
|
||||
Python-related dependencies. `pyvenv` is built-in Python but, unlike its `virtualenv` predecessor,
|
||||
it doesn't install setuptools and pip (unless you use Python 3.4+), so it has to be installed
|
||||
manually:
|
||||
|
||||
$ pyvenv --system-site-packages env
|
||||
$ source env/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ python get-pip.py
|
||||
|
||||
For OS X:
|
||||
Then, you can install pip requirements in your virtualenv:
|
||||
|
||||
$ pyvenv env
|
||||
$ source env/bin/activate
|
||||
$ pip install -r requirements-osx.txt
|
||||
$ pip install -r requirements-[osx|win].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:
|
||||
|
||||
$ python configure.py
|
||||
$ python build.py
|
||||
$ python run.py
|
||||
|
||||
You can also package dupeGuru into an installable package with:
|
||||
|
||||
|
||||
$ python package.py
|
||||
|
||||
|
||||
### Generate Ubuntu packages
|
||||
|
||||
$ bash -c "pyvenv --system-site-packages env && source env/bin/activate && pip install -r requirements.txt && python3 build.py --clean && python3 package.py"
|
||||
|
||||
### Running tests
|
||||
|
||||
The complete test suite is ran with [Tox 1.7+][tox]. If you have it installed system-wide, you
|
||||
don't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`.
|
||||
|
||||
If you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then
|
||||
run `tox`.
|
||||
|
||||
You can also run automated tests without Tox. Extra requirements for running tests are in
|
||||
`requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your
|
||||
virtualenv and then `py.test core hscommon`
|
||||
|
||||
[dupeguru]: http://www.hardcoded.net/dupeguru/
|
||||
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
|
||||
[nowindows]: https://www.hardcoded.net/archive2015#2015-11-01
|
||||
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
|
||||
[python]: http://www.python.org/
|
||||
[setuptools]: https://pypi.python.org/pypi/setuptools
|
||||
[pyqt]: http://www.riverbankcomputing.com
|
||||
[pyenv]: https://github.com/yyuu/pyenv
|
||||
[tox]: https://tox.readthedocs.org/en/latest/
|
||||
[cxfreeze]: http://cx-freeze.sourceforge.net/
|
||||
[advinst]: http://www.advancedinstaller.com
|
||||
|
||||
|
||||
44
bootstrap.sh
44
bootstrap.sh
@@ -1,16 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
PYTHON=python3
|
||||
ret=`$PYTHON -c "import sys; print(int(sys.version_info[:2] >= (3, 4)))"`
|
||||
if [ $ret -ne 1 ]; then
|
||||
echo "Python 3.4+ required. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
command -v $PYTHON -m venv >/dev/null 2>&1 || { echo >&2 "Python 3.3 required. Install it and try again. Aborting"; exit 1; }
|
||||
|
||||
|
||||
if [ -d ".git" ]; then
|
||||
git submodule init
|
||||
git submodule update
|
||||
if [ -d "deps" ]; then
|
||||
# We have a collection of dependencies in our source package. We might as well use it instead
|
||||
# of downloading it from PyPI.
|
||||
PIPARGS="--no-index --find-links=deps"
|
||||
fi
|
||||
|
||||
if [ ! -d "env" ]; then
|
||||
@@ -19,11 +15,27 @@ if [ ! -d "env" ]; then
|
||||
# install. To achieve our latter goal, we start with a normal venv, which we later upgrade to
|
||||
# a system-site-packages once pip is installed.
|
||||
if ! $PYTHON -m venv env ; then
|
||||
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
|
||||
exit 1
|
||||
# We're probably under braindead Ubuntu 14.04 which completely messed up ensurepip.
|
||||
# Work around it :(
|
||||
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
|
||||
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
|
||||
$PYTHON -m venv env --upgrade --system-site-packages
|
||||
# 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
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -31,11 +43,11 @@ source env/bin/activate
|
||||
|
||||
echo "Installing pip requirements"
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
./env/bin/pip install -r requirements-osx.txt
|
||||
pip install $PIPARGS -r requirements-osx.txt
|
||||
else
|
||||
./env/bin/python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
|
||||
./env/bin/pip install -r requirements.txt
|
||||
python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.1+ required. Install it and try again. Aborting"; exit 1; }
|
||||
pip install $PIPARGS -r requirements.txt
|
||||
fi
|
||||
|
||||
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
|
||||
echo ". env/bin/activate && python build.py && python run.py"
|
||||
echo ". env/bin/activate && python configure.py && python build.py && python run.py"
|
||||
|
||||
209
build.py
209
build.py
@@ -1,4 +1,6 @@
|
||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||
# Created By: Virgil Dupras
|
||||
# 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,
|
||||
# which should be included with this package. The terms are also available at
|
||||
@@ -9,6 +11,8 @@ import os
|
||||
import os.path as op
|
||||
from optparse import OptionParser
|
||||
import shutil
|
||||
import json
|
||||
import importlib
|
||||
import compileall
|
||||
|
||||
from setuptools import setup, Extension
|
||||
@@ -18,10 +22,10 @@ from hscommon.build import (
|
||||
add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
||||
collect_stdlib_dependencies
|
||||
collect_stdlib_dependencies, copy
|
||||
)
|
||||
from hscommon import loc
|
||||
from hscommon.plat import ISOSX
|
||||
from hscommon.plat import ISOSX, ISLINUX
|
||||
from hscommon.util import ensure_folder, delete_files_with_pattern
|
||||
|
||||
def parse_args():
|
||||
@@ -35,14 +39,6 @@ def parse_args():
|
||||
'--doc', action='store_true', dest='doc',
|
||||
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(
|
||||
'--loc', action='store_true', dest='loc',
|
||||
help="Build only localization"
|
||||
@@ -74,11 +70,18 @@ def parse_args():
|
||||
(options, args) = parser.parse_args()
|
||||
return options
|
||||
|
||||
def cocoa_app():
|
||||
app_path = 'build/dupeGuru.app'
|
||||
def cocoa_compile_command(edition):
|
||||
return '{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition)
|
||||
|
||||
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)
|
||||
|
||||
def build_xibless(dest='cocoa/autogen'):
|
||||
def build_xibless(edition, dest='cocoa/autogen'):
|
||||
import xibless
|
||||
ensure_folder(dest)
|
||||
FNPAIRS = [
|
||||
@@ -89,57 +92,62 @@ def build_xibless(dest='cocoa/autogen'):
|
||||
('prioritize_dialog.py', 'PrioritizeDialog_UI'),
|
||||
('result_window.py', 'ResultWindow_UI'),
|
||||
('main_menu.py', 'MainMenu_UI'),
|
||||
('details_panel.py', 'DetailsPanel_UI'),
|
||||
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
|
||||
('preferences_panel.py', 'PreferencesPanel_UI'),
|
||||
]
|
||||
for srcname, dstname in FNPAIRS:
|
||||
xibless.generate(
|
||||
op.join('cocoa', 'ui', srcname), op.join(dest, dstname),
|
||||
op.join('cocoa', 'base', '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'
|
||||
)
|
||||
for appmode in ('standard', 'music', 'picture'):
|
||||
else:
|
||||
xibless.generate(
|
||||
op.join('cocoa', 'ui', 'preferences_panel.py'),
|
||||
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()),
|
||||
localizationTable='Localizable',
|
||||
args={'appmode': appmode},
|
||||
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
||||
localizationTable='Localizable'
|
||||
)
|
||||
|
||||
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('..', '..'))
|
||||
def build_cocoa(edition, dev):
|
||||
print("Creating OS X app structure")
|
||||
app = cocoa_app()
|
||||
app_version = get_module_version('core')
|
||||
cocoa_project_path = 'cocoa'
|
||||
ed = lambda s: s.format(edition)
|
||||
app = cocoa_app(edition)
|
||||
app_version = get_module_version(ed('core_{}'))
|
||||
cocoa_project_path = ed('cocoa/{}')
|
||||
filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version)
|
||||
app.create(op.join('build', 'Info.plist'))
|
||||
print("Building localizations")
|
||||
build_localizations('cocoa')
|
||||
build_localizations('cocoa', edition)
|
||||
print("Building xibless UIs")
|
||||
build_cocoalib_xibless()
|
||||
build_xibless()
|
||||
build_xibless(edition)
|
||||
print("Building Python extensions")
|
||||
build_cocoa_proxy_module()
|
||||
build_cocoa_bridging_interfaces()
|
||||
build_cocoa_bridging_interfaces(edition)
|
||||
print("Building the cocoa layer")
|
||||
copy_embeddable_python_dylib('build')
|
||||
pydep_folder = op.join(app.resources, 'py')
|
||||
if not op.exists(pydep_folder):
|
||||
os.mkdir(pydep_folder)
|
||||
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 = [
|
||||
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
|
||||
]
|
||||
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
|
||||
] + specific_packages
|
||||
copy_packages(tocopy, pydep_folder, create_links=dev)
|
||||
sys.path.insert(0, 'build')
|
||||
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
||||
# to manually specify it.
|
||||
extra_deps = ['multiprocessing']
|
||||
extra_deps = None
|
||||
if edition == 'pe':
|
||||
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
||||
# to manually specify it.
|
||||
extra_deps = ['multiprocessing']
|
||||
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
|
||||
del sys.path[0]
|
||||
# Views are not referenced by python code, so they're not found by the collector.
|
||||
@@ -152,38 +160,40 @@ def build_cocoa(dev):
|
||||
delete_files_with_pattern(pydep_folder, '__pycache__')
|
||||
print("Compiling with WAF")
|
||||
os.chdir('cocoa')
|
||||
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
|
||||
print_and_do(cocoa_compile_command(edition))
|
||||
os.chdir('..')
|
||||
app.copy_executable('cocoa/build/dupeGuru')
|
||||
build_help()
|
||||
build_help(edition)
|
||||
print("Copying resources and frameworks")
|
||||
image_path = 'cocoa/dupeguru.icns'
|
||||
resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
|
||||
image_path = ed('cocoa/{}/dupeguru.icns')
|
||||
resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
|
||||
app.copy_resources(*resources, use_symlinks=dev)
|
||||
app.copy_frameworks('build/Python', sparkle_framework_path)
|
||||
app.copy_frameworks('build/Python', 'cocoalib/Sparkle.framework')
|
||||
print("Creating the run.py file")
|
||||
tmpl = open('cocoa/run_template.py', 'rt').read()
|
||||
run_contents = tmpl.replace('{{app_path}}', app.dest)
|
||||
open('run.py', 'wt').write(run_contents)
|
||||
|
||||
def build_qt(dev):
|
||||
def build_qt(edition, dev, conf):
|
||||
print("Building localizations")
|
||||
build_localizations('qt')
|
||||
build_localizations('qt', edition)
|
||||
print("Building Qt stuff")
|
||||
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', 'dg_rc.py'))
|
||||
build_help()
|
||||
print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
|
||||
fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py'))
|
||||
build_help(edition)
|
||||
print("Creating the run.py file")
|
||||
shutil.copy(op.join('qt', 'run_template.py'), 'run.py')
|
||||
filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition)
|
||||
|
||||
def build_help():
|
||||
def build_help(edition):
|
||||
print("Generating Help")
|
||||
current_path = op.abspath('.')
|
||||
help_basepath = op.join(current_path, 'help', 'en')
|
||||
help_destpath = op.join(current_path, 'build', 'help')
|
||||
changelog_path = op.join(current_path, 'help', 'changelog')
|
||||
help_destpath = op.join(current_path, 'build', 'help'.format(edition))
|
||||
changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition))
|
||||
tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
|
||||
confrepl = {'language': 'en'}
|
||||
appname = {'se': 'dupeGuru', 'me': 'dupeGuru Music Edition', 'pe': 'dupeGuru Picture Edition'}[edition]
|
||||
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')
|
||||
conftmpl = op.join(current_path, 'help', 'conf.tmpl')
|
||||
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
|
||||
@@ -192,11 +202,11 @@ def build_qt_localizations():
|
||||
loc.compile_all_po(op.join('qtlib', 'locale'))
|
||||
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
|
||||
|
||||
def build_localizations(ui):
|
||||
def build_localizations(ui, edition):
|
||||
loc.compile_all_po('locale')
|
||||
if ui == 'cocoa':
|
||||
app = cocoa_app()
|
||||
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings'))
|
||||
app = cocoa_app(edition)
|
||||
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'))
|
||||
locale_dest = op.join(app.resources, 'locale')
|
||||
elif ui == 'qt':
|
||||
build_qt_localizations()
|
||||
@@ -204,19 +214,32 @@ def build_localizations(ui):
|
||||
if op.exists(locale_dest):
|
||||
shutil.rmtree(locale_dest)
|
||||
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():
|
||||
if ISOSX:
|
||||
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')
|
||||
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
|
||||
build_xibless('se', op.join('cocoa', 'autogen', 'se'))
|
||||
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj')
|
||||
for edition in ('se', 'me', 'pe'):
|
||||
build_xibless(edition, op.join('cocoa', 'autogen', edition))
|
||||
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/base/en.lproj')
|
||||
print("Building .pot files from source files")
|
||||
print("Building core.pot")
|
||||
loc.generate_pot(['core'], op.join('locale', 'core.pot'), ['tr'])
|
||||
all_cores = ['core', 'core_se', 'core_me', 'core_pe']
|
||||
loc.generate_pot(all_cores, op.join('locale', 'core.pot'), ['tr'])
|
||||
print("Building columns.pot")
|
||||
loc.generate_pot(['core'], op.join('locale', 'columns.pot'), ['coltr'])
|
||||
loc.generate_pot(all_cores, op.join('locale', 'columns.pot'), ['coltr'])
|
||||
print("Building ui.pot")
|
||||
# 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.
|
||||
@@ -231,7 +254,7 @@ def build_updatepot():
|
||||
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
||||
print("Enhancing ui.pot with Cocoa's strings files")
|
||||
loc.strings2pot(
|
||||
op.join('cocoa', 'en.lproj', 'Localizable.strings'),
|
||||
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
||||
op.join('locale', 'ui.pot')
|
||||
)
|
||||
|
||||
@@ -260,7 +283,7 @@ def build_cocoa_proxy_module():
|
||||
['cocoalib', 'cocoa/autogen']
|
||||
)
|
||||
|
||||
def build_cocoa_bridging_interfaces():
|
||||
def build_cocoa_bridging_interfaces(edition):
|
||||
print("Building Cocoa Bridging Interfaces")
|
||||
import objp.o2p
|
||||
import objp.p2o
|
||||
@@ -280,12 +303,13 @@ def build_cocoa_bridging_interfaces():
|
||||
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
|
||||
from inter.result_table import PyResultTable, ResultTableView
|
||||
from inter.stats_label import PyStatsLabel, StatsLabelView
|
||||
from inter.app import PyDupeGuru, DupeGuruView
|
||||
from inter.app import PyDupeGuruBase, DupeGuruView
|
||||
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
||||
allclasses = [
|
||||
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
||||
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru,
|
||||
PyTextField, PyProgressWindow
|
||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru
|
||||
]
|
||||
for class_ in allclasses:
|
||||
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
|
||||
@@ -302,21 +326,14 @@ def build_cocoa_bridging_interfaces():
|
||||
def build_pe_modules(ui):
|
||||
print("Building PE Modules")
|
||||
exts = [
|
||||
Extension(
|
||||
"_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')]
|
||||
),
|
||||
Extension("_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':
|
||||
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
|
||||
elif ui == 'cocoa':
|
||||
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=[
|
||||
"-framework", "CoreFoundation",
|
||||
"-framework", "Foundation",
|
||||
@@ -328,25 +345,27 @@ def build_pe_modules(ui):
|
||||
ext_modules=exts,
|
||||
)
|
||||
move_all('_block_qt*', op.join('qt', 'pe'))
|
||||
move_all('_block*', op.join('core', 'pe'))
|
||||
move_all('_cache*', op.join('core', 'pe'))
|
||||
move_all('_block*', 'core_pe')
|
||||
move_all('_cache*', 'core_pe')
|
||||
|
||||
def build_normal(ui, dev):
|
||||
print("Building dupeGuru with UI {}".format(ui))
|
||||
def build_normal(edition, ui, dev, conf):
|
||||
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
|
||||
add_to_pythonpath('.')
|
||||
print("Building dupeGuru")
|
||||
build_pe_modules(ui)
|
||||
if edition == 'pe':
|
||||
build_pe_modules(ui)
|
||||
if ui == 'cocoa':
|
||||
build_cocoa(dev)
|
||||
build_cocoa(edition, dev)
|
||||
elif ui == 'qt':
|
||||
build_qt(dev)
|
||||
build_qt(edition, dev, conf)
|
||||
|
||||
def main():
|
||||
options = parse_args()
|
||||
ui = options.ui
|
||||
if ui not in ('cocoa', 'qt'):
|
||||
ui = 'cocoa' if ISOSX else 'qt'
|
||||
if options.dev:
|
||||
conf = json.load(open('conf.json'))
|
||||
edition = conf['edition']
|
||||
ui = conf['ui']
|
||||
dev = conf['dev']
|
||||
if dev:
|
||||
print("Building in Dev mode")
|
||||
if options.clean:
|
||||
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
|
||||
@@ -355,9 +374,9 @@ def main():
|
||||
if not op.exists('build'):
|
||||
os.mkdir('build')
|
||||
if options.doc:
|
||||
build_help()
|
||||
build_help(edition)
|
||||
elif options.loc:
|
||||
build_localizations(ui)
|
||||
build_localizations(ui, edition)
|
||||
elif options.updatepot:
|
||||
build_updatepot()
|
||||
elif options.mergepot:
|
||||
@@ -366,17 +385,17 @@ def main():
|
||||
build_normpo()
|
||||
elif options.cocoa_ext:
|
||||
build_cocoa_proxy_module()
|
||||
build_cocoa_bridging_interfaces()
|
||||
build_cocoa_bridging_interfaces(edition)
|
||||
elif options.cocoa_compile:
|
||||
os.chdir('cocoa')
|
||||
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
|
||||
print_and_do(cocoa_compile_command(edition))
|
||||
os.chdir('..')
|
||||
cocoa_app().copy_executable('cocoa/build/dupeGuru')
|
||||
cocoa_app(edition).copy_executable('cocoa/build/dupeGuru')
|
||||
elif options.xibless:
|
||||
build_cocoalib_xibless()
|
||||
build_xibless()
|
||||
build_xibless(edition)
|
||||
else:
|
||||
build_normal(ui, options.dev)
|
||||
build_normal(edition, ui, dev, conf)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Submodule cocoa/Sparkle deleted from 1c8d54166b
@@ -10,29 +10,24 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import <Sparkle/SUUpdater.h>
|
||||
#import "PyDupeGuru.h"
|
||||
#import "ResultWindow.h"
|
||||
#import "ResultTable.h"
|
||||
#import "DetailsPanel.h"
|
||||
#import "DirectoryPanel.h"
|
||||
#import "IgnoreListDialog.h"
|
||||
#import "ProblemDialog.h"
|
||||
#import "DeletionOptions.h"
|
||||
#import "HSAboutBox.h"
|
||||
#import "HSRecentFiles.h"
|
||||
#import "HSProgressWindow.h"
|
||||
|
||||
@interface AppDelegate : NSObject <NSFileManagerDelegate>
|
||||
@interface AppDelegateBase : NSObject
|
||||
{
|
||||
NSMenu *recentResultsMenu;
|
||||
NSMenu *columnsMenu;
|
||||
SUUpdater *updater;
|
||||
|
||||
PyDupeGuru *model;
|
||||
ResultWindow *_resultWindow;
|
||||
ResultWindowBase *_resultWindow;
|
||||
DirectoryPanel *_directoryPanel;
|
||||
DetailsPanel *_detailsPanel;
|
||||
IgnoreListDialog *_ignoreListDialog;
|
||||
ProblemDialog *_problemDialog;
|
||||
DeletionOptions *_deletionOptions;
|
||||
HSProgressWindow *_progressWindow;
|
||||
NSWindowController *_preferencesPanel;
|
||||
HSAboutBox *_aboutBox;
|
||||
@@ -46,17 +41,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
/* Virtual */
|
||||
+ (NSDictionary *)defaultPreferences;
|
||||
- (PyDupeGuru *)model;
|
||||
- (ResultWindowBase *)createResultWindow;
|
||||
- (DirectoryPanel *)createDirectoryPanel;
|
||||
- (DetailsPanel *)createDetailsPanel;
|
||||
- (void)setScanOptions;
|
||||
- (NSString *)homepageURL;
|
||||
|
||||
/* Public */
|
||||
- (void)finalizeInit;
|
||||
- (ResultWindow *)resultWindow;
|
||||
- (ResultWindowBase *)resultWindow;
|
||||
- (DirectoryPanel *)directoryPanel;
|
||||
- (DetailsPanel *)detailsPanel;
|
||||
- (HSRecentFiles *)recentResults;
|
||||
- (NSInteger)getAppMode;
|
||||
- (void)setAppMode:(NSInteger)appMode;
|
||||
|
||||
/* Delegate */
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||
@@ -66,7 +61,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
- (void)recentFileClicked:(NSString *)path;
|
||||
|
||||
/* Actions */
|
||||
- (void)clearPictureCache;
|
||||
- (void)loadResults;
|
||||
- (void)openWebsite;
|
||||
- (void)openHelp;
|
||||
@@ -6,19 +6,16 @@ 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 "AppDelegateBase.h"
|
||||
#import "ProgressController.h"
|
||||
#import "HSPyUtil.h"
|
||||
#import "Consts.h"
|
||||
#import "Dialogs.h"
|
||||
#import "Utils.h"
|
||||
#import "ValueTransformers.h"
|
||||
#import "DetailsPanelPicture.h"
|
||||
#import "PreferencesPanelStandard_UI.h"
|
||||
#import "PreferencesPanelMusic_UI.h"
|
||||
#import "PreferencesPanelPicture_UI.h"
|
||||
#import "PreferencesPanel_UI.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
@implementation AppDelegateBase
|
||||
|
||||
@synthesize recentResultsMenu;
|
||||
@synthesize columnsMenu;
|
||||
@@ -27,21 +24,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
+ (NSDictionary *)defaultPreferences
|
||||
{
|
||||
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(11) forKey:TableFontSize];
|
||||
[d setObject:b2n(YES) forKey:@"mixFileKind"];
|
||||
@@ -71,19 +53,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
model = [[PyDupeGuru alloc] init];
|
||||
[model bindCallback:createCallback(@"DupeGuruView", self)];
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -100,17 +69,14 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
}
|
||||
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
|
||||
[_recentResults setDelegate:self];
|
||||
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
|
||||
_resultWindow = [self createResultWindow];
|
||||
_directoryPanel = [self createDirectoryPanel];
|
||||
_detailsPanel = [self createDetailsPanel];
|
||||
_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 setParentWindow:[_directoryPanel window]];
|
||||
// Lazily loaded
|
||||
_aboutBox = nil;
|
||||
_preferencesPanel = nil;
|
||||
_resultWindow = nil;
|
||||
_detailsPanel = nil;
|
||||
[_progressWindow setParentWindow:[_resultWindow window]];
|
||||
_aboutBox = nil; // Lazily loaded
|
||||
_preferencesPanel = nil; // Lazily loaded
|
||||
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
|
||||
}
|
||||
|
||||
@@ -121,51 +87,28 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
return model;
|
||||
}
|
||||
|
||||
- (DetailsPanel *)createDetailsPanel
|
||||
- (ResultWindowBase *)createResultWindow
|
||||
{
|
||||
NSInteger appMode = [self getAppMode];
|
||||
if (appMode == AppModePicture) {
|
||||
return [[DetailsPanelPicture alloc] initWithApp:model];
|
||||
}
|
||||
else {
|
||||
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
|
||||
}
|
||||
return nil; // must be overriden by all editions
|
||||
}
|
||||
|
||||
- (void)setScanOptions
|
||||
- (DirectoryPanel *)createDirectoryPanel
|
||||
{
|
||||
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"])];
|
||||
return [[DirectoryPanel alloc] initWithParentApp:self];
|
||||
}
|
||||
|
||||
- (DetailsPanel *)createDetailsPanel
|
||||
{
|
||||
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
|
||||
}
|
||||
|
||||
- (NSString *)homepageURL
|
||||
{
|
||||
return @""; // must be overriden by all editions
|
||||
}
|
||||
|
||||
/* Public */
|
||||
- (ResultWindow *)resultWindow
|
||||
- (ResultWindowBase *)resultWindow
|
||||
{
|
||||
return _resultWindow;
|
||||
}
|
||||
@@ -185,29 +128,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
return _recentResults;
|
||||
}
|
||||
|
||||
- (NSInteger)getAppMode
|
||||
{
|
||||
return [model getAppMode];
|
||||
}
|
||||
|
||||
- (void)setAppMode:(NSInteger)appMode
|
||||
{
|
||||
[model setAppMode:appMode];
|
||||
if (_preferencesPanel != nil) {
|
||||
[_preferencesPanel release];
|
||||
_preferencesPanel = nil;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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
|
||||
{
|
||||
NSOpenPanel *op = [NSOpenPanel openPanel];
|
||||
@@ -226,7 +147,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
- (void)openWebsite
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]];
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self homepageURL]]];
|
||||
}
|
||||
|
||||
- (void)openHelp
|
||||
@@ -253,18 +174,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
- (void)showPreferencesPanel
|
||||
{
|
||||
if (_preferencesPanel == 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 = [[NSWindowController alloc] initWithWindow:createPreferencesPanel_UI(nil)];
|
||||
}
|
||||
[_preferencesPanel showWindow:nil];
|
||||
}
|
||||
@@ -281,7 +191,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
- (void)startScanning
|
||||
{
|
||||
[[self directoryPanel] startDuplicateScan];
|
||||
[[self resultWindow] startDuplicateScan];
|
||||
}
|
||||
|
||||
|
||||
@@ -344,25 +254,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
|
||||
}
|
||||
|
||||
- (void)createResultsWindow
|
||||
{
|
||||
if (_resultWindow != nil) {
|
||||
[_resultWindow release];
|
||||
}
|
||||
if (_detailsPanel != nil) {
|
||||
[_detailsPanel release];
|
||||
}
|
||||
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
|
||||
_detailsPanel = [self createDetailsPanel];
|
||||
}
|
||||
- (void)showResultsWindow
|
||||
{
|
||||
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
|
||||
}
|
||||
|
||||
- (void)showProblemDialog
|
||||
{
|
||||
[_problemDialog showWindow:self];
|
||||
[[self resultWindow] showProblemDialog];
|
||||
}
|
||||
|
||||
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
|
||||
@@ -17,8 +17,3 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#define jobDelete @"job_delete"
|
||||
|
||||
#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 "PyDetailsPanel.h"
|
||||
|
||||
@interface DetailsPanel : NSWindowController <NSTableViewDataSource>
|
||||
@interface DetailsPanelBase : NSWindowController <NSTableViewDataSource>
|
||||
{
|
||||
NSTableView *detailsTable;
|
||||
|
||||
@@ -6,11 +6,10 @@ 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 "DetailsPanelBase.h"
|
||||
#import "HSPyUtil.h"
|
||||
#import "DetailsPanel_UI.h"
|
||||
|
||||
@implementation DetailsPanel
|
||||
@implementation DetailsPanelBase
|
||||
|
||||
@synthesize detailsTable;
|
||||
|
||||
@@ -36,7 +35,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
- (NSWindow *)createWindow
|
||||
{
|
||||
return createDetailsPanel_UI(self);
|
||||
return nil; // Virtual
|
||||
}
|
||||
|
||||
- (void)refreshDetails
|
||||
@@ -12,17 +12,15 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import "DirectoryOutline.h"
|
||||
#import "PyDupeGuru.h"
|
||||
|
||||
@class AppDelegate;
|
||||
@class AppDelegateBase;
|
||||
|
||||
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
|
||||
{
|
||||
AppDelegate *_app;
|
||||
AppDelegateBase *_app;
|
||||
PyDupeGuru *model;
|
||||
HSRecentFiles *_recentDirectories;
|
||||
DirectoryOutline *outline;
|
||||
BOOL _alwaysShowPopUp;
|
||||
NSSegmentedControl *appModeSelector;
|
||||
NSPopUpButton *scanTypePopup;
|
||||
NSPopUpButton *addButtonPopUp;
|
||||
NSPopUpButton *loadRecentButtonPopUp;
|
||||
HSOutlineView *outlineView;
|
||||
@@ -30,25 +28,21 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
NSButton *loadResultsButton;
|
||||
}
|
||||
|
||||
@property (readwrite, retain) NSSegmentedControl *appModeSelector;
|
||||
@property (readwrite, retain) NSPopUpButton *scanTypePopup;
|
||||
@property (readwrite, retain) NSPopUpButton *addButtonPopUp;
|
||||
@property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp;
|
||||
@property (readwrite, retain) HSOutlineView *outlineView;
|
||||
@property (readwrite, retain) NSButton *removeButton;
|
||||
@property (readwrite, retain) NSButton *loadResultsButton;
|
||||
|
||||
- (id)initWithParentApp:(AppDelegate *)aParentApp;
|
||||
- (id)initWithParentApp:(AppDelegateBase *)aParentApp;
|
||||
|
||||
- (void)fillPopUpMenu;
|
||||
- (void)fillScanTypeMenu;
|
||||
- (void)fillPopUpMenu; // Virtual
|
||||
- (void)adjustUIToLocalization;
|
||||
|
||||
- (void)askForDirectory;
|
||||
- (void)popupAddDirectoryMenu:(id)sender;
|
||||
- (void)popupLoadRecentMenu:(id)sender;
|
||||
- (void)removeSelectedDirectory;
|
||||
- (void)startDuplicateScan;
|
||||
|
||||
- (void)addDirectory:(NSString *)directory;
|
||||
- (void)refreshRemoveButtonText;
|
||||
@@ -11,27 +11,22 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import "Dialogs.h"
|
||||
#import "Utils.h"
|
||||
#import "AppDelegate.h"
|
||||
#import "Consts.h"
|
||||
|
||||
@implementation DirectoryPanel
|
||||
|
||||
@synthesize appModeSelector;
|
||||
@synthesize scanTypePopup;
|
||||
@synthesize addButtonPopUp;
|
||||
@synthesize loadRecentButtonPopUp;
|
||||
@synthesize outlineView;
|
||||
@synthesize removeButton;
|
||||
@synthesize loadResultsButton;
|
||||
|
||||
- (id)initWithParentApp:(AppDelegate *)aParentApp
|
||||
- (id)initWithParentApp:(AppDelegateBase *)aParentApp
|
||||
{
|
||||
self = [super initWithWindow:nil];
|
||||
[self setWindow:createDirectoryPanel_UI(self)];
|
||||
_app = aParentApp;
|
||||
model = [_app model];
|
||||
[[self window] setTitle:[model appName]];
|
||||
self.appModeSelector.selectedSegment = 0;
|
||||
[self fillScanTypeMenu];
|
||||
_alwaysShowPopUp = NO;
|
||||
[self fillPopUpMenu];
|
||||
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
|
||||
@@ -64,25 +59,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
[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
|
||||
{
|
||||
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
|
||||
@@ -121,23 +97,6 @@ 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
|
||||
{
|
||||
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
|
||||
@@ -175,16 +134,6 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
[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 */
|
||||
- (void)addDirectory:(NSString *)directory
|
||||
{
|
||||
@@ -10,12 +10,14 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import <Quartz/Quartz.h>
|
||||
#import "StatsLabel.h"
|
||||
#import "ResultTable.h"
|
||||
#import "ProblemDialog.h"
|
||||
#import "DeletionOptions.h"
|
||||
#import "HSTableView.h"
|
||||
#import "PyDupeGuru.h"
|
||||
|
||||
@class AppDelegate;
|
||||
@class AppDelegateBase;
|
||||
|
||||
@interface ResultWindow : NSWindowController
|
||||
@interface ResultWindowBase : NSWindowController
|
||||
{
|
||||
@protected
|
||||
NSSegmentedControl *optionsSwitch;
|
||||
@@ -24,10 +26,12 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
NSTextField *stats;
|
||||
NSSearchField *filterField;
|
||||
|
||||
AppDelegate *app;
|
||||
AppDelegateBase *app;
|
||||
PyDupeGuru *model;
|
||||
ResultTable *table;
|
||||
StatsLabel *statsLabel;
|
||||
ProblemDialog *problemDialog;
|
||||
DeletionOptions *deletionOptions;
|
||||
QLPreviewPanel* previewPanel;
|
||||
}
|
||||
|
||||
@@ -37,13 +41,17 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
@property (readwrite, retain) NSTextField *stats;
|
||||
@property (readwrite, retain) NSSearchField *filterField;
|
||||
|
||||
- (id)initWithParentApp:(AppDelegate *)app;
|
||||
- (id)initWithParentApp:(AppDelegateBase *)app;
|
||||
|
||||
/* Virtual */
|
||||
- (void)initResultColumns;
|
||||
- (void)setScanOptions;
|
||||
|
||||
/* Helpers */
|
||||
- (void)fillColumnsMenu;
|
||||
- (void)updateOptionSegments;
|
||||
- (void)showProblemDialog;
|
||||
- (void)adjustUIToLocalization;
|
||||
- (void)initResultColumns:(ResultTable *)aTable;
|
||||
|
||||
/* Actions */
|
||||
- (void)changeOptions;
|
||||
@@ -67,6 +75,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
- (void)resetColumnsToDefault;
|
||||
- (void)revealSelected;
|
||||
- (void)saveResults;
|
||||
- (void)startDuplicateScan;
|
||||
- (void)switchSelected;
|
||||
- (void)toggleColumn:(id)sender;
|
||||
- (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
|
||||
*/
|
||||
|
||||
#import "ResultWindow.h"
|
||||
#import "ResultWindowBase.h"
|
||||
#import "ResultWindow_UI.h"
|
||||
#import "Dialogs.h"
|
||||
#import "ProgressController.h"
|
||||
@@ -15,7 +15,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import "Consts.h"
|
||||
#import "PrioritizeDialog.h"
|
||||
|
||||
@implementation ResultWindow
|
||||
@implementation ResultWindowBase
|
||||
|
||||
@synthesize optionsSwitch;
|
||||
@synthesize optionsToolbarItem;
|
||||
@@ -23,7 +23,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
@synthesize stats;
|
||||
@synthesize filterField;
|
||||
|
||||
- (id)initWithParentApp:(AppDelegate *)aApp;
|
||||
- (id)initWithParentApp:(AppDelegateBase *)aApp;
|
||||
{
|
||||
self = [super initWithWindow:nil];
|
||||
app = aApp;
|
||||
@@ -34,7 +34,9 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
|
||||
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
|
||||
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
|
||||
[self initResultColumns:table];
|
||||
problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
|
||||
deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
|
||||
[self initResultColumns];
|
||||
[[table columns] setColumnsAsReadOnly];
|
||||
[self fillColumnsMenu];
|
||||
[matches setTarget:self];
|
||||
@@ -47,9 +49,19 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
{
|
||||
[table release];
|
||||
[statsLabel release];
|
||||
[problemDialog release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
/* Virtual */
|
||||
- (void)initResultColumns
|
||||
{
|
||||
}
|
||||
|
||||
- (void)setScanOptions
|
||||
{
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
- (void)fillColumnsMenu
|
||||
{
|
||||
@@ -77,6 +89,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
|
||||
}
|
||||
|
||||
- (void)showProblemDialog
|
||||
{
|
||||
[problemDialog showWindow:self];
|
||||
}
|
||||
|
||||
- (void)adjustUIToLocalization
|
||||
{
|
||||
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
|
||||
@@ -101,87 +118,6 @@ 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 */
|
||||
- (void)changeOptions
|
||||
{
|
||||
@@ -327,6 +263,16 @@ 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
|
||||
{
|
||||
[model makeSelectedReference];
|
||||
@@ -5,10 +5,6 @@ result = Window(425, 300, "dupeGuru")
|
||||
promptLabel = Label(result, "Select folders to scan and press \"Scan\".")
|
||||
directoryOutline = OutlineView(result)
|
||||
directoryOutline.OBJC_CLASS = 'HSOutlineView'
|
||||
appModeSelector = SegmentedControl(result)
|
||||
appModeLabel = Label(result, "Application Mode:")
|
||||
scanTypePopup = Popup(result)
|
||||
scanTypeLabel = Label(result, "Scan Type:")
|
||||
addButton = Button(result, "")
|
||||
removeButton = Button(result, "")
|
||||
loadResultsButton = Button(result, "Load Results")
|
||||
@@ -17,8 +13,6 @@ addPopup = Popup(None)
|
||||
loadRecentPopup = Popup(None)
|
||||
|
||||
owner.outlineView = directoryOutline
|
||||
owner.appModeSelector = appModeSelector
|
||||
owner.scanTypePopup = scanTypePopup
|
||||
owner.removeButton = removeButton
|
||||
owner.loadResultsButton = loadResultsButton
|
||||
owner.addButtonPopUp = addPopup
|
||||
@@ -26,9 +20,7 @@ owner.loadRecentButtonPopUp = loadRecentPopup
|
||||
|
||||
result.autosaveName = 'DirectoryPanel'
|
||||
result.canMinimize = False
|
||||
result.minSize = Size(400, 270)
|
||||
for label in ["Standard", "Music", "Picture"]:
|
||||
appModeSelector.addSegment(label, 80)
|
||||
result.minSize = Size(370, 270)
|
||||
addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle
|
||||
addButton.image = 'NSAddTemplate'
|
||||
removeButton.image = 'NSRemoveTemplate'
|
||||
@@ -36,7 +28,6 @@ for button in (addButton, removeButton):
|
||||
button.style = const.NSTexturedRoundedBezelStyle
|
||||
button.imagePosition = const.NSImageOnly
|
||||
scanButton.keyEquivalent = '\\r'
|
||||
appModeSelector.action = Action(owner, 'changeAppMode:')
|
||||
addButton.action = Action(owner, 'popupAddDirectoryMenu:')
|
||||
removeButton.action = Action(owner, 'removeSelectedDirectory')
|
||||
loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:')
|
||||
@@ -55,21 +46,18 @@ directoryOutline.allowsColumnReordering = False
|
||||
directoryOutline.allowsColumnSelection = False
|
||||
directoryOutline.allowsMultipleSelection = True
|
||||
|
||||
appModeLabel.width = scanTypeLabel.width = 110
|
||||
scanTypePopup.width = 248
|
||||
appModeLayout = HLayout([appModeLabel, appModeSelector])
|
||||
scanTypeLayout = HLayout([scanTypeLabel, scanTypePopup])
|
||||
|
||||
for button in (addButton, removeButton):
|
||||
button.width = 28
|
||||
for button in (loadResultsButton, scanButton):
|
||||
button.width = 118
|
||||
|
||||
buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton])
|
||||
mainLayout = VLayout([appModeLayout, scanTypeLayout, promptLabel, directoryOutline, buttonLayout], filler=directoryOutline)
|
||||
mainLayout.packToCorner(Pack.UpperLeft)
|
||||
mainLayout.fill(Pack.LowerRight)
|
||||
promptLabel.packToCorner(Pack.UpperLeft)
|
||||
promptLabel.fill(Pack.Right)
|
||||
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)
|
||||
directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True)
|
||||
@@ -1,5 +1,6 @@
|
||||
ownerclass = 'AppDelegate'
|
||||
ownerimport = 'AppDelegate.h'
|
||||
ownerclass = 'AppDelegateBase'
|
||||
ownerimport = 'AppDelegateBase.h'
|
||||
edition = args.get('edition', 'se')
|
||||
|
||||
result = Menu("")
|
||||
appMenu = result.addMenu("dupeGuru")
|
||||
@@ -29,7 +30,10 @@ owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
|
||||
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 CSV", Action(owner.model, 'exportToCSV'))
|
||||
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
|
||||
if edition == 'pe':
|
||||
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 None", Action(None, 'markNone'), 'cmd+shift+a')
|
||||
@@ -1,14 +1,26 @@
|
||||
appmode = args.get('appmode', 'standard')
|
||||
edition = args.get('edition', 'se')
|
||||
dialogTitles = {
|
||||
'se': "dupeGuru Preferences",
|
||||
'me': "dupeGuru ME Preferences",
|
||||
'pe': "dupeGuru PE Preferences",
|
||||
}
|
||||
dialogHeights = {
|
||||
'standard': 325,
|
||||
'music': 345,
|
||||
'picture': 255,
|
||||
'se': 345,
|
||||
'me': 365,
|
||||
'pe': 275,
|
||||
}
|
||||
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[appmode], "dupeGuru Preferences")
|
||||
result = Window(410, dialogHeights[edition], dialogTitles[edition])
|
||||
tabView = TabView(result)
|
||||
basicTab = tabView.addTab("Basic")
|
||||
advancedTab = tabView.addTab("Advanced")
|
||||
scanTypePopup = Popup(basicTab.view, scanTypeNames[edition])
|
||||
scanTypeLabel = Label(basicTab.view, "Scan Type:")
|
||||
thresholdSlider = Slider(basicTab.view, 1, 100, 80)
|
||||
thresholdLabel = Label(basicTab.view, "Filter hardness:")
|
||||
moreResultsLabel = Label(basicTab.view, "More results")
|
||||
@@ -16,19 +28,19 @@ fewerResultsLabel = Label(basicTab.view, "Fewer results")
|
||||
thresholdValueLabel = Label(basicTab.view, "")
|
||||
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
|
||||
fontSizeLabel = Label(basicTab.view, "Font Size:")
|
||||
if appmode in ('standard', 'music'):
|
||||
if edition in ('se', 'me'):
|
||||
wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
|
||||
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
|
||||
elif appmode == 'picture':
|
||||
elif edition == 'pe':
|
||||
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
|
||||
mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
|
||||
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
|
||||
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
|
||||
if appmode == 'standard':
|
||||
if edition == 'se':
|
||||
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
|
||||
smallFilesThresholdText = TextField(basicTab.view, "")
|
||||
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
|
||||
elif appmode == 'music':
|
||||
elif edition == 'me':
|
||||
tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
|
||||
trackBox = Checkbox(basicTab.view, "Track")
|
||||
artistBox = Checkbox(basicTab.view, "Artist")
|
||||
@@ -47,6 +59,8 @@ copyMoveLabel = Label(advancedTab.view, "Copy and Move:")
|
||||
copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"])
|
||||
|
||||
resetToDefaultsButton = Button(result, "Reset To Defaults")
|
||||
|
||||
scanTypePopup.bind('selectedIndex', defaults, 'values.scanType')
|
||||
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
|
||||
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
|
||||
fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
|
||||
@@ -58,61 +72,59 @@ ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
|
||||
debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
|
||||
customCommandText.bind('value', defaults, 'values.CustomCommand')
|
||||
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
|
||||
if appmode in ('standard', 'music'):
|
||||
if edition in ('se', 'me'):
|
||||
wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
|
||||
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
|
||||
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
|
||||
for control in disableWhenContentScan:
|
||||
vtname = 'vtScanTypeMusicIsNotContent' if appmode == 'music' else 'vtScanTypeIsNotContent'
|
||||
prefname = 'values.scanTypeMusic' if appmode == 'music' else 'values.scanTypeStandard'
|
||||
control.bind('enabled', defaults, prefname, valueTransformer=vtname)
|
||||
if appmode == 'standard':
|
||||
control.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsNotContent')
|
||||
if edition == 'se':
|
||||
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
|
||||
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
|
||||
elif appmode == 'music':
|
||||
elif edition == 'me':
|
||||
for box in tagBoxes:
|
||||
box.bind('enabled', defaults, 'values.scanTypeMusic', valueTransformer='vtScanTypeIsTag')
|
||||
box.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsTag')
|
||||
trackBox.bind('value', defaults, 'values.scanTagTrack')
|
||||
artistBox.bind('value', defaults, 'values.scanTagArtist')
|
||||
albumBox.bind('value', defaults, 'values.scanTagAlbum')
|
||||
titleBox.bind('value', defaults, 'values.scanTagTitle')
|
||||
genreBox.bind('value', defaults, 'values.scanTagGenre')
|
||||
yearBox.bind('value', defaults, 'values.scanTagYear')
|
||||
elif appmode == 'picture':
|
||||
elif edition == 'pe':
|
||||
matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled')
|
||||
thresholdSlider.bind('enabled', defaults, 'values.scanTypePicture', valueTransformer='vtScanTypeIsFuzzy')
|
||||
thresholdSlider.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsFuzzy')
|
||||
|
||||
result.canResize = False
|
||||
result.canMinimize = False
|
||||
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
|
||||
thresholdValueLabel.formatter.maximumFractionDigits = 0
|
||||
allLabels = [thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
|
||||
allLabels = [scanTypeLabel, thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
|
||||
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
|
||||
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
|
||||
ignoreHardlinksBox, debugModeCheckbox]
|
||||
if appmode == 'standard':
|
||||
if edition == 'se':
|
||||
allLabels += [smallFilesThresholdSuffixLabel]
|
||||
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
|
||||
elif appmode == 'music':
|
||||
elif edition == 'me':
|
||||
allLabels += [tagsToScanLabel]
|
||||
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
|
||||
elif appmode == 'picture':
|
||||
elif edition == 'pe':
|
||||
allCheckboxes += [matchDifferentDimensionsBox]
|
||||
for label in allLabels:
|
||||
label.controlSize = ControlSize.Small
|
||||
fewerResultsLabel.alignment = TextAlignment.Right
|
||||
for checkbox in allCheckboxes:
|
||||
checkbox.font = thresholdValueLabel.font
|
||||
checkbox.font = scanTypeLabel.font
|
||||
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
|
||||
|
||||
thresholdLabel.width = fontSizeLabel.width = 94
|
||||
scanTypeLabel.width = thresholdLabel.width = fontSizeLabel.width = 94
|
||||
fontSizeCombo.width = 66
|
||||
thresholdValueLabel.width = 25
|
||||
resetToDefaultsButton.width = 136
|
||||
if appmode == 'standard':
|
||||
if edition == 'se':
|
||||
smallFilesThresholdText.width = 60
|
||||
smallFilesThresholdSuffixLabel.width = 40
|
||||
elif appmode == 'music':
|
||||
elif edition == 'me':
|
||||
for box in tagBoxes:
|
||||
box.width = 70
|
||||
|
||||
@@ -121,18 +133,22 @@ tabView.fill(Pack.Right)
|
||||
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
|
||||
tabView.fill(Pack.Below, margin=14)
|
||||
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
|
||||
thresholdLayout = HLayout([thresholdLabel, thresholdSlider, thresholdValueLabel], filler=thresholdSlider)
|
||||
thresholdLayout.packToCorner(Pack.UpperLeft)
|
||||
thresholdLayout.fill(Pack.Right)
|
||||
scanTypePopup.packToCorner(Pack.UpperRight)
|
||||
scanTypeLabel.packRelativeTo(scanTypePopup, Pack.Left)
|
||||
scanTypePopup.fill(Pack.Left)
|
||||
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
|
||||
# after the slider's fill operation.
|
||||
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
|
||||
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
|
||||
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
|
||||
thresholdLabel.packRelativeTo(thresholdSlider, Pack.Left)
|
||||
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
|
||||
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
|
||||
|
||||
if appmode == 'music':
|
||||
if edition == 'me':
|
||||
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
|
||||
tagsToScanLabel.fill(Pack.Left)
|
||||
tagsToScanLabel.fill(Pack.Right)
|
||||
@@ -147,13 +163,13 @@ if appmode == 'music':
|
||||
else:
|
||||
viewToPackCheckboxesUnder = fontSizeCombo
|
||||
|
||||
if appmode == 'standard':
|
||||
if edition == 'se':
|
||||
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
|
||||
ignoreSmallFilesBox]
|
||||
elif appmode == 'music':
|
||||
elif edition == 'me':
|
||||
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
|
||||
checkForUpdatesBox]
|
||||
elif appmode == 'picture':
|
||||
elif edition == 'pe':
|
||||
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
|
||||
checkForUpdatesBox]
|
||||
checkboxLayout = VLayout(checkboxesToLayout)
|
||||
@@ -161,7 +177,7 @@ checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
|
||||
checkboxLayout.fill(Pack.Left)
|
||||
checkboxLayout.fill(Pack.Right)
|
||||
|
||||
if appmode == 'standard':
|
||||
if edition == 'se':
|
||||
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
|
||||
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
|
||||
checkForUpdatesBox.fill(Pack.Right)
|
||||
@@ -1,5 +1,5 @@
|
||||
ownerclass = 'ResultWindow'
|
||||
ownerimport = 'ResultWindow.h'
|
||||
ownerclass = 'ResultWindowBase'
|
||||
ownerimport = 'ResultWindowBase.h'
|
||||
|
||||
result = Window(557, 400, "dupeGuru Results")
|
||||
toolbar = result.createToolbar('ResultsToolbar')
|
||||
@@ -1,56 +1,23 @@
|
||||
import logging
|
||||
|
||||
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.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):
|
||||
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
|
||||
def createResultsWindow(self): pass
|
||||
def showResultsWindow(self): pass
|
||||
def showProblemDialog(self): pass
|
||||
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
|
||||
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
|
||||
|
||||
class PyDupeGuru(PyBaseApp):
|
||||
class PyDupeGuruBase(PyBaseApp):
|
||||
@dontwrap
|
||||
def __init__(self):
|
||||
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
|
||||
def _init(self, modelclass):
|
||||
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
|
||||
install_exception_hook('https://github.com/hsoft/dupeguru/issues')
|
||||
install_cocoa_logger()
|
||||
patch_threaded_job_performer()
|
||||
self.model = DupeGuru(self)
|
||||
self.model = modelclass(self)
|
||||
|
||||
#---Sub-proxies
|
||||
def detailsPanel(self) -> pyref:
|
||||
@@ -155,62 +122,13 @@ class PyDupeGuru(PyBaseApp):
|
||||
def showIgnoreList(self):
|
||||
self.model.ignore_list_dialog.show()
|
||||
|
||||
def clearPictureCache(self):
|
||||
self.model.clear_picture_cache()
|
||||
|
||||
#---Information
|
||||
def getScanOptions(self) -> list:
|
||||
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
|
||||
|
||||
def resultsAreModified(self) -> bool:
|
||||
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
|
||||
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):
|
||||
self.model.options['mix_file_kind'] = mix_file_kind
|
||||
self.model.scanner.mix_file_kind = mix_file_kind
|
||||
|
||||
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
|
||||
self.model.options['escape_filter_regexp'] = escape_filter_regexp
|
||||
@@ -229,13 +147,12 @@ class PyDupeGuru(PyBaseApp):
|
||||
def ask_yes_no(self, prompt):
|
||||
return self.callback.askYesNoWithPrompt_(prompt)
|
||||
|
||||
@dontwrap
|
||||
def create_results_window(self):
|
||||
self.callback.createResultsWindow()
|
||||
|
||||
@dontwrap
|
||||
def show_results_window(self):
|
||||
self.callback.showResultsWindow()
|
||||
# Not needed yet because our progress dialog is shown as a sheet of the results window,
|
||||
# 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
|
||||
def show_problem_dialog(self):
|
||||
|
||||
287
cocoa/inter/app_me.py
Normal file
287
cocoa/inter/app_me.py
Normal file
@@ -0,0 +1,287 @@
|
||||
# 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)
|
||||
342
cocoa/inter/app_pe.py
Normal file
342
cocoa/inter/app_pe.py
Normal file
@@ -0,0 +1,342 @@
|
||||
# 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,13 +1,22 @@
|
||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||
# Created By: Virgil Dupras
|
||||
# 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,
|
||||
# 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
|
||||
import logging
|
||||
import os.path as op
|
||||
|
||||
from hscommon.path import Path, pathify
|
||||
from core.se import fs
|
||||
from cocoa import proxy
|
||||
|
||||
from core.scanner import ScanType
|
||||
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):
|
||||
uti = proxy.getUTI_(str_path)
|
||||
@@ -20,11 +29,15 @@ class Bundle(fs.Folder):
|
||||
@pathify
|
||||
def can_handle(cls, path: Path):
|
||||
return not path.islink() and path.isdir() and is_bundle(str(path))
|
||||
|
||||
|
||||
class Directories(DirectoriesBase):
|
||||
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
|
||||
HOME_PATH_TO_EXCLUDE = [Path('Library')]
|
||||
|
||||
def __init__(self):
|
||||
DirectoriesBase.__init__(self, fileclasses=[Bundle, fs.File])
|
||||
self.folderclass = fs.Folder
|
||||
|
||||
def _default_state_for_path(self, path):
|
||||
result = DirectoriesBase._default_state_for_path(self, path)
|
||||
if result is not None:
|
||||
@@ -45,9 +58,47 @@ class Directories(DirectoriesBase):
|
||||
yield from_folder
|
||||
return
|
||||
else:
|
||||
yield from DirectoriesBase._get_folders(self, from_folder, j)
|
||||
for folder in DirectoriesBase._get_folders(self, from_folder, j):
|
||||
yield folder
|
||||
|
||||
@staticmethod
|
||||
def get_subfolders(path):
|
||||
result = DirectoriesBase.get_subfolders(path)
|
||||
return [p for p in result if not is_bundle(str(p))]
|
||||
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
def __init__(self, view):
|
||||
# appdata = 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
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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 ''
|
||||
282
cocoa/inter/tunes.py
Normal file
282
cocoa/inter/tunes.py
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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', [])]
|
||||
16
cocoa/me/AppDelegate.h
Normal file
16
cocoa/me/AppDelegate.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import "AppDelegateBase.h"
|
||||
#import "ResultWindow.h"
|
||||
#import "PyDupeGuru.h"
|
||||
|
||||
@interface AppDelegate : AppDelegateBase {}
|
||||
- (void)removeDeadTracks;
|
||||
@end
|
||||
68
cocoa/me/AppDelegate.m
Normal file
68
cocoa/me/AppDelegate.m
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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
|
||||
11
cocoa/me/Consts.h
Normal file
11
cocoa/me/Consts.h
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
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"
|
||||
13
cocoa/me/DetailsPanel.h
Normal file
13
cocoa/me/DetailsPanel.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import "DetailsPanelBase.h"
|
||||
|
||||
@interface DetailsPanel : DetailsPanelBase
|
||||
@end
|
||||
17
cocoa/me/DetailsPanel.m
Normal file
17
cocoa/me/DetailsPanel.m
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
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
|
||||
16
cocoa/me/DirectoryPanel.h
Normal file
16
cocoa/me/DirectoryPanel.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import "../base/DirectoryPanel.h"
|
||||
|
||||
@interface DirectoryPanelME : DirectoryPanel
|
||||
{
|
||||
}
|
||||
- (IBAction)addiTunes:(id)sender;
|
||||
@end
|
||||
32
cocoa/me/DirectoryPanel.m
Normal file
32
cocoa/me/DirectoryPanel.m
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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
|
||||
40
cocoa/me/InfoTemplate.plist
Normal file
40
cocoa/me/InfoTemplate.plist
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
14
cocoa/me/ResultWindow.h
Normal file
14
cocoa/me/ResultWindow.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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
|
||||
77
cocoa/me/ResultWindow.m
Normal file
77
cocoa/me/ResultWindow.m
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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
|
||||
@@ -10,7 +10,7 @@ install_gettext_trans_under_cocoa()
|
||||
from cocoa.inter import PySelectableList, PyColumns, PyTable
|
||||
|
||||
from inter.all import *
|
||||
from inter.app import PyDupeGuru
|
||||
from inter.app_me import PyDupeGuru
|
||||
|
||||
# When built under virtualenv, the dependency collector misses this module, so we have to force it
|
||||
# to see the module.
|
||||
BIN
cocoa/me/dupeguru.icns
Executable file
BIN
cocoa/me/dupeguru.icns
Executable file
Binary file not shown.
14
cocoa/pe/AppDelegate.h
Normal file
14
cocoa/pe/AppDelegate.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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
|
||||
61
cocoa/pe/AppDelegate.m
Normal file
61
cocoa/pe/AppDelegate.m
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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
|
||||
11
cocoa/pe/Consts.h
Normal file
11
cocoa/pe/Consts.h
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
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"
|
||||
@@ -7,10 +7,10 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "DetailsPanel.h"
|
||||
#import "DetailsPanelBase.h"
|
||||
#import "PyDupeGuru.h"
|
||||
|
||||
@interface DetailsPanelPicture : DetailsPanel
|
||||
@interface DetailsPanel : DetailsPanelBase
|
||||
{
|
||||
NSImageView *dupeImage;
|
||||
NSProgressIndicator *dupeProgressIndicator;
|
||||
@@ -10,11 +10,11 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
#import "NSNotificationAdditions.h"
|
||||
#import "NSImageAdditions.h"
|
||||
#import "PyDupeGuru.h"
|
||||
#import "DetailsPanelPicture.h"
|
||||
#import "DetailsPanel.h"
|
||||
#import "Consts.h"
|
||||
#import "DetailsPanelPicture_UI.h"
|
||||
#import "DetailsPanel_UI.h"
|
||||
|
||||
@implementation DetailsPanelPicture
|
||||
@implementation DetailsPanel
|
||||
|
||||
@synthesize dupeImage;
|
||||
@synthesize dupeProgressIndicator;
|
||||
@@ -32,7 +32,7 @@ http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
- (NSWindow *)createWindow
|
||||
{
|
||||
return createDetailsPanelPicture_UI(self);
|
||||
return createDetailsPanel_UI(self);
|
||||
}
|
||||
|
||||
- (void)loadImageAsync:(NSString *)imagePath
|
||||
17
cocoa/pe/DirectoryPanel.h
Normal file
17
cocoa/pe/DirectoryPanel.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
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
|
||||
40
cocoa/pe/DirectoryPanel.m
Normal file
40
cocoa/pe/DirectoryPanel.m
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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
|
||||
40
cocoa/pe/InfoTemplate.plist
Normal file
40
cocoa/pe/InfoTemplate.plist
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
14
cocoa/pe/ResultWindow.h
Normal file
14
cocoa/pe/ResultWindow.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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
|
||||
58
cocoa/pe/ResultWindow.m
Normal file
58
cocoa/pe/ResultWindow.m
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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
|
||||
17
cocoa/pe/dg_cocoa.py
Normal file
17
cocoa/pe/dg_cocoa.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
BIN
cocoa/pe/dupeguru.icns
Executable file
BIN
cocoa/pe/dupeguru.icns
Executable file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
ownerclass = 'DetailsPanelPicture'
|
||||
ownerimport = 'DetailsPanelPicture.h'
|
||||
ownerclass = 'DetailsPanel'
|
||||
ownerimport = 'DetailsPanel.h'
|
||||
|
||||
result = Panel(593, 398, "Details of Selected File")
|
||||
table = TableView(result)
|
||||
14
cocoa/se/AppDelegate.h
Normal file
14
cocoa/se/AppDelegate.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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
|
||||
52
cocoa/se/AppDelegate.m
Normal file
52
cocoa/se/AppDelegate.m
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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
|
||||
13
cocoa/se/DetailsPanel.h
Normal file
13
cocoa/se/DetailsPanel.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import "DetailsPanelBase.h"
|
||||
|
||||
@interface DetailsPanel : DetailsPanelBase
|
||||
@end
|
||||
17
cocoa/se/DetailsPanel.m
Normal file
17
cocoa/se/DetailsPanel.m
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
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
|
||||
@@ -29,9 +29,9 @@
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© Hardcoded Software, 2016</string>
|
||||
<string>© Hardcoded Software, 2014</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://www.hardcoded.net/updates/dupeguru.appcast</string>
|
||||
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string>
|
||||
<key>SUPublicDSAKeyFile</key>
|
||||
<string>dsa_pub.pem</string>
|
||||
</dict>
|
||||
13
cocoa/se/ResultWindow.h
Normal file
13
cocoa/se/ResultWindow.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import "ResultWindowBase.h"
|
||||
|
||||
@interface ResultWindow : ResultWindowBase {}
|
||||
@end
|
||||
52
cocoa/se/ResultWindow.m
Normal file
52
cocoa/se/ResultWindow.m
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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
|
||||
17
cocoa/se/dg_cocoa.py
Normal file
17
cocoa/se/dg_cocoa.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
@@ -9,8 +9,13 @@ out = 'build'
|
||||
|
||||
def options(opt):
|
||||
opt.load('compiler_c python')
|
||||
opt.add_option('--edition', default='se', help="dupeGuru edition to build (se, me pe)")
|
||||
|
||||
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
|
||||
conf.env.CC = 'clang'
|
||||
# WAF has a "pyembed" feature allowing us to automatically find Python and compile by linking
|
||||
@@ -21,18 +26,18 @@ def configure(conf):
|
||||
# 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
|
||||
# with the name WAF is looking for.
|
||||
versioned_dylib_path = '../build/libpython{}m.dylib'.format(sys.version[:3])
|
||||
versioned_dylib_path = '../build/libpython{}.dylib'.format(sys.version[:3])
|
||||
if not op.exists(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.
|
||||
conf.load('compiler_c python')
|
||||
conf.check_python_version((3,4,0))
|
||||
conf.check_python_version((3,3,0))
|
||||
conf.check_python_headers()
|
||||
conf.env.FRAMEWORK_COCOA = 'Cocoa'
|
||||
conf.env.ARCH_COCOA = ['x86_64']
|
||||
conf.env.MACOSX_DEPLOYMENT_TARGET = '10.8'
|
||||
conf.env.CFLAGS = ['-F'+op.abspath('Sparkle/build/Release')]
|
||||
conf.env.LINKFLAGS = ['-F'+op.abspath('Sparkle/build/Release')]
|
||||
# Add cocoalib dir to the framework search path so we can find Sparkle.
|
||||
conf.env.CFLAGS = ['-F'+op.abspath('../cocoalib')]
|
||||
conf.env.LINKFLAGS = ['-F'+op.abspath('../cocoalib')]
|
||||
|
||||
def build(ctx):
|
||||
# What do we compile?
|
||||
@@ -48,8 +53,8 @@ def build(ctx):
|
||||
'controllers/HSOutline', 'controllers/HSPopUpList', 'controllers/HSSelectableList',
|
||||
'controllers/HSTextField', 'controllers/HSProgressWindow']
|
||||
cocoalib_src = [cocoalib_node.find_node(usename + '.m') for usename in cocoalib_uses] + cocoalib_node.ant_glob('autogen/*.m')
|
||||
project_folders = [ctx.srcnode, ctx.srcnode.find_dir('autogen')]
|
||||
project_src = ctx.srcnode.ant_glob('autogen/*.m') + ctx.srcnode.ant_glob('*.m')
|
||||
project_folders = ['autogen', 'base', ctx.env.DGEDITION]
|
||||
project_src = sum([ctx.srcnode.ant_glob('%s/*.m' % folder) for folder in project_folders], [])
|
||||
|
||||
# Compile
|
||||
ctx.program(
|
||||
|
||||
1
cocoalib
1
cocoalib
Submodule cocoalib deleted from bb41785aaa
15
cocoalib/.gitignore
vendored
Normal file
15
cocoalib/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
.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
|
||||
8
cocoalib/.tx/config
Normal file
8
cocoalib/.tx/config
Normal file
@@ -0,0 +1,8 @@
|
||||
[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
|
||||
14
cocoalib/Dialogs.h
Normal file
14
cocoalib/Dialogs.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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
|
||||
31
cocoalib/Dialogs.m
Normal file
31
cocoalib/Dialogs.m
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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
|
||||
27
cocoalib/HSAboutBox.h
Normal file
27
cocoalib/HSAboutBox.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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
|
||||
42
cocoalib/HSAboutBox.m
Normal file
42
cocoalib/HSAboutBox.m
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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
|
||||
26
cocoalib/HSErrorReportWindow.h
Normal file
26
cocoalib/HSErrorReportWindow.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
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
|
||||
44
cocoalib/HSErrorReportWindow.m
Normal file
44
cocoalib/HSErrorReportWindow.m
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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
|
||||
15
cocoalib/HSGeometry.h
Normal file
15
cocoalib/HSGeometry.h
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
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);
|
||||
71
cocoalib/HSGeometry.m
Normal file
71
cocoalib/HSGeometry.m
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
13
cocoalib/HSPyUtil.h
Normal file
13
cocoalib/HSPyUtil.h
Normal file
@@ -0,0 +1,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 <Cocoa/Cocoa.h>
|
||||
#import <Python.h>
|
||||
|
||||
void setCocoaViewsModuleName(NSString *moduleName);
|
||||
PyObject* createCallback(NSString *aViewClassName, id aViewRef);
|
||||
34
cocoalib/HSPyUtil.m
Normal file
34
cocoalib/HSPyUtil.m
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
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 "HSPyUtil.h"
|
||||
#import "ObjP.h"
|
||||
|
||||
static NSString *gCocoaViewsModuleName;
|
||||
void setCocoaViewsModuleName(NSString *moduleName)
|
||||
{
|
||||
if (gCocoaViewsModuleName != nil) {
|
||||
[gCocoaViewsModuleName release];
|
||||
}
|
||||
gCocoaViewsModuleName = [moduleName retain];
|
||||
}
|
||||
|
||||
PyObject* createCallback(NSString *aViewClassName, id aViewRef)
|
||||
{
|
||||
NSString *moduleName;
|
||||
if (gCocoaViewsModuleName != nil) {
|
||||
moduleName = gCocoaViewsModuleName;
|
||||
}
|
||||
else {
|
||||
moduleName = @"inter.CocoaViews";
|
||||
}
|
||||
PyGILState_STATE gilState = PyGILState_Ensure();
|
||||
PyObject *pCallback = ObjP_classInstanceWithRef(aViewClassName, moduleName, aViewRef);
|
||||
PyGILState_Release(gilState);
|
||||
return pCallback;
|
||||
}
|
||||
18
cocoalib/HSQuicklook.h
Normal file
18
cocoalib/HSQuicklook.h
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
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 <Quartz/Quartz.h>
|
||||
|
||||
@interface HSQLPreviewItem : NSObject <QLPreviewItem>
|
||||
{
|
||||
NSURL *url;
|
||||
NSString *title;
|
||||
}
|
||||
- (id)initWithUrl:(NSURL *)aUrl title:(NSString *)aTitle;
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user