Merge commit 'a65077f871481ca98ce51810751e66f228cb096a'

# Conflicts:
#	build.py
#	core/pe/iphoto_plist.py
This commit is contained in:
Virgil Dupras 2016-06-05 13:18:33 -04:00
commit b780816e3c
92 changed files with 659 additions and 1514 deletions

8
.gitignore vendored
View File

@ -1,25 +1,21 @@
.DS_Store
*.pyc
__pycache__
*.so
*.mo
*.pyd
*.waf*
.lock-waf*
.idea
.tox
build
dist
install
installer_tmp-cache
env
/deps
cocoa/autogen
/run.py
/conf.json
/cocoa/*/Info.plist
/cocoa/*/build
/qt/base/*_rc.py
/qt/*_rc.py
/help/*/conf.py
/help/*/changelog.rst

View File

@ -5,9 +5,6 @@ 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.
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
@ -121,7 +118,6 @@ For OS X:
With your virtualenv activated, you can build and run dupeGuru with these commands:
$ python configure.py
$ python build.py
$ python run.py

View File

@ -44,4 +44,4 @@ else
fi
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
echo ". env/bin/activate && python configure.py && python build.py && python run.py"
echo ". env/bin/activate && python build.py && python run.py"

189
build.py
View File

@ -1,6 +1,4 @@
# Created By: Virgil Dupras
# Created On: 2009-12-30
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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
@ -11,7 +9,6 @@ import os
import os.path as op
from optparse import OptionParser
import shutil
import json
import importlib
import compileall
@ -22,10 +19,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, copy
collect_stdlib_dependencies
)
from hscommon import loc
from hscommon.plat import ISOSX, ISLINUX
from hscommon.plat import ISOSX
from hscommon.util import ensure_folder, delete_files_with_pattern
def parse_args():
@ -39,6 +36,14 @@ 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"
@ -70,15 +75,11 @@ def parse_args():
(options, args) = parser.parse_args()
return options
def cocoa_app(edition):
app_path = {
'se': 'build/dupeGuru.app',
'me': 'build/dupeGuru ME.app',
'pe': 'build/dupeGuru PE.app',
}[edition]
def cocoa_app():
app_path = 'build/dupeGuru.app'
return OSXAppStructure(app_path)
def build_xibless(edition, dest='cocoa/autogen'):
def build_xibless(dest='cocoa/autogen'):
import xibless
ensure_folder(dest)
FNPAIRS = [
@ -94,56 +95,50 @@ def build_xibless(edition, dest='cocoa/autogen'):
for srcname, dstname in FNPAIRS:
xibless.generate(
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'
)
else:
xibless.generate(
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
localizationTable='Localizable'
)
# XXX This is broken
assert False
# if edition == 'pe':
# xibless.generate(
# 'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
# localizationTable='Localizable'
# )
# else:
# xibless.generate(
# 'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
# localizationTable='Localizable'
# )
def build_cocoa(edition, dev):
def build_cocoa(dev):
print("Creating OS X app structure")
ed = lambda s: s.format(edition)
app = cocoa_app(edition)
app_version = get_module_version(ed('core_{}'))
cocoa_project_path = ed('cocoa/{}')
app = cocoa_app()
app_version = get_module_version('core')
cocoa_project_path = 'cocoa/se'
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', edition)
build_localizations('cocoa')
print("Building xibless UIs")
build_cocoalib_xibless()
build_xibless(edition)
build_xibless()
print("Building Python extensions")
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces(edition)
build_cocoa_bridging_interfaces()
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')
specific_packages = {
'se': ['core_se'],
'me': ['core_me', 'hsaudiotag'],
'pe': ['core_pe'],
}[edition]
tocopy = [
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash'
] + specific_packages
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
]
copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build')
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']
# 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.
@ -156,12 +151,12 @@ def build_cocoa(edition, dev):
delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF")
os.chdir('cocoa')
print_and_do('{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition))
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru')
build_help(edition)
build_help()
print("Copying resources and frameworks")
image_path = ed('cocoa/{}/dupeguru.icns')
image_path = 'cocoa/se/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', 'cocoalib/Sparkle.framework')
@ -170,26 +165,26 @@ def build_cocoa(edition, dev):
run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents)
def build_qt(edition, dev, conf):
def build_qt(dev):
print("Building localizations")
build_localizations('qt', edition)
build_localizations('qt')
print("Building Qt stuff")
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_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("Creating the run.py file")
filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition)
shutil.copy(op.join('qt', 'run_template.py'), 'run.py')
def build_help(edition):
def build_help():
print("Generating Help")
current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help'.format(edition))
changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition))
help_destpath = op.join(current_path, 'build', 'help')
changelog_path = op.join(current_path, 'help', 'changelog_se')
tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
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'}
appname = 'dupeGuru'
homepage = 'https://www.hardcoded.net/dupeguru/'
confrepl = {'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)
@ -198,10 +193,10 @@ def build_qt_localizations():
loc.compile_all_po(op.join('qtlib', 'locale'))
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
def build_localizations(ui, edition):
def build_localizations(ui):
loc.compile_all_po('locale')
if ui == 'cocoa':
app = cocoa_app(edition)
app = cocoa_app()
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale')
elif ui == 'qt':
@ -210,32 +205,19 @@ def build_localizations(ui, edition):
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')
for edition in ('se', 'me', 'pe'):
build_xibless(edition, op.join('cocoa', 'autogen', edition))
build_xibless('se', op.join('cocoa', 'autogen', 'se'))
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/base/en.lproj')
print("Building .pot files from source files")
print("Building core.pot")
all_cores = ['core', 'core_se', 'core_me', 'core_pe']
loc.generate_pot(all_cores, op.join('locale', 'core.pot'), ['tr'])
loc.generate_pot(['core'], op.join('locale', 'core.pot'), ['tr'])
print("Building columns.pot")
loc.generate_pot(all_cores, op.join('locale', 'columns.pot'), ['coltr'])
loc.generate_pot(['core'], op.join('locale', 'columns.pot'), ['coltr'])
print("Building ui.pot")
# 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.
@ -279,7 +261,7 @@ def build_cocoa_proxy_module():
['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces(edition):
def build_cocoa_bridging_interfaces():
print("Building Cocoa Bridging Interfaces")
import objp.o2p
import objp.p2o
@ -300,7 +282,7 @@ def build_cocoa_bridging_interfaces(edition):
from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuruBase, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition))
appmod = importlib.import_module('inter.app_se')
allclasses = [
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
@ -322,14 +304,21 @@ def build_cocoa_bridging_interfaces(edition):
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",
@ -341,27 +330,25 @@ def build_pe_modules(ui):
ext_modules=exts,
)
move_all('_block_qt*', op.join('qt', 'pe'))
move_all('_block*', 'core_pe')
move_all('_cache*', 'core_pe')
move_all('_block*', op.join('core', 'pe'))
move_all('_cache*', op.join('core', 'pe'))
def build_normal(edition, ui, dev, conf):
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
def build_normal(ui, dev):
print("Building dupeGuru with UI {}".format(ui))
add_to_pythonpath('.')
print("Building dupeGuru")
if edition == 'pe':
build_pe_modules(ui)
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(edition, dev)
build_cocoa(dev)
elif ui == 'qt':
build_qt(edition, dev, conf)
build_qt(dev)
def main():
options = parse_args()
conf = json.load(open('conf.json'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
if dev:
ui = options.ui
if ui not in ('cocoa', 'qt'):
ui = 'cocoa' if ISOSX else 'qt'
if options.dev:
print("Building in Dev mode")
if options.clean:
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
@ -370,9 +357,9 @@ def main():
if not op.exists('build'):
os.mkdir('build')
if options.doc:
build_help(edition)
build_help()
elif options.loc:
build_localizations(ui, edition)
build_localizations(ui)
elif options.updatepot:
build_updatepot()
elif options.mergepot:
@ -381,17 +368,17 @@ def main():
build_normpo()
elif options.cocoa_ext:
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces(edition)
build_cocoa_bridging_interfaces()
elif options.cocoa_compile:
os.chdir('cocoa')
print_and_do('{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition))
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
cocoa_app(edition).copy_executable('cocoa/build/dupeGuru')
cocoa_app().copy_executable('cocoa/build/dupeGuru')
elif options.xibless:
build_cocoalib_xibless()
build_xibless(edition)
build_xibless()
else:
build_normal(edition, ui, dev, conf)
build_normal(ui, options.dev)
if __name__ == '__main__':
main()

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import os.path as op
import logging
import subprocess
import re
import time
import shutil
from send2trash import send2trash
@ -18,14 +17,16 @@ from hscommon.notify import Broadcaster
from hscommon.path import Path
from hscommon.conflict import smart_move, smart_copy
from hscommon.gui.progress_window import ProgressWindow
from hscommon.util import delete_if_empty, first, escape, nonone, format_time_decimal, allsame
from hscommon.util import delete_if_empty, first, escape, nonone, allsame
from hscommon.trans import tr
from hscommon.plat import ISWINDOWS
from hscommon import desktop
from . import directories, results, export, fs
from . import se, me, pe
from .pe.photo import get_delta_dimensions
from .util import cmp_value, fix_surrogate_encoding
from . import directories, results, export, fs, prioritize
from .ignore import IgnoreList
from .scanner import ScanType, Scanner
from .scanner import ScanType
from .gui.deletion_options import DeletionOptions
from .gui.details_panel import DetailsPanel
from .gui.directory_tree import DirectoryTree
@ -55,6 +56,11 @@ class JobType:
Copy = 'job_copy'
Delete = 'job_delete'
class AppMode:
Standard = 0
Music = 1
Picture = 2
JOBID2TITLE = {
JobType.Scan: tr("Scanning for duplicates"),
JobType.Load: tr("Loading"),
@ -62,53 +68,6 @@ JOBID2TITLE = {
JobType.Copy: tr("Copying"),
JobType.Delete: tr("Sending to Trash"),
}
if ISWINDOWS:
JOBID2TITLE[JobType.Delete] = tr("Sending files to the recycle bin")
def format_timestamp(t, delta):
if delta:
return format_time_decimal(t)
else:
if t > 0:
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
else:
return '---'
def format_words(w):
def do_format(w):
if isinstance(w, list):
return '(%s)' % ', '.join(do_format(item) for item in w)
else:
return w.replace('\n', ' ')
return ', '.join(do_format(item) for item in w)
def format_perc(p):
return "%0.0f" % p
def format_dupe_count(c):
return str(c) if c else '---'
def cmp_value(dupe, attrname):
value = getattr(dupe, attrname, '')
return value.lower() if isinstance(value, str) else value
def fix_surrogate_encoding(s, encoding='utf-8'):
# ref #210. It's possible to end up with file paths that, while correct unicode strings, are
# decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix
# these strings here by trying to encode them and, if it fails, we do an encode/decode dance
# to remove the problematic characters. This dance is *lossy* but there's not much we can do
# because if we end up with this type of string, it means that we don't know the encoding of the
# underlying filesystem that brought them. Don't use this for strings you're going to re-use in
# fs-related functions because you're going to lose your path (it's going to change). Use this
# if you need to export the path somewhere else, outside of the unicode realm.
# See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/
try:
s.encode(encoding)
except UnicodeEncodeError:
return s.encode(encoding, 'replace').decode(encoding)
else:
return s
class DupeGuru(Broadcaster):
"""Holds everything together.
@ -149,13 +108,13 @@ class DupeGuru(Broadcaster):
# open_path(path)
# reveal_path(path)
# ask_yes_no(prompt) --> bool
# create_results_window()
# show_results_window()
# show_problem_dialog()
# select_dest_folder(prompt: str) --> str
# select_dest_file(prompt: str, ext: str) --> str
PROMPT_NAME = "dupeGuru"
SCANNER_CLASS = Scanner
NAME = PROMPT_NAME = "dupeGuru"
def __init__(self, view):
if view.get_default(DEBUG_MODE_PREFERENCE):
@ -166,9 +125,8 @@ class DupeGuru(Broadcaster):
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.app_mode = AppMode.Standard
self.discarded_file_count = 0
self.fileclasses = [fs.File]
self.folderclass = fs.Folder
self.directories = directories.Directories()
self.results = results.Results(self)
self.ignore_list = IgnoreList()
@ -180,6 +138,7 @@ class DupeGuru(Broadcaster):
'clean_empty_dirs': False,
'ignore_hardlink_matches': False,
'copymove_dest_type': DestType.Relative,
'cache_path': op.join(self.appdata, 'cached_pictures.db'),
}
self.selected_dupes = []
self.details_panel = DetailsPanel(self)
@ -187,22 +146,32 @@ class DupeGuru(Broadcaster):
self.problem_dialog = ProblemDialog(self)
self.ignore_list_dialog = IgnoreListDialog(self)
self.stats_label = StatsLabel(self)
self.result_table = self._create_result_table()
self.result_table = None
self.deletion_options = DeletionOptions()
self.progress_window = ProgressWindow(self._job_completed)
children = [self.result_table, self.directory_tree, self.stats_label, self.details_panel]
children = [self.directory_tree, self.stats_label, self.details_panel]
for child in children:
child.connect()
#--- Virtual
def _prioritization_categories(self):
raise NotImplementedError()
def _create_result_table(self):
raise NotImplementedError()
#--- Private
def _create_result_table(self):
if self.app_mode == AppMode.Picture:
return pe.result_table.ResultTable(self)
elif self.app_mode == AppMode.Music:
return me.result_table.ResultTable(self)
else:
return se.result_table.ResultTable(self)
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
if self.app_mode == AppMode.Picture:
if delta and key == 'dimensions':
r = cmp_value(dupe, key)
ref_value = cmp_value(get_group().ref, key)
return get_delta_dimensions(r, ref_value)
if key == 'marked':
return self.results.is_marked(dupe)
if key == 'percentage':
@ -222,6 +191,10 @@ class DupeGuru(Broadcaster):
return result
def _get_group_sort_key(self, group, key):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
if key == 'percentage':
return group.percentage
if key == 'dupe_count':
@ -349,6 +322,15 @@ class DupeGuru(Broadcaster):
self.selected_dupes = dupes
self.notify('dupes_selected')
#--- Protected
def _prioritization_categories(self):
if self.app_mode == AppMode.Picture:
return pe.prioritize.all_categories()
elif self.app_mode == AppMode.Music:
return me.prioritize.all_categories()
else:
return prioritize.all_categories()
#--- Public
def add_directory(self, d):
"""Adds folder ``d`` to :attr:`directories`.
@ -400,6 +382,11 @@ class DupeGuru(Broadcaster):
while delete_if_empty(path, ['.DS_Store']):
path = path.parent()
def clear_picture_cache(self):
cache = pe.cache.Cache(self.options['cache_path'])
cache.clear()
cache.close()
def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):
source_path = dupe.path
location_path = first(p for p in self.directories if dupe.path in p)
@ -746,12 +733,17 @@ class DupeGuru(Broadcaster):
if hasattr(scanner, k):
setattr(scanner, k, v)
self.results.groups = []
if self.result_table is not None:
self.result_table.disconnect()
self.result_table = self._create_result_table()
self.result_table.connect()
self.view.create_results_window()
self._results_changed()
def do(j):
j.set_progress(0, tr("Collecting files to scan"))
if scanner.scan_type == ScanType.Folders:
files = list(self.directories.get_folders(folderclass=self.folderclass, j=j))
files = list(self.directories.get_folders(folderclass=se.fs.folder, j=j))
else:
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
if self.options['ignore_hardlink_matches']:
@ -800,3 +792,33 @@ class DupeGuru(Broadcaster):
result = tr("%s (%d discarded)") % (result, self.discarded_file_count)
return result
@property
def fileclasses(self):
if self.app_mode == AppMode.Picture:
return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
elif self.app_mode == AppMode.Music:
return [me.fs.MusicFile]
else:
return [se.fs.File]
@property
def SCANNER_CLASS(self):
if self.app_mode == AppMode.Picture:
return pe.scanner.ScannerPE
elif self.app_mode == AppMode.Music:
return me.scanner.ScannerME
else:
return se.scanner.ScannerSE
@property
def METADATA_TO_READ(self):
if self.app_mode == AppMode.Picture:
return ['size', 'mtime', 'dimensions', 'exif_timestamp']
elif self.app_mode == AppMode.Music:
return [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
else:
return ['size', 'mtime']

View File

@ -1,9 +1,9 @@
# Created By: Virgil Dupras
# Created On: 2010-02-05
# 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
#
# 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.gui.base import GUIObject
@ -11,14 +11,14 @@ from .base import DupeGuruGUIObject
class DetailsPanel(GUIObject, DupeGuruGUIObject):
def __init__(self, app):
GUIObject.__init__(self)
GUIObject.__init__(self, multibind=True)
DupeGuruGUIObject.__init__(self, app)
self._table = []
def _view_updated(self):
self._refresh()
self.view.refresh()
#--- Private
def _refresh(self):
if self.app.selected_dupes:
@ -33,16 +33,16 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
data2 = self.app.get_display_info(ref, group, False)
columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
#--- Public
def row_count(self):
return len(self._table)
def row(self, row_index):
return self._table[row_index]
#--- Event Handlers
def dupes_selected(self):
self._refresh()
self.view.refresh()

1
core/me/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import fs, prioritize, result_table, scanner # noqa

View File

@ -9,7 +9,7 @@
from hsaudiotag import auto
from hscommon.util import get_file_ext, format_size, format_time
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
from core import fs
TAG_FIELDS = {

1
core/pe/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import block, cache, exif, iphoto_plist, matchblock, matchexif, photo, prioritize, result_table, scanner # noqa

31
core/pe/iphoto_plist.py Normal file
View File

@ -0,0 +1,31 @@
# Created By: Virgil Dupras
# Created On: 2014-03-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 plistlib
class IPhotoPlistParser(plistlib._PlistParser):
"""A parser for iPhoto plists.
iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more
lenient.
"""
def __init__(self):
plistlib._PlistParser.__init__(self, use_builtin_types=True, dict_type=dict)
# For debugging purposes, we remember the last bit of data to be analyzed so that we can
# log it in case of an exception
self.lastdata = ''
def get_data(self):
self.lastdata = plistlib._PlistParser.get_data(self)
return self.lastdata
def end_integer(self):
try:
self.add_object(int(self.get_data()))
except ValueError:
self.add_object(0)

View File

@ -1,6 +1,4 @@
# Created By: Virgil Dupras
# Created On: 2011-05-29
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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
@ -9,10 +7,13 @@
import logging
from hscommon.util import get_file_ext, format_size
from core.app import format_timestamp, format_perc, format_dupe_count
from core.util import format_timestamp, format_perc, format_dupe_count
from core import fs
from . import exif
# This global value is set by the platform-specific subclasser of the Photo base class
PLAT_SPECIFIC_PHOTO_CLASS = None
def format_dimensions(dimensions):
return '%d x %d' % (dimensions[0], dimensions[1])

View File

@ -9,7 +9,6 @@ from hscommon.trans import tr
from core.scanner import Scanner, ScanType, ScanOption
from . import matchblock, matchexif
from .cache import Cache
class ScannerPE(Scanner):
cache_path = None
@ -31,8 +30,3 @@ class ScannerPE(Scanner):
else:
raise Exception("Invalid scan type")
def clear_picture_cache(self):
cache = Cache(self.cache_path)
cache.clear()
cache.close()

1
core/se/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import fs, result_table, scanner # noqa

View File

@ -1,15 +1,15 @@
# Created By: Virgil Dupras
# Created On: 2013-07-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
#
# 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.util import format_size
from core import fs
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
def get_display_info(dupe, group, delta):
size = dupe.size
@ -39,9 +39,9 @@ def get_display_info(dupe, group, delta):
class File(fs.File):
def get_display_info(self, group, delta):
return get_display_info(self, group, delta)
class Folder(fs.Folder):
def get_display_info(self, group, delta):
return get_display_info(self, group, delta)

View File

@ -384,7 +384,7 @@ class TestCaseDupeGuruWithResults:
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
app.start_scanning() # will be cancelled immediately
eq_(len(self.rtable), 0)
eq_(len(app.result_table), 0)
def test_selected_dupes_after_removal(self, do_setup):
# Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a

View File

@ -4,7 +4,7 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from hscommon.testutil import TestApp as TestAppBase, eq_, with_app # noqa
from hscommon.testutil import TestApp as TestAppBase, CallLogger, eq_, with_app # noqa
from hscommon.path import Path
from hscommon.util import get_file_ext, format_size
from hscommon.gui.column import Column
@ -41,6 +41,8 @@ class DupeGuruView:
def ask_yes_no(self, prompt):
return True # always answer yes
def create_results_window(self):
pass
class ResultTable(ResultTableBase):
COLUMNS = [
@ -59,12 +61,16 @@ class DupeGuru(DupeGuruBase):
def __init__(self):
DupeGuruBase.__init__(self, DupeGuruView())
self.appdata = '/tmp'
self.result_table = self._create_result_table()
self.result_table.connect()
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)
result = ResultTable(self)
result.view = CallLogger()
return result
class NamedObject:
@ -141,7 +147,6 @@ class TestApp(TestAppBase):
TestAppBase.__init__(self)
self.app = DupeGuru()
self.default_parent = self.app
self.rtable = link_gui(self.app.result_table)
self.dtree = link_gui(self.app.directory_tree)
self.dpanel = link_gui(self.app.details_panel)
self.slabel = link_gui(self.app.stats_label)
@ -155,6 +160,11 @@ class TestApp(TestAppBase):
link_gui(self.app.progress_window.jobdesc_textfield)
link_gui(self.app.progress_window.progressdesc_textfield)
@property
def rtable(self):
# rtable is a property because its instance can be replaced during execution
return self.app.result_table
#--- Helpers
def select_pri_criterion(self, name):
# Select a main prioritize criterion by name instead of by index. Makes tests more

View File

@ -9,7 +9,7 @@ from pytest import raises, skip
from hscommon.testutil import eq_
try:
from ..block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError
from ..pe.block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError
except ImportError:
skip("Can't import the block module, probably hasn't been compiled.")

View File

@ -10,7 +10,7 @@ from pytest import raises, skip
from hscommon.testutil import eq_
try:
from ..cache import Cache, colors_to_string, string_to_colors
from ..pe.cache import Cache, colors_to_string, string_to_colors
except ImportError:
skip("Can't import the cache module, probably hasn't been compiled.")

View File

@ -12,6 +12,7 @@ from .. import fs
from ..engine import getwords, Match
from ..ignore import IgnoreList
from ..scanner import Scanner, ScanType
from ..me.scanner import ScannerME
class NamedObject:
def __init__(self, name="foobar", size=1, path=None):
@ -528,3 +529,13 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
o2.is_ref = True
eq_(len(s.get_dupe_groups([o1, o2, o3])), 1)
eq_(s.discarded_file_count, 0)
def test_priorize_me(fake_fileexists):
# in ScannerME, bitrate goes first (right after is_ref) in priorization
s = ScannerME()
o1, o2 = no('foo', path='p1'), no('foo', path='p2')
o1.bitrate = 1
o2.bitrate = 2
[group] = s.get_dupe_groups([o1, o2])
assert group.ref is o2

56
core/util.py Normal file
View File

@ -0,0 +1,56 @@
# 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
import time
from hscommon.util import format_time_decimal
def format_timestamp(t, delta):
if delta:
return format_time_decimal(t)
else:
if t > 0:
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
else:
return '---'
def format_words(w):
def do_format(w):
if isinstance(w, list):
return '(%s)' % ', '.join(do_format(item) for item in w)
else:
return w.replace('\n', ' ')
return ', '.join(do_format(item) for item in w)
def format_perc(p):
return "%0.0f" % p
def format_dupe_count(c):
return str(c) if c else '---'
def cmp_value(dupe, attrname):
value = getattr(dupe, attrname, '')
return value.lower() if isinstance(value, str) else value
def fix_surrogate_encoding(s, encoding='utf-8'):
# ref #210. It's possible to end up with file paths that, while correct unicode strings, are
# decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix
# these strings here by trying to encode them and, if it fails, we do an encode/decode dance
# to remove the problematic characters. This dance is *lossy* but there's not much we can do
# because if we end up with this type of string, it means that we don't know the encoding of the
# underlying filesystem that brought them. Don't use this for strings you're going to re-use in
# fs-related functions because you're going to lose your path (it's going to change). Use this
# if you need to export the path somewhere else, outside of the unicode realm.
# See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/
try:
s.encode(encoding)
except UnicodeEncodeError:
return s.encode(encoding, 'replace').decode(encoding)
else:
return s

View File

@ -1,3 +0,0 @@
__version__ = '6.8.1'
__appname__ = 'dupeGuru Music Edition'

View File

@ -1,41 +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 core.app import DupeGuru as DupeGuruBase
from . import prioritize
from . import __appname__
from . import scanner, fs
from .result_table import ResultTable
class DupeGuru(DupeGuruBase):
NAME = __appname__
METADATA_TO_READ = [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
SCANNER_CLASS = scanner.ScannerME
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.fileclasses = [fs.MusicFile]
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
def _get_group_sort_key(self, group, key):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_group_sort_key(self, group, key)
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)

View File

@ -1,26 +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 hscommon.path import Path
from core.tests.scanner_test import no
from ..scanner import ScannerME
def pytest_funcarg__fake_fileexists(request):
# This is a hack to avoid invalidating all previous tests since the scanner started to test
# for file existence before doing the match grouping.
monkeypatch = request.getfuncargvalue('monkeypatch')
monkeypatch.setattr(Path, 'exists', lambda _: True)
def test_priorize_me(fake_fileexists):
# in ScannerME, bitrate goes first (right after is_ref) in priorization
s = ScannerME()
o1, o2 = no('foo', path='p1'), no('foo', path='p2')
o1.bitrate = 1
o2.bitrate = 2
[group] = s.get_dupe_groups([o1, o2])
assert group.ref is o2

View File

@ -1,2 +0,0 @@
__version__ = '2.10.1'
__appname__ = 'dupeGuru Picture Edition'

View File

@ -1,45 +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
import os.path as op
from core.app import DupeGuru as DupeGuruBase, cmp_value
from .scanner import ScannerPE
from . import prioritize
from . import __appname__
from .photo import get_delta_dimensions
from .result_table import ResultTable
class DupeGuru(DupeGuruBase):
NAME = __appname__
METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
SCANNER_CLASS = ScannerPE
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.options['cache_path'] = op.join(self.appdata, 'cached_pictures.db')
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
if delta and key == 'dimensions':
r = cmp_value(dupe, key)
ref_value = cmp_value(get_group().ref, key)
return get_delta_dimensions(r, ref_value)
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
def _get_group_sort_key(self, group, key):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_group_sort_key(self, group, key)
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)

View File

@ -1,3 +0,0 @@
__version__ = '3.9.1'
__appname__ = 'dupeGuru'

View File

@ -1,27 +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 core.app import DupeGuru as DupeGuruBase
from core import prioritize
from . import __appname__, fs, scanner
from .result_table import ResultTable
class DupeGuru(DupeGuruBase):
NAME = __appname__
METADATA_TO_READ = ['size', 'mtime']
SCANNER_CLASS = scanner.ScannerSE
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.fileclasses = [fs.File]
self.folderclass = fs.Folder
def _prioritization_categories(self):
return prioritize.all_categories()
def _create_result_table(self):
return ResultTable(self)

@ -1 +1 @@
Subproject commit ea634cefdf78ae9e4c7470e571fce859760f6f38
Subproject commit 316af1bca53915f99b9bb874064cba6bee881cc1

View File

@ -1,6 +1,4 @@
# Created By: Virgil Dupras
# Created On: 2009-12-30
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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
@ -13,13 +11,12 @@ import shutil
import json
from argparse import ArgumentParser
import platform
import glob
from hscommon.plat import ISWINDOWS, ISLINUX
from hscommon.plat import ISOSX
from hscommon.build import (
add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
package_cocoa_app_in_dmg, copy_all, find_in_path
print_and_do, copy_packages, build_debian_changelog,
get_module_version, filereplace, copy, setup_package_argparser,
package_cocoa_app_in_dmg, copy_all
)
def parse_args():
@ -27,90 +24,10 @@ def parse_args():
setup_package_argparser(parser)
return parser.parse_args()
def package_cocoa(edition, args):
app_path = {
'se': 'build/dupeGuru.app',
'me': 'build/dupeGuru ME.app',
'pe': 'build/dupeGuru PE.app',
}[edition]
def package_cocoa(args):
app_path = 'build/dupeGuru.app'
package_cocoa_app_in_dmg(app_path, '.', args)
def package_windows(edition, dev):
if not ISWINDOWS:
print("Qt packaging only works under Windows.")
return
from cx_Freeze import setup, Executable
from PyQt5.QtCore import QLibraryInfo
add_to_pythonpath('.')
app_version = get_module_version('core_{}'.format(edition))
distdir = 'dist'
if op.exists(distdir):
shutil.rmtree(distdir)
if not dev:
# Copy qt plugins
plugin_dest = distdir
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
copy_qt_plugins(plugin_names, plugin_dest)
# Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it
# explicitly because of that.
options = {
'build_exe': {
'includes': 'atexit',
'excludes': ['tkinter'],
'bin_excludes': ['icudt51', 'icuin51.dll', 'icuuc51.dll'],
'icon': 'images\\dg{0}_logo.ico'.format(edition),
'include_msvcr': True,
},
'install_exe': {
'install_dir': 'dist',
}
}
executables = [
Executable(
'run.py',
base='Win32GUI',
targetDir=distdir,
targetName={'se': 'dupeGuru', 'me': 'dupeGuru ME', 'pe': 'dupeGuru PE'}[edition] + '.exe',
)
]
setup(
script_args=['install'],
options=options,
executables=executables
)
print("Removing useless files")
# Debug info that cx_freeze brings in.
for fn in glob.glob(op.join(distdir, '*', '*.pdb')):
os.remove(fn)
print("Copying forgotten DLLs")
qtlibpath = QLibraryInfo.location(QLibraryInfo.LibrariesPath)
shutil.copy(op.join(qtlibpath, 'libEGL.dll'), distdir)
shutil.copy(find_in_path('msvcp110.dll'), distdir)
print("Copying the rest")
help_path = op.join('build', 'help')
print("Copying {} to dist\\help".format(help_path))
shutil.copytree(help_path, op.join(distdir, 'help'))
locale_path = op.join('build', 'locale')
print("Copying {} to dist\\locale".format(locale_path))
shutil.copytree(locale_path, op.join(distdir, 'locale'))
# AdvancedInstaller.com has to be in your PATH
# this is so we don'a have to re-commit installer.aip at every version change
installer_file = 'installer.aip'
installer_path = op.join('qt', edition, installer_file)
shutil.copy(installer_path, 'installer_tmp.aip')
print_and_do('AdvancedInstaller.com /edit installer_tmp.aip /SetVersion %s' % app_version)
print_and_do('AdvancedInstaller.com /build installer_tmp.aip -force')
os.remove('installer_tmp.aip')
if op.exists('installer_tmp.back.aip'):
os.remove('installer_tmp.back.aip')
def copy_files_to_package(destpath, packages, with_so):
# when with_so is true, we keep .so files in the package, and otherwise, we don't. We need this
# flag because when building debian src pkg, we *don't* want .so files (they're compiled later)
@ -126,71 +43,68 @@ def copy_files_to_package(destpath, packages, with_so):
shutil.copytree(op.join('build', 'locale'), op.join(destpath, 'locale'))
compileall.compile_dir(destpath)
def package_debian_distribution(edition, distribution):
app_version = get_module_version('core_{}'.format(edition))
def package_debian_distribution(distribution):
app_version = get_module_version('core')
version = '{}~{}'.format(app_version, distribution)
ed = lambda s: s.format(edition)
destpath = op.join('build', 'dupeguru-{0}-{1}'.format(edition, version))
destpath = op.join('build', 'dupeguru-{}'.format(version))
srcpath = op.join(destpath, 'src')
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
if edition == 'me':
packages.append('hsaudiotag')
packages = [
'hscommon', 'core', 'qtlib', 'qt', 'send2trash', 'hsaudiotag'
]
copy_files_to_package(srcpath, packages, with_so=False)
if edition == 'pe':
os.mkdir(op.join(destpath, 'modules'))
copy_all(op.join('core_pe', 'modules', '*.*'), op.join(destpath, 'modules'))
copy(op.join('qt', 'pe', 'modules', 'block.c'), op.join(destpath, 'modules', 'block_qt.c'))
copy(op.join('pkg', 'debian', 'build_pe_modules.py'), op.join(destpath, 'build_pe_modules.py'))
os.mkdir(op.join(destpath, 'modules'))
copy_all(op.join('core', 'pe', 'modules', '*.*'), op.join(destpath, 'modules'))
copy(op.join('qt', 'pe', 'modules', 'block.c'), op.join(destpath, 'modules', 'block_qt.c'))
copy(op.join('pkg', 'debian', 'build_pe_modules.py'), op.join(destpath, 'build_pe_modules.py'))
debdest = op.join(destpath, 'debian')
debskel = op.join('pkg', 'debian')
os.makedirs(debdest)
debopts = json.load(open(op.join(debskel, ed('{}.json'))))
debopts = json.load(open(op.join(debskel, 'se.json')))
for fn in ['compat', 'copyright', 'dirs', 'rules']:
copy(op.join(debskel, fn), op.join(debdest, fn))
filereplace(op.join(debskel, 'control'), op.join(debdest, 'control'), **debopts)
filereplace(op.join(debskel, 'Makefile'), op.join(destpath, 'Makefile'), **debopts)
filereplace(op.join(debskel, 'dupeguru.desktop'), op.join(debdest, ed('dupeguru_{}.desktop')), **debopts)
changelogpath = op.join('help', ed('changelog_{}'))
filereplace(op.join(debskel, 'dupeguru.desktop'), op.join(debdest, 'dupeguru.desktop'), **debopts)
changelogpath = op.join('help', 'changelog_se')
changelog_dest = op.join(debdest, 'changelog')
project_name = debopts['pkgname']
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition]
from_version = '2.9.2'
build_debian_changelog(
changelogpath, changelog_dest, project_name, from_version=from_version,
distribution=distribution
)
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
shutil.copy(op.join('images', 'dgse_logo_128.png'), srcpath)
os.chdir(destpath)
cmd = "dpkg-buildpackage -S"
os.system(cmd)
os.chdir('../..')
def package_debian(edition):
def package_debian():
print("Packaging for Ubuntu")
for distribution in ['trusty', 'utopic']:
package_debian_distribution(edition, distribution)
package_debian_distribution(distribution)
def package_arch(edition):
def package_arch():
# For now, package_arch() will only copy the source files into build/. It copies less packages
# than package_debian because there are more python packages available in Arch (so we don't
# need to include them).
print("Packaging for Arch")
ed = lambda s: s.format(edition)
srcpath = op.join('build', ed('dupeguru-{}-arch'))
packages = ['hscommon', 'core', ed('core_{0}'), 'qtlib', 'qt', 'send2trash']
if edition == 'me':
packages.append('hsaudiotag')
srcpath = op.join('build', 'dupeguru-arch')
packages = [
'hscommon', 'core', 'qtlib', 'qt', 'send2trash', 'hsaudiotag',
]
copy_files_to_package(srcpath, packages, with_so=True)
shutil.copy(op.join('images', ed('dg{}_logo_128.png')), srcpath)
debopts = json.load(open(op.join('pkg', 'arch', ed('{}.json'))))
filereplace(op.join('pkg', 'arch', 'dupeguru.desktop'), op.join(srcpath, ed('dupeguru-{}.desktop')), **debopts)
shutil.copy(op.join('images', 'dgse_logo_128.png'), srcpath)
debopts = json.load(open(op.join('pkg', 'arch', 'se.json')))
filereplace(op.join('pkg', 'arch', 'dupeguru.desktop'), op.join(srcpath, 'dupeguru.desktop'), **debopts)
def package_source_tgz(edition):
def package_source_tgz():
if not op.exists('deps'):
print("Downloading PyPI dependencies")
print_and_do('./download_deps.sh')
print("Creating git archive")
app_version = get_module_version('core_{}'.format(edition))
name = 'dupeguru-{}-src-{}.tar'.format(edition, app_version)
app_version = get_module_version('core')
name = 'dupeguru-src-{}.tar'.format(app_version)
dest = op.join('build', name)
print_and_do('git archive -o {} HEAD'.format(dest))
print("Adding dependencies and wrapping up")
@ -199,31 +113,23 @@ def package_source_tgz(edition):
def main():
args = parse_args()
conf = json.load(open('conf.json'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
ui = 'cocoa' if ISOSX else 'qt'
if args.src_pkg:
print("Creating source package for dupeGuru {}".format(edition.upper()))
package_source_tgz(edition)
print("Creating source package for dupeGuru")
package_source_tgz()
return
print("Packaging dupeGuru {0} with UI {1}".format(edition.upper(), ui))
print("Packaging dupeGuru with UI {}".format(ui))
if ui == 'cocoa':
package_cocoa(edition, args)
package_cocoa(args)
elif ui == 'qt':
if ISWINDOWS:
package_windows(edition, dev)
elif ISLINUX:
if not args.arch_pkg:
distname, _, _ = platform.dist()
else:
distname = 'arch'
if distname == 'arch':
package_arch(edition)
else:
package_debian(edition)
if not args.arch_pkg:
distname, _, _ = platform.dist()
else:
print("Qt packaging only works under Windows or Linux.")
distname = 'arch'
if distname == 'arch':
package_arch()
else:
package_debian()
if __name__ == '__main__':
main()

View File

@ -1,6 +1,4 @@
# Created By: Virgil Dupras
# Created On: 2009-04-25
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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
@ -21,49 +19,51 @@ from qtlib.recent import Recent
from qtlib.util import createActions
from qtlib.progress_window import ProgressWindow
from core.app import AppMode, DupeGuru as DupeGuruModel
import core.pe.photo
from . import platform
from .preferences import Preferences
from .result_window import ResultWindow
from .directories_dialog import DirectoriesDialog
from .problem_dialog import ProblemDialog
from .ignore_list_dialog import IgnoreListDialog
from .deletion_options import DeletionOptions
from .se.details_dialog import DetailsDialog as DetailsDialogStandard
from .me.details_dialog import DetailsDialog as DetailsDialogMusic
from .pe.details_dialog import DetailsDialog as DetailsDialogPicture
from .se.preferences_dialog import PreferencesDialog as PreferencesDialogStandard
from .me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic
from .pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture
from .pe.photo import File as PlatSpecificPhoto
tr = trget('ui')
class DupeGuru(QObject):
MODELCLASS = None
LOGO_NAME = '<replace this>'
NAME = '<replace this>'
DETAILS_DIALOG_CLASS = None
RESULT_WINDOW_CLASS = ResultWindow
RESULT_MODEL_CLASS = None
PREFERENCES_CLASS = None
PREFERENCES_DIALOG_CLASS = None
LOGO_NAME = 'logo_se'
NAME = 'dupeGuru'
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.prefs = self.PREFERENCES_CLASS()
self.prefs = Preferences()
self.prefs.load()
self.model = self.MODELCLASS(view=self)
self.model = DupeGuruModel(view=self)
self._setup()
self.prefsChanged.emit(self.prefs)
#--- Private
def _setup(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
self._setupActions()
self._update_options()
self.recentResults = Recent(self, 'recentResults')
self.recentResults.mustOpenItem.connect(self.model.load_from)
self.resultWindow = None
self.details_dialog = None
self.directories_dialog = DirectoriesDialog(self)
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
self.about_box = AboutBox(self.resultWindow, self)
self.progress_window = ProgressWindow(self.directories_dialog, self.model.progress_window)
self.problemDialog = ProblemDialog(parent=self.directories_dialog, model=self.model.problem_dialog)
self.ignoreListDialog = IgnoreListDialog(parent=self.directories_dialog, model=self.model.ignore_list_dialog)
self.deletionOptions = DeletionOptions(parent=self.directories_dialog, model=self.model.deletion_options)
self.about_box = AboutBox(self.directories_dialog, self)
self.directories_dialog.show()
self.model.load()
@ -82,6 +82,7 @@ class DupeGuru(QObject):
('actionQuit', 'Ctrl+Q', '', tr("Quit"), self.quitTriggered),
('actionPreferences', 'Ctrl+P', '', tr("Options"), self.preferencesTriggered),
('actionIgnoreList', '', '', tr("Ignore List"), self.ignoreListTriggered),
('actionClearPictureCache', 'Ctrl+Shift+P', '', tr("Clear Picture Cache"), self.clearPictureCacheTriggered),
('actionShowHelp', 'F1', '', tr("dupeGuru Help"), self.showHelpTriggered),
('actionAbout', '', '', tr("About dupeGuru"), self.showAboutBoxTriggered),
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
@ -94,6 +95,44 @@ class DupeGuru(QObject):
self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
self.model.options['copymove_dest_type'] = self.prefs.destination_type
self.model.options['scan_type'] = self.prefs.get_scan_type(self.model.app_mode)
self.model.options['min_match_percentage'] = self.prefs.filter_hardness
self.model.options['word_weighting'] = self.prefs.word_weighting
self.model.options['match_similar_words'] = self.prefs.match_similar
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
self.model.options['size_threshold'] = threshold * 1024 # threshold is in KB. the scanner wants bytes
scanned_tags = set()
if self.prefs.scan_tag_track:
scanned_tags.add('track')
if self.prefs.scan_tag_artist:
scanned_tags.add('artist')
if self.prefs.scan_tag_album:
scanned_tags.add('album')
if self.prefs.scan_tag_title:
scanned_tags.add('title')
if self.prefs.scan_tag_genre:
scanned_tags.add('genre')
if self.prefs.scan_tag_year:
scanned_tags.add('year')
self.model.options['scanned_tags'] = scanned_tags
self.model.options['match_scaled'] = self.prefs.match_scaled
#--- Private
def _get_details_dialog_class(self):
if self.model.app_mode == AppMode.Picture:
return DetailsDialogPicture
elif self.model.app_mode == AppMode.Music:
return DetailsDialogMusic
else:
return DetailsDialogStandard
def _get_preferences_dialog_class(self):
if self.model.app_mode == AppMode.Picture:
return PreferencesDialogPicture
elif self.model.app_mode == AppMode.Music:
return PreferencesDialogMusic
else:
return PreferencesDialogStandard
#--- Public
def add_selected_to_ignore_list(self):
@ -112,14 +151,15 @@ class DupeGuru(QObject):
self.model.invoke_custom_command()
def show_details(self):
self.details_dialog.show()
if self.details_dialog is not None:
self.details_dialog.show()
def showResultsWindow(self):
self.resultWindow.show()
if self.resultWindow is not None:
self.resultWindow.show()
#--- Signals
willSavePrefs = pyqtSignal()
prefsChanged = pyqtSignal(object)
#--- Events
def finishedLaunching(self):
@ -135,6 +175,14 @@ class DupeGuru(QObject):
self.prefs.save()
self.model.save()
def clearPictureCacheTriggered(self):
title = tr("Clear Picture Cache")
msg = tr("Do you really want to remove all your cached picture analysis?")
if self.confirm(title, msg, QMessageBox.No):
self.model.clear_picture_cache()
active = QApplication.activeWindow()
QMessageBox.information(active, title, tr("Picture cache cleared."))
def ignoreListTriggered(self):
self.model.ignore_list_dialog.show()
@ -143,13 +191,14 @@ class DupeGuru(QObject):
desktop.open_path(debugLogPath)
def preferencesTriggered(self):
self.preferences_dialog.load()
result = self.preferences_dialog.exec()
preferences_dialog = self._get_preferences_dialog_class()(self.directories_dialog, self)
preferences_dialog.load()
result = preferences_dialog.exec()
if result == QDialog.Accepted:
self.preferences_dialog.save()
preferences_dialog.save()
self.prefs.save()
self._update_options()
self.prefsChanged.emit(self.prefs)
preferences_dialog.setParent(None)
def quitTriggered(self):
self.directories_dialog.close()
@ -176,6 +225,18 @@ class DupeGuru(QObject):
def ask_yes_no(self, prompt):
return self.confirm('', prompt)
def create_results_window(self):
"""Creates resultWindow and details_dialog depending on the selected ``app_mode``.
"""
if self.details_dialog is not None:
self.details_dialog.close()
self.details_dialog.setParent(None)
if self.resultWindow is not None:
self.resultWindow.close()
self.resultWindow.setParent(None)
self.resultWindow = ResultWindow(self.directories_dialog, self)
self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self)
def show_results_window(self):
self.showResultsWindow()

View File

View File

@ -1,8 +0,0 @@
# cxfreeze has some problems detecting all dependencies.
# This modules explicitly import those problematic modules.
# flake8: noqa
import xml.etree.ElementPath
import gzip
import os

View File

@ -1,13 +0,0 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="logo_pe">../../images/dgpe_logo_32.png</file>
<file alias="logo_pe_big">../../images/dgpe_logo_128.png</file>
<file alias="logo_me">../../images/dgme_logo_32.png</file>
<file alias="logo_me_big">../../images/dgme_logo_128.png</file>
<file alias="logo_se">../../images/dgse_logo_32.png</file>
<file alias="logo_se_big">../../images/dgse_logo_128.png</file>
<file alias="plus">../../images/plus_8.png</file>
<file alias="minus">../../images/minus_8.png</file>
<file alias="search_clear_13">../../qtlib/images/search_clear_13.png</file>
</qresource>
</RCC>

9
qt/dg.qrc Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="logo_se">../images/dgse_logo_32.png</file>
<file alias="logo_se_big">../images/dgse_logo_128.png</file>
<file alias="plus">../images/plus_8.png</file>
<file alias="minus">../images/minus_8.png</file>
<file alias="search_clear_13">../qtlib/images/search_clear_13.png</file>
</qresource>
</RCC>

View File

@ -13,6 +13,8 @@ from PyQt5.QtWidgets import (
from PyQt5.QtGui import QPixmap, QIcon
from hscommon.trans import trget
from core.app import AppMode
from qtlib.radio_box import RadioBox
from qtlib.recent import Recent
from qtlib.util import moveToScreenCenter, createActions
@ -28,9 +30,7 @@ class DirectoriesDialog(QMainWindow):
self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS
self.recentFolders = Recent(self.app, 'recentFolders')
self._setupUi()
SCAN_TYPE_ORDER = [so.scan_type for so in self.app.model.SCANNER_CLASS.get_scan_options()]
scan_type_index = SCAN_TYPE_ORDER.index(self.app.prefs.scan_type)
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
self._updateScanTypeList()
self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)
self.directoriesDelegate = DirectoriesDelegate()
self.treeView.setItemDelegate(self.directoriesDelegate)
@ -41,11 +41,12 @@ class DirectoriesDialog(QMainWindow):
self._updateAddButton()
self._updateRemoveButton()
self._updateLoadResultsButton()
self._updateActionsState()
self._setupBindings()
def _setupBindings(self):
self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected)
self.showPreferencesButton.clicked.connect(self.app.actionPreferences.trigger)
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
self.scanButton.clicked.connect(self.scanButtonClicked)
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
self.addFolderButton.clicked.connect(self.actionAddFolder.trigger)
@ -82,6 +83,8 @@ class DirectoriesDialog(QMainWindow):
self.menuFile.addAction(self.actionLoadResults)
self.menuFile.addAction(self.menuLoadRecent.menuAction())
self.menuFile.addSeparator()
self.menuFile.addAction(self.app.actionClearPictureCache)
self.menuFile.addSeparator()
self.menuFile.addAction(self.app.actionQuit)
self.menuView.addAction(self.app.actionPreferences)
self.menuView.addAction(self.actionShowResultsWindow)
@ -123,6 +126,17 @@ class DirectoriesDialog(QMainWindow):
self.treeView.setUniformRowHeights(True)
self.verticalLayout.addWidget(self.treeView)
hl = QHBoxLayout()
label = QLabel(tr("Application Mode:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label)
self.appModeRadioBox = RadioBox(
self,
items=[tr("Standard"), tr("Music"), tr("Picture")],
spread=False
)
hl.addWidget(self.appModeRadioBox)
self.verticalLayout.addLayout(hl)
hl = QHBoxLayout()
hl.setAlignment(Qt.AlignLeft)
label = QLabel(tr("Scan Type:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
@ -130,10 +144,8 @@ class DirectoriesDialog(QMainWindow):
self.scanTypeComboBox = QComboBox(self)
self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
self.scanTypeComboBox.setMaximumWidth(400)
for scan_option in self.app.model.SCANNER_CLASS.get_scan_options():
self.scanTypeComboBox.addItem(scan_option.label)
hl.addWidget(self.scanTypeComboBox)
self.showPreferencesButton = QPushButton(tr("Options"), self.centralwidget)
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(self.showPreferencesButton)
self.verticalLayout.addLayout(hl)
@ -172,6 +184,9 @@ class DirectoriesDialog(QMainWindow):
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 100)
def _updateActionsState(self):
self.actionShowResultsWindow.setEnabled(self.app.resultWindow is not None)
def _updateAddButton(self):
if self.recentFolders.isEmpty():
self.addFolderButton.setMenu(None)
@ -191,6 +206,23 @@ class DirectoriesDialog(QMainWindow):
else:
self.loadResultsButton.setMenu(self.menuRecentResults)
def _updateScanTypeList(self):
try:
self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)
except TypeError:
# Not connected, ignore
pass
self.scanTypeComboBox.clear()
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
for scan_option in scan_options:
self.scanTypeComboBox.addItem(scan_option.label)
SCAN_TYPE_ORDER = [so.scan_type for so in scan_options]
selected_scan_type = self.app.prefs.get_scan_type(self.app.model.app_mode)
scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type)
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
self.app._update_options()
#--- QWidget overrides
def closeEvent(self, event):
event.accept()
@ -213,6 +245,16 @@ class DirectoriesDialog(QMainWindow):
self.app.model.add_directory(dirpath)
self.recentFolders.insertItem(dirpath)
def appModeButtonSelected(self, index):
if index == 2:
mode = AppMode.Picture
elif index == 1:
mode = AppMode.Music
else:
mode = AppMode.Standard
self.app.model.app_mode = mode
self._updateScanTypeList()
def appWillSavePrefs(self):
self.app.prefs.directoriesWindowRect = self.geometry()
@ -241,7 +283,7 @@ class DirectoriesDialog(QMainWindow):
def scanTypeChanged(self, index):
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
self.app.prefs.scan_type = scan_options[index].scan_type
self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)
self.app._update_options()
def selectionChanged(self, selected, deselected):

View File

@ -1,47 +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 core_me import __appname__
from core_me.app import DupeGuru as DupeGuruModel
from ..base.app import DupeGuru as DupeGuruBase
from .details_dialog import DetailsDialog
from .results_model import ResultsModel
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog
class DupeGuru(DupeGuruBase):
MODELCLASS = DupeGuruModel
EDITION = 'me'
LOGO_NAME = 'logo_me'
NAME = __appname__
DETAILS_DIALOG_CLASS = DetailsDialog
RESULT_MODEL_CLASS = ResultsModel
PREFERENCES_CLASS = Preferences
PREFERENCES_DIALOG_CLASS = PreferencesDialog
def _update_options(self):
DupeGuruBase._update_options(self)
self.model.options['min_match_percentage'] = self.prefs.filter_hardness
self.model.options['scan_type'] = self.prefs.scan_type
self.model.options['word_weighting'] = self.prefs.word_weighting
self.model.options['match_similar_words'] = self.prefs.match_similar
scanned_tags = set()
if self.prefs.scan_tag_track:
scanned_tags.add('track')
if self.prefs.scan_tag_artist:
scanned_tags.add('artist')
if self.prefs.scan_tag_album:
scanned_tags.add('album')
if self.prefs.scan_tag_title:
scanned_tags.add('title')
if self.prefs.scan_tag_genre:
scanned_tags.add('genre')
if self.prefs.scan_tag_year:
scanned_tags.add('year')
self.model.options['scanned_tags'] = scanned_tags

View File

@ -1,17 +1,15 @@
# Created By: Virgil Dupras
# Created On: 2009-04-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
# 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 PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QAbstractItemView
from hscommon.trans import trget
from ..base.details_dialog import DetailsDialog as DetailsDialogBase
from ..base.details_table import DetailsTable
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
tr = trget('ui')
@ -28,4 +26,4 @@ class DetailsDialog(DetailsDialogBase):
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)

View File

@ -1,164 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DOCUMENT Type="Advanced Installer" CreateVersion="4.7.2" version="8.0.2" Modules="professional" RootPath="." Language="en">
<COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
<ROW Property="AI_SHORTCUTSREG" Value="0|0|0|"/>
<ROW Property="ALLUSERS" Value="2"/>
<ROW Property="ARPCOMMENTS" Value="This installer database contains the logic and data required to install [|ProductName]." ValueLocId="*"/>
<ROW Property="ARPCONTACT" Value="support@hardcoded.net"/>
<ROW Property="ARPHELPLINK" Value="http://www.hardcoded.net/support/"/>
<ROW Property="ARPURLINFOABOUT" Value="http://www.hardcoded.net/dupeguru_me/"/>
<ROW Property="ARPURLUPDATEINFO" Value="http://www.hardcoded.net/dupeguru_me/"/>
<ROW Property="BannerBitmap" MultiBuildValue="DefaultBuild:banner_image.jpg" Type="1"/>
<ROW Property="CTRLS" Value="2"/>
<ROW Property="DialogBitmap" MultiBuildValue="DefaultBuild:dialog_image.jpg" Type="1"/>
<ROW Property="Manufacturer" Value="Hardcoded Software" ValueLocId="*"/>
<ROW Property="ProductCode" Value="1033:{A7037226-389C-4704-81C3-E2CA17D11058} " Type="16"/>
<ROW Property="ProductLanguage" Value="1033"/>
<ROW Property="ProductName" Value="dupeGuru Music Edition" ValueLocId="*"/>
<ROW Property="ProductVersion" Value="5.6.0"/>
<ROW Property="RUNAPPLICATION" Value="1" Type="4"/>
<ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND"/>
<ROW Property="UpgradeCode" Value="{E11BFC48-7639-44BD-BB5B-A6AC934BC12D}"/>
<ROW Property="WindowsType9X" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsType9XDisplay" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsTypeNT" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
<ROW Property="WindowsTypeNT64" MultiBuildValue="DefaultBuild:Windows XP x64, Windows XP x64 ServicePack 1, Windows XP x64 ServicePack 2, Windows Server 2003 x64, Windows Server 2003 x64 Service Pack 1, Windows Server 2003 x64 Service pack 2" ValueLocId="-"/>
<ROW Property="WindowsTypeNT64Display" MultiBuildValue="DefaultBuild:Windows XP x64, Windows Server 2003 x64" ValueLocId="-"/>
<ROW Property="WindowsTypeNTDisplay" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiDirsComponent">
<ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1"/>
<ROW Directory="DesktopFolder" Directory_Parent="TARGETDIR" DefaultDir="Deskto~1|DesktopFolder" IsPseudoRoot="1"/>
<ROW Directory="SHORTCUTDIR" Directory_Parent="TARGETDIR" DefaultDir="SHORTC~1|SHORTCUTDIR" IsPseudoRoot="1"/>
<ROW Directory="TARGETDIR" DefaultDir="SourceDir"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
<ROW Component="AIShRegAnswer" ComponentId="{7DB5C163-2978-436F-A47D-54F1C76F1D64}" Directory_="APPDIR" Attributes="4" KeyPath="AIShRegAnswer"/>
<ROW Component="CurrentVersion" ComponentId="{1EF88868-DCB1-4F7D-83F6-CC17C9F65740}" Directory_="APPDIR" Attributes="4" KeyPath="CurrentVersion"/>
<ROW Component="SHORTCUTDIR" ComponentId="{B21C7D52-4D02-4470-AD46-9EF2780EEB12}" Directory_="SHORTCUTDIR" Attributes="0"/>
<ROW Component="dupeGuru_ME.exe" ComponentId="{14F7B73A-FA85-4C10-AA92-10BE05FE103B}" Directory_="APPDIR" Attributes="0" KeyPath="dupeGuru_ME.exe"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="dupeGuru_ME.exe AIShRegAnswer CurrentVersion SHORTCUTDIR"/>
<ATTRIBUTE name="CurrentFeature" value="MainFeature"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFilesComponent">
<ROW File="dupeGuru_ME.exe" Component_="dupeGuru_ME.exe" FileName="dupeGu~1.exe|dupeGuru ME.exe" Version="65535.65535.65535.65535" Attributes="0" SourcePath="dist\dupeGuru ME.exe" SelfReg="false" Sequence="1"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="0" PackageFolder="install" PackageFileName="dupeguru_me_win64_[|ProductVersion]" Languages="en" InstallationType="4" CreateMd5="true" ExtUI="true" MsiPackageType="x64"/>
<ATTRIBUTE name="CurrentBuild" value="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.CacheComponent">
<ATTRIBUTE name="Enable" value="false"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
<ROW Path="&lt;AI_DICTS&gt;ui.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.FragmentComponent">
<ROW Fragment="CommonUI.aip" Path="&lt;AI_FRAGS&gt;CommonUI.aip"/>
<ROW Fragment="FolderDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\FolderDlg.aip"/>
<ROW Fragment="SequenceDialogs.aip" Path="&lt;AI_THEMES&gt;classic\fragments\SequenceDialogs.aip"/>
<ROW Fragment="Sequences.aip" Path="&lt;AI_FRAGS&gt;Sequences.aip"/>
<ROW Fragment="ShortcutsDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\ShortcutsDlg.aip"/>
<ROW Fragment="StaticUIStrings.aip" Path="&lt;AI_FRAGS&gt;StaticUIStrings.aip"/>
<ROW Fragment="UI.aip" Path="&lt;AI_THEMES&gt;classic\fragments\UI.aip"/>
<ROW Fragment="Validation.aip" Path="&lt;AI_FRAGS&gt;Validation.aip"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiAppSearchComponent">
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionMachine"/>
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionUser"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiBinaryComponent">
<ROW Name="aicustact.dll" SourcePath="&lt;AI_CUSTACTS&gt;aicustact.dll"/>
<ROW Name="banner_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\banner-image.jpg"/>
<ROW Name="dialog_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\dialog-image.jpg"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlComponent">
<ATTRIBUTE name="FixedSizeBitmaps" value="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlConditionComponent">
<ROW Dialog_="ShortcutsDlg" Control_="QuickLaunchShorcutsCheckBox" Action="Hide" Condition="(Not Installed) AND (VersionNT&lt;&quot;601&quot;)"/>
<ROW Dialog_="ShortcutsDlg" Control_="StartupShorcutsCheckBox" Action="Hide" Condition="(Not Installed)"/>
<ATTRIBUTE name="DeletedRows" value="ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed) AND (VersionNT&lt;&quot;601&quot;)@ShortcutsDlg#StartupShorcutsCheckBox#Show#(Not Installed)@ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed)"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlEventComponent">
<ROW Dialog_="FolderDlg" Control_="Back" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="WelcomeDlg" Control_="Next" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="FolderDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_INSTALL" Ordering="3"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceWelcomeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceWelcomeDlg" Control_="Next" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="2"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="PatchWelcomeDlg" Condition="AI_PATCH" Ordering="1"/>
<ROW Dialog_="PatchWelcomeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_PATCH" Ordering="3"/>
<ROW Dialog_="ShortcutsDlg" Control_="Back" Event="NewDialog" Argument="WelcomeDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="ShortcutsDlg" Control_="Next" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="ChangeButton" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="301"/>
<ROW Dialog_="ResumeDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_RESUME" Ordering="299"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_MAINT" Ordering="197"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_PATCH" Ordering="198"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_INSTALL" Ordering="199"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="201"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCreateFolderComponent">
<ROW Directory_="SHORTCUTDIR" Component_="SHORTCUTDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCustActComponent">
<ROW Action="AI_DELETE_SHORTCUTS" Type="1" Source="aicustact.dll" Target="DeleteShortcuts"/>
<ROW Action="AI_DOWNGRADE" Type="19" Target="4010"/>
<ROW Action="AI_LaunchApp" Type="1" Source="aicustact.dll" Target="[#dupeGuru_ME.exe]"/>
<ROW Action="AI_PREPARE_UPGRADE" Type="65" Source="aicustact.dll" Target="PrepareUpgrade"/>
<ROW Action="AI_RESTORE_LOCATION" Type="65" Source="aicustact.dll" Target="RestoreLocation"/>
<ROW Action="AI_ResolveKnownFolders" Type="1" Source="aicustact.dll" Target="AI_ResolveKnownFolders"/>
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
<ROW Action="SET_APPDIR" Type="307" Source="APPDIR" Target="[ProgramFilesFolder][Manufacturer]\[ProductName]" MultiBuildTarget="DefaultBuild:[ProgramFiles64Folder][Manufacturer]\[ProductName]"/>
<ROW Action="SET_SHORTCUTDIR" Type="307" Source="SHORTCUTDIR" Target="[ProgramMenuFolder][ProductName]"/>
<ROW Action="SET_TARGETDIR_TO_APPDIR" Type="51" Source="TARGETDIR" Target="[APPDIR]"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiIconsComponent">
<ROW Name="SystemFolder_msiexec.exe" SourcePath="&lt;AI_RES&gt;uninstall.ico" Index="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel &lt;&gt; 5)" Sequence="210"/>
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_STORE_LOCATION" Condition="Not Installed" Sequence="1545"/>
<ROW Action="AI_PREPARE_UPGRADE" Condition="AI_UPGRADE=&quot;No&quot; AND (Not Installed)" Sequence="1300"/>
<ROW Action="AI_DELETE_SHORTCUTS" Condition="NOT (REMOVE=&quot;ALL&quot;)" Sequence="1449"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstallUISequenceComponent">
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
<ROW Condition="( Version9X OR ( NOT VersionNT64 ) OR ( VersionNT64 AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &gt;= 1)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &lt;&gt; 1)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR (((VersionNT64 = 502) AND (ServicePackLevel &lt;&gt; 2)) OR (MsiNTProductType &lt;&gt; 1))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &gt;= 1)))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &lt;&gt; 1)))) AND ((VersionNT64 &lt;&gt; 502) OR ((VersionNT64 = 502) AND ((MsiNTProductType = 1) OR (ServicePackLevel &lt;&gt; 2)))) ) )" Description="[ProductName] cannot be installed on the following Windows versions: [WindowsTypeNT64Display]" DescriptionLocId="AI.LaunchCondition.NoSpecificNT64" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="( Version9X OR VersionNT64 )" Description="[ProductName] cannot be installed on [WindowsTypeNTDisplay]" DescriptionLocId="AI.LaunchCondition.NoNT" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="VersionNT" Description="[ProductName] cannot be installed on [WindowsType9XDisplay]" DescriptionLocId="AI.LaunchCondition.No9X" IsPredefined="true" Builds="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegLocatorComponent">
<ROW Signature_="AI_ShRegOptionMachine" Root="2" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
<ROW Signature_="AI_ShRegOptionUser" Root="1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegsComponent">
<ROW Registry="AIShRegAnswer" Root="-1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Value="[AI_SHORTCUTSREG]" Component_="AIShRegAnswer"/>
<ROW Registry="CurrentVersion" Root="-1" Key="Software\[Manufacturer]\[ProductName]" Name="CurrentVersion" Value="[ProductVersion]" Component_="CurrentVersion"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiShortsComponent">
<ROW Shortcut="Uninstall_dupeGuru_ME" Directory_="SHORTCUTDIR" Name="Uninst~1|Uninstall dupeGuru ME" Component_="AIShRegAnswer" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]" Hotkey="0" Icon_="SystemFolder_msiexec.exe" IconIndex="0" ShowCmd="1"/>
<ROW Shortcut="dupeGuru_ME" Directory_="SHORTCUTDIR" Name="dupeGu~1|dupeGuru ME" Component_="dupeGuru_ME.exe" Target="[#dupeGuru_ME.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
<ROW Shortcut="dupeGuru_ME_1" Directory_="DesktopFolder" Name="dupeGu~1|dupeGuru ME" Component_="dupeGuru_ME.exe" Target="[#dupeGuru_ME.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiThemeComponent">
<ATTRIBUTE name="UsedTheme" value="classic"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiUpgradeComponent">
<ROW UpgradeCode="[|UpgradeCode]" VersionMax="[|ProductVersion]" Attributes="1025" ActionProperty="OLDPRODUCTS"/>
<ROW UpgradeCode="[|UpgradeCode]" VersionMin="[|ProductVersion]" Attributes="2" ActionProperty="AI_NEWERPRODUCTFOUND"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.SynchronizedFolderComponent">
<ROW Directory_="APPDIR" SourcePath="dist" Feature="MainFeature" ExcludePattern="*~|#*#|%*%|._|CVS|.cvsignore|SCCS|vssver.scc|mssccprj.scc|vssver2.scc|.svn|.DS_Store|*.pdb|*.vshost.*" ExcludeFlags="6"/>
</COMPONENT>
</DOCUMENT>

View File

@ -1,46 +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 core.scanner import ScanType
from ..base.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = ScanType.Tag
def _load_specific(self, settings):
get = self.get_value
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
self.scan_tag_track = get('ScanTagTrack', self.scan_tag_track)
self.scan_tag_artist = get('ScanTagArtist', self.scan_tag_artist)
self.scan_tag_album = get('ScanTagAlbum', self.scan_tag_album)
self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title)
self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre)
self.scan_tag_year = get('ScanTagYear', self.scan_tag_year)
def _reset_specific(self):
self.filter_hardness = 80
self.word_weighting = True
self.match_similar = False
self.scan_tag_track = False
self.scan_tag_artist = True
self.scan_tag_album = True
self.scan_tag_title = True
self.scan_tag_genre = False
self.scan_tag_year = False
def _save_specific(self, settings):
set_ = self.set_value
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)
set_('ScanTagTrack', self.scan_tag_track)
set_('ScanTagArtist', self.scan_tag_artist)
set_('ScanTagAlbum', self.scan_tag_album)
set_('ScanTagTitle', self.scan_tag_title)
set_('ScanTagGenre', self.scan_tag_genre)
set_('ScanTagYear', self.scan_tag_year)

View File

@ -10,10 +10,10 @@ from PyQt5.QtWidgets import (
)
from hscommon.trans import trget
from core.app import AppMode
from core.scanner import ScanType
from ..base.preferences_dialog import PreferencesDialogBase
from . import preferences
from ..preferences_dialog import PreferencesDialogBase
tr = trget('ui')
@ -74,7 +74,7 @@ class PreferencesDialog(PreferencesDialogBase):
setchecked(self.wordWeightingBox, prefs.word_weighting)
# Update UI state based on selected scan type
scan_type = prefs.scan_type
scan_type = prefs.get_scan_type(AppMode.Music)
word_based = scan_type in (
ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
ScanType.Tag
@ -100,6 +100,3 @@ class PreferencesDialog(PreferencesDialogBase):
prefs.match_similar = ischecked(self.matchSimilarBox)
prefs.word_weighting = ischecked(self.wordWeightingBox)
def resetToDefaults(self):
self.load(preferences.Preferences())

View File

@ -1,12 +1,11 @@
# Created On: 2011-11-27
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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 qtlib.column import Column
from ..base.results_model import ResultsModel as ResultsModelBase
from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [

View File

@ -1,9 +1,7 @@
# Created By: Virgil Dupras
# Created On: 2009-04-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
# 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 PyQt5.QtCore import Qt, QSize
@ -11,8 +9,8 @@ from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QVBoxLayout, QAbstractItemView, QHBoxLayout, QLabel, QSizePolicy
from hscommon.trans import trget
from ..base.details_dialog import DetailsDialog as DetailsDialogBase
from ..base.details_table import DetailsTable
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
tr = trget('ui')
@ -21,7 +19,7 @@ class DetailsDialog(DetailsDialogBase):
DetailsDialogBase.__init__(self, parent, app)
self.selectedPixmap = None
self.referencePixmap = None
def _setupUi(self):
self.setWindowTitle(tr("Details"))
self.resize(502, 295)
@ -61,21 +59,21 @@ class DetailsDialog(DetailsDialogBase):
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)
def _update(self):
if not self.app.model.selected_dupes:
return
dupe = self.app.model.selected_dupes[0]
group = self.app.model.results.get_group_of_duplicate(dupe)
ref = group.ref
self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe:
self.referencePixmap = None
else:
self.referencePixmap = QPixmap(str(ref.path))
self._updateImages()
def _updateImages(self):
if self.selectedPixmap is not None:
target_size = self.selectedImage.size()
@ -89,18 +87,18 @@ class DetailsDialog(DetailsDialogBase):
self.referenceImage.setPixmap(scaledPixmap)
else:
self.referenceImage.setPixmap(QPixmap())
#--- Override
def resizeEvent(self, event):
self._updateImages()
def show(self):
DetailsDialogBase.show(self)
self._update()
# model --> view
def refresh(self):
DetailsDialogBase.refresh(self)
if self.isVisible():
self._update()

View File

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DOCUMENT Type="Advanced Installer" CreateVersion="4.7.2" version="8.0.2" Modules="professional" RootPath="." Language="en">
<COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
<ROW Property="AI_SHORTCUTSREG" Value="0|0|0|"/>
<ROW Property="ALLUSERS" Value="2"/>
<ROW Property="ARPCOMMENTS" Value="This installer database contains the logic and data required to install [|ProductName]." ValueLocId="*"/>
<ROW Property="ARPCONTACT" Value="support@hardcoded.net"/>
<ROW Property="ARPHELPLINK" Value="http://www.hardcoded.net/support/"/>
<ROW Property="ARPURLINFOABOUT" Value="http://www.hardcoded.net/dupeguru_pe/"/>
<ROW Property="ARPURLUPDATEINFO" Value="http://www.hardcoded.net/dupeguru_pe/"/>
<ROW Property="BannerBitmap" MultiBuildValue="DefaultBuild:banner_image.jpg" Type="1"/>
<ROW Property="CTRLS" Value="2"/>
<ROW Property="DialogBitmap" MultiBuildValue="DefaultBuild:dialog_image.jpg" Type="1"/>
<ROW Property="Manufacturer" Value="Hardcoded Software" ValueLocId="*"/>
<ROW Property="ProductCode" Value="1033:{189C7FAD-CA63-4A56-B592-B68C34889265} " Type="16"/>
<ROW Property="ProductLanguage" Value="1033"/>
<ROW Property="ProductName" Value="dupeGuru Picture Edition" ValueLocId="*"/>
<ROW Property="ProductVersion" Value="1.7.0"/>
<ROW Property="RUNAPPLICATION" Value="1" Type="4"/>
<ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND"/>
<ROW Property="UpgradeCode" Value="{B1E28F97-9CE2-45E2-B19D-C4137F4DEB85}"/>
<ROW Property="WindowsType9X" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsType9XDisplay" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsTypeNT" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
<ROW Property="WindowsTypeNTDisplay" MultiBuildValue="DefaultBuild:Windows NT/2k/XP/Vista/Windows7 x86" ValueLocId="-"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiDirsComponent">
<ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1"/>
<ROW Directory="DesktopFolder" Directory_Parent="TARGETDIR" DefaultDir="Deskto~1|DesktopFolder" IsPseudoRoot="1"/>
<ROW Directory="SHORTCUTDIR" Directory_Parent="TARGETDIR" DefaultDir="SHORTC~1|SHORTCUTDIR" IsPseudoRoot="1"/>
<ROW Directory="TARGETDIR" DefaultDir="SourceDir"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
<ROW Component="AIShRegAnswer" ComponentId="{F315B3C7-7C86-41EB-BE7D-2A6A8E3073B4}" Directory_="APPDIR" Attributes="4" KeyPath="AIShRegAnswer"/>
<ROW Component="CurrentVersion" ComponentId="{1A0BFEFD-F9D9-4861-93AC-D9717B7A635E}" Directory_="APPDIR" Attributes="4" KeyPath="CurrentVersion"/>
<ROW Component="SHORTCUTDIR" ComponentId="{29E7E841-7820-418B-8542-7F8CCC9777A8}" Directory_="SHORTCUTDIR" Attributes="0"/>
<ROW Component="dupeGuru_PE.exe" ComponentId="{4A31F2AE-F42E-4B0F-BC4D-A09F312D469B}" Directory_="APPDIR" Attributes="0" KeyPath="dupeGuru_PE.exe"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="dupeGuru_PE.exe AIShRegAnswer SHORTCUTDIR CurrentVersion"/>
<ATTRIBUTE name="CurrentFeature" value="MainFeature"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFilesComponent">
<ROW File="dupeGuru_PE.exe" Component_="dupeGuru_PE.exe" FileName="dupeGu~2.exe|dupeGuru PE.exe" Version="65535.65535.65535.65535" Attributes="0" SourcePath="dist\dupeGuru PE.exe" SelfReg="false" Sequence="1"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="0" PackageFolder="install" PackageFileName="dupeguru_pe_win64_[|ProductVersion]" Languages="en" InstallationType="4" CreateMd5="true" ExtUI="true" MsiPackageType="x64"/>
<ATTRIBUTE name="CurrentBuild" value="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
<ROW Path="&lt;AI_DICTS&gt;ui.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.FragmentComponent">
<ROW Fragment="CommonUI.aip" Path="&lt;AI_FRAGS&gt;CommonUI.aip"/>
<ROW Fragment="FolderDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\FolderDlg.aip"/>
<ROW Fragment="SequenceDialogs.aip" Path="&lt;AI_THEMES&gt;classic\fragments\SequenceDialogs.aip"/>
<ROW Fragment="Sequences.aip" Path="&lt;AI_FRAGS&gt;Sequences.aip"/>
<ROW Fragment="ShortcutsDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\ShortcutsDlg.aip"/>
<ROW Fragment="StaticUIStrings.aip" Path="&lt;AI_FRAGS&gt;StaticUIStrings.aip"/>
<ROW Fragment="UI.aip" Path="&lt;AI_THEMES&gt;classic\fragments\UI.aip"/>
<ROW Fragment="Validation.aip" Path="&lt;AI_FRAGS&gt;Validation.aip"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiAppSearchComponent">
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionMachine"/>
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionUser"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiBinaryComponent">
<ROW Name="aicustact.dll" SourcePath="&lt;AI_CUSTACTS&gt;aicustact.dll"/>
<ROW Name="banner_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\banner-image.jpg"/>
<ROW Name="dialog_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\dialog-image.jpg"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlComponent">
<ATTRIBUTE name="FixedSizeBitmaps" value="0"/>
<ATTRIBUTE name="MultiBuildFixedSizeBitmaps" value="DefaultBuild:1"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlConditionComponent">
<ROW Dialog_="ShortcutsDlg" Control_="QuickLaunchShorcutsCheckBox" Action="Hide" Condition="(Not Installed) AND (VersionNT&lt;&quot;601&quot;)"/>
<ROW Dialog_="ShortcutsDlg" Control_="StartupShorcutsCheckBox" Action="Hide" Condition="(Not Installed)"/>
<ATTRIBUTE name="DeletedRows" value="ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed) AND (VersionNT&lt;&quot;601&quot;)@ShortcutsDlg#StartupShorcutsCheckBox#Show#(Not Installed)@ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed)"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlEventComponent">
<ROW Dialog_="FolderDlg" Control_="Back" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="WelcomeDlg" Control_="Next" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="FolderDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_INSTALL" Ordering="3"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceWelcomeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceWelcomeDlg" Control_="Next" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="2"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="PatchWelcomeDlg" Condition="AI_PATCH" Ordering="1"/>
<ROW Dialog_="PatchWelcomeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_PATCH" Ordering="3"/>
<ROW Dialog_="ShortcutsDlg" Control_="Back" Event="NewDialog" Argument="WelcomeDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="ShortcutsDlg" Control_="Next" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="ChangeButton" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="301"/>
<ROW Dialog_="ResumeDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_RESUME" Ordering="299"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_MAINT" Ordering="197"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_PATCH" Ordering="198"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_INSTALL" Ordering="199"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="201"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCreateFolderComponent">
<ROW Directory_="SHORTCUTDIR" Component_="SHORTCUTDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCustActComponent">
<ROW Action="AI_DELETE_SHORTCUTS" Type="1" Source="aicustact.dll" Target="DeleteShortcuts"/>
<ROW Action="AI_DOWNGRADE" Type="19" Target="4010"/>
<ROW Action="AI_LaunchApp" Type="1" Source="aicustact.dll" Target="[#dupeGuru_PE.exe]"/>
<ROW Action="AI_PREPARE_UPGRADE" Type="65" Source="aicustact.dll" Target="PrepareUpgrade"/>
<ROW Action="AI_RESTORE_LOCATION" Type="65" Source="aicustact.dll" Target="RestoreLocation"/>
<ROW Action="AI_ResolveKnownFolders" Type="1" Source="aicustact.dll" Target="AI_ResolveKnownFolders"/>
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
<ROW Action="SET_APPDIR" Type="307" Source="APPDIR" Target="[ProgramFilesFolder][Manufacturer]\[ProductName]" MultiBuildTarget="DefaultBuild:[ProgramFiles64Folder][Manufacturer]\[ProductName]"/>
<ROW Action="SET_SHORTCUTDIR" Type="307" Source="SHORTCUTDIR" Target="[ProgramMenuFolder][ProductName]"/>
<ROW Action="SET_TARGETDIR_TO_APPDIR" Type="51" Source="TARGETDIR" Target="[APPDIR]"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiIconsComponent">
<ROW Name="SystemFolder_msiexec.exe" SourcePath="&lt;AI_RES&gt;uninstall.ico" Index="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel &lt;&gt; 5)" Sequence="210"/>
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_STORE_LOCATION" Condition="Not Installed" Sequence="1545"/>
<ROW Action="AI_PREPARE_UPGRADE" Condition="AI_UPGRADE=&quot;No&quot; AND (Not Installed)" Sequence="1300"/>
<ROW Action="AI_DELETE_SHORTCUTS" Condition="NOT (REMOVE=&quot;ALL&quot;)" Sequence="1449"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstallUISequenceComponent">
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
<ROW Condition="( Version9X OR VersionNT64 )" Description="[ProductName] cannot be installed on [WindowsTypeNTDisplay]" DescriptionLocId="AI.LaunchCondition.NoNT" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="VersionNT" Description="[ProductName] cannot be installed on [WindowsType9XDisplay]" DescriptionLocId="AI.LaunchCondition.No9X" IsPredefined="true" Builds="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegLocatorComponent">
<ROW Signature_="AI_ShRegOptionMachine" Root="2" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
<ROW Signature_="AI_ShRegOptionUser" Root="1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegsComponent">
<ROW Registry="AIShRegAnswer" Root="-1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Value="[AI_SHORTCUTSREG]" Component_="AIShRegAnswer"/>
<ROW Registry="CurrentVersion" Root="-1" Key="Software\[Manufacturer]\[ProductName]" Name="CurrentVersion" Value="[ProductVersion]" Component_="CurrentVersion"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiShortsComponent">
<ROW Shortcut="Uninstall_dupeGuru_ME" Directory_="SHORTCUTDIR" Name="Uninst~1|Uninstall dupeGuru PE" Component_="AIShRegAnswer" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]" Hotkey="0" Icon_="SystemFolder_msiexec.exe" IconIndex="0" ShowCmd="1"/>
<ROW Shortcut="dupeGuru_PE" Directory_="SHORTCUTDIR" Name="dupeGu~1|dupeGuru PE" Component_="dupeGuru_PE.exe" Target="[#dupeGuru_PE.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
<ROW Shortcut="dupeGuru_PE_1" Directory_="DesktopFolder" Name="dupeGu~1|dupeGuru PE" Component_="dupeGuru_PE.exe" Target="[#dupeGuru_PE.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiThemeComponent">
<ATTRIBUTE name="UsedTheme" value="classic"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiUpgradeComponent">
<ROW UpgradeCode="[|UpgradeCode]" VersionMax="[|ProductVersion]" Attributes="1025" ActionProperty="OLDPRODUCTS"/>
<ROW UpgradeCode="[|UpgradeCode]" VersionMin="[|ProductVersion]" Attributes="2" ActionProperty="AI_NEWERPRODUCTFOUND"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.SynchronizedFolderComponent">
<ROW Directory_="APPDIR" SourcePath="dist" Feature="MainFeature" ExcludePattern="*~|#*#|%*%|._|CVS|.cvsignore|SCCS|vssver.scc|mssccprj.scc|vssver2.scc|.svn|.DS_Store" ExcludeFlags="6"/>
</COMPONENT>
</DOCUMENT>

View File

@ -8,17 +8,9 @@ import logging
from PyQt5.QtGui import QImage, QImageReader, QTransform
from core_pe import __appname__
from core_pe.photo import Photo as PhotoBase
from core_pe.app import DupeGuru as DupeGuruModel
from core.pe.photo import Photo as PhotoBase
from ..base.app import DupeGuru as DupeGuruBase
from .block import getblocks
from .details_dialog import DetailsDialog
from .result_window import ResultWindow
from .results_model import ResultsModel
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog
class File(PhotoBase):
def _plat_get_dimensions(self):
@ -62,29 +54,3 @@ class File(PhotoBase):
image = image.transformed(t)
return getblocks(image, block_count_per_side)
class DupeGuru(DupeGuruBase):
MODELCLASS = DupeGuruModel
EDITION = 'pe'
LOGO_NAME = 'logo_pe'
NAME = __appname__
DETAILS_DIALOG_CLASS = DetailsDialog
RESULT_WINDOW_CLASS = ResultWindow
RESULT_MODEL_CLASS = ResultsModel
PREFERENCES_CLASS = Preferences
PREFERENCES_DIALOG_CLASS = PreferencesDialog
def _setup(self):
self.model.fileclasses = [File]
DupeGuruBase._setup(self)
self.directories_dialog.menuFile.insertAction(
self.directories_dialog.actionLoadResults, self.resultWindow.actionClearPictureCache
)
def _update_options(self):
DupeGuruBase._update_options(self)
self.model.options['scan_type'] = self.prefs.scan_type
self.model.options['match_scaled'] = self.prefs.match_scaled
self.model.options['threshold'] = self.prefs.filter_hardness

View File

@ -1,25 +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 core.scanner import ScanType
from ..base.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = ScanType.FuzzyBlock
def _load_specific(self, settings):
get = self.get_value
self.match_scaled = get('MatchScaled', self.match_scaled)
def _reset_specific(self):
self.filter_hardness = 95
self.match_scaled = False
def _save_specific(self, settings):
set_ = self.set_value
set_('MatchScaled', self.match_scaled)

View File

@ -6,9 +6,9 @@
from hscommon.trans import trget
from core.scanner import ScanType
from core.app import AppMode
from ..base.preferences_dialog import PreferencesDialogBase
from . import preferences
from ..preferences_dialog import PreferencesDialogBase
tr = trget('ui')
@ -34,13 +34,10 @@ class PreferencesDialog(PreferencesDialogBase):
setchecked(self.matchScaledBox, prefs.match_scaled)
# Update UI state based on selected scan type
scan_type = prefs.scan_type
scan_type = prefs.get_scan_type(AppMode.Picture)
fuzzy_scan = scan_type == ScanType.FuzzyBlock
self.filterHardnessSlider.setEnabled(fuzzy_scan)
def _save(self, prefs, ischecked):
prefs.match_scaled = ischecked(self.matchScaledBox)
def resetToDefaults(self):
self.load(preferences.Preferences())

View File

@ -1,30 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-23
# 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 PyQt5.QtWidgets import QMessageBox, QAction
from hscommon.trans import trget
from ..base.result_window import ResultWindow as ResultWindowBase
tr = trget('ui')
class ResultWindow(ResultWindowBase):
def _setupMenu(self):
ResultWindowBase._setupMenu(self)
self.actionClearPictureCache = QAction(tr("Clear Picture Cache"), self)
self.actionClearPictureCache.setShortcut('Ctrl+Shift+P')
self.menuFile.insertAction(self.actionSaveResults, self.actionClearPictureCache)
self.actionClearPictureCache.triggered.connect(self.clearPictureCacheTriggered)
def clearPictureCacheTriggered(self):
title = tr("Clear Picture Cache")
msg = tr("Do you really want to remove all your cached picture analysis?")
if self.app.confirm(title, msg, QMessageBox.No):
self.app.model.scanner.clear_picture_cache()
QMessageBox.information(self, title, tr("Picture cache cleared."))

View File

@ -1,12 +1,11 @@
# Created On: 2011-11-27
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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 qtlib.column import Column
from ..base.results_model import ResultsModel as ResultsModelBase
from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [

View File

@ -7,15 +7,11 @@
from PyQt5.QtWidgets import QApplication
from hscommon import trans
from core.app import AppMode
from core.scanner import ScanType
from qtlib.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = None # edition-specific
def _load_specific(self, settings):
# load prefs specific to the dg edition
pass
def _load_values(self, settings):
get = self.get_value
self.filter_hardness = get('FilterHardness', self.filter_hardness)
@ -37,11 +33,17 @@ class Preferences(PreferencesBase):
self.recentResults = get('RecentResults', self.recentResults)
self.recentFolders = get('RecentFolders', self.recentFolders)
self._load_specific(settings)
def _reset_specific(self):
# reset prefs specific to the dg edition
pass
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
self.ignore_small_files = get('IgnoreSmallFiles', self.ignore_small_files)
self.small_file_threshold = get('SmallFileThreshold', self.small_file_threshold)
self.scan_tag_track = get('ScanTagTrack', self.scan_tag_track)
self.scan_tag_artist = get('ScanTagArtist', self.scan_tag_artist)
self.scan_tag_album = get('ScanTagAlbum', self.scan_tag_album)
self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title)
self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre)
self.scan_tag_year = get('ScanTagYear', self.scan_tag_year)
self.match_scaled = get('MatchScaled', self.match_scaled)
def reset(self):
self.filter_hardness = 95
@ -61,11 +63,17 @@ class Preferences(PreferencesBase):
self.recentResults = []
self.recentFolders = []
self._reset_specific()
def _save_specific(self, settings):
# save prefs specific to the dg edition
pass
self.word_weighting = True
self.match_similar = False
self.ignore_small_files = True
self.small_file_threshold = 10 # KB
self.scan_tag_track = False
self.scan_tag_artist = True
self.scan_tag_album = True
self.scan_tag_title = True
self.scan_tag_genre = False
self.scan_tag_year = False
self.match_scaled = False
def _save_values(self, settings):
set_ = self.set_value
@ -86,13 +94,31 @@ class Preferences(PreferencesBase):
set_('RecentResults', self.recentResults)
set_('RecentFolders', self.recentFolders)
self._save_specific(settings)
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)
set_('IgnoreSmallFiles', self.ignore_small_files)
set_('SmallFileThreshold', self.small_file_threshold)
set_('ScanTagTrack', self.scan_tag_track)
set_('ScanTagArtist', self.scan_tag_artist)
set_('ScanTagAlbum', self.scan_tag_album)
set_('ScanTagTitle', self.scan_tag_title)
set_('ScanTagGenre', self.scan_tag_genre)
set_('ScanTagYear', self.scan_tag_year)
set_('MatchScaled', self.match_scaled)
# scan_type is special because we save it immediately when we set it.
@property
def scan_type(self):
return self.get_value('ScanType', self.DEFAULT_SCAN_TYPE)
def get_scan_type(self, app_mode):
if app_mode == AppMode.Picture:
return self.get_value('ScanTypePicture', ScanType.FuzzyBlock)
elif app_mode == AppMode.Music:
return self.get_value('ScanTypeMusic', ScanType.Tag)
else:
return self.get_value('ScanTypeStandard', ScanType.Contents)
@scan_type.setter
def scan_type(self, value):
self.set_value('ScanType', value)
def set_scan_type(self, app_mode, value):
if app_mode == AppMode.Picture:
self.set_value('ScanTypePicture', value)
elif app_mode == AppMode.Music:
self.set_value('ScanTypeMusic', value)
else:
self.set_value('ScanTypeStandard', value)

View File

@ -10,11 +10,12 @@ from PyQt5.QtWidgets import (
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox
)
from hscommon.plat import ISOSX, ISLINUX
from hscommon.trans import trget
from qtlib.util import horizontalWrap
from qtlib.preferences import get_langnames
from .preferences import Preferences
tr = trget('ui')
SUPPORTED_LANGUAGES = [
@ -123,9 +124,6 @@ class PreferencesDialogBase(QDialog):
self.buttonBox = QDialogButtonBox(self)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok|QDialogButtonBox.RestoreDefaults)
self.mainVLayout.addWidget(self.buttonBox)
if (not ISOSX) and (not ISLINUX):
self.mainVLayout.removeWidget(self.ignoreHardlinkMatches)
self.ignoreHardlinkMatches.setHidden(True)
def _load(self, prefs, setchecked):
# Edition-specific
@ -177,6 +175,9 @@ class PreferencesDialogBase(QDialog):
self.app.prefs.language = lang
self._save(prefs, ischecked)
def resetToDefaults(self):
self.load(Preferences())
#--- Events
def buttonClicked(self, button):
role = self.buttonBox.buttonRole(button)

View File

@ -9,7 +9,7 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
QLabel, QTableView, QAbstractItemView, QApplication
QLabel, QTableView, QAbstractItemView
)
from hscommon.trans import trget
@ -63,12 +63,3 @@ class ProblemDialog(QDialog):
self.horizontalLayout.addWidget(self.closeButton)
self.verticalLayout.addLayout(self.horizontalLayout)
if __name__ == '__main__':
import sys
from ..testapp import TestApp
app = QApplication([])
dgapp = TestApp()
dialog = ProblemDialog(None, dgapp)
dialog.show()
sys.exit(app.exec_())

View File

@ -16,9 +16,13 @@ from hscommon.trans import trget
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
from qtlib.search_edit import SearchEdit
from core.app import AppMode
from .results_model import ResultsView
from .stats_label import StatsLabel
from .prioritize_dialog import PrioritizeDialog
from .se.results_model import ResultsModel as ResultsModelStandard
from .me.results_model import ResultsModel as ResultsModelMusic
from .pe.results_model import ResultsModel as ResultsModelPicture
tr = trget('ui')
@ -27,7 +31,13 @@ class ResultWindow(QMainWindow):
super().__init__(parent, **kwargs)
self.app = app
self._setupUi()
self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView)
if app.model.app_mode == AppMode.Picture:
MODEL_CLASS = ResultsModelPicture
elif app.model.app_mode == AppMode.Music:
MODEL_CLASS = ResultsModelMusic
else:
MODEL_CLASS = ResultsModelStandard
self.resultsModel = MODEL_CLASS(self.app, self.resultsView)
self.stats = StatsLabel(app.model.stats_label, self.statusLabel)
self._update_column_actions_status()

View File

@ -1,9 +1,9 @@
# Created By: Virgil Dupras
# Created On: 2009-04-23
# 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
#
# 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 PyQt5.QtCore import Qt, pyqtSignal, QModelIndex
@ -17,13 +17,17 @@ class ResultsModel(Table):
model = app.model.result_table
super().__init__(model, view, **kwargs)
view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)
app.prefsChanged.connect(self.appPrefsChanged)
font = view.font()
font.setPointSize(app.prefs.tableFontSize)
self.view.setFont(font)
fm = QFontMetrics(font)
view.verticalHeader().setDefaultSectionSize(fm.height()+2)
app.willSavePrefs.connect(self.appWillSavePrefs)
def _getData(self, row, column, role):
if column.name == 'marked':
if role == Qt.CheckStateRole and row.markable:
if role == Qt.CheckStateRole and row.markable:
return Qt.Checked if row.marked else Qt.Unchecked
return None
if role == Qt.DisplayRole:
@ -43,7 +47,7 @@ class ResultsModel(Table):
if column.name == 'name':
return row.data[column.name]
return None
def _getFlags(self, row, column):
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if column.name == 'marked':
@ -52,7 +56,7 @@ class ResultsModel(Table):
elif column.name == 'name':
flags |= Qt.ItemIsEditable
return flags
def _setData(self, row, column, value, role):
if role == Qt.CheckStateRole:
if column.name == 'marked':
@ -62,46 +66,39 @@ class ResultsModel(Table):
if column.name == 'name':
return self.model.rename_selected(value)
return False
def sort(self, column, order):
column = self.model.COLUMNS[column]
self.model.sort(column.name, order == Qt.AscendingOrder)
#--- Properties
@property
def power_marker(self):
return self.model.power_marker
@power_marker.setter
def power_marker(self, value):
self.model.power_marker = value
@property
def delta_values(self):
return self.model.delta_values
@delta_values.setter
def delta_values(self, value):
self.model.delta_values = value
#--- Events
def appPrefsChanged(self, prefs):
font = self.view.font()
font.setPointSize(prefs.tableFontSize)
self.view.setFont(font)
fm = QFontMetrics(font)
self.view.verticalHeader().setDefaultSectionSize(fm.height()+2)
def appWillSavePrefs(self):
self.model.columns.save_columns()
#--- model --> view
def invalidate_markings(self):
# redraw view
# HACK. this is the only way I found to update the widget without reseting everything
self.view.scroll(0, 1)
self.view.scroll(0, -1)
class ResultsView(QTableView):
#--- Override
@ -110,10 +107,10 @@ class ResultsView(QTableView):
self.spacePressed.emit()
return
super().keyPressEvent(event)
def mouseDoubleClickEvent(self, event):
self.doubleClicked.emit(QModelIndex())
# We don't call the superclass' method because the default behavior is to rename the cell.
#--- Signals
spacePressed = pyqtSignal()

View File

@ -1,8 +1,8 @@
#!/usr/bin/python3
# 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
# 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
import sys
@ -13,16 +13,12 @@ from PyQt5.QtCore import QCoreApplication, QSettings
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QApplication
from hscommon.plat import ISWINDOWS
from hscommon.trans import install_gettext_trans_under_qt
from qtlib.error_report_dialog import install_excepthook
from qtlib.util import setupQtLogging
from qt.base import dg_rc
from qt.base.platform import BASE_PATH
from core_{edition} import __version__, __appname__
if ISWINDOWS:
import qt.base.cxfreeze_fix
from qt import dg_rc
from qt.platform import BASE_PATH
from core import __version__, __appname__
def main():
app = QApplication(sys.argv)
@ -36,7 +32,7 @@ def main():
install_gettext_trans_under_qt(locale_folder, lang)
# Many strings are translated at import time, so this is why we only import after the translator
# has been installed
from qt.{edition}.app import DupeGuru
from qt.app import DupeGuru
app.setWindowIcon(QIcon(QPixmap(":/{0}".format(DupeGuru.LOGO_NAME))))
dgapp = DupeGuru()
install_excepthook('https://github.com/hsoft/dupeguru/issues')

View File

@ -1,50 +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 core_se import __appname__
from core_se.app import DupeGuru as DupeGuruModel
from core.directories import Directories as DirectoriesBase, DirectoryState
from ..base.app import DupeGuru as DupeGuruBase
from .details_dialog import DetailsDialog
from .results_model import ResultsModel
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if len(path) == 2 and path[1].lower() in self.ROOT_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
class DupeGuru(DupeGuruBase):
MODELCLASS = DupeGuruModel
EDITION = 'se'
LOGO_NAME = 'logo_se'
NAME = __appname__
DETAILS_DIALOG_CLASS = DetailsDialog
RESULT_MODEL_CLASS = ResultsModel
PREFERENCES_CLASS = Preferences
PREFERENCES_DIALOG_CLASS = PreferencesDialog
def _setup(self):
self.directories = Directories()
DupeGuruBase._setup(self)
def _update_options(self):
DupeGuruBase._update_options(self)
self.model.options['min_match_percentage'] = self.prefs.filter_hardness
self.model.options['scan_type'] = self.prefs.scan_type
self.model.options['word_weighting'] = self.prefs.word_weighting
self.model.options['match_similar_words'] = self.prefs.match_similar
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
self.model.options['size_threshold'] = threshold * 1024 # threshold is in KB. the scanner wants bytes

View File

@ -1,17 +1,15 @@
# 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
# 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 PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QVBoxLayout, QAbstractItemView
from hscommon.trans import trget
from ..base.details_dialog import DetailsDialog as DetailsDialogBase
from ..base.details_table import DetailsTable
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
tr = trget('ui')
@ -28,4 +26,4 @@ class DetailsDialog(DetailsDialogBase):
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
self.verticalLayout.addWidget(self.tableView)

View File

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DOCUMENT Type="Advanced Installer" CreateVersion="4.4.1" version="8.0.2" Modules="professional" RootPath="." Language="en">
<COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
<ROW Property="AI_SHORTCUTSREG" Value="0|0|0|"/>
<ROW Property="ALLUSERS" Value="2"/>
<ROW Property="ARPCOMMENTS" Value="This installer database contains the logic and data required to install [|ProductName]." ValueLocId="*"/>
<ROW Property="ARPCONTACT" Value="support@hardcoded.net"/>
<ROW Property="ARPHELPLINK" Value="http://www.hardcoded.net/support/"/>
<ROW Property="ARPURLINFOABOUT" Value="http://www.hardcoded.net/dupeguru/"/>
<ROW Property="ARPURLUPDATEINFO" Value="http://www.hardcoded.net/dupeguru/"/>
<ROW Property="BannerBitmap" MultiBuildValue="DefaultBuild:banner_image.jpg" Type="1"/>
<ROW Property="CTRLS" Value="2"/>
<ROW Property="DialogBitmap" MultiBuildValue="DefaultBuild:dialog_image.jpg" Type="1"/>
<ROW Property="Manufacturer" Value="Hardcoded Software" ValueLocId="*"/>
<ROW Property="ProductCode" Value="1033:{D1E765C2-98C4-49AF-80DA-A5F803EB4FC3} " Type="16"/>
<ROW Property="ProductLanguage" Value="1033"/>
<ROW Property="ProductName" Value="dupeGuru" ValueLocId="*"/>
<ROW Property="ProductVersion" Value="2.7.0"/>
<ROW Property="RUNAPPLICATION" Value="1" Type="4"/>
<ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND"/>
<ROW Property="UpgradeCode" Value="{33E0D6C8-D7C6-46ED-B1A9-ECFE409EC9D5}"/>
<ROW Property="WindowsType9X" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsType9XDisplay" MultiBuildValue="DefaultBuild:Windows 9x/ME" ValueLocId="-"/>
<ROW Property="WindowsTypeNT" MultiBuildValue="DefaultBuild:Windows NT 4.0, Windows NT 4.0 Service Pack 1, Windows NT 4.0 Service Pack 2, Windows NT 4.0 Service Pack 3, Windows NT 4.0 Service Pack 4, Windows NT 4.0 Service Pack 5, Windows NT 4.0 Service Pack 6" ValueLocId="-"/>
<ROW Property="WindowsTypeNTDisplay" MultiBuildValue="DefaultBuild:Windows NT 4.0" ValueLocId="-"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiDirsComponent">
<ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1"/>
<ROW Directory="DesktopFolder" Directory_Parent="TARGETDIR" DefaultDir="Deskto~1|DesktopFolder" IsPseudoRoot="1"/>
<ROW Directory="SHORTCUTDIR" Directory_Parent="TARGETDIR" DefaultDir="SHORTC~1|SHORTCUTDIR" IsPseudoRoot="1"/>
<ROW Directory="TARGETDIR" DefaultDir="SourceDir"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
<ROW Component="AIShRegAnswer" ComponentId="{775090B3-2E56-40F5-9DD8-24A2F82DA601}" Directory_="APPDIR" Attributes="4" KeyPath="AIShRegAnswer"/>
<ROW Component="CurrentVersion" ComponentId="{398CD484-F093-4CEC-BE84-1BD0DD461942}" Directory_="APPDIR" Attributes="4" KeyPath="CurrentVersion"/>
<ROW Component="SHORTCUTDIR" ComponentId="{D4E1AAB3-42FD-4997-A393-19949091495D}" Directory_="SHORTCUTDIR" Attributes="0"/>
<ROW Component="dupeGuru.exe" ComponentId="{A8FFC84F-B54B-4883-B9FD-5C545AF0E51C}" Directory_="APPDIR" Attributes="0" KeyPath="dupeGuru.exe"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
<ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0" Components="dupeGuru.exe AIShRegAnswer CurrentVersion SHORTCUTDIR"/>
<ATTRIBUTE name="CurrentFeature" value="MainFeature"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiFilesComponent">
<ROW File="dupeGuru.exe" Component_="dupeGuru.exe" FileName="dupeGuru.exe" Version="65535.65535.65535.65535" Attributes="0" SourcePath="dist\dupeGuru.exe" SelfReg="false" Sequence="1"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="0" PackageFolder="install" PackageFileName="dupeguru_win64_[|ProductVersion]" Languages="en" InstallationType="4" CreateMd5="true" ExtUI="true" MsiPackageType="x64"/>
<ATTRIBUTE name="CurrentBuild" value="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
<ROW Path="&lt;AI_DICTS&gt;ui.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.FragmentComponent">
<ROW Fragment="CommonUI.aip" Path="&lt;AI_FRAGS&gt;CommonUI.aip"/>
<ROW Fragment="FolderDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\FolderDlg.aip"/>
<ROW Fragment="SequenceDialogs.aip" Path="&lt;AI_THEMES&gt;classic\fragments\SequenceDialogs.aip"/>
<ROW Fragment="Sequences.aip" Path="&lt;AI_FRAGS&gt;Sequences.aip"/>
<ROW Fragment="ShortcutsDlg.aip" Path="&lt;AI_THEMES&gt;classic\fragments\ShortcutsDlg.aip"/>
<ROW Fragment="StaticUIStrings.aip" Path="&lt;AI_FRAGS&gt;StaticUIStrings.aip"/>
<ROW Fragment="UI.aip" Path="&lt;AI_THEMES&gt;classic\fragments\UI.aip"/>
<ROW Fragment="Validation.aip" Path="&lt;AI_FRAGS&gt;Validation.aip"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiAppSearchComponent">
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionMachine"/>
<ROW Property="AI_SHORTCUTSREG" Signature_="AI_ShRegOptionUser"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiBinaryComponent">
<ROW Name="aicustact.dll" SourcePath="&lt;AI_CUSTACTS&gt;aicustact.dll"/>
<ROW Name="banner_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\banner-image.jpg"/>
<ROW Name="dialog_image.jpg" SourcePath="&lt;AI_THEMES&gt;classic\resources\dialog-image.jpg"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlComponent">
<ATTRIBUTE name="FixedSizeBitmaps" value="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlConditionComponent">
<ROW Dialog_="ShortcutsDlg" Control_="StartmenuShortcutsCheckBox" Action="Show" Condition="(Not Installed)"/>
<ROW Dialog_="ShortcutsDlg" Control_="QuickLaunchShorcutsCheckBox" Action="Hide" Condition="(Not Installed) AND (VersionNT&lt;&quot;601&quot;)"/>
<ROW Dialog_="ShortcutsDlg" Control_="StartupShorcutsCheckBox" Action="Hide" Condition="(Not Installed)"/>
<ATTRIBUTE name="DeletedRows" value="ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed) AND (VersionNT&lt;&quot;601&quot;)@ShortcutsDlg#StartupShorcutsCheckBox#Show#(Not Installed)@ShortcutsDlg#StartmenuShortcutsCheckBox#Show#(Not Installed)@ShortcutsDlg#QuickLaunchShorcutsCheckBox#Show#(Not Installed)"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiControlEventComponent">
<ROW Dialog_="FolderDlg" Control_="Back" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="WelcomeDlg" Control_="Next" Event="NewDialog" Argument="ShortcutsDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="FolderDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_INSTALL" Ordering="3"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceWelcomeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceWelcomeDlg" Control_="Next" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="2"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="PatchWelcomeDlg" Condition="AI_PATCH" Ordering="1"/>
<ROW Dialog_="PatchWelcomeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_PATCH" Ordering="3"/>
<ROW Dialog_="ShortcutsDlg" Control_="Back" Event="NewDialog" Argument="WelcomeDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="ShortcutsDlg" Control_="Next" Event="NewDialog" Argument="FolderDlg" Condition="AI_INSTALL" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Back" Event="NewDialog" Argument="MaintenanceTypeDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="CustomizeDlg" Control_="Next" Event="NewDialog" Argument="VerifyReadyDlg" Condition="AI_MAINT" Ordering="1"/>
<ROW Dialog_="MaintenanceTypeDlg" Control_="ChangeButton" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="301"/>
<ROW Dialog_="ResumeDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_RESUME" Ordering="299"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_MAINT" Ordering="197"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_PATCH" Ordering="198"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_INSTALL" Ordering="199"/>
<ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="CustomizeDlg" Condition="AI_MAINT" Ordering="201"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCreateFolderComponent">
<ROW Directory_="SHORTCUTDIR" Component_="SHORTCUTDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiCustActComponent">
<ROW Action="AI_DELETE_SHORTCUTS" Type="1" Source="aicustact.dll" Target="DeleteShortcuts"/>
<ROW Action="AI_DOWNGRADE" Type="19" Target="4010"/>
<ROW Action="AI_LaunchApp" Type="1" Source="aicustact.dll" Target="[#dupeGuru.exe]"/>
<ROW Action="AI_PREPARE_UPGRADE" Type="65" Source="aicustact.dll" Target="PrepareUpgrade"/>
<ROW Action="AI_RESTORE_LOCATION" Type="65" Source="aicustact.dll" Target="RestoreLocation"/>
<ROW Action="AI_ResolveKnownFolders" Type="1" Source="aicustact.dll" Target="AI_ResolveKnownFolders"/>
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
<ROW Action="SET_APPDIR" Type="307" Source="APPDIR" Target="[ProgramFilesFolder][Manufacturer]\[ProductName]" MultiBuildTarget="DefaultBuild:[ProgramFiles64Folder][Manufacturer]\[ProductName]"/>
<ROW Action="SET_SHORTCUTDIR" Type="307" Source="SHORTCUTDIR" Target="[ProgramMenuFolder][ProductName]"/>
<ROW Action="SET_TARGETDIR_TO_APPDIR" Type="51" Source="TARGETDIR" Target="[APPDIR]"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiIconsComponent">
<ROW Name="SystemFolder_msiexec.exe" SourcePath="&lt;AI_RES&gt;uninstall.ico" Index="0"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel &lt;&gt; 5)" Sequence="210"/>
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_STORE_LOCATION" Condition="Not Installed" Sequence="1545"/>
<ROW Action="AI_PREPARE_UPGRADE" Condition="AI_UPGRADE=&quot;No&quot; AND (Not Installed)" Sequence="1300"/>
<ROW Action="AI_DELETE_SHORTCUTS" Condition="NOT (REMOVE=&quot;ALL&quot;)" Sequence="1449"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstallUISequenceComponent">
<ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="740"/>
<ROW Action="AI_ResolveKnownFolders" Sequence="51"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
<ROW Condition="( Version9X OR VersionNT64 OR ( VersionNT AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &gt;= 1))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 1))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 2))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 3))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 4))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 5))) AND ((VersionNT &lt;&gt; 400) OR ((VersionNT = 400) AND (ServicePackLevel &lt;&gt; 6))) ) )" Description="[ProductName] cannot be installed on the following Windows versions: [WindowsTypeNTDisplay]" DescriptionLocId="AI.LaunchCondition.NoSpecificNT" IsPredefined="true" Builds="DefaultBuild"/>
<ROW Condition="VersionNT" Description="[ProductName] cannot be installed on [WindowsType9XDisplay]" DescriptionLocId="AI.LaunchCondition.No9X" IsPredefined="true" Builds="DefaultBuild"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegLocatorComponent">
<ROW Signature_="AI_ShRegOptionMachine" Root="2" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
<ROW Signature_="AI_ShRegOptionUser" Root="1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Type="2"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiRegsComponent">
<ROW Registry="AIShRegAnswer" Root="-1" Key="Software\Caphyon\Advanced Installer\Installs\[ProductCode]" Name="AIShRegAnswer" Value="[AI_SHORTCUTSREG]" Component_="AIShRegAnswer"/>
<ROW Registry="CurrentVersion" Root="-1" Key="Software\[Manufacturer]\[ProductName]" Name="CurrentVersion" Value="[ProductVersion]" Component_="CurrentVersion"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiShortsComponent">
<ROW Shortcut="Uninstall_dupeGuru" Directory_="SHORTCUTDIR" Name="Uninst~1|Uninstall dupeGuru" Component_="AIShRegAnswer" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]" Hotkey="0" Icon_="SystemFolder_msiexec.exe" IconIndex="0" ShowCmd="1"/>
<ROW Shortcut="dupeGuru" Directory_="DesktopFolder" Name="dupeGuru" Component_="dupeGuru.exe" Target="[#dupeGuru.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
<ROW Shortcut="dupeGuru_1" Directory_="SHORTCUTDIR" Name="dupeGuru" Component_="dupeGuru.exe" Target="[#dupeGuru.exe]" Hotkey="0" IconIndex="0" ShowCmd="1" WkDir="APPDIR"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiThemeComponent">
<ATTRIBUTE name="UsedTheme" value="classic"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiUpgradeComponent">
<ROW UpgradeCode="[|UpgradeCode]" VersionMax="[|ProductVersion]" Attributes="1025" ActionProperty="OLDPRODUCTS"/>
<ROW UpgradeCode="[|UpgradeCode]" VersionMin="[|ProductVersion]" Attributes="2" ActionProperty="AI_NEWERPRODUCTFOUND"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.SynchronizedFolderComponent">
<ROW Directory_="APPDIR" SourcePath="dist" Feature="MainFeature" ExcludePattern="*~|#*#|%*%|._|CVS|.cvsignore|SCCS|vssver.scc|mssccprj.scc|vssver2.scc|.svn|.DS_Store|*.vshost.*" ExcludeFlags="6"/>
</COMPONENT>
</DOCUMENT>

View File

@ -1,34 +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 core.scanner import ScanType
from ..base.preferences import Preferences as PreferencesBase
class Preferences(PreferencesBase):
DEFAULT_SCAN_TYPE = ScanType.Contents
def _load_specific(self, settings):
get = self.get_value
self.word_weighting = get('WordWeighting', self.word_weighting)
self.match_similar = get('MatchSimilar', self.match_similar)
self.ignore_small_files = get('IgnoreSmallFiles', self.ignore_small_files)
self.small_file_threshold = get('SmallFileThreshold', self.small_file_threshold)
def _reset_specific(self):
self.filter_hardness = 80
self.word_weighting = True
self.match_similar = False
self.ignore_small_files = True
self.small_file_threshold = 10 # KB
def _save_specific(self, settings):
set_ = self.set_value
set_('WordWeighting', self.word_weighting)
set_('MatchSimilar', self.match_similar)
set_('IgnoreSmallFiles', self.ignore_small_files)
set_('SmallFileThreshold', self.small_file_threshold)

View File

@ -13,10 +13,10 @@ from hscommon.plat import ISWINDOWS, ISLINUX
from hscommon.trans import trget
from hscommon.util import tryint
from core.app import AppMode
from core.scanner import ScanType
from ..base.preferences_dialog import PreferencesDialogBase
from . import preferences
from ..preferences_dialog import PreferencesDialogBase
tr = trget('ui')
@ -81,7 +81,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
# Update UI state based on selected scan type
scan_type = prefs.scan_type
scan_type = prefs.get_scan_type(AppMode.Standard)
word_based = scan_type == ScanType.Filename
self.filterHardnessSlider.setEnabled(word_based)
self.matchSimilarBox.setEnabled(word_based)
@ -93,6 +93,3 @@ class PreferencesDialog(PreferencesDialogBase):
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text())
def resetToDefaults(self):
self.load(preferences.Preferences())

View File

@ -1,12 +1,11 @@
# Created On: 2011-11-27
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# 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 qtlib.column import Column
from ..base.results_model import ResultsModel as ResultsModelBase
from ..results_model import ResultsModel as ResultsModelBase
class ResultsModel(ResultsModelBase):
COLUMNS = [

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-10-04
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from qtlib.recent import Recent
from .se.app import DupeGuru
class TestApp(DupeGuru):
# Use this for as a mock for UI testing.
def mustShowNag(self):
pass
def _setup(self):
self.prefs = self._create_preferences()
self.prefs.load()
self.recentResults = Recent(self, 'recentResults')
self._setupActions()