1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-24 23:51:38 +00:00

Compare commits

..

45 Commits

Author SHA1 Message Date
06eca11f0b Remove format check from lint job 2021-08-17 00:52:14 -05:00
2879f18e0d Run linting and formatting check in parallel before test 2021-08-17 00:50:41 -05:00
3ee21771f9 Fix workflow file format 2021-08-17 00:33:54 -05:00
c0ba6fb57a Test out github actions
Add a workflow to test
2021-08-17 00:31:15 -05:00
bc942b8263 Add black format check to tox runs 2021-08-15 04:10:46 -05:00
ffe6b7047c Format all files with black correcting line length 2021-08-15 04:10:18 -05:00
9446f37fad Remove flake8 E731 Errors
Note: black formatting is now applying correctly as well.
2021-08-15 03:53:43 -05:00
af19660c18 Update flake8 and black configuration
- Update black to now use 120 lines
- Update flake8 to use recommended settings for black integration
2021-08-15 03:32:31 -05:00
99ad297906 Change preferences to use spinboxes where applicable
- Change LineEdit to Spinbox for minimum file size 0-1,000,000KB
- Change LineEdit to Spinbox for big file size 0-1,000,000MB
2021-08-15 02:11:42 -05:00
e11f996dfc Merge pull request #908 from glubsy/hash_sample_optimization
Hash sample optimization
2021-08-13 23:41:17 -05:00
glubsy
e95306e58f Fix flake 8 2021-08-14 02:52:00 +02:00
glubsy
891a875990 Cache constant expression
Perhaps the python byte code is already optimized, but just in case it is not, keep pre-compute the constant expression.
2021-08-13 21:33:21 +02:00
glubsy
545a5a75fb Fix for older python versions
The "walrus" operator is only available in python 3.8 and later. Fall back to more traditional notation.
2021-08-13 20:56:33 +02:00
glubsy
7b764f183e Avoid partially hashing small files
Computing 3 hash samples for files less than 3MiB (3 * CHUNK_SIZE) is not efficient since spans of later samples would overlap a previous one.
Therefore we can simply return the hash of the entire small file instead.
2021-08-13 20:47:01 +02:00
fdc8a17d26 Update .travis.yml
- Windows test uses 3.9.6 now
- Intentation changes
2021-08-07 19:35:57 -05:00
cb3bbbec6e Upgrade Requirement Minimums
- Upgrade requirements to specify more current minimums
- Remove compatability code from sphinxgen for old versions
- Upgrade pyinstaller to a minimum version that works with latest macOS
2021-08-07 19:28:41 -05:00
c51a82a2ce Fix Issues from Translation Update
- Add Qtlib to transifex config
- Pull latest qtlib translations
- Fix flake8 error
- Remove code for manual translation import, use transifex-client instead
2021-08-06 22:21:35 -05:00
0cd8f5e948 Update translation pot files 2021-08-06 21:41:52 -05:00
9c09607c08 Add Turkish & Updates from Transifex
- Pull updates from Transifex
- Add Turkish
- Sort language lists in code
- Remove old locale conversion code as it appears to work correctly on
windows without different conversions.
2021-08-06 21:41:52 -05:00
3bd342770c Update configurations
- Enable Unicode for NSIS Installer
- Update transifex config to new project
2021-08-06 21:41:52 -05:00
14b456dcf9 Merge pull request #927 from glubsy/fix_directories_tests
Fix Directories regex test
2021-08-06 20:08:27 -05:00
glubsy
3dccb686e2 Fix Directories regex test
The entire path to the file would match unless another path separator is added.
2021-08-06 17:18:23 +02:00
0db66baace Merge pull request #907 from glubsy/missing_renamed_regex
Missing renamed regex
2021-08-03 22:26:08 -05:00
e3828ae2ca Merge pull request #911 from glubsy/fix_757_fix_regression
Fix infinite recursion
2021-06-22 22:44:12 -05:00
glubsy
23c59787e5 Fix infinite recursion
Force the Results to update its internal __dupes list whenever at least one group has re-prioritized and changed its dupes/ref.
2021-06-23 05:36:10 +02:00
2f8d603251 Merge pull request #910 from glubsy/757_fix
Fix refs appearing in dupes-only view
2021-06-22 21:54:49 -05:00
glubsy
a51f263632 Fix refs appearing in dupes-only view
* Some refs appeared in the dupes-only view after a re-prioritization was done a second time.
* It seems the core.Results.__dupes list was not properly updated whenever core.app.Dupeguru.reprioritize_groups() -> core.Results.sort_dupes() was called.
When a re-prioritization is done, some refs became dupe, and some dupes became ref in their place. So we need to update the new state of the internal list of dupes kept by the Results object, instead of relying on the outdated cached one.
* Fix #757.
2021-06-22 22:57:57 +02:00
glubsy
718ca5b313 Remove unused import 2021-06-22 02:41:33 +02:00
glubsy
277bc3fbb8 Add unit tests for hash sample optimization
* Instead of keeping md5 samples separate, merge them as one hash computed from the various selected chunks we picked.
* We don't need to keep a boolean to see whether or not the user chose to optimize; we can simply compare the value of the threshold, since 0 means no optimization currently active.
2021-06-21 22:44:05 +02:00
glubsy
e07dfd5955 Add partial hashes optimization for big files
* Big files above the user selected threshold can be partially hashed in 3 places.
* If the user is willing to take the risk, we consider files with identical md5samples as being identical.
2021-06-21 19:03:21 +02:00
4641bd6ec9 Merge pull request #905 from glubsy/fix_863
Fix exception when deleting while in delta view
2021-06-19 20:29:47 -05:00
glubsy
a6f83ad3d7 Fix missing regexp after rename
* Doing a full match should be safer to avoid partial results which would result in overly aggressive filtering.
* Add new tests to test suite to cover this issue.
* Fixes #903.
2021-06-19 02:00:25 +02:00
glubsy
ab8750eedb Fix partial regex match yielding false positive 2021-06-17 03:49:59 +02:00
glubsy
22033211d6 Fix exception when deleting while in delta view 2021-05-31 23:49:21 +02:00
0b46ca2222 Merge pull request #879 from glubsy/fix_unicode
Fix stripping (japanese) unicode characters
2021-05-25 19:11:19 -05:00
72e0f76242 Merge pull request #898 from AlttiRi/master
Change reference background color #894
2021-05-25 19:10:31 -05:00
[Alt'tiRi]
65c1d463f8 Change reference background color #894 2021-05-22 02:52:41 +03:00
e6c791ab0a Merge pull request #884 from samusz/master
Small typo
2021-05-09 23:32:32 -05:00
Sacha Muszlak
78f5088101 Merge pull request #1 from samusz/samusz-patch-1
typo correction
2021-05-07 09:41:47 +02:00
Sacha Muszlak
095df5eb95 typo correction 2021-05-07 09:40:08 +02:00
glubsy
f1ae478433 Fix including character at the border 2021-04-29 05:29:35 +02:00
glubsy
c4dcfd3d4b Fix stripping (japanese) unicode characters
* Accents are getting removed from Unicode characters to generate similar "words".
* Non-latin characters which cannot be processed that way (eg. japanese, greek, russian, etc.) should not be filtered out at all otherwise files are erroneously skipped or detected as dupes if only some characters make it passed the filter.
* Starting from an arbitrary unicode codepoint (converted to decimal), above which we know it is pointless to try any sort of processing, we leave the characters as is.
* Fix #878.
2021-04-29 05:15:34 +02:00
0840104edf Merge pull request #873 from glubsy/fix_857
Fix 857
2021-04-20 20:05:05 -05:00
glubsy
6b4b436251 Fix crash on shutdown
* Fixes "'DetailsPanel' object has no attribute '_table'" error on shutdown if the Results table is updated (item removed) while the Details Dialog is shown as a floating window.
* It seems that QApplication.quit() triggers some sort of refresh on the floating QDockWidget, which in turn makes calls to the underlying model that is possibly being destroyed, ie. there might be a race condition here.
* Closing or hiding the QDockWidget before the cal to quit() is a workaround. Similarly, this is already done in the quitTriggered() method anyway.
* This fixes #857.
2021-04-16 17:54:49 +02:00
glubsy
d18b8c10ec Remove redundant assignment
The "app" field is already set in the parent class.
2021-04-15 18:03:00 +02:00
123 changed files with 3756 additions and 2349 deletions

74
.github/workflows/default.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
# Workflow lints, and checks format in parallel then runs tests on all platforms
name: Default CI/CD
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-extra.txt
- name: Lint with flake8
run: |
flake8 .
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-extra.txt
- name: Check format with black
run: |
black .
test:
needs: [lint, format]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9]
exclude:
- os: macos-latest
python-version: 3.6
- os: macos-latest
python-version: 3.7
- os: windows-latest
python-version: 3.6
- os: windows-latest
python-version: 3.7
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-extra.txt
- name: Build python modules
run: |
python build.py --modules
- name: Run tests
run: |
pytest core hscommon

View File

@@ -19,9 +19,9 @@ matrix:
python: "3.9" python: "3.9"
- os: "windows" - os: "windows"
language: shell language: shell
python: "3.8" python: "3.9"
env: "PATH=/c/python38:/c/python38/Scripts:$PATH" env: "PATH=/c/python39:/c/python39/Scripts:$PATH"
before_install: before_install:
- choco install python --version=3.8.6 - choco install python --version=3.9.6
- cp /c/python38/python.exe /c/python38/python3.exe - cp /c/python39/python.exe /c/python39/python3.exe
script: tox -e py38 script: tox -e py39

View File

@@ -1,21 +1,26 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
[dupeguru.core] [dupeguru-1.core]
file_filter = locale/<lang>/LC_MESSAGES/core.po file_filter = locale/<lang>/LC_MESSAGES/core.po
source_file = locale/core.pot source_file = locale/core.pot
source_lang = en source_lang = en
type = PO type = PO
[dupeguru.columns] [dupeguru-1.columns]
file_filter = locale/<lang>/LC_MESSAGES/columns.po file_filter = locale/<lang>/LC_MESSAGES/columns.po
source_file = locale/columns.pot source_file = locale/columns.pot
source_lang = en source_lang = en
type = PO type = PO
[dupeguru.ui] [dupeguru-1.ui]
file_filter = locale/<lang>/LC_MESSAGES/ui.po file_filter = locale/<lang>/LC_MESSAGES/ui.po
source_file = locale/ui.pot source_file = locale/ui.pot
source_lang = en source_lang = en
type = PO type = PO
[dupeguru-1.qtlib]
file_filter = qtlib/locale/<lang>/LC_MESSAGES/qtlib.po
source_file = qtlib/locale/qtlib.pot
source_lang = en
type = PO

View File

@@ -8,7 +8,6 @@ import os
import os.path as op import os.path as op
from optparse import OptionParser from optparse import OptionParser
import shutil import shutil
from pathlib import Path
from setuptools import setup, Extension from setuptools import setup, Extension
@@ -31,12 +30,8 @@ def parse_args():
dest="clean", dest="clean",
help="Clean build folder before building", help="Clean build folder before building",
) )
parser.add_option( parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file")
"--doc", action="store_true", dest="doc", help="Build only the help file" parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
)
parser.add_option(
"--loc", action="store_true", dest="loc", help="Build only localization"
)
parser.add_option( parser.add_option(
"--updatepot", "--updatepot",
action="store_true", action="store_true",
@@ -61,12 +56,6 @@ def parse_args():
dest="modules", dest="modules",
help="Build the python modules.", help="Build the python modules.",
) )
parser.add_option(
"--importpo",
action="store_true",
dest="importpo",
help="Import all PO files downloaded from transifex.",
)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
return options return options
@@ -103,9 +92,7 @@ def build_localizations():
locale_dest = op.join("build", "locale") locale_dest = op.join("build", "locale")
if op.exists(locale_dest): if op.exists(locale_dest):
shutil.rmtree(locale_dest) shutil.rmtree(locale_dest)
shutil.copytree( shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
"locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot")
)
def build_updatepot(): def build_updatepot():
@@ -136,33 +123,6 @@ def build_normpo():
# loc.normalize_all_pos(op.join("cocoalib", "locale")) # loc.normalize_all_pos(op.join("cocoalib", "locale"))
def build_importpo():
basePath = Path.cwd()
# expect a folder named transifex with all the .po files from the exports
translationsPath = basePath.joinpath("transifex")
# locations where the translation files go
qtlibPath = basePath.joinpath("qtlib", "locale")
localePath = basePath.joinpath("locale")
for translation in translationsPath.iterdir():
# transifex files are named resource_lang.po so split on first '_'
parts = translation.stem.split("_", 1)
resource = parts[0]
language = parts[1]
# make sure qtlib resources go to dedicated folder
if resource == "qtlib":
outputPath = qtlibPath
else:
outputPath = localePath
outputFolder = outputPath.joinpath(language, "LC_MESSAGES")
# create the language folder if it is new
if not outputFolder.exists():
outputFolder.mkdir(parents=True)
# copy the po file over
shutil.copy(translation, outputFolder.joinpath(resource + ".po"))
# normalize files after complete
build_normpo()
def build_pe_modules(): def build_pe_modules():
print("Building PE Modules") print("Building PE Modules")
exts = [ exts = [
@@ -199,9 +159,7 @@ def build_normal():
print("Building localizations") print("Building localizations")
build_localizations() build_localizations()
print("Building Qt stuff") print("Building Qt stuff")
print_and_do( print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py")))
"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")) fix_qt_resource_file(op.join("qt", "dg_rc.py"))
build_help() build_help()
@@ -225,8 +183,6 @@ def main():
build_normpo() build_normpo()
elif options.modules: elif options.modules:
build_pe_modules() build_pe_modules()
elif options.importpo:
build_importpo()
else: else:
build_normal() build_normal()

View File

@@ -132,9 +132,7 @@ class DupeGuru(Broadcaster):
logging.debug("Debug mode enabled") logging.debug("Debug mode enabled")
Broadcaster.__init__(self) Broadcaster.__init__(self)
self.view = view self.view = view
self.appdata = desktop.special_folder_path( self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
desktop.SpecialFolder.AppData, appname=self.NAME
)
if not op.exists(self.appdata): if not op.exists(self.appdata):
os.makedirs(self.appdata) os.makedirs(self.appdata)
self.app_mode = AppMode.Standard self.app_mode = AppMode.Standard
@@ -182,17 +180,13 @@ class DupeGuru(Broadcaster):
def _get_picture_cache_path(self): def _get_picture_cache_path(self):
cache_type = self.options["picture_cache_type"] cache_type = self.options["picture_cache_type"]
cache_name = ( cache_name = "cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
"cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
)
return op.join(self.appdata, cache_name) return op.join(self.appdata, cache_name)
def _get_dupe_sort_key(self, dupe, get_group, key, delta): def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode in (AppMode.Music, AppMode.Picture): if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == "folder_path": if key == "folder_path":
dupe_folder_path = getattr( dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
dupe, "display_folder_path", dupe.folder_path
)
return str(dupe_folder_path).lower() return str(dupe_folder_path).lower()
if self.app_mode == AppMode.Picture: if self.app_mode == AppMode.Picture:
if delta and key == "dimensions": if delta and key == "dimensions":
@@ -220,9 +214,7 @@ class DupeGuru(Broadcaster):
def _get_group_sort_key(self, group, key): def _get_group_sort_key(self, group, key):
if self.app_mode in (AppMode.Music, AppMode.Picture): if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == "folder_path": if key == "folder_path":
dupe_folder_path = getattr( dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
group.ref, "display_folder_path", group.ref.folder_path
)
return str(dupe_folder_path).lower() return str(dupe_folder_path).lower()
if key == "percentage": if key == "percentage":
return group.percentage return group.percentage
@@ -235,9 +227,7 @@ class DupeGuru(Broadcaster):
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion): def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
def op(dupe): def op(dupe):
j.add_progress() j.add_progress()
return self._do_delete_dupe( return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
dupe, link_deleted, use_hardlinks, direct_deletion
)
j.start_job(self.results.mark_count) j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, True) self.results.perform_on_marked(op, True)
@@ -277,11 +267,7 @@ class DupeGuru(Broadcaster):
return None return None
def _get_export_data(self): def _get_export_data(self):
columns = [ columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"]
col
for col in self.result_table.columns.ordered_columns
if col.visible and col.name != "marked"
]
colnames = [col.display for col in columns] colnames = [col.display for col in columns]
rows = [] rows = []
for group_id, group in enumerate(self.results.groups): for group_id, group in enumerate(self.results.groups):
@@ -293,11 +279,7 @@ class DupeGuru(Broadcaster):
return colnames, rows return colnames, rows
def _results_changed(self): def _results_changed(self):
self.selected_dupes = [ self.selected_dupes = [d for d in self.selected_dupes if self.results.get_group_of_duplicate(d) is not None]
d
for d in self.selected_dupes
if self.results.get_group_of_duplicate(d) is not None
]
self.notify("results_changed") self.notify("results_changed")
def _start_job(self, jobid, func, args=()): def _start_job(self, jobid, func, args=()):
@@ -332,9 +314,7 @@ class DupeGuru(Broadcaster):
msg = { msg = {
JobType.Copy: tr("All marked files were copied successfully."), JobType.Copy: tr("All marked files were copied successfully."),
JobType.Move: tr("All marked files were moved successfully."), JobType.Move: tr("All marked files were moved successfully."),
JobType.Delete: tr( JobType.Delete: tr("All marked files were successfully sent to Trash."),
"All marked files were successfully sent to Trash."
),
}[jobid] }[jobid]
self.view.show_message(msg) self.view.show_message(msg)
@@ -401,15 +381,12 @@ class DupeGuru(Broadcaster):
self.view.show_message(tr("'{}' does not exist.").format(d)) self.view.show_message(tr("'{}' does not exist.").format(d))
def add_selected_to_ignore_list(self): def add_selected_to_ignore_list(self):
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`. """Adds :attr:`selected_dupes` to :attr:`ignore_list`."""
"""
dupes = self.without_ref(self.selected_dupes) dupes = self.without_ref(self.selected_dupes)
if not dupes: if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES) self.view.show_message(MSG_NO_SELECTED_DUPES)
return return
msg = tr( msg = tr("All selected %d matches are going to be ignored in all subsequent scans. Continue?")
"All selected %d matches are going to be ignored in all subsequent scans. Continue?"
)
if not self.view.ask_yes_no(msg % len(dupes)): if not self.view.ask_yes_no(msg % len(dupes)):
return return
for dupe in dupes: for dupe in dupes:
@@ -483,16 +460,17 @@ class DupeGuru(Broadcaster):
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
destination = self.view.select_dest_folder( destination = self.view.select_dest_folder(
tr("Select a directory to copy marked files to") if copy tr("Select a directory to copy marked files to")
else tr("Select a directory to move marked files to")) if copy
else tr("Select a directory to move marked files to")
)
if destination: if destination:
desttype = self.options["copymove_dest_type"] desttype = self.options["copymove_dest_type"]
jobid = JobType.Copy if copy else JobType.Move jobid = JobType.Copy if copy else JobType.Move
self._start_job(jobid, do) self._start_job(jobid, do)
def delete_marked(self): def delete_marked(self):
"""Start an async job to send marked duplicates to the trash. """Start an async job to send marked duplicates to the trash."""
"""
if not self.results.mark_count: if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
@@ -523,9 +501,7 @@ class DupeGuru(Broadcaster):
The columns and their order in the resulting CSV file is determined in the same way as in The columns and their order in the resulting CSV file is determined in the same way as in
:meth:`export_to_xhtml`. :meth:`export_to_xhtml`.
""" """
dest_file = self.view.select_dest_file( dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), "csv")
tr("Select a destination for your exported CSV"), "csv"
)
if dest_file: if dest_file:
colnames, rows = self._get_export_data() colnames, rows = self._get_export_data()
try: try:
@@ -542,9 +518,7 @@ class DupeGuru(Broadcaster):
try: try:
return dupe.get_display_info(group, delta) return dupe.get_display_info(group, delta)
except Exception as e: except Exception as e:
logging.warning( logging.warning("Exception (type: %s) on GetDisplayInfo for %s: %s", type(e), str(dupe.path), str(e))
"Exception (type: %s) on GetDisplayInfo for %s: %s",
type(e), str(dupe.path), str(e))
return empty_data() return empty_data()
def invoke_custom_command(self): def invoke_custom_command(self):
@@ -556,9 +530,7 @@ class DupeGuru(Broadcaster):
""" """
cmd = self.view.get_default("CustomCommand") cmd = self.view.get_default("CustomCommand")
if not cmd: if not cmd:
msg = tr( msg = tr("You have no custom command set up. Set it up in your preferences.")
"You have no custom command set up. Set it up in your preferences."
)
self.view.show_message(msg) self.view.show_message(msg)
return return
if not self.selected_dupes: if not self.selected_dupes:
@@ -634,9 +606,7 @@ class DupeGuru(Broadcaster):
if not self.result_table.power_marker: if not self.result_table.power_marker:
if changed_groups: if changed_groups:
self.selected_dupes = [ self.selected_dupes = [
d d for d in self.selected_dupes if self.results.get_group_of_duplicate(d).ref is d
for d in self.selected_dupes
if self.results.get_group_of_duplicate(d).ref is d
] ]
self.notify("results_changed") self.notify("results_changed")
else: else:
@@ -648,20 +618,17 @@ class DupeGuru(Broadcaster):
self.notify("results_changed_but_keep_selection") self.notify("results_changed_but_keep_selection")
def mark_all(self): def mark_all(self):
"""Set all dupes in the results as marked. """Set all dupes in the results as marked."""
"""
self.results.mark_all() self.results.mark_all()
self.notify("marking_changed") self.notify("marking_changed")
def mark_none(self): def mark_none(self):
"""Set all dupes in the results as unmarked. """Set all dupes in the results as unmarked."""
"""
self.results.mark_none() self.results.mark_none()
self.notify("marking_changed") self.notify("marking_changed")
def mark_invert(self): def mark_invert(self):
"""Invert the marked state of all dupes in the results. """Invert the marked state of all dupes in the results."""
"""
self.results.mark_invert() self.results.mark_invert()
self.notify("marking_changed") self.notify("marking_changed")
@@ -679,8 +646,7 @@ class DupeGuru(Broadcaster):
self.notify("marking_changed") self.notify("marking_changed")
def open_selected(self): def open_selected(self):
"""Open :attr:`selected_dupes` with their associated application. """Open :attr:`selected_dupes` with their associated application."""
"""
if len(self.selected_dupes) > 10: if len(self.selected_dupes) > 10:
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN): if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
return return
@@ -688,8 +654,7 @@ class DupeGuru(Broadcaster):
desktop.open_path(dupe.path) desktop.open_path(dupe.path)
def purge_ignore_list(self): def purge_ignore_list(self):
"""Remove files that don't exist from :attr:`ignore_list`. """Remove files that don't exist from :attr:`ignore_list`."""
"""
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s)) self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
self.ignore_list_dialog.refresh() self.ignore_list_dialog.refresh()
@@ -719,8 +684,7 @@ class DupeGuru(Broadcaster):
self.notify("results_changed_but_keep_selection") self.notify("results_changed_but_keep_selection")
def remove_marked(self): def remove_marked(self):
"""Removed marked duplicates from the results (without touching the files themselves). """Removed marked duplicates from the results (without touching the files themselves)."""
"""
if not self.results.mark_count: if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES) self.view.show_message(MSG_NO_MARKED_DUPES)
return return
@@ -731,8 +695,7 @@ class DupeGuru(Broadcaster):
self._results_changed() self._results_changed()
def remove_selected(self): def remove_selected(self):
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves). """Removed :attr:`selected_dupes` from the results (without touching the files themselves)."""
"""
dupes = self.without_ref(self.selected_dupes) dupes = self.without_ref(self.selected_dupes)
if not dupes: if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES) self.view.show_message(MSG_NO_SELECTED_DUPES)
@@ -770,10 +733,10 @@ class DupeGuru(Broadcaster):
for group in self.results.groups: for group in self.results.groups:
if group.prioritize(key_func=sort_key): if group.prioritize(key_func=sort_key):
count += 1 count += 1
if count:
self.results.refresh_required = True
self._results_changed() self._results_changed()
msg = tr("{} duplicate groups were changed by the re-prioritization.").format( msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
count
)
self.view.show_message(msg) self.view.show_message(msg)
def reveal_selected(self): def reveal_selected(self):
@@ -817,9 +780,7 @@ class DupeGuru(Broadcaster):
""" """
scanner = self.SCANNER_CLASS() scanner = self.SCANNER_CLASS()
if not self.directories.has_any_file(): if not self.directories.has_any_file():
self.view.show_message( self.view.show_message(tr("The selected directories contain no scannable file."))
tr("The selected directories contain no scannable file.")
)
return return
# Send relevant options down to the scanner instance # Send relevant options down to the scanner instance
for k, v in self.options.items(): for k, v in self.options.items():
@@ -834,13 +795,9 @@ class DupeGuru(Broadcaster):
def do(j): def do(j):
j.set_progress(0, tr("Collecting files to scan")) j.set_progress(0, tr("Collecting files to scan"))
if scanner.scan_type == ScanType.Folders: if scanner.scan_type == ScanType.Folders:
files = list( files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
self.directories.get_folders(folderclass=se.fs.Folder, j=j)
)
else: else:
files = list( files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
self.directories.get_files(fileclasses=self.fileclasses, j=j)
)
if self.options["ignore_hardlink_matches"]: if self.options["ignore_hardlink_matches"]:
files = self._remove_hardlink_dupes(files) files = self._remove_hardlink_dupes(files)
logging.info("Scanning %d files" % len(files)) logging.info("Scanning %d files" % len(files))
@@ -862,13 +819,8 @@ class DupeGuru(Broadcaster):
self.notify("marking_changed") self.notify("marking_changed")
def without_ref(self, dupes): def without_ref(self, dupes):
"""Returns ``dupes`` with all reference elements removed. """Returns ``dupes`` with all reference elements removed."""
""" return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
return [
dupe
for dupe in dupes
if self.results.get_group_of_duplicate(dupe).ref is not dupe
]
def get_default(self, key, fallback_value=None): def get_default(self, key, fallback_value=None):
result = nonone(self.view.get_default(key), fallback_value) result = nonone(self.view.get_default(key), fallback_value)

View File

@@ -108,17 +108,7 @@ class Directories:
found_files = [] found_files = []
# print(f"len of files: {len(files)} {files}") # print(f"len of files: {len(files)} {files}")
for f in files: for f in files:
found = False if not self._exclude_list.is_excluded(root, f):
for expr in self._exclude_list.compiled_files:
if expr.match(f):
found = True
break
if not found:
for expr in self._exclude_list.compiled_paths:
if expr.match(root + os.sep + f):
found = True
break
if not found:
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses)) found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
found_files = [f for f in found_files if f is not None] found_files = [f for f in found_files if f is not None]
# In some cases, directories can be considered as files by dupeGuru, which is # In some cases, directories can be considered as files by dupeGuru, which is

View File

@@ -17,7 +17,11 @@ from hscommon.util import flatten, multi_replace
from hscommon.trans import tr from hscommon.trans import tr
from hscommon.jobprogress import job from hscommon.jobprogress import job
(WEIGHT_WORDS, MATCH_SIMILAR_WORDS, NO_FIELD_ORDER,) = range(3) (
WEIGHT_WORDS,
MATCH_SIMILAR_WORDS,
NO_FIELD_ORDER,
) = range(3)
JOB_REFRESH_RATE = 100 JOB_REFRESH_RATE = 100
@@ -26,8 +30,17 @@ def getwords(s):
# We decompose the string so that ascii letters with accents can be part of the word. # We decompose the string so that ascii letters with accents can be part of the word.
s = normalize("NFD", s) s = normalize("NFD", s)
s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", " ").lower() s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", " ").lower()
# logging.debug(f"DEBUG chars for: {s}\n"
# f"{[c for c in s if ord(c) != 32]}\n"
# f"{[ord(c) for c in s if ord(c) != 32]}")
# HACK We shouldn't ignore non-ascii characters altogether. Any Unicode char
# above common european characters that cannot be "sanitized" (ie. stripped
# of their accents, etc.) are preserved as is. The arbitrary limit is
# obtained from this one: ord("\u037e") GREEK QUESTION MARK
s = "".join( s = "".join(
c for c in s if c in string.ascii_letters + string.digits + string.whitespace c
for c in s
if (ord(c) <= 894 and c in string.ascii_letters + string.digits + string.whitespace) or ord(c) > 894
) )
return [_f for _f in s.split(" ") if _f] # remove empty elements return [_f for _f in s.split(" ") if _f] # remove empty elements
@@ -104,9 +117,7 @@ def compare_fields(first, second, flags=()):
if matched_field: if matched_field:
second.remove(matched_field) second.remove(matched_field)
else: else:
results = [ results = [compare(field1, field2, flags) for field1, field2 in zip(first, second)]
compare(field1, field2, flags) for field1, field2 in zip(first, second)
]
return min(results) if results else 0 return min(results) if results else 0
@@ -119,9 +130,7 @@ def build_word_dict(objects, j=job.nulljob):
The result will be a dict with words as keys, lists of objects as values. The result will be a dict with words as keys, lists of objects as values.
""" """
result = defaultdict(set) result = defaultdict(set)
for object in j.iter_with_progress( for object in j.iter_with_progress(objects, "Prepared %d/%d files", JOB_REFRESH_RATE):
objects, "Prepared %d/%d files", JOB_REFRESH_RATE
):
for word in unpack_fields(object.words): for word in unpack_fields(object.words):
result[word].add(object) result[word].add(object)
return result return result
@@ -156,9 +165,7 @@ def reduce_common_words(word_dict, threshold):
The exception to this removal are the objects where all the words of the object are common. The exception to this removal are the objects where all the words of the object are common.
Because if we remove them, we will miss some duplicates! Because if we remove them, we will miss some duplicates!
""" """
uncommon_words = set( uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold)
word for word, objects in word_dict.items() if len(objects) < threshold
)
for word, objects in list(word_dict.items()): for word, objects in list(word_dict.items()):
if len(objects) < threshold: if len(objects) < threshold:
continue continue
@@ -264,17 +271,16 @@ def getmatches(
# This is the place where the memory usage is at its peak during the scan. # This is the place where the memory usage is at its peak during the scan.
# Just continue the process with an incomplete list of matches. # Just continue the process with an incomplete list of matches.
del compared # This should give us enough room to call logging. del compared # This should give us enough room to call logging.
logging.warning( logging.warning("Memory Overflow. Matches: %d. Word dict: %d" % (len(result), len(word_dict)))
"Memory Overflow. Matches: %d. Word dict: %d"
% (len(result), len(word_dict))
)
return result return result
return result return result
def getmatches_by_contents(files, j=job.nulljob): def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
"""Returns a list of :class:`Match` within ``files`` if their contents is the same. """Returns a list of :class:`Match` within ``files`` if their contents is the same.
:param bigsize: The size in bytes over which we consider files big enough to
justify taking samples of md5. If 0, compute md5 as usual.
:param j: A :ref:`job progress instance <jobs>`. :param j: A :ref:`job progress instance <jobs>`.
""" """
size2files = defaultdict(set) size2files = defaultdict(set)
@@ -291,6 +297,10 @@ def getmatches_by_contents(files, j=job.nulljob):
if first.is_ref and second.is_ref: if first.is_ref and second.is_ref:
continue # Don't spend time comparing two ref pics together. continue # Don't spend time comparing two ref pics together.
if first.md5partial == second.md5partial: if first.md5partial == second.md5partial:
if bigsize > 0 and first.size > bigsize:
if first.md5samples == second.md5samples:
result.append(Match(first, second, 100))
else:
if first.md5 == second.md5: if first.md5 == second.md5:
result.append(Match(first, second, 100)) result.append(Match(first, second, 100))
j.add_progress(desc=tr("%d matches found") % len(result)) j.add_progress(desc=tr("%d matches found") % len(result))
@@ -391,18 +401,13 @@ class Group:
You can call this after the duplicate scanning process to free a bit of memory. You can call this after the duplicate scanning process to free a bit of memory.
""" """
discarded = set( discarded = set(m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second]))
m
for m in self.matches
if not all(obj in self.unordered for obj in [m.first, m.second])
)
self.matches -= discarded self.matches -= discarded
self.candidates = defaultdict(set) self.candidates = defaultdict(set)
return discarded return discarded
def get_match_of(self, item): def get_match_of(self, item):
"""Returns the match pair between ``item`` and :attr:`ref`. """Returns the match pair between ``item`` and :attr:`ref`."""
"""
if item is self.ref: if item is self.ref:
return return
for m in self._get_matches_for_ref(): for m in self._get_matches_for_ref():
@@ -418,8 +423,7 @@ class Group:
""" """
# tie_breaker(ref, dupe) --> True if dupe should be ref # tie_breaker(ref, dupe) --> True if dupe should be ref
# Returns True if anything changed during prioritization. # Returns True if anything changed during prioritization.
master_key_func = lambda x: (-x.is_ref, key_func(x)) new_order = sorted(self.ordered, key=lambda x: (-x.is_ref, key_func(x)))
new_order = sorted(self.ordered, key=master_key_func)
changed = new_order != self.ordered changed = new_order != self.ordered
self.ordered = new_order self.ordered = new_order
if tie_breaker is None: if tie_breaker is None:
@@ -442,9 +446,7 @@ class Group:
self.unordered.remove(item) self.unordered.remove(item)
self._percentage = None self._percentage = None
self._matches_for_ref = None self._matches_for_ref = None
if (len(self) > 1) and any( if (len(self) > 1) and any(not getattr(item, "is_ref", False) for item in self):
not getattr(item, "is_ref", False) for item in self
):
if discard_matches: if discard_matches:
self.matches = set(m for m in self.matches if item not in m) self.matches = set(m for m in self.matches if item not in m)
else: else:
@@ -453,8 +455,7 @@ class Group:
pass pass
def switch_ref(self, with_dupe): def switch_ref(self, with_dupe):
"""Make the :attr:`ref` dupe of the group switch position with ``with_dupe``. """Make the :attr:`ref` dupe of the group switch position with ``with_dupe``."""
"""
if self.ref.is_ref: if self.ref.is_ref:
return False return False
try: try:
@@ -473,9 +474,7 @@ class Group:
if self._percentage is None: if self._percentage is None:
if self.dupes: if self.dupes:
matches = self._get_matches_for_ref() matches = self._get_matches_for_ref()
self._percentage = sum(match.percentage for match in matches) // len( self._percentage = sum(match.percentage for match in matches) // len(matches)
matches
)
else: else:
self._percentage = 0 self._percentage = 0
return self._percentage return self._percentage
@@ -530,12 +529,8 @@ def get_groups(matches):
orphan_matches = [] orphan_matches = []
for group in groups: for group in groups:
orphan_matches += { orphan_matches += {
m m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second])
for m in group.discard_matches()
if not any(obj in matched_files for obj in [m.first, m.second])
} }
if groups and orphan_matches: if groups and orphan_matches:
groups += get_groups( groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
orphan_matches
) # no job, as it isn't supposed to take a long time
return groups return groups

View File

@@ -4,6 +4,7 @@
from .markable import Markable from .markable import Markable
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
# TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/ # TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/
# also https://pypi.org/project/re2/ # also https://pypi.org/project/re2/
# TODO update the Result list with newly added regexes if possible # TODO update the Result list with newly added regexes if possible
@@ -15,7 +16,8 @@ from hscommon.util import FileOrPath
from hscommon.plat import ISWINDOWS from hscommon.plat import ISWINDOWS
import time import time
default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP default_regexes = [
r"^thumbs\.db$", # Obsolete after WindowsXP
r"^desktop\.ini$", # Windows metadata r"^desktop\.ini$", # Windows metadata
r"^\.DS_Store$", # MacOS metadata r"^\.DS_Store$", # MacOS metadata
r"^\.Trash\-.*", # Linux trash directories r"^\.Trash\-.*", # Linux trash directories
@@ -34,6 +36,7 @@ def timer(func):
end = time.perf_counter_ns() end = time.perf_counter_ns()
print(f"DEBUG: func {func.__name__!r} took {end - start} ns.") print(f"DEBUG: func {func.__name__!r} took {end - start} ns.")
return value return value
return wrapper_timer return wrapper_timer
@@ -45,11 +48,13 @@ def memoize(func):
if args not in func.cache: if args not in func.cache:
func.cache[args] = func(*args) func.cache[args] = func(*args)
return func.cache[args] return func.cache[args]
return _memoize return _memoize
class AlreadyThereException(Exception): class AlreadyThereException(Exception):
"""Expression already in the list""" """Expression already in the list"""
def __init__(self, arg="Expression is already in excluded list."): def __init__(self, arg="Expression is already in excluded list."):
super().__init__(arg) super().__init__(arg)
@@ -81,7 +86,7 @@ class ExcludeList(Markable):
yield self.is_marked(regex), regex yield self.is_marked(regex), regex
def __contains__(self, item): def __contains__(self, item):
return self.isExcluded(item) return self.has_entry(item)
def __len__(self): def __len__(self):
"""Returns the total number of regexes regardless of mark status.""" """Returns the total number of regexes regardless of mark status."""
@@ -169,11 +174,11 @@ class ExcludeList(Markable):
def build_compiled_caches(self, union=False): def build_compiled_caches(self, union=False):
if not union: if not union:
self._cached_compiled_files =\ self._cached_compiled_files = [x for x in self._excluded_compiled if not has_sep(x.pattern)]
[x for x in self._excluded_compiled if not has_sep(x.pattern)] self._cached_compiled_paths = [x for x in self._excluded_compiled if has_sep(x.pattern)]
self._cached_compiled_paths =\ self._dirty = False
[x for x in self._excluded_compiled if has_sep(x.pattern)]
return return
marked_count = [x for marked, x in self if marked] marked_count = [x for marked, x in self if marked]
# If there is no item, the compiled Pattern will be '' and match everything! # If there is no item, the compiled Pattern will be '' and match everything!
if not marked_count: if not marked_count:
@@ -183,28 +188,25 @@ class ExcludeList(Markable):
else: else:
# HACK returned as a tuple to get a free iterator and keep interface # HACK returned as a tuple to get a free iterator and keep interface
# the same regardless of whether the client asked for union or not # the same regardless of whether the client asked for union or not
self._cached_compiled_union_all =\ self._cached_compiled_union_all = (re.compile("|".join(marked_count)),)
(re.compile('|'.join(marked_count)),)
files_marked = [x for x in marked_count if not has_sep(x)] files_marked = [x for x in marked_count if not has_sep(x)]
if not files_marked: if not files_marked:
self._cached_compiled_union_files = tuple() self._cached_compiled_union_files = tuple()
else: else:
self._cached_compiled_union_files =\ self._cached_compiled_union_files = (re.compile("|".join(files_marked)),)
(re.compile('|'.join(files_marked)),)
paths_marked = [x for x in marked_count if has_sep(x)] paths_marked = [x for x in marked_count if has_sep(x)]
if not paths_marked: if not paths_marked:
self._cached_compiled_union_paths = tuple() self._cached_compiled_union_paths = tuple()
else: else:
self._cached_compiled_union_paths =\ self._cached_compiled_union_paths = (re.compile("|".join(paths_marked)),)
(re.compile('|'.join(paths_marked)),) self._dirty = False
@property @property
def compiled(self): def compiled(self):
"""Should be used by other classes to retrieve the up-to-date list of patterns.""" """Should be used by other classes to retrieve the up-to-date list of patterns."""
if self._use_union: if self._use_union:
if self._dirty: if self._dirty:
self.build_compiled_caches(True) self.build_compiled_caches(self._use_union)
self._dirty = False
return self._cached_compiled_union_all return self._cached_compiled_union_all
return self._excluded_compiled return self._excluded_compiled
@@ -215,25 +217,21 @@ class ExcludeList(Markable):
The interface should be expected to be a generator, even if it returns only The interface should be expected to be a generator, even if it returns only
one item (one Pattern in the union case).""" one item (one Pattern in the union case)."""
if self._dirty: if self._dirty:
self.build_compiled_caches(True if self._use_union else False) self.build_compiled_caches(self._use_union)
self._dirty = False return self._cached_compiled_union_files if self._use_union else self._cached_compiled_files
return self._cached_compiled_union_files if self._use_union\
else self._cached_compiled_files
@property @property
def compiled_paths(self): def compiled_paths(self):
"""Returns patterns with only separators in them, for more precise filtering.""" """Returns patterns with only separators in them, for more precise filtering."""
if self._dirty: if self._dirty:
self.build_compiled_caches(True if self._use_union else False) self.build_compiled_caches(self._use_union)
self._dirty = False return self._cached_compiled_union_paths if self._use_union else self._cached_compiled_paths
return self._cached_compiled_union_paths if self._use_union\
else self._cached_compiled_paths
# ---Public # ---Public
def add(self, regex, forced=False): def add(self, regex, forced=False):
"""This interface should throw exceptions if there is an error during """This interface should throw exceptions if there is an error during
regex compilation""" regex compilation"""
if self.isExcluded(regex): if self.has_entry(regex):
# This exception should never be ignored # This exception should never be ignored
raise AlreadyThereException() raise AlreadyThereException()
if regex in forbidden_regexes: if regex in forbidden_regexes:
@@ -256,12 +254,27 @@ class ExcludeList(Markable):
"""Returns the number of marked regexes only.""" """Returns the number of marked regexes only."""
return len([x for marked, x in self if marked]) return len([x for marked, x in self if marked])
def isExcluded(self, regex): def has_entry(self, regex):
for item in self._excluded: for item in self._excluded:
if regex == item[0]: if regex == item[0]:
return True return True
return False return False
def is_excluded(self, dirname, filename):
"""Return True if the file or the absolute path to file is supposed to be
filtered out, False otherwise."""
matched = False
for expr in self.compiled_files:
if expr.fullmatch(filename):
matched = True
break
if not matched:
for expr in self.compiled_paths:
if expr.fullmatch(dirname + sep + filename):
matched = True
break
return matched
def remove(self, regex): def remove(self, regex):
for item in self._excluded: for item in self._excluded:
if item[0] == regex: if item[0] == regex:
@@ -280,13 +293,14 @@ class ExcludeList(Markable):
was_marked = self.is_marked(regex) was_marked = self.is_marked(regex)
is_compilable, exception, compiled = self.compile_re(newregex) is_compilable, exception, compiled = self.compile_re(newregex)
# We overwrite the found entry # We overwrite the found entry
self._excluded[self._excluded.index(item)] =\ self._excluded[self._excluded.index(item)] = [newregex, is_compilable, exception, compiled]
[newregex, is_compilable, exception, compiled]
self._remove_compiled(regex) self._remove_compiled(regex)
break break
if not found: if not found:
return return
if is_compilable and was_marked: if is_compilable:
self._add_compiled(newregex)
if was_marked:
# Not marked by default when added, add it back # Not marked by default when added, add it back
self.mark(newregex) self.mark(newregex)
@@ -300,7 +314,7 @@ class ExcludeList(Markable):
if regex not in default_regexes: if regex not in default_regexes:
self.unmark(regex) self.unmark(regex)
for default_regex in default_regexes: for default_regex in default_regexes:
if not self.isExcluded(default_regex): if not self.has_entry(default_regex):
self.add(default_regex) self.add(default_regex)
self.mark(default_regex) self.mark(default_regex)
@@ -326,8 +340,10 @@ class ExcludeList(Markable):
# "forced" avoids compilation exceptions and adds anyway # "forced" avoids compilation exceptions and adds anyway
self.add(regex_string, forced=True) self.add(regex_string, forced=True)
except AlreadyThereException: except AlreadyThereException:
logging.error(f"Regex \"{regex_string}\" \ logging.error(
loaded from XML was already present in the list.") f'Regex "{regex_string}" \
loaded from XML was already present in the list.'
)
continue continue
if exclude_item.get("marked") == "y": if exclude_item.get("marked") == "y":
marked.add(regex_string) marked.add(regex_string)
@@ -352,6 +368,7 @@ loaded from XML was already present in the list.")
class ExcludeDict(ExcludeList): class ExcludeDict(ExcludeList):
"""Exclusion list holding a set of regular expressions as keys, the compiled """Exclusion list holding a set of regular expressions as keys, the compiled
Pattern, compilation error and compilable boolean as values.""" Pattern, compilation error and compilable boolean as values."""
# Implemntation around a dictionary instead of a list, which implies # Implemntation around a dictionary instead of a list, which implies
# to keep the index of each string-key as its sub-element and keep it updated # to keep the index of each string-key as its sub-element and keep it updated
# whenever insert/remove is done. # whenever insert/remove is done.
@@ -399,9 +416,9 @@ class ExcludeDict(ExcludeList):
if self._use_union: if self._use_union:
return return
try: try:
self._excluded_compiled.add(self._excluded[regex]["compiled"]) self._excluded_compiled.add(self._excluded.get(regex).get("compiled"))
except Exception as e: except Exception as e:
logging.warning(f"Exception while adding regex {regex} to compiled set: {e}") logging.error(f"Exception while adding regex {regex} to compiled set: {e}")
return return
def is_compilable(self, regex): def is_compilable(self, regex):
@@ -418,14 +435,9 @@ class ExcludeDict(ExcludeList):
# and other indices should be pushed by one # and other indices should be pushed by one
for value in self._excluded.values(): for value in self._excluded.values():
value["index"] += 1 value["index"] += 1
self._excluded[regex] = { self._excluded[regex] = {"index": 0, "compilable": iscompilable, "error": exception, "compiled": compiled}
"index": 0,
"compilable": iscompilable,
"error": exception,
"compiled": compiled
}
def isExcluded(self, regex): def has_entry(self, regex):
if regex in self._excluded.keys(): if regex in self._excluded.keys():
return True return True
return False return False
@@ -451,13 +463,15 @@ class ExcludeDict(ExcludeList):
previous = self._excluded.pop(regex) previous = self._excluded.pop(regex)
iscompilable, error, compiled = self.compile_re(newregex) iscompilable, error, compiled = self.compile_re(newregex)
self._excluded[newregex] = { self._excluded[newregex] = {
"index": previous["index"], "index": previous.get("index"),
"compilable": iscompilable, "compilable": iscompilable,
"error": error, "error": error,
"compiled": compiled "compiled": compiled,
} }
self._remove_compiled(regex) self._remove_compiled(regex)
if was_marked and iscompilable: if iscompilable:
self._add_compiled(newregex)
if was_marked:
self.mark(newregex) self.mark(newregex)
def save_to_xml(self, outfile): def save_to_xml(self, outfile):
@@ -492,8 +506,12 @@ def ordered_keys(_dict):
if ISWINDOWS: if ISWINDOWS:
def has_sep(x):
return '\\' + sep in x def has_sep(regexp):
return "\\" + sep in regexp
else: else:
def has_sep(x):
return sep in x def has_sep(regexp):
return sep in regexp

View File

@@ -131,15 +131,11 @@ def export_to_xhtml(colnames, rows):
indented = "indented" indented = "indented"
filename = row[1] filename = row[1]
cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:]) cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:])
rendered_rows.append( rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells)
)
previous_group_id = row[0] previous_group_id = row[0]
rendered_rows = "".join(rendered_rows) rendered_rows = "".join(rendered_rows)
# The main template can't use format because the css code uses {} # The main template can't use format because the css code uses {}
content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace( content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace("$rows", rendered_rows)
"$rows", rendered_rows
)
folder = mkdtemp() folder = mkdtemp()
destpath = op.join(folder, "export.htm") destpath = op.join(folder, "export.htm")
fp = open(destpath, "wt", encoding="utf-8") fp = open(destpath, "wt", encoding="utf-8")

View File

@@ -12,6 +12,7 @@
# and I'm doing it now. # and I'm doing it now.
import hashlib import hashlib
from math import floor
import logging import logging
from hscommon.util import nonone, get_file_ext from hscommon.util import nonone, get_file_ext
@@ -30,6 +31,14 @@ __all__ = [
NOT_SET = object() NOT_SET = object()
# The goal here is to not run out of memory on really big files. However, the chunk
# size has to be large enough so that the python loop isn't too costly in terms of
# CPU.
CHUNK_SIZE = 1024 * 1024 # 1 MiB
# Minimum size below which partial hashes don't need to be computed
MIN_FILE_SIZE = 3 * CHUNK_SIZE # 3MiB, because we take 3 samples
class FSError(Exception): class FSError(Exception):
cls_message = "An error has occured on '{name}' in '{parent}'" cls_message = "An error has occured on '{name}' in '{parent}'"
@@ -70,15 +79,9 @@ class OperationError(FSError):
class File: class File:
"""Represents a file and holds metadata to be used for scanning. """Represents a file and holds metadata to be used for scanning."""
"""
INITIAL_INFO = { INITIAL_INFO = {"size": 0, "mtime": 0, "md5": b"", "md5partial": b"", "md5samples": b""}
"size": 0,
"mtime": 0,
"md5": "",
"md5partial": "",
}
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of # Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become # files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
# even greater when we take into account read attributes (70%!). Yeah, it's worth it. # even greater when we take into account read attributes (70%!). Yeah, it's worth it.
@@ -98,9 +101,7 @@ class File:
try: try:
self._read_info(attrname) self._read_info(attrname)
except Exception as e: except Exception as e:
logging.warning( logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path))
"An error '%s' was raised while decoding '%s'", e, repr(self.path)
)
result = object.__getattribute__(self, attrname) result = object.__getattribute__(self, attrname)
if result is NOT_SET: if result is NOT_SET:
result = self.INITIAL_INFO[attrname] result = self.INITIAL_INFO[attrname]
@@ -112,37 +113,62 @@ class File:
return (0x4000, 0x4000) # 16Kb return (0x4000, 0x4000) # 16Kb
def _read_info(self, field): def _read_info(self, field):
# print(f"_read_info({field}) for {self}")
if field in ("size", "mtime"): if field in ("size", "mtime"):
stats = self.path.stat() stats = self.path.stat()
self.size = nonone(stats.st_size, 0) self.size = nonone(stats.st_size, 0)
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field == "md5partial": elif field == "md5partial":
try: try:
fp = self.path.open("rb") with self.path.open("rb") as fp:
offset, size = self._get_md5partial_offset_and_size() offset, size = self._get_md5partial_offset_and_size()
fp.seek(offset) fp.seek(offset)
partialdata = fp.read(size) partialdata = fp.read(size)
md5 = hashlib.md5(partialdata) md5 = hashlib.md5(partialdata)
self.md5partial = md5.digest() self.md5partial = md5.digest()
fp.close()
except Exception: except Exception:
pass pass
elif field == "md5": elif field == "md5":
try: try:
fp = self.path.open("rb") with self.path.open("rb") as fp:
md5 = hashlib.md5() md5 = hashlib.md5()
# The goal here is to not run out of memory on really big files. However, the chunk
# size has to be large enough so that the python loop isn't too costly in terms of
# CPU.
CHUNK_SIZE = 1024 * 1024 # 1 mb
filedata = fp.read(CHUNK_SIZE) filedata = fp.read(CHUNK_SIZE)
while filedata: while filedata:
md5.update(filedata) md5.update(filedata)
filedata = fp.read(CHUNK_SIZE) filedata = fp.read(CHUNK_SIZE)
# FIXME For python 3.8 and later
# while filedata := fp.read(CHUNK_SIZE):
# md5.update(filedata)
self.md5 = md5.digest() self.md5 = md5.digest()
fp.close()
except Exception: except Exception:
pass pass
elif field == "md5samples":
try:
with self.path.open("rb") as fp:
size = self.size
# Might as well hash such small files entirely.
if size <= MIN_FILE_SIZE:
setattr(self, field, self.md5)
return
# Chunk at 25% of the file
fp.seek(floor(size * 25 / 100), 0)
filedata = fp.read(CHUNK_SIZE)
md5 = hashlib.md5(filedata)
# Chunk at 60% of the file
fp.seek(floor(size * 60 / 100), 0)
filedata = fp.read(CHUNK_SIZE)
md5.update(filedata)
# Last chunk of the file
fp.seek(-CHUNK_SIZE, 2)
filedata = fp.read(CHUNK_SIZE)
md5.update(filedata)
setattr(self, field, md5.digest())
except Exception as e:
logging.error(f"Error computing md5samples: {e}")
pass
def _read_all_info(self, attrnames=None): def _read_all_info(self, attrnames=None):
"""Cache all possible info. """Cache all possible info.
@@ -157,8 +183,7 @@ class File:
# --- Public # --- Public
@classmethod @classmethod
def can_handle(cls, path): def can_handle(cls, path):
"""Returns whether this file wrapper class can handle ``path``. """Returns whether this file wrapper class can handle ``path``."""
"""
return not path.islink() and path.isfile() return not path.islink() and path.isfile()
def rename(self, newname): def rename(self, newname):
@@ -176,8 +201,7 @@ class File:
self.path = destpath self.path = destpath
def get_display_info(self, group, delta): def get_display_info(self, group, delta):
"""Returns a display-ready dict of dupe's data. """Returns a display-ready dict of dupe's data."""
"""
raise NotImplementedError() raise NotImplementedError()
# --- Properties # --- Properties
@@ -197,7 +221,7 @@ class File:
class Folder(File): class Folder(File):
"""A wrapper around a folder path. """A wrapper around a folder path.
It has the size/md5 info of a File, but it's value are the sum of its subitems. It has the size/md5 info of a File, but its value is the sum of its subitems.
""" """
__slots__ = File.__slots__ + ("_subfolders",) __slots__ = File.__slots__ + ("_subfolders",)
@@ -212,15 +236,17 @@ class Folder(File):
return folders + files return folders + files
def _read_info(self, field): def _read_info(self, field):
# print(f"_read_info({field}) for Folder {self}")
if field in {"size", "mtime"}: if field in {"size", "mtime"}:
size = sum((f.size for f in self._all_items()), 0) size = sum((f.size for f in self._all_items()), 0)
self.size = size self.size = size
stats = self.path.stat() stats = self.path.stat()
self.mtime = nonone(stats.st_mtime, 0) self.mtime = nonone(stats.st_mtime, 0)
elif field in {"md5", "md5partial"}: elif field in {"md5", "md5partial", "md5samples"}:
# What's sensitive here is that we must make sure that subfiles' # What's sensitive here is that we must make sure that subfiles'
# md5 are always added up in the same order, but we also want a # md5 are always added up in the same order, but we also want a
# different md5 if a file gets moved in a different subdirectory. # different md5 if a file gets moved in a different subdirectory.
def get_dir_md5_concat(): def get_dir_md5_concat():
items = self._all_items() items = self._all_items()
items.sort(key=lambda f: f.path) items.sort(key=lambda f: f.path)
@@ -234,9 +260,7 @@ class Folder(File):
@property @property
def subfolders(self): def subfolders(self):
if self._subfolders is None: if self._subfolders is None:
subfolders = [ subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
p for p in self.path.listdir() if not p.islink() and p.isdir()
]
self._subfolders = [self.__class__(p) for p in subfolders] self._subfolders = [self.__class__(p) for p in subfolders]
return self._subfolders return self._subfolders

View File

@@ -29,8 +29,7 @@ class DeletionOptionsView:
""" """
def update_msg(self, msg: str): def update_msg(self, msg: str):
"""Update the dialog's prompt with ``str``. """Update the dialog's prompt with ``str``."""
"""
def show(self): def show(self):
"""Show the dialog in a modal fashion. """Show the dialog in a modal fashion.
@@ -39,8 +38,7 @@ class DeletionOptionsView:
""" """
def set_hardlink_option_enabled(self, is_enabled: bool): def set_hardlink_option_enabled(self, is_enabled: bool):
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`. """Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`."""
"""
class DeletionOptions(GUIObject): class DeletionOptions(GUIObject):
@@ -75,8 +73,7 @@ class DeletionOptions(GUIObject):
return self.view.show() return self.view.show()
def supports_links(self): def supports_links(self):
"""Returns whether our platform supports symlinks. """Returns whether our platform supports symlinks."""
"""
# When on a platform that doesn't implement it, calling os.symlink() (with the wrong number # When on a platform that doesn't implement it, calling os.symlink() (with the wrong number
# of arguments) raises NotImplementedError, which allows us to gracefully check for the # of arguments) raises NotImplementedError, which allows us to gracefully check for the
# feature. # feature.

View File

@@ -32,9 +32,7 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
# we don't want the two sides of the table to display the stats for the same file # we don't want the two sides of the table to display the stats for the same file
ref = group.ref if group is not None and group.ref is not dupe else None ref = group.ref if group is not None and group.ref is not dupe else None
data2 = self.app.get_display_info(ref, group, False) data2 = self.app.get_display_info(ref, group, False)
columns = self.app.result_table.COLUMNS[ columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column
1:
] # first column is the 'marked' column
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns] self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
# --- Public # --- Public

View File

@@ -36,9 +36,7 @@ class DirectoryNode(Node):
self._loaded = True self._loaded = True
def update_all_states(self): def update_all_states(self):
self._state = STATE_ORDER.index( self._state = STATE_ORDER.index(self._tree.app.directories.get_state(self._directory_path))
self._tree.app.directories.get_state(self._directory_path)
)
for node in self: for node in self:
node.update_all_states() node.update_all_states()

View File

@@ -7,6 +7,8 @@
# from hscommon.trans import tr # from hscommon.trans import tr
from .exclude_list_table import ExcludeListTable from .exclude_list_table import ExcludeListTable
from core.exclude import has_sep
from os import sep
import logging import logging
@@ -30,9 +32,10 @@ class ExcludeListDialogCore:
self.refresh() self.refresh()
def rename_selected(self, newregex): def rename_selected(self, newregex):
"""Renames the selected regex to ``newregex``. """Rename the selected regex to ``newregex``.
If there's more than one selected row, the first one is used. If there is more than one selected row, the first one is used.
:param str newregex: The regex to rename the row's regex to. :param str newregex: The regex to rename the row's regex to.
:return bool: true if success, false if error.
""" """
try: try:
r = self.exclude_list_table.selected_rows[0] r = self.exclude_list_table.selected_rows[0]
@@ -52,17 +55,37 @@ class ExcludeListDialogCore:
self.exclude_list_table.add(regex) self.exclude_list_table.add(regex)
def test_string(self, test_string): def test_string(self, test_string):
"""Sets property on row to highlight if its regex matches test_string supplied.""" """Set the highlight property on each row when its regex matches the
test_string supplied. Return True if any row matched."""
matched = False matched = False
for row in self.exclude_list_table.rows: for row in self.exclude_list_table.rows:
compiled_regex = self.exclude_list.get_compiled(row.regex) compiled_regex = self.exclude_list.get_compiled(row.regex)
if compiled_regex and compiled_regex.match(test_string):
matched = True if self.is_match(test_string, compiled_regex):
row.highlight = True row.highlight = True
matched = True
else: else:
row.highlight = False row.highlight = False
return matched return matched
def is_match(self, test_string, compiled_regex):
# This method is like an inverted version of ExcludeList.is_excluded()
if not compiled_regex:
return False
matched = False
# Test only the filename portion of the path
if not has_sep(compiled_regex.pattern) and sep in test_string:
filename = test_string.rsplit(sep, 1)[1]
if compiled_regex.fullmatch(filename):
matched = True
return matched
# Test the entire path + filename
if compiled_regex.fullmatch(test_string):
matched = True
return matched
def reset_rows_highlight(self): def reset_rows_highlight(self):
for row in self.exclude_list_table.rows: for row in self.exclude_list_table.rows:
row.highlight = False row.highlight = False

View File

@@ -6,14 +6,12 @@ from .base import DupeGuruGUIObject
from hscommon.gui.table import GUITable, Row from hscommon.gui.table import GUITable, Row
from hscommon.gui.column import Column, Columns from hscommon.gui.column import Column, Columns
from hscommon.trans import trget from hscommon.trans import trget
tr = trget("ui") tr = trget("ui")
class ExcludeListTable(GUITable, DupeGuruGUIObject): class ExcludeListTable(GUITable, DupeGuruGUIObject):
COLUMNS = [ COLUMNS = [Column("marked", ""), Column("regex", tr("Regular Expressions"))]
Column("marked", ""),
Column("regex", tr("Regular Expressions"))
]
def __init__(self, exclude_list_dialog, app): def __init__(self, exclude_list_dialog, app):
GUITable.__init__(self) GUITable.__init__(self)
@@ -36,7 +34,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0 return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0
def _do_delete(self): def _do_delete(self):
self.dalog.exclude_list.remove(self.selected_row.regex) self.dialog.exclude_list.remove(self.selected_row.regex)
# --- Override # --- Override
def add(self, regex): def add(self, regex):

View File

@@ -22,9 +22,7 @@ class IgnoreListDialog:
def clear(self): def clear(self):
if not self.ignore_list: if not self.ignore_list:
return return
msg = tr( msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
"Do you really want to remove all %d items from the ignore list?"
) % len(self.ignore_list)
if self.app.view.ask_yes_no(msg): if self.app.view.ask_yes_no(msg):
self.ignore_list.Clear() self.ignore_list.Clear()
self.refresh() self.refresh()

View File

@@ -41,11 +41,11 @@ class DupeRow(Row):
# table.DELTA_COLUMNS are always "delta" # table.DELTA_COLUMNS are always "delta"
self._delta_columns = self.table.DELTA_COLUMNS.copy() self._delta_columns = self.table.DELTA_COLUMNS.copy()
dupe_info = self.data dupe_info = self.data
if self._group.ref is None:
return False
ref_info = self._group.ref.get_display_info(group=self._group, delta=False) ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
for key, value in dupe_info.items(): for key, value in dupe_info.items():
if (key not in self._delta_columns) and ( if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()):
ref_info[key].lower() != value.lower()
):
self._delta_columns.add(key) self._delta_columns.add(key)
return column_name in self._delta_columns return column_name in self._delta_columns

View File

@@ -33,8 +33,7 @@ CacheRow = namedtuple("CacheRow", "id path blocks mtime")
class ShelveCache: class ShelveCache:
"""A class to cache picture blocks in a shelve backend. """A class to cache picture blocks in a shelve backend."""
"""
def __init__(self, db=None, readonly=False): def __init__(self, db=None, readonly=False):
self.istmp = db is None self.istmp = db is None
@@ -81,9 +80,7 @@ class ShelveCache:
self.shelve[wrap_id(rowid)] = wrap_path(path_str) self.shelve[wrap_id(rowid)] = wrap_path(path_str)
def _compute_maxid(self): def _compute_maxid(self):
return max( return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1)
(unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1
)
def _get_new_id(self): def _get_new_id(self):
self.maxid += 1 self.maxid += 1

View File

@@ -13,8 +13,7 @@ from .cache import string_to_colors, colors_to_string
class SqliteCache: class SqliteCache:
"""A class to cache picture blocks in a sqlite backend. """A class to cache picture blocks in a sqlite backend."""
"""
def __init__(self, db=":memory:", readonly=False): def __init__(self, db=":memory:", readonly=False):
# readonly is not used in the sqlite version of the cache # readonly is not used in the sqlite version of the cache
@@ -71,18 +70,14 @@ class SqliteCache:
except sqlite.OperationalError: except sqlite.OperationalError:
logging.warning("Picture cache could not set value for key %r", path_str) logging.warning("Picture cache could not set value for key %r", path_str)
except sqlite.DatabaseError as e: except sqlite.DatabaseError as e:
logging.warning( logging.warning("DatabaseError while setting value for key %r: %s", path_str, str(e))
"DatabaseError while setting value for key %r: %s", path_str, str(e)
)
def _create_con(self, second_try=False): def _create_con(self, second_try=False):
def create_tables(): def create_tables():
logging.debug("Creating picture cache tables.") logging.debug("Creating picture cache tables.")
self.con.execute("drop table if exists pictures") self.con.execute("drop table if exists pictures")
self.con.execute("drop index if exists idx_path") self.con.execute("drop index if exists idx_path")
self.con.execute( self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
"create table pictures(path TEXT, mtime INTEGER, blocks TEXT)"
)
self.con.execute("create index idx_path on pictures (path)") self.con.execute("create index idx_path on pictures (path)")
self.con = sqlite.connect(self.dbname, isolation_level=None) self.con = sqlite.connect(self.dbname, isolation_level=None)
@@ -93,9 +88,7 @@ class SqliteCache:
except sqlite.DatabaseError as e: # corrupted db except sqlite.DatabaseError as e: # corrupted db
if second_try: if second_try:
raise # Something really strange is happening raise # Something really strange is happening
logging.warning( logging.warning("Could not create picture cache because of an error: %s", str(e))
"Could not create picture cache because of an error: %s", str(e)
)
self.con.close() self.con.close()
os.remove(self.dbname) os.remove(self.dbname)
self._create_con(second_try=True) self._create_con(second_try=True)
@@ -125,9 +118,7 @@ class SqliteCache:
raise ValueError(path) raise ValueError(path)
def get_multiple(self, rowids): def get_multiple(self, rowids):
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join( sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(map(str, rowids))
map(str, rowids)
)
cur = self.con.execute(sql) cur = self.con.execute(sql)
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
@@ -148,7 +139,5 @@ class SqliteCache:
continue continue
todelete.append(rowid) todelete.append(rowid)
if todelete: if todelete:
sql = "delete from pictures where rowid in (%s)" % ",".join( sql = "delete from pictures where rowid in (%s)" % ",".join(map(str, todelete))
map(str, todelete)
)
self.con.execute(sql) self.con.execute(sql)

View File

@@ -256,9 +256,7 @@ class TIFF_file:
for j in range(count): for j in range(count):
if type in {5, 10}: if type in {5, 10}:
# The type is either 5 or 10 # The type is either 5 or 10
value_j = Fraction( value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed)
)
else: else:
# Not a fraction # Not a fraction
value_j = self.s2n(offset, typelen, signed) value_j = self.s2n(offset, typelen, signed)
@@ -296,9 +294,7 @@ def get_fields(fp):
logging.debug("Exif header length: %d bytes", length) logging.debug("Exif header length: %d bytes", length)
data = fp.read(length - 8) data = fp.read(length - 8)
data_format = data[0] data_format = data[0]
logging.debug( logging.debug("%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format])
"%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format]
)
T = TIFF_file(data) T = TIFF_file(data)
# There may be more than one IFD per file, but we only read the first one because others are # There may be more than one IFD per file, but we only read the first one because others are
# most likely thumbnails. # most likely thumbnails.

View File

@@ -95,9 +95,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
picture.unicode_path, picture.unicode_path,
picture.size, picture.size,
) )
if ( if picture.size < 10 * 1024 * 1024: # We're really running out of memory
picture.size < 10 * 1024 * 1024
): # We're really running out of memory
raise raise
except MemoryError: except MemoryError:
logging.warning("Ran out of memory while preparing pictures") logging.warning("Ran out of memory while preparing pictures")
@@ -106,9 +104,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
def get_chunks(pictures): def get_chunks(pictures):
min_chunk_count = ( min_chunk_count = multiprocessing.cpu_count() * 2 # have enough chunks to feed all subprocesses
multiprocessing.cpu_count() * 2
) # have enough chunks to feed all subprocesses
chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE
chunk_count = max(min_chunk_count, chunk_count) chunk_count = max(min_chunk_count, chunk_count)
chunk_size = (len(pictures) // chunk_count) + 1 chunk_size = (len(pictures) // chunk_count) + 1
@@ -185,9 +181,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
j.set_progress(comparison_count, progress_msg) j.set_progress(comparison_count, progress_msg)
j = j.start_subjob([3, 7]) j = j.start_subjob([3, 7])
pictures = prepare_pictures( pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
pictures, cache_path, with_dimensions=not match_scaled, j=j
)
j = j.start_subjob([9, 1], tr("Preparing for matching")) j = j.start_subjob([9, 1], tr("Preparing for matching"))
cache = get_cache(cache_path) cache = get_cache(cache_path)
id2picture = {} id2picture = {}
@@ -231,12 +225,8 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
chunks, chunks,
pictures, pictures,
) # some wiggle room for the next statements ) # some wiggle room for the next statements
logging.warning( logging.warning("Ran out of memory when scanning! We had %d matches.", len(matches))
"Ran out of memory when scanning! We had %d matches.", len(matches) del matches[-len(matches) // 3 :] # some wiggle room to ensure we don't run out of memory again.
)
del matches[
-len(matches) // 3 :
] # some wiggle room to ensure we don't run out of memory again.
pool.close() pool.close()
result = [] result = []
myiter = j.iter_with_progress( myiter = j.iter_with_progress(

View File

@@ -52,6 +52,7 @@ class Results(Markable):
self.app = app self.app = app
self.problems = [] # (dupe, error_msg) self.problems = [] # (dupe, error_msg)
self.is_modified = False self.is_modified = False
self.refresh_required = False
def _did_mark(self, dupe): def _did_mark(self, dupe):
self.__marked_size += dupe.size self.__marked_size += dupe.size
@@ -94,8 +95,9 @@ class Results(Markable):
# ---Private # ---Private
def __get_dupe_list(self): def __get_dupe_list(self):
if self.__dupes is None: if self.__dupes is None or self.refresh_required:
self.__dupes = flatten(group.dupes for group in self.groups) self.__dupes = flatten(group.dupes for group in self.groups)
self.refresh_required = False
if None in self.__dupes: if None in self.__dupes:
# This is debug logging to try to figure out #44 # This is debug logging to try to figure out #44
logging.warning( logging.warning(
@@ -104,9 +106,7 @@ class Results(Markable):
self.groups, self.groups,
) )
if self.__filtered_dupes: if self.__filtered_dupes:
self.__dupes = [ self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
dupe for dupe in self.__dupes if dupe in self.__filtered_dupes
]
sd = self.__dupes_sort_descriptor sd = self.__dupes_sort_descriptor
if sd: if sd:
self.sort_dupes(sd[0], sd[1], sd[2]) self.sort_dupes(sd[0], sd[1], sd[2])
@@ -125,18 +125,10 @@ class Results(Markable):
total_count = self.__total_count total_count = self.__total_count
total_size = self.__total_size total_size = self.__total_size
else: else:
mark_count = len( mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)])
[dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)] marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe))
) total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)])
marked_size = sum( total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe))
dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe)
)
total_count = len(
[dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)]
)
total_size = sum(
dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe)
)
if self.mark_inverted: if self.mark_inverted:
marked_size = self.__total_size - marked_size marked_size = self.__total_size - marked_size
result = tr("%d / %d (%s / %s) duplicates marked.") % ( result = tr("%d / %d (%s / %s) duplicates marked.") % (
@@ -199,11 +191,7 @@ class Results(Markable):
self.__filters.append(filter_str) self.__filters.append(filter_str)
if self.__filtered_dupes is None: if self.__filtered_dupes is None:
self.__filtered_dupes = flatten(g[:] for g in self.groups) self.__filtered_dupes = flatten(g[:] for g in self.groups)
self.__filtered_dupes = set( self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path)))
dupe
for dupe in self.__filtered_dupes
if filter_re.search(str(dupe.path))
)
filtered_groups = set() filtered_groups = set()
for dupe in self.__filtered_dupes: for dupe in self.__filtered_dupes:
filtered_groups.add(self.get_group_of_duplicate(dupe)) filtered_groups.add(self.get_group_of_duplicate(dupe))
@@ -215,8 +203,7 @@ class Results(Markable):
self.__dupes = None self.__dupes = None
def get_group_of_duplicate(self, dupe): def get_group_of_duplicate(self, dupe):
"""Returns :class:`~core.engine.Group` in which ``dupe`` belongs. """Returns :class:`~core.engine.Group` in which ``dupe`` belongs."""
"""
try: try:
return self.__group_of_duplicate[dupe] return self.__group_of_duplicate[dupe]
except (TypeError, KeyError): except (TypeError, KeyError):
@@ -282,8 +269,7 @@ class Results(Markable):
self.is_modified = False self.is_modified = False
def make_ref(self, dupe): def make_ref(self, dupe):
"""Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group. """Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group."""
"""
g = self.get_group_of_duplicate(dupe) g = self.get_group_of_duplicate(dupe)
r = g.ref r = g.ref
if not g.switch_ref(dupe): if not g.switch_ref(dupe):
@@ -410,10 +396,10 @@ class Results(Markable):
""" """
if not self.__dupes: if not self.__dupes:
self.__get_dupe_list() self.__get_dupe_list()
keyfunc = lambda d: self.app._get_dupe_sort_key( self.__dupes.sort(
d, lambda: self.get_group_of_duplicate(d), key, delta key=lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta),
reverse=not asc,
) )
self.__dupes.sort(key=keyfunc, reverse=not asc)
self.__dupes_sort_descriptor = (key, asc, delta) self.__dupes_sort_descriptor = (key, asc, delta)
def sort_groups(self, key, asc=True): def sort_groups(self, key, asc=True):
@@ -424,8 +410,7 @@ class Results(Markable):
:param str key: key attribute name to sort with. :param str key: key attribute name to sort with.
:param bool asc: If false, sorting is reversed. :param bool asc: If false, sorting is reversed.
""" """
keyfunc = lambda g: self.app._get_group_sort_key(g, key) self.groups.sort(key=lambda g: self.app._get_group_sort_key(g, key), reverse=not asc)
self.groups.sort(key=keyfunc, reverse=not asc)
self.__groups_sort_descriptor = (key, asc) self.__groups_sort_descriptor = (key, asc)
# ---Properties # ---Properties

View File

@@ -87,7 +87,7 @@ class Scanner:
if self.size_threshold: if self.size_threshold:
files = [f for f in files if f.size >= self.size_threshold] files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in {ScanType.Contents, ScanType.Folders}: if self.scan_type in {ScanType.Contents, ScanType.Folders}:
return engine.getmatches_by_contents(files, j=j) return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
else: else:
j = j.start_subjob([2, 8]) j = j.start_subjob([2, 8])
kw = {} kw = {}
@@ -161,27 +161,13 @@ class Scanner:
toremove.add(p) toremove.add(p)
else: else:
last_parent_path = p last_parent_path = p
matches = [ matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
m
for m in matches
if m.first.path not in toremove or m.second.path not in toremove
]
if not self.mix_file_kind: if not self.mix_file_kind:
matches = [ matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
m matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
for m in matches
if get_file_ext(m.first.name) == get_file_ext(m.second.name)
]
matches = [
m for m in matches if m.first.path.exists() and m.second.path.exists()
]
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)] matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
if ignore_list: if ignore_list:
matches = [ matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
m
for m in matches
if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
]
logging.info("Grouping matches") logging.info("Grouping matches")
groups = engine.get_groups(matches) groups = engine.get_groups(matches)
if self.scan_type in { if self.scan_type in {
@@ -190,9 +176,7 @@ class Scanner:
ScanType.FieldsNoOrder, ScanType.FieldsNoOrder,
ScanType.Tag, ScanType.Tag,
}: }:
matched_files = dedupe( matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
[m.first for m in matches] + [m.second for m in matches]
)
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups) self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
else: else:
# Ticket #195 # Ticket #195
@@ -218,4 +202,5 @@ class Scanner:
scan_type = ScanType.Filename scan_type = ScanType.Filename
scanned_tags = {"artist", "title"} scanned_tags = {"artist", "title"}
size_threshold = 0 size_threshold = 0
big_file_size_threshold = 0
word_weighting = False word_weighting = False

View File

@@ -29,9 +29,7 @@ def add_fake_files_to_directories(directories, files):
class TestCaseDupeGuru: class TestCaseDupeGuru:
def test_apply_filter_calls_results_apply_filter(self, monkeypatch): def test_apply_filter_calls_results_apply_filter(self, monkeypatch):
dgapp = TestApp().app dgapp = TestApp().app
monkeypatch.setattr( monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
)
dgapp.apply_filter("foo") dgapp.apply_filter("foo")
eq_(2, len(dgapp.results.apply_filter.calls)) eq_(2, len(dgapp.results.apply_filter.calls))
call = dgapp.results.apply_filter.calls[0] call = dgapp.results.apply_filter.calls[0]
@@ -41,15 +39,11 @@ class TestCaseDupeGuru:
def test_apply_filter_escapes_regexp(self, monkeypatch): def test_apply_filter_escapes_regexp(self, monkeypatch):
dgapp = TestApp().app dgapp = TestApp().app
monkeypatch.setattr( monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
)
dgapp.apply_filter("()[]\\.|+?^abc") dgapp.apply_filter("()[]\\.|+?^abc")
call = dgapp.results.apply_filter.calls[1] call = dgapp.results.apply_filter.calls[1]
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"]) eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
dgapp.apply_filter( dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard
"(*)"
) # In "simple mode", we want the * to behave as a wilcard
call = dgapp.results.apply_filter.calls[3] call = dgapp.results.apply_filter.calls[3]
eq_(r"\(.*\)", call["filter_str"]) eq_(r"\(.*\)", call["filter_str"])
dgapp.options["escape_filter_regexp"] = False dgapp.options["escape_filter_regexp"] = False
@@ -70,9 +64,7 @@ class TestCaseDupeGuru:
) )
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher. # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy) monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy)
monkeypatch.setattr( monkeypatch.setattr(os, "makedirs", lambda path: None) # We don't want the test to create that fake directory
os, "makedirs", lambda path: None
) # We don't want the test to create that fake directory
dgapp = TestApp().app dgapp = TestApp().app
dgapp.directories.add_path(p) dgapp.directories.add_path(p)
[f] = dgapp.directories.get_files() [f] = dgapp.directories.get_files()
@@ -320,9 +312,7 @@ class TestCaseDupeGuruWithResults:
assert groups[0].ref is objects[1] assert groups[0].ref is objects[1]
assert groups[1].ref is objects[4] assert groups[1].ref is objects[4]
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group( def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
self, do_setup
):
app = self.app app = self.app
objects = self.objects objects = self.objects
groups = self.groups groups = self.groups
@@ -404,9 +394,7 @@ class TestCaseDupeGuruWithResults:
# results table. # results table.
app = self.app app = self.app
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
add_fake_files_to_directories( add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
app.directories, self.objects
) # We want the scan to at least start
app.start_scanning() # will be cancelled immediately app.start_scanning() # will be cancelled immediately
eq_(len(app.result_table), 0) eq_(len(app.result_table), 0)

View File

@@ -88,6 +88,7 @@ class NamedObject:
self.size = size self.size = size
self.md5partial = name self.md5partial = name
self.md5 = name self.md5 = name
self.md5samples = name
if with_words: if with_words:
self.words = getwords(name) self.words = getwords(name)
self.is_ref = False self.is_ref = False
@@ -139,9 +140,7 @@ def GetTestGroups():
matches = engine.getmatches(objects) # we should have 5 matches matches = engine.getmatches(objects) # we should have 5 matches
groups = engine.get_groups(matches) # We should have 2 groups groups = engine.get_groups(matches) # We should have 2 groups
for g in groups: for g in groups:
g.prioritize( g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
lambda x: objects.index(x)
) # We want the dupes to be in the same order as the list is
groups.sort(key=len, reverse=True) # We want the group with 3 members to be first. groups.sort(key=len, reverse=True) # We want the group with 3 members to be first.
return (objects, matches, groups) return (objects, matches, groups)

View File

@@ -14,9 +14,7 @@ except ImportError:
skip("Can't import the block module, probably hasn't been compiled.") skip("Can't import the block module, probably hasn't been compiled.")
def my_avgdiff( def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call
first, second, limit=768, min_iter=3
): # this is so I don't have to re-write every call
return avgdiff(first, second, limit, min_iter) return avgdiff(first, second, limit, min_iter)

View File

@@ -254,7 +254,12 @@ def test_invalid_path():
def test_set_state_on_invalid_path(): def test_set_state_on_invalid_path():
d = Directories() d = Directories()
try: try:
d.set_state(Path("foobar",), DirectoryState.Normal) d.set_state(
Path(
"foobar",
),
DirectoryState.Normal,
)
except LookupError: except LookupError:
assert False assert False
@@ -345,15 +350,17 @@ def test_default_path_state_override(tmpdir):
eq_(len(list(d.get_files())), 2) eq_(len(list(d.get_files())), 2)
class TestExcludeList(): class TestExcludeList:
def setup_method(self, method): def setup_method(self, method):
self.d = Directories(exclude_list=ExcludeList(union_regex=False)) self.d = Directories(exclude_list=ExcludeList(union_regex=False))
def get_files_and_expect_num_result(self, num_result): def get_files_and_expect_num_result(self, num_result):
"""Calls get_files(), get the filenames only, print for debugging. """Calls get_files(), get the filenames only, print for debugging.
num_result is how many files are expected as a result.""" num_result is how many files are expected as a result."""
print(f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \ print(
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}") f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}"
)
files = list(self.d.get_files()) files = list(self.d.get_files())
files = [file.name for file in files] files = [file.name for file in files]
print(f"FINAL FILES {files}") print(f"FINAL FILES {files}")
@@ -473,6 +480,29 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
assert "file_ending_with_subdir" not in files assert "file_ending_with_subdir" not in files
assert "file_which_shouldnt_match" in files assert "file_which_shouldnt_match" in files
# This should match the directory only
regex6 = r".*/.*subdir.*/.*"
if ISWINDOWS:
regex6 = r".*\\.*subdir.*\\.*"
assert os.sep in regex6
self.d._exclude_list.rename(regex5, regex6)
self.d._exclude_list.remove(regex1)
eq_(len(self.d._exclude_list.compiled), 1)
assert regex1 not in self.d._exclude_list
assert regex5 not in self.d._exclude_list
assert self.d._exclude_list.error(regex6) is None
assert regex6 in self.d._exclude_list
# This still should not be affected
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
files = self.get_files_and_expect_num_result(5)
# These files are under the "/subdir" directory
assert "somesubdirfile.png" not in files
assert "unwanted_subdirfile.gif" not in files
# This file under "subdar" directory should not be filtered out
assert "file_ending_with_subdir" in files
# This file is in a directory that should be filtered out
assert "file_which_shouldnt_match" not in files
def test_japanese_unicode(self, tmpdir): def test_japanese_unicode(self, tmpdir):
p1 = Path(str(tmpdir)) p1 = Path(str(tmpdir))
p1["$Recycle.Bin"].mkdir() p1["$Recycle.Bin"].mkdir()

View File

@@ -69,6 +69,10 @@ class TestCasegetwords:
eq_(["a", "b", "c", "d"], getwords("a b c d")) eq_(["a", "b", "c", "d"], getwords("a b c d"))
eq_(["a", "b", "c", "d"], getwords(" a b c d ")) eq_(["a", "b", "c", "d"], getwords(" a b c d "))
def test_unicode(self):
eq_(["e", "c", "0", "a", "o", "u", "e", "u"], getwords("é ç 0 à ö û è ¤ ù"))
eq_(["02", "君のこころは輝いてるかい?", "国木田花丸", "solo", "ver"], getwords("02 君のこころは輝いてるかい? 国木田花丸 Solo Ver"))
def test_splitter_chars(self): def test_splitter_chars(self):
eq_( eq_(
[chr(i) for i in range(ord("a"), ord("z") + 1)], [chr(i) for i in range(ord("a"), ord("z") + 1)],
@@ -85,7 +89,7 @@ class TestCasegetwords:
eq_(["foo", "bar"], getwords("FOO BAR")) eq_(["foo", "bar"], getwords("FOO BAR"))
def test_decompose_unicode(self): def test_decompose_unicode(self):
eq_(getwords("foo\xe9bar"), ["fooebar"]) eq_(["fooebar"], getwords("foo\xe9bar"))
class TestCasegetfields: class TestCasegetfields:
@@ -173,9 +177,7 @@ class TestCaseWordCompareWithFields:
def test_simple(self): def test_simple(self):
eq_( eq_(
67, 67,
compare_fields( compare_fields([["a", "b"], ["c", "d", "e"]], [["a", "b"], ["c", "d", "f"]]),
[["a", "b"], ["c", "d", "e"]], [["a", "b"], ["c", "d", "f"]]
),
) )
def test_empty(self): def test_empty(self):
@@ -261,9 +263,7 @@ class TestCasebuild_word_dict:
j = job.Job(1, do_progress) j = job.Job(1, do_progress)
self.log = [] self.log = []
s = "foo bar" s = "foo bar"
build_word_dict( build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j)
[NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j
)
# We don't have intermediate log because iter_with_progress is called with every > 1 # We don't have intermediate log because iter_with_progress is called with every > 1
eq_(0, self.log[0]) eq_(0, self.log[0])
eq_(100, self.log[1]) eq_(100, self.log[1])
@@ -293,10 +293,7 @@ class TestCasereduce_common_words:
def test_dont_remove_objects_with_only_common_words(self): def test_dont_remove_objects_with_only_common_words(self):
d = { d = {
"common": set( "common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
[NamedObject("common uncommon", True) for i in range(50)]
+ [NamedObject("common", True)]
),
"uncommon": set([NamedObject("common uncommon", True)]), "uncommon": set([NamedObject("common uncommon", True)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
@@ -305,10 +302,7 @@ class TestCasereduce_common_words:
def test_values_still_are_set_instances(self): def test_values_still_are_set_instances(self):
d = { d = {
"common": set( "common": set([NamedObject("common uncommon", True) for i in range(50)] + [NamedObject("common", True)]),
[NamedObject("common uncommon", True) for i in range(50)]
+ [NamedObject("common", True)]
),
"uncommon": set([NamedObject("common uncommon", True)]), "uncommon": set([NamedObject("common uncommon", True)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
@@ -348,12 +342,8 @@ class TestCasereduce_common_words:
# would not stay in 'bar' because 'foo' is not a common word anymore. # would not stay in 'bar' because 'foo' is not a common word anymore.
only_common = NamedObject("foo bar", True) only_common = NamedObject("foo bar", True)
d = { d = {
"foo": set( "foo": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
[NamedObject("foo bar baz", True) for i in range(49)] + [only_common] "bar": set([NamedObject("foo bar baz", True) for i in range(49)] + [only_common]),
),
"bar": set(
[NamedObject("foo bar baz", True) for i in range(49)] + [only_common]
),
"baz": set([NamedObject("foo bar baz", True) for i in range(49)]), "baz": set([NamedObject("foo bar baz", True) for i in range(49)]),
} }
reduce_common_words(d, 50) reduce_common_words(d, 50)
@@ -382,9 +372,7 @@ class TestCaseget_match:
assert object() not in m assert object() not in m
def test_word_weight(self): def test_word_weight(self):
m = get_match( m = get_match(NamedObject("foo bar", True), NamedObject("bar bleh", True), (WEIGHT_WORDS,))
NamedObject("foo bar", True), NamedObject("bar bleh", True), (WEIGHT_WORDS,)
)
eq_(m.percentage, int((6.0 / 13.0) * 100)) eq_(m.percentage, int((6.0 / 13.0) * 100))
@@ -547,6 +535,32 @@ class TestCaseGetMatchesByContents:
o1, o2 = no(size=0), no(size=0) o1, o2 = no(size=0), no(size=0)
assert not getmatches_by_contents([o1, o2]) assert not getmatches_by_contents([o1, o2])
def test_big_file_partial_hashes(self):
smallsize = 1
bigsize = 100 * 1024 * 1024 # 100MB
f = [
no("bigfoo", size=bigsize),
no("bigbar", size=bigsize),
no("smallfoo", size=smallsize),
no("smallbar", size=smallsize),
]
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
f[2].md5 = f[2].md5partial = "bleh"
f[3].md5 = f[3].md5partial = "bleh"
r = getmatches_by_contents(f, bigsize=bigsize)
eq_(len(r), 2)
# User disabled optimization for big files, compute hashes as usual
r = getmatches_by_contents(f, bigsize=0)
eq_(len(r), 2)
# Other file is now slightly different, md5partial is still the same
f[1].md5 = f[1].md5samples = "foobardiff"
r = getmatches_by_contents(f, bigsize=bigsize)
# Successfully filter it out
eq_(len(r), 1)
r = getmatches_by_contents(f, bigsize=0)
eq_(len(r), 1)
class TestCaseGroup: class TestCaseGroup:
def test_empy(self): def test_empy(self):
@@ -723,8 +737,7 @@ class TestCaseGroup:
# if the ref has the same key as one or more of the dupe, run the tie_breaker func among them # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them
g = get_test_group() g = get_test_group()
o1, o2, o3 = g.ordered o1, o2, o3 = g.ordered
tie_breaker = lambda ref, dupe: dupe is o3 g.prioritize(lambda x: 0, lambda ref, dupe: dupe is o3)
g.prioritize(lambda x: 0, tie_breaker)
assert g.ref is o3 assert g.ref is o3
def test_prioritize_with_tie_breaker_runs_on_all_dupes(self): def test_prioritize_with_tie_breaker_runs_on_all_dupes(self):
@@ -735,8 +748,7 @@ class TestCaseGroup:
o1.foo = 1 o1.foo = 1
o2.foo = 2 o2.foo = 2
o3.foo = 3 o3.foo = 3
tie_breaker = lambda ref, dupe: dupe.foo > ref.foo g.prioritize(lambda x: 0, lambda ref, dupe: dupe.foo > ref.foo)
g.prioritize(lambda x: 0, tie_breaker)
assert g.ref is o3 assert g.ref is o3
def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self): def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self):
@@ -749,9 +761,7 @@ class TestCaseGroup:
o1.bar = 1 o1.bar = 1
o2.bar = 2 o2.bar = 2
o3.bar = 3 o3.bar = 3
key_func = lambda x: -x.foo g.prioritize(lambda x: -x.foo, lambda ref, dupe: dupe.bar > ref.bar)
tie_breaker = lambda ref, dupe: dupe.bar > ref.bar
g.prioritize(key_func, tie_breaker)
assert g.ref is o2 assert g.ref is o2
def test_prioritize_with_ref_dupe(self): def test_prioritize_with_ref_dupe(self):
@@ -883,9 +893,7 @@ class TestCaseget_groups:
m1 = Match(A, B, 90) # This is the strongest "A" match m1 = Match(A, B, 90) # This is the strongest "A" match
m2 = Match(A, C, 80) # Because C doesn't match with B, it won't be in the group m2 = Match(A, C, 80) # Because C doesn't match with B, it won't be in the group
m3 = Match(A, D, 80) # Same thing for D m3 = Match(A, D, 80) # Same thing for D
m4 = Match( m4 = Match(C, D, 70) # However, because C and D match, they should have their own group.
C, D, 70
) # However, because C and D match, they should have their own group.
groups = get_groups([m1, m2, m3, m4]) groups = get_groups([m1, m2, m3, m4])
eq_(len(groups), 2) eq_(len(groups), 2)
g1, g2 = groups g1, g2 = groups

View File

@@ -5,6 +5,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import io import io
# import os.path as op # import os.path as op
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
@@ -104,7 +105,7 @@ class TestCaseListEmpty:
regex1 = r"one" regex1 = r"one"
regex2 = r"two" regex2 = r"two"
self.exclude_list.add(regex1) self.exclude_list.add(regex1)
assert(regex1 in self.exclude_list) assert regex1 in self.exclude_list
self.exclude_list.add(regex2) self.exclude_list.add(regex2)
self.exclude_list.mark(regex1) self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2) self.exclude_list.mark(regex2)
@@ -113,7 +114,7 @@ class TestCaseListEmpty:
compiled_files = [x for x in self.exclude_list.compiled_files] compiled_files = [x for x in self.exclude_list.compiled_files]
eq_(len(compiled_files), 2) eq_(len(compiled_files), 2)
self.exclude_list.remove(regex2) self.exclude_list.remove(regex2)
assert(regex2 not in self.exclude_list) assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1) eq_(len(self.exclude_list), 1)
def test_add_duplicate(self): def test_add_duplicate(self):
@@ -188,6 +189,28 @@ class TestCaseListEmpty:
self.exclude_list.rename(regex_renamed_compilable, regex_compilable) self.exclude_list.rename(regex_renamed_compilable, regex_compilable)
eq_(self.exclude_list.is_marked(regex_compilable), True) eq_(self.exclude_list.is_marked(regex_compilable), True)
def test_rename_regex_file_to_path(self):
regex = r".*/one.*"
if ISWINDOWS:
regex = r".*\\one.*"
regex2 = r".*one.*"
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex in compiled_re
assert regex not in files_re
assert regex in paths_re
self.exclude_list.rename(regex, regex2)
compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex not in compiled_re
assert regex2 in compiled_re
assert regex2 in files_re
assert regex2 not in paths_re
def test_restore_default(self): def test_restore_default(self):
"""Only unmark previously added regexes and mark the pre-defined ones""" """Only unmark previously added regexes and mark the pre-defined ones"""
regex = r"one" regex = r"one"
@@ -213,21 +236,159 @@ class TestCaseListEmpty:
eq_(len(default_regexes), len(self.exclude_list.compiled)) eq_(len(default_regexes), len(self.exclude_list.compiled))
class TestCaseListEmptyUnion(TestCaseListEmpty):
"""Same but with union regex"""
def setup_method(self, method):
self.app = DupeGuru()
self.app.exclude_list = ExcludeList(union_regex=True)
self.exclude_list = self.app.exclude_list
def test_add_mark_and_remove_regex(self):
regex1 = r"one"
regex2 = r"two"
self.exclude_list.add(regex1)
assert regex1 in self.exclude_list
self.exclude_list.add(regex2)
self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2)
eq_(len(self.exclude_list), 2)
eq_(len(self.exclude_list.compiled), 1)
compiled_files = [x for x in self.exclude_list.compiled_files]
eq_(len(compiled_files), 1) # Two patterns joined together into one
assert "|" in compiled_files[0].pattern
self.exclude_list.remove(regex2)
assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1)
def test_rename_regex_file_to_path(self):
regex = r".*/one.*"
if ISWINDOWS:
regex = r".*\\one.*"
regex2 = r".*one.*"
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
eq_(len([x for x in self.exclude_list]), 1)
compiled_re = [x.pattern for x in self.exclude_list.compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex in compiled_re
assert regex not in files_re
assert regex in paths_re
self.exclude_list.rename(regex, regex2)
eq_(len([x for x in self.exclude_list]), 1)
compiled_re = [x.pattern for x in self.exclude_list.compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex not in compiled_re
assert regex2 in compiled_re
assert regex2 in files_re
assert regex2 not in paths_re
def test_restore_default(self):
"""Only unmark previously added regexes and mark the pre-defined ones"""
regex = r"one"
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
self.exclude_list.restore_defaults()
eq_(len(default_regexes), self.exclude_list.marked_count)
# added regex shouldn't be marked
eq_(self.exclude_list.is_marked(regex), False)
# added regex shouldn't be in compiled list either
compiled = [x for x in self.exclude_list.compiled]
assert regex not in compiled
# Need to escape both to get the same strings after compilation
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
assert compiled_escaped == default_escaped
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
class TestCaseDictEmpty(TestCaseListEmpty): class TestCaseDictEmpty(TestCaseListEmpty):
"""Same, but with dictionary implementation""" """Same, but with dictionary implementation"""
def setup_method(self, method): def setup_method(self, method):
self.app = DupeGuru() self.app = DupeGuru()
self.app.exclude_list = ExcludeDict(union_regex=False) self.app.exclude_list = ExcludeDict(union_regex=False)
self.exclude_list = self.app.exclude_list self.exclude_list = self.app.exclude_list
class TestCaseDictEmptyUnion(TestCaseDictEmpty):
"""Same, but with union regex"""
def setup_method(self, method):
self.app = DupeGuru()
self.app.exclude_list = ExcludeDict(union_regex=True)
self.exclude_list = self.app.exclude_list
def test_add_mark_and_remove_regex(self):
regex1 = r"one"
regex2 = r"two"
self.exclude_list.add(regex1)
assert regex1 in self.exclude_list
self.exclude_list.add(regex2)
self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2)
eq_(len(self.exclude_list), 2)
eq_(len(self.exclude_list.compiled), 1)
compiled_files = [x for x in self.exclude_list.compiled_files]
# two patterns joined into one
eq_(len(compiled_files), 1)
self.exclude_list.remove(regex2)
assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1)
def test_rename_regex_file_to_path(self):
regex = r".*/one.*"
if ISWINDOWS:
regex = r".*\\one.*"
regex2 = r".*one.*"
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
marked_re = [x for marked, x in self.exclude_list if marked]
eq_(len(marked_re), 1)
compiled_re = [x.pattern for x in self.exclude_list.compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex in compiled_re
assert regex not in files_re
assert regex in paths_re
self.exclude_list.rename(regex, regex2)
compiled_re = [x.pattern for x in self.exclude_list.compiled]
files_re = [x.pattern for x in self.exclude_list.compiled_files]
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
assert regex not in compiled_re
assert regex2 in compiled_re
assert regex2 in files_re
assert regex2 not in paths_re
def test_restore_default(self):
"""Only unmark previously added regexes and mark the pre-defined ones"""
regex = r"one"
self.exclude_list.add(regex)
self.exclude_list.mark(regex)
self.exclude_list.restore_defaults()
eq_(len(default_regexes), self.exclude_list.marked_count)
# added regex shouldn't be marked
eq_(self.exclude_list.is_marked(regex), False)
# added regex shouldn't be in compiled list either
compiled = [x for x in self.exclude_list.compiled]
assert regex not in compiled
# Need to escape both to get the same strings after compilation
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
assert compiled_escaped == default_escaped
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
def split_union(pattern_object): def split_union(pattern_object):
"""Returns list of strings for each union pattern""" """Returns list of strings for each union pattern"""
return [x for x in pattern_object.pattern.split("|")] return [x for x in pattern_object.pattern.split("|")]
class TestCaseCompiledList(): class TestCaseCompiledList:
"""Test consistency between union or and separate versions.""" """Test consistency between union or and separate versions."""
def setup_method(self, method): def setup_method(self, method):
self.e_separate = ExcludeList(union_regex=False) self.e_separate = ExcludeList(union_regex=False)
self.e_separate.restore_defaults() self.e_separate.restore_defaults()
@@ -275,6 +436,7 @@ class TestCaseCompiledList():
class TestCaseCompiledDict(TestCaseCompiledList): class TestCaseCompiledDict(TestCaseCompiledList):
"""Test the dictionary version""" """Test the dictionary version"""
def setup_method(self, method): def setup_method(self, method):
self.e_separate = ExcludeDict(union_regex=False) self.e_separate = ExcludeDict(union_regex=False)
self.e_separate.restore_defaults() self.e_separate.restore_defaults()

View File

@@ -7,6 +7,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
import hashlib import hashlib
from os import urandom
from hscommon.path import Path from hscommon.path import Path
from hscommon.testutil import eq_ from hscommon.testutil import eq_
@@ -15,6 +16,36 @@ from core.tests.directories_test import create_fake_fs
from .. import fs from .. import fs
def create_fake_fs_with_random_data(rootpath):
rootpath = rootpath["fs"]
rootpath.mkdir()
rootpath["dir1"].mkdir()
rootpath["dir2"].mkdir()
rootpath["dir3"].mkdir()
fp = rootpath["file1.test"].open("wb")
data1 = urandom(200 * 1024) # 200KiB
data2 = urandom(1024 * 1024) # 1MiB
data3 = urandom(10 * 1024 * 1024) # 10MiB
fp.write(data1)
fp.close()
fp = rootpath["file2.test"].open("wb")
fp.write(data2)
fp.close()
fp = rootpath["file3.test"].open("wb")
fp.write(data3)
fp.close()
fp = rootpath["dir1"]["file1.test"].open("wb")
fp.write(data1)
fp.close()
fp = rootpath["dir2"]["file2.test"].open("wb")
fp.write(data2)
fp.close()
fp = rootpath["dir3"]["file3.test"].open("wb")
fp.write(data3)
fp.close()
return rootpath
def test_size_aggregates_subfiles(tmpdir): def test_size_aggregates_subfiles(tmpdir):
p = create_fake_fs(Path(str(tmpdir))) p = create_fake_fs(Path(str(tmpdir)))
b = fs.Folder(p) b = fs.Folder(p)
@@ -25,7 +56,7 @@ def test_md5_aggregate_subfiles_sorted(tmpdir):
# dir.allfiles can return child in any order. Thus, bundle.md5 must aggregate # dir.allfiles can return child in any order. Thus, bundle.md5 must aggregate
# all files' md5 it contains, but it must make sure that it does so in the # all files' md5 it contains, but it must make sure that it does so in the
# same order everytime. # same order everytime.
p = create_fake_fs(Path(str(tmpdir))) p = create_fake_fs_with_random_data(Path(str(tmpdir)))
b = fs.Folder(p) b = fs.Folder(p)
md51 = fs.File(p["dir1"]["file1.test"]).md5 md51 = fs.File(p["dir1"]["file1.test"]).md5
md52 = fs.File(p["dir2"]["file2.test"]).md5 md52 = fs.File(p["dir2"]["file2.test"]).md5
@@ -41,6 +72,36 @@ def test_md5_aggregate_subfiles_sorted(tmpdir):
eq_(b.md5, md5.digest()) eq_(b.md5, md5.digest())
def test_partial_md5_aggregate_subfile_sorted(tmpdir):
p = create_fake_fs_with_random_data(Path(str(tmpdir)))
b = fs.Folder(p)
md51 = fs.File(p["dir1"]["file1.test"]).md5partial
md52 = fs.File(p["dir2"]["file2.test"]).md5partial
md53 = fs.File(p["dir3"]["file3.test"]).md5partial
md54 = fs.File(p["file1.test"]).md5partial
md55 = fs.File(p["file2.test"]).md5partial
md56 = fs.File(p["file3.test"]).md5partial
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
folder_md51 = hashlib.md5(md51).digest()
folder_md52 = hashlib.md5(md52).digest()
folder_md53 = hashlib.md5(md53).digest()
md5 = hashlib.md5(folder_md51 + folder_md52 + folder_md53 + md54 + md55 + md56)
eq_(b.md5partial, md5.digest())
md51 = fs.File(p["dir1"]["file1.test"]).md5samples
md52 = fs.File(p["dir2"]["file2.test"]).md5samples
md53 = fs.File(p["dir3"]["file3.test"]).md5samples
md54 = fs.File(p["file1.test"]).md5samples
md55 = fs.File(p["file2.test"]).md5samples
md56 = fs.File(p["file3.test"]).md5samples
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
folder_md51 = hashlib.md5(md51).digest()
folder_md52 = hashlib.md5(md52).digest()
folder_md53 = hashlib.md5(md53).digest()
md5 = hashlib.md5(folder_md51 + folder_md52 + folder_md53 + md54 + md55 + md56)
eq_(b.md5samples, md5.digest())
def test_has_file_attrs(tmpdir): def test_has_file_attrs(tmpdir):
# a Folder must behave like a file, so it must have mtime attributes # a Folder must behave like a file, so it must have mtime attributes
b = fs.Folder(Path(str(tmpdir))) b = fs.Folder(Path(str(tmpdir)))

View File

@@ -73,9 +73,7 @@ def test_save_to_xml():
eq_(len(root), 2) eq_(len(root), 2)
eq_(len([c for c in root if c.tag == "file"]), 2) eq_(len([c for c in root if c.tag == "file"]), 2)
f1, f2 = root[:] f1, f2 = root[:]
subchildren = [c for c in f1 if c.tag == "file"] + [ subchildren = [c for c in f1 if c.tag == "file"] + [c for c in f2 if c.tag == "file"]
c for c in f2 if c.tag == "file"
]
eq_(len(subchildren), 3) eq_(len(subchildren), 3)
@@ -96,9 +94,7 @@ def test_SaveThenLoad():
def test_LoadXML_with_empty_file_tags(): def test_LoadXML_with_empty_file_tags():
f = io.BytesIO() f = io.BytesIO()
f.write( f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>'
)
f.seek(0) f.seek(0)
il = IgnoreList() il = IgnoreList()
il.load_from_xml(f) il.load_from_xml(f)

View File

@@ -117,9 +117,7 @@ class TestCaseResultsWithSomeGroups:
assert d is g.ref assert d is g.ref
def test_sort_groups(self): def test_sort_groups(self):
self.results.make_ref( self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
self.objects[1]
) # We want to make the 1024 sized object to go ref.
g1, g2 = self.groups g1, g2 = self.groups
self.results.sort_groups("size") self.results.sort_groups("size")
assert self.results.groups[0] is g2 assert self.results.groups[0] is g2
@@ -129,9 +127,7 @@ class TestCaseResultsWithSomeGroups:
assert self.results.groups[1] is g2 assert self.results.groups[1] is g2
def test_set_groups_when_sorted(self): def test_set_groups_when_sorted(self):
self.results.make_ref( self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
self.objects[1]
) # We want to make the 1024 sized object to go ref.
self.results.sort_groups("size") self.results.sort_groups("size")
objects, matches, groups = GetTestGroups() objects, matches, groups = GetTestGroups()
g1, g2 = groups g1, g2 = groups
@@ -601,9 +597,7 @@ class TestCaseResultsXML:
matches = engine.getmatches(objects) # we should have 5 matches matches = engine.getmatches(objects) # we should have 5 matches
groups = engine.get_groups(matches) # We should have 2 groups groups = engine.get_groups(matches) # We should have 2 groups
for g in groups: for g in groups:
g.prioritize( g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
lambda x: objects.index(x)
) # We want the dupes to be in the same order as the list is
app = DupeGuru() app = DupeGuru()
results = Results(app) results = Results(app)
results.groups = groups results.groups = groups
@@ -807,9 +801,7 @@ class TestCaseResultsFilter:
# Now the stats should display *2* markable dupes (instead of 1) # Now the stats should display *2* markable dupes (instead of 1)
expected = "0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo" expected = "0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo"
eq_(expected, self.results.stat_line) eq_(expected, self.results.stat_line)
self.results.apply_filter( self.results.apply_filter(None) # Now let's make sure our unfiltered results aren't fucked up
None
) # Now let's make sure our unfiltered results aren't fucked up
expected = "0 / 3 (0.00 B / 3.00 B) duplicates marked." expected = "0 / 3 (0.00 B / 3.00 B) duplicates marked."
eq_(expected, self.results.stat_line) eq_(expected, self.results.stat_line)

View File

@@ -56,6 +56,7 @@ def test_default_settings(fake_fileexists):
eq_(s.mix_file_kind, True) eq_(s.mix_file_kind, True)
eq_(s.word_weighting, False) eq_(s.word_weighting, False)
eq_(s.match_similar_words, False) eq_(s.match_similar_words, False)
eq_(s.big_file_size_threshold, 0)
def test_simple_with_default_settings(fake_fileexists): def test_simple_with_default_settings(fake_fileexists):
@@ -120,9 +121,9 @@ def test_content_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.Contents
f = [no("foo"), no("bar"), no("bleh")] f = [no("foo"), no("bar"), no("bleh")]
f[0].md5 = f[0].md5partial = "foobar" f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = "foobar" f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
f[2].md5 = f[2].md5partial = "bleh" f[2].md5 = f[2].md5partial = f[1].md5samples = "bleh"
r = s.get_dupe_groups(f) r = s.get_dupe_groups(f)
eq_(len(r), 1) eq_(len(r), 1)
eq_(len(r[0]), 2) eq_(len(r[0]), 2)
@@ -141,13 +142,42 @@ def test_content_scan_compare_sizes_first(fake_fileexists):
eq_(len(s.get_dupe_groups(f)), 0) eq_(len(s.get_dupe_groups(f)), 0)
def test_big_file_partial_hashes(fake_fileexists):
s = Scanner()
s.scan_type = ScanType.Contents
smallsize = 1
bigsize = 100 * 1024 * 1024 # 100MB
s.big_file_size_threshold = bigsize
f = [no("bigfoo", bigsize), no("bigbar", bigsize), no("smallfoo", smallsize), no("smallbar", smallsize)]
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
f[2].md5 = f[2].md5partial = "bleh"
f[3].md5 = f[3].md5partial = "bleh"
r = s.get_dupe_groups(f)
eq_(len(r), 2)
# md5partial is still the same, but the file is actually different
f[1].md5 = f[1].md5samples = "difffoobar"
# here we compare the full md5s, as the user disabled the optimization
s.big_file_size_threshold = 0
r = s.get_dupe_groups(f)
eq_(len(r), 1)
# here we should compare the md5samples, and see they are different
s.big_file_size_threshold = bigsize
r = s.get_dupe_groups(f)
eq_(len(r), 1)
def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists): def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.Contents
f = [no("foo"), no("bar"), no("bleh")] f = [no("foo"), no("bar"), no("bleh")]
f[0].md5 = f[0].md5partial = "foobar" f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = "foobar" f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
f[2].md5 = f[2].md5partial = "bleh" f[2].md5 = f[2].md5partial = f[2].md5samples = "bleh"
s.min_match_percentage = 101 s.min_match_percentage = 101
r = s.get_dupe_groups(f) r = s.get_dupe_groups(f)
eq_(len(r), 1) eq_(len(r), 1)
@@ -162,13 +192,10 @@ def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Contents s.scan_type = ScanType.Contents
f = [no("foo"), no("bar")] f = [no("foo"), no("bar")]
f[0].md5 = f[ f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
0 f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
].md5partial = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[1].md5 = f[
1
].md5partial = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
r = s.get_dupe_groups(f) r = s.get_dupe_groups(f)
# FIXME looks like we are missing something here?
r[0] r[0]
@@ -514,21 +541,21 @@ def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
s = Scanner() s = Scanner()
s.scan_type = ScanType.Folders s.scan_type = ScanType.Folders
topf1 = no("top folder 1", size=42) topf1 = no("top folder 1", size=42)
topf1.md5 = topf1.md5partial = b"some_md5_1" topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
topf1.path = Path("/topf1") topf1.path = Path("/topf1")
topf2 = no("top folder 2", size=42) topf2 = no("top folder 2", size=42)
topf2.md5 = topf2.md5partial = b"some_md5_1" topf2.md5 = topf2.md5partial = topf2.md5samples = b"some_md5_1"
topf2.path = Path("/topf2") topf2.path = Path("/topf2")
subf1 = no("sub folder 1", size=41) subf1 = no("sub folder 1", size=41)
subf1.md5 = subf1.md5partial = b"some_md5_2" subf1.md5 = subf1.md5partial = subf1.md5samples = b"some_md5_2"
subf1.path = Path("/topf1/sub") subf1.path = Path("/topf1/sub")
subf2 = no("sub folder 2", size=41) subf2 = no("sub folder 2", size=41)
subf2.md5 = subf2.md5partial = b"some_md5_2" subf2.md5 = subf2.md5partial = subf2.md5samples = b"some_md5_2"
subf2.path = Path("/topf2/sub") subf2.path = Path("/topf2/sub")
eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2])), 1) # only top folders eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2])), 1) # only top folders
# however, if another folder matches a subfolder, keep in in the matches # however, if another folder matches a subfolder, keep in in the matches
otherf = no("other folder", size=41) otherf = no("other folder", size=41)
otherf.md5 = otherf.md5partial = b"some_md5_2" otherf.md5 = otherf.md5partial = otherf.md5samples = b"some_md5_2"
otherf.path = Path("/otherfolder") otherf.path = Path("/otherfolder")
eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2, otherf])), 2) eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2, otherf])), 2)
@@ -551,9 +578,9 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
o1 = no("foo", path="p1") o1 = no("foo", path="p1")
o2 = no("foo", path="p2") o2 = no("foo", path="p2")
o3 = no("foo", path="p3") o3 = no("foo", path="p3")
o1.md5 = o1.md5partial = "foobar" o1.md5 = o1.md5partial = o1.md5samples = "foobar"
o2.md5 = o2.md5partial = "foobar" o2.md5 = o2.md5partial = o2.md5samples = "foobar"
o3.md5 = o3.md5partial = "foobar" o3.md5 = o3.md5partial = o3.md5samples = "foobar"
o1.is_ref = True o1.is_ref = True
o2.is_ref = True o2.is_ref = True
eq_(len(s.get_dupe_groups([o1, o2, o3])), 1) eq_(len(s.get_dupe_groups([o1, o2, o3])), 1)

View File

@@ -30,8 +30,7 @@ from .util import ensure_folder, delete_files_with_pattern
def print_and_do(cmd): def print_and_do(cmd):
"""Prints ``cmd`` and executes it in the shell. """Prints ``cmd`` and executes it in the shell."""
"""
print(cmd) print(cmd)
p = Popen(cmd, shell=True) p = Popen(cmd, shell=True)
return p.wait() return p.wait()
@@ -91,16 +90,14 @@ def copy_all(pattern, dst):
def ensure_empty_folder(path): def ensure_empty_folder(path):
"""Make sure that the path exists and that it's an empty folder. """Make sure that the path exists and that it's an empty folder."""
"""
if op.exists(path): if op.exists(path):
shutil.rmtree(path) shutil.rmtree(path)
os.mkdir(path) os.mkdir(path)
def filereplace(filename, outfilename=None, **kwargs): def filereplace(filename, outfilename=None, **kwargs):
"""Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`. """Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`."""
"""
if outfilename is None: if outfilename is None:
outfilename = filename outfilename = filename
fp = open(filename, "rt", encoding="utf-8") fp = open(filename, "rt", encoding="utf-8")
@@ -152,9 +149,7 @@ def package_cocoa_app_in_dmg(app_path, destfolder, args):
# a valid signature. # a valid signature.
if args.sign_identity: if args.sign_identity:
sign_identity = "Developer ID Application: {}".format(args.sign_identity) sign_identity = "Developer ID Application: {}".format(args.sign_identity)
result = print_and_do( result = print_and_do('codesign --force --deep --sign "{}" "{}"'.format(sign_identity, app_path))
'codesign --force --deep --sign "{}" "{}"'.format(sign_identity, app_path)
)
if result != 0: if result != 0:
print("ERROR: Signing failed. Aborting packaging.") print("ERROR: Signing failed. Aborting packaging.")
return return
@@ -182,10 +177,7 @@ def build_dmg(app_path, destfolder):
) )
print("Building %s" % dmgname) print("Building %s" % dmgname)
# UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less. # UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less.
print_and_do( print_and_do('hdiutil create "%s" -format UDBZ -nocrossdev -srcdir "%s"' % (op.join(destfolder, dmgname), dmgpath))
'hdiutil create "%s" -format UDBZ -nocrossdev -srcdir "%s"'
% (op.join(destfolder, dmgname), dmgpath)
)
print("Build Complete") print("Build Complete")
@@ -207,8 +199,7 @@ sysconfig.get_config_h_filename = lambda: op.join(op.dirname(__file__), 'pyconfi
def add_to_pythonpath(path): def add_to_pythonpath(path):
"""Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``. """Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``."""
"""
abspath = op.abspath(path) abspath = op.abspath(path)
pythonpath = os.environ.get("PYTHONPATH", "") pythonpath = os.environ.get("PYTHONPATH", "")
pathsep = ";" if ISWINDOWS else ":" pathsep = ";" if ISWINDOWS else ":"
@@ -231,9 +222,7 @@ def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
create_links = False create_links = False
if not extra_ignores: if not extra_ignores:
extra_ignores = [] extra_ignores = []
ignore = shutil.ignore_patterns( ignore = shutil.ignore_patterns(".hg*", "tests", "testdata", "modules", "docs", "locale", *extra_ignores)
".hg*", "tests", "testdata", "modules", "docs", "locale", *extra_ignores
)
for package_name in packages_names: for package_name in packages_names:
if op.exists(package_name): if op.exists(package_name):
source_path = package_name source_path = package_name
@@ -444,11 +433,10 @@ class OSXFrameworkStructure:
def create_symlinks(self): def create_symlinks(self):
# Only call this after create() and copy_executable() # Only call this after create() and copy_executable()
rel = lambda path: op.relpath(path, self.dest)
os.symlink("A", op.join(self.dest, "Versions", "Current")) os.symlink("A", op.join(self.dest, "Versions", "Current"))
os.symlink(rel(self.executablepath), op.join(self.dest, self.executablename)) os.symlink(op.relpath(self.executablepath, self.dest), op.join(self.dest, self.executablename))
os.symlink(rel(self.headers), op.join(self.dest, "Headers")) os.symlink(op.relpath(self.headers, self.dest), op.join(self.dest, "Headers"))
os.symlink(rel(self.resources), op.join(self.dest, "Resources")) os.symlink(op.relpath(self.resources, self.dest), op.join(self.dest, "Resources"))
def copy_executable(self, executable): def copy_executable(self, executable):
copy(executable, self.executablepath) copy(executable, self.executablepath)
@@ -481,9 +469,7 @@ def copy_embeddable_python_dylib(dst):
def collect_stdlib_dependencies(script, dest_folder, extra_deps=None): def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
sysprefix = sys.prefix # could be a virtualenv sysprefix = sys.prefix # could be a virtualenv
basesysprefix = sys.base_prefix # seems to be path to non-virtual sys basesysprefix = sys.base_prefix # seems to be path to non-virtual sys
real_lib_prefix = sysconfig.get_config_var( real_lib_prefix = sysconfig.get_config_var("LIBDEST") # leaving this in case it is neede
"LIBDEST"
) # leaving this in case it is neede
def is_stdlib_path(path): def is_stdlib_path(path):
# A module path is only a stdlib path if it's in either sys.prefix or # A module path is only a stdlib path if it's in either sys.prefix or
@@ -493,11 +479,7 @@ def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
return False return False
if "site-package" in path: if "site-package" in path:
return False return False
if not ( if not (path.startswith(sysprefix) or path.startswith(basesysprefix) or path.startswith(real_lib_prefix)):
path.startswith(sysprefix)
or path.startswith(basesysprefix)
or path.startswith(real_lib_prefix)
):
return False return False
return True return True
@@ -511,9 +493,7 @@ def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
relpath = op.relpath(p, real_lib_prefix) relpath = op.relpath(p, real_lib_prefix)
elif p.startswith(sysprefix): elif p.startswith(sysprefix):
relpath = op.relpath(p, sysprefix) relpath = op.relpath(p, sysprefix)
assert relpath.startswith( assert relpath.startswith("lib/python3.") # we want to get rid of that lib/python3.x part
"lib/python3."
) # we want to get rid of that lib/python3.x part
relpath = relpath[len("lib/python3.X/") :] relpath = relpath[len("lib/python3.X/") :]
elif p.startswith(basesysprefix): elif p.startswith(basesysprefix):
relpath = op.relpath(p, basesysprefix) relpath = op.relpath(p, basesysprefix)
@@ -521,9 +501,7 @@ def collect_stdlib_dependencies(script, dest_folder, extra_deps=None):
relpath = relpath[len("lib/python3.X/") :] relpath = relpath[len("lib/python3.X/") :]
else: else:
raise AssertionError() raise AssertionError()
if relpath.startswith( if relpath.startswith("lib-dynload"): # We copy .so files in lib-dynload directly in our dest
"lib-dynload"
): # We copy .so files in lib-dynload directly in our dest
relpath = relpath[len("lib-dynload/") :] relpath = relpath[len("lib-dynload/") :]
if relpath.startswith("encodings") or relpath.startswith("distutils"): if relpath.startswith("encodings") or relpath.startswith("distutils"):
# We force their inclusion later. # We force their inclusion later.
@@ -562,9 +540,7 @@ def fix_qt_resource_file(path):
fp.write(b"\n".join(lines)) fp.write(b"\n".join(lines))
def build_cocoa_ext( def build_cocoa_ext(extname, dest, source_files, extra_frameworks=(), extra_includes=()):
extname, dest, source_files, extra_frameworks=(), extra_includes=()
):
extra_link_args = ["-framework", "CoreFoundation", "-framework", "Foundation"] extra_link_args = ["-framework", "CoreFoundation", "-framework", "Foundation"]
for extra in extra_frameworks: for extra in extra_frameworks:
extra_link_args += ["-framework", extra] extra_link_args += ["-framework", extra]

View File

@@ -11,9 +11,7 @@ from setuptools import setup, Extension
def get_parser(): def get_parser():
parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.") parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.")
parser.add_argument( parser.add_argument("source_files", nargs="+", help="List of source files to compile")
"source_files", nargs="+", help="List of source files to compile"
)
parser.add_argument("name", nargs=1, help="Name of the resulting extension") parser.add_argument("name", nargs=1, help="Name of the resulting extension")
return parser return parser
@@ -23,7 +21,8 @@ def main():
print("Building {}...".format(args.name[0])) print("Building {}...".format(args.name[0]))
ext = Extension(args.name[0], args.source_files) ext = Extension(args.name[0], args.source_files)
setup( setup(
script_args=["build_ext", "--inplace"], ext_modules=[ext], script_args=["build_ext", "--inplace"],
ext_modules=[ext],
) )

View File

@@ -48,15 +48,13 @@ def get_unconflicted_name(name):
def is_conflicted(name): def is_conflicted(name):
"""Returns whether ``name`` is prepended with a bracketed number. """Returns whether ``name`` is prepended with a bracketed number."""
"""
return re_conflict.match(name) is not None return re_conflict.match(name) is not None
@pathify @pathify
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path): def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
"""Use move() or copy() to move and copy file with the conflict management. """Use move() or copy() to move and copy file with the conflict management."""
"""
if dest_path.isdir() and not source_path.isdir(): if dest_path.isdir() and not source_path.isdir():
dest_path = dest_path[source_path.name] dest_path = dest_path[source_path.name]
if dest_path.exists(): if dest_path.exists():
@@ -68,14 +66,12 @@ def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
def smart_move(source_path, dest_path): def smart_move(source_path, dest_path):
"""Same as :func:`smart_copy`, but it moves files instead. """Same as :func:`smart_copy`, but it moves files instead."""
"""
_smart_move_or_copy(shutil.move, source_path, dest_path) _smart_move_or_copy(shutil.move, source_path, dest_path)
def smart_copy(source_path, dest_path): def smart_copy(source_path, dest_path):
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution. """Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution."""
"""
try: try:
_smart_move_or_copy(shutil.copy, source_path, dest_path) _smart_move_or_copy(shutil.copy, source_path, dest_path)
except IOError as e: except IOError as e:

View File

@@ -16,20 +16,17 @@ class SpecialFolder:
def open_url(url): def open_url(url):
"""Open ``url`` with the default browser. """Open ``url`` with the default browser."""
"""
_open_url(url) _open_url(url)
def open_path(path): def open_path(path):
"""Open ``path`` with its associated application. """Open ``path`` with its associated application."""
"""
_open_path(str(path)) _open_path(str(path))
def reveal_path(path): def reveal_path(path):
"""Open the folder containing ``path`` with the default file browser. """Open the folder containing ``path`` with the default file browser."""
"""
_reveal_path(str(path)) _reveal_path(str(path))

View File

@@ -149,8 +149,7 @@ class Rect:
return l1, l2, l3, l4 return l1, l2, l3, l4
def scaled_rect(self, dx, dy): def scaled_rect(self, dx, dy):
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side. """Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side."""
"""
x, y, w, h = self x, y, w, h = self
x -= dx x -= dx
y -= dy y -= dy
@@ -159,8 +158,7 @@ class Rect:
return Rect(x, y, w, h) return Rect(x, y, w, h)
def united(self, other): def united(self, other):
"""Returns the bounding rectangle of this rectangle and `other`. """Returns the bounding rectangle of this rectangle and `other`."""
"""
# ul=upper left lr=lower right # ul=upper left lr=lower right
ulcorner1, lrcorner1 = self.corners() ulcorner1, lrcorner1 = self.corners()
ulcorner2, lrcorner2 = other.corners() ulcorner2, lrcorner2 = other.corners()

View File

@@ -80,8 +80,7 @@ class PrefAccessInterface:
""" """
def set_default(self, key, value): def set_default(self, key, value):
"""Set the value ``value`` for ``key`` in the currently running app's preference store. """Set the value ``value`` for ``key`` in the currently running app's preference store."""
"""
class Columns(GUIObject): class Columns(GUIObject):
@@ -140,33 +139,27 @@ class Columns(GUIObject):
# --- Public # --- Public
def column_by_index(self, index): def column_by_index(self, index):
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``. """Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
"""
return self.column_list[index] return self.column_list[index]
def column_by_name(self, name): def column_by_name(self, name):
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``. """Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
"""
return self.coldata[name] return self.coldata[name]
def columns_count(self): def columns_count(self):
"""Returns the number of columns in our set. """Returns the number of columns in our set."""
"""
return len(self.column_list) return len(self.column_list)
def column_display(self, colname): def column_display(self, colname):
"""Returns display name for column named ``colname``, or ``''`` if there's none. """Returns display name for column named ``colname``, or ``''`` if there's none."""
"""
return self._get_colname_attr(colname, "display", "") return self._get_colname_attr(colname, "display", "")
def column_is_visible(self, colname): def column_is_visible(self, colname):
"""Returns visibility for column named ``colname``, or ``True`` if there's none. """Returns visibility for column named ``colname``, or ``True`` if there's none."""
"""
return self._get_colname_attr(colname, "visible", True) return self._get_colname_attr(colname, "visible", True)
def column_width(self, colname): def column_width(self, colname):
"""Returns width for column named ``colname``, or ``0`` if there's none. """Returns width for column named ``colname``, or ``0`` if there's none."""
"""
return self._get_colname_attr(colname, "width", 0) return self._get_colname_attr(colname, "width", 0)
def columns_to_right(self, colname): def columns_to_right(self, colname):
@@ -177,11 +170,7 @@ class Columns(GUIObject):
""" """
column = self.coldata[colname] column = self.coldata[colname]
index = column.ordered_index index = column.ordered_index
return [ return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
col.name
for col in self.column_list
if (col.visible and col.ordered_index > index)
]
def menu_items(self): def menu_items(self):
"""Returns a list of items convenient for quick visibility menu generation. """Returns a list of items convenient for quick visibility menu generation.
@@ -207,8 +196,7 @@ class Columns(GUIObject):
self.set_column_order(colnames) self.set_column_order(colnames)
def reset_to_defaults(self): def reset_to_defaults(self):
"""Reset all columns' width and visibility to their default values. """Reset all columns' width and visibility to their default values."""
"""
self.set_column_order([col.name for col in self.column_list]) self.set_column_order([col.name for col in self.column_list])
for col in self._optional_columns(): for col in self._optional_columns():
col.visible = col.default_visible col.visible = col.default_visible
@@ -216,13 +204,11 @@ class Columns(GUIObject):
self.view.restore_columns() self.view.restore_columns()
def resize_column(self, colname, newwidth): def resize_column(self, colname, newwidth):
"""Set column ``colname``'s width to ``newwidth``. """Set column ``colname``'s width to ``newwidth``."""
"""
self._set_colname_attr(colname, "width", newwidth) self._set_colname_attr(colname, "width", newwidth)
def restore_columns(self): def restore_columns(self):
"""Restore's column persistent attributes from the last :meth:`save_columns`. """Restore's column persistent attributes from the last :meth:`save_columns`."""
"""
if not (self.prefaccess and self.savename and self.coldata): if not (self.prefaccess and self.savename and self.coldata):
if (not self.savename) and (self.coldata): if (not self.savename) and (self.coldata):
# This is a table that will not have its coldata saved/restored. we should # This is a table that will not have its coldata saved/restored. we should
@@ -241,8 +227,7 @@ class Columns(GUIObject):
self.view.restore_columns() self.view.restore_columns()
def save_columns(self): def save_columns(self):
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`. """Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
"""
if not (self.prefaccess and self.savename and self.coldata): if not (self.prefaccess and self.savename and self.coldata):
return return
for col in self.column_list: for col in self.column_list:
@@ -263,15 +248,13 @@ class Columns(GUIObject):
col.ordered_index = i col.ordered_index = i
def set_column_visible(self, colname, visible): def set_column_visible(self, colname, visible):
"""Set the visibility of column ``colname``. """Set the visibility of column ``colname``."""
"""
self.table.save_edits() # the table on the GUI side will stop editing when the columns change self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, "visible", visible) self._set_colname_attr(colname, "visible", visible)
self.view.set_column_visible(colname, visible) self.view.set_column_visible(colname, visible)
def set_default_width(self, colname, width): def set_default_width(self, colname, width):
"""Set the default width or column ``colname``. """Set the default width or column ``colname``."""
"""
self._set_colname_attr(colname, "default_width", width) self._set_colname_attr(colname, "default_width", width)
def toggle_menu_item(self, index): def toggle_menu_item(self, index):
@@ -289,14 +272,10 @@ class Columns(GUIObject):
# --- Properties # --- Properties
@property @property
def ordered_columns(self): def ordered_columns(self):
"""List of :class:`Column` in visible order. """List of :class:`Column` in visible order."""
""" return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
return [
col for col in sorted(self.column_list, key=lambda col: col.ordered_index)
]
@property @property
def colnames(self): def colnames(self):
"""List of column names in visible order. """List of column names in visible order."""
"""
return [col.name for col in self.ordered_columns] return [col.name for col in self.ordered_columns]

View File

@@ -21,12 +21,10 @@ class ProgressWindowView:
""" """
def show(self): def show(self):
"""Show the dialog. """Show the dialog."""
"""
def close(self): def close(self):
"""Close the dialog. """Close the dialog."""
"""
def set_progress(self, progress): def set_progress(self, progress):
"""Set the progress of the progress bar to ``progress``. """Set the progress of the progress bar to ``progress``.
@@ -76,8 +74,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
self.jobid = None self.jobid = None
def cancel(self): def cancel(self):
"""Call for a user-initiated job cancellation. """Call for a user-initiated job cancellation."""
"""
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to # The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
# make sure that this doesn't lead us to think that the user acually cancelled the task, so # make sure that this doesn't lead us to think that the user acually cancelled the task, so
# we verify that the job is still running. # we verify that the job is still running.

View File

@@ -27,9 +27,7 @@ class Selectable(Sequence):
self._selected_indexes = [] self._selected_indexes = []
if not self._selected_indexes: if not self._selected_indexes:
return return
self._selected_indexes = [ self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]
index for index in self._selected_indexes if index < len(self)
]
if not self._selected_indexes: if not self._selected_indexes:
self._selected_indexes = [len(self) - 1] self._selected_indexes = [len(self) - 1]

View File

@@ -97,8 +97,7 @@ class Table(MutableSequence, Selectable):
self._rows.pop(0) self._rows.pop(0)
if self._footer is not None: if self._footer is not None:
self._rows.pop() self._rows.pop()
key = lambda row: row.sort_key_for_column(column_name) self._rows.sort(key=lambda row: row.sort_key_for_column(column_name), reverse=desc)
self._rows.sort(key=key, reverse=desc)
if self._header is not None: if self._header is not None:
self._rows.insert(0, self._header) self._rows.insert(0, self._header)
if self._footer is not None: if self._footer is not None:
@@ -277,8 +276,7 @@ class GUITable(Table, GUIObject):
raise NotImplementedError() raise NotImplementedError()
def _do_delete(self): def _do_delete(self):
"""(Virtual) Delete the selected rows. """(Virtual) Delete the selected rows."""
"""
pass pass
def _fill(self): def _fill(self):

View File

@@ -71,8 +71,7 @@ class TextField(GUIObject):
# --- Public # --- Public
def refresh(self): def refresh(self):
"""Triggers a view :meth:`~TextFieldView.refresh`. """Triggers a view :meth:`~TextFieldView.refresh`."""
"""
self.view.refresh() self.view.refresh()
@property @property

View File

@@ -55,8 +55,7 @@ class Node(MutableSequence):
# --- Public # --- Public
def clear(self): def clear(self):
"""Clears the node of all its children. """Clears the node of all its children."""
"""
del self[:] del self[:]
def find(self, predicate, include_self=True): def find(self, predicate, include_self=True):
@@ -103,14 +102,12 @@ class Node(MutableSequence):
@property @property
def children_count(self): def children_count(self):
"""Same as ``len(self)``. """Same as ``len(self)``."""
"""
return len(self) return len(self)
@property @property
def name(self): def name(self):
"""Name for the node, supplied on init. """Name for the node, supplied on init."""
"""
return self._name return self._name
@property @property

View File

@@ -56,8 +56,7 @@ class Job:
# ---Private # ---Private
def _subjob_callback(self, progress, desc=""): def _subjob_callback(self, progress, desc=""):
"""This is the callback passed to children jobs. """This is the callback passed to children jobs."""
"""
self.set_progress(progress, desc) self.set_progress(progress, desc)
return True # if JobCancelled has to be raised, it will be at the highest level return True # if JobCancelled has to be raised, it will be at the highest level

View File

@@ -154,9 +154,7 @@ def strings2pot(target, dest):
def allstrings2pot(lprojpath, dest, excludes=None): def allstrings2pot(lprojpath, dest, excludes=None):
allstrings = files_with_ext(lprojpath, ".strings") allstrings = files_with_ext(lprojpath, ".strings")
if excludes: if excludes:
allstrings = [ allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes
]
for strings_path in allstrings: for strings_path in allstrings:
strings2pot(strings_path, dest) strings2pot(strings_path, dest)
@@ -195,11 +193,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
# genstrings produces utf-16 files with comments. After having generated the files, we convert # genstrings produces utf-16 files with comments. After having generated the files, we convert
# them to utf-8 and remove the comments. # them to utf-8 and remove the comments.
ensure_empty_folder(dest_folder) ensure_empty_folder(dest_folder)
print_and_do( print_and_do('genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(dest_folder, code_folder))
'genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(
dest_folder, code_folder
)
)
for stringsfile in os.listdir(dest_folder): for stringsfile in os.listdir(dest_folder):
stringspath = op.join(dest_folder, stringsfile) stringspath = op.join(dest_folder, stringsfile)
with open(stringspath, "rt", encoding="utf-16") as fp: with open(stringspath, "rt", encoding="utf-16") as fp:
@@ -214,9 +208,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
def generate_cocoa_strings_from_xib(xib_folder): def generate_cocoa_strings_from_xib(xib_folder):
xibs = [ xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")
]
for xib in xibs: for xib in xibs:
dest = xib.replace(".xib", ".strings") dest = xib.replace(".xib", ".strings")
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest)) print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
@@ -234,10 +226,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
def localize_all_stringsfiles(src_folder, dest_root_folder): def localize_all_stringsfiles(src_folder, dest_root_folder):
stringsfiles = [ stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")]
op.join(src_folder, fn)
for fn in os.listdir(src_folder)
if fn.endswith(".strings")
]
for path in stringsfiles: for path in stringsfiles:
localize_stringsfile(path, dest_root_folder) localize_stringsfile(path, dest_root_folder)

View File

@@ -16,8 +16,7 @@ from collections import defaultdict
class Broadcaster: class Broadcaster:
"""Broadcasts messages that are received by all listeners. """Broadcasts messages that are received by all listeners."""
"""
def __init__(self): def __init__(self):
self.listeners = set() self.listeners = set()
@@ -39,8 +38,7 @@ class Broadcaster:
class Listener: class Listener:
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected. """A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected."""
"""
def __init__(self, broadcaster): def __init__(self, broadcaster):
self.broadcaster = broadcaster self.broadcaster = broadcaster
@@ -57,13 +55,11 @@ class Listener:
self._bound_notifications[message].append(func) self._bound_notifications[message].append(func)
def connect(self): def connect(self):
"""Connects the listener to its broadcaster. """Connects the listener to its broadcaster."""
"""
self.broadcaster.add_listener(self) self.broadcaster.add_listener(self)
def disconnect(self): def disconnect(self):
"""Disconnects the listener from its broadcaster. """Disconnects the listener from its broadcaster."""
"""
self.broadcaster.remove_listener(self) self.broadcaster.remove_listener(self)
def dispatch(self, msg): def dispatch(self, msg):

View File

@@ -85,9 +85,7 @@ class Path(tuple):
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, slice): if isinstance(key, slice):
if isinstance(key.start, Path): if isinstance(key.start, Path):
equal_elems = list( equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start)))
takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start))
)
key = slice(len(equal_elems), key.stop, key.step) key = slice(len(equal_elems), key.stop, key.step)
if isinstance(key.stop, Path): if isinstance(key.stop, Path):
equal_elems = list( equal_elems = list(
@@ -226,9 +224,7 @@ def pathify(f):
Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``. Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``.
""" """
sig = signature(f) sig = signature(f)
pindexes = { pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path}
i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path
}
pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path} pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path}
def path_or_none(p): def path_or_none(p):
@@ -236,9 +232,7 @@ def pathify(f):
@wraps(f) @wraps(f)
def wrapped(*args, **kwargs): def wrapped(*args, **kwargs):
args = tuple( args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args))
(path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)
)
kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()} kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()}
return f(*args, **kwargs) return f(*args, **kwargs)
@@ -246,8 +240,7 @@ def pathify(f):
def log_io_error(func): def log_io_error(func):
""" Catches OSError, IOError and WindowsError and log them """Catches OSError, IOError and WindowsError and log them"""
"""
@wraps(func) @wraps(func)
def wrapper(path, *args, **kwargs): def wrapper(path, *args, **kwargs):

View File

@@ -110,22 +110,14 @@ def _visit_pyfiles(list, dirname, names):
# get extension for python source files # get extension for python source files
if "_py_ext" not in globals(): if "_py_ext" not in globals():
global _py_ext global _py_ext
_py_ext = [ _py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0]
triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE
][0]
# don't recurse into CVS directories # don't recurse into CVS directories
if "CVS" in names: if "CVS" in names:
names.remove("CVS") names.remove("CVS")
# add all *.py files to list # add all *.py files to list
list.extend( list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext])
[
os.path.join(dirname, file)
for file in names
if os.path.splitext(file)[1] == _py_ext
]
)
def _get_modpkg_path(dotted_name, pathlist=None): def _get_modpkg_path(dotted_name, pathlist=None):
@@ -406,8 +398,7 @@ def main(source_files, outpath, keywords=None):
eater(*_token) eater(*_token)
except tokenize.TokenError as e: except tokenize.TokenError as e:
print( print(
"%s: %s, line %d, column %d" "%s: %s, line %d, column %d" % (e.args[0], filename, e.args[1][0], e.args[1][1]),
% (e.args[0], filename, e.args[1][0], e.args[1][1]),
file=sys.stderr, file=sys.stderr,
) )
finally: finally:

View File

@@ -7,10 +7,8 @@
import os.path as op import os.path as op
import re import re
from distutils.version import LooseVersion
from pkg_resources import load_entry_point, get_distribution
from .build import read_changelog_file, filereplace from .build import read_changelog_file, filereplace
from sphinx.cmd.build import build_main as sphinx_build
CHANGELOG_FORMAT = """ CHANGELOG_FORMAT = """
{version} ({date}) {version} ({date})
@@ -24,9 +22,7 @@ def tixgen(tixurl):
"""This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder """This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder
for the tix # for the tix #
""" """
urlpattern = tixurl.format( urlpattern = tixurl.format("\\1") # will be replaced buy the content of the first group in re
"\\1"
) # will be replaced buy the content of the first group in re
R = re.compile(r"#(\d+)") R = re.compile(r"#(\d+)")
repl = "`#\\1 <{}>`__".format(urlpattern) repl = "`#\\1 <{}>`__".format(urlpattern)
return lambda text: R.sub(repl, text) return lambda text: R.sub(repl, text)
@@ -63,9 +59,7 @@ def gen(
# The format of the changelog descriptions is in markdown, but since we only use bulled list # The format of the changelog descriptions is in markdown, but since we only use bulled list
# and links, it's not worth depending on the markdown package. A simple regexp suffice. # and links, it's not worth depending on the markdown package. A simple regexp suffice.
description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description) description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description)
rendered = CHANGELOG_FORMAT.format( rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
version=log["version"], date=log["date_str"], description=description
)
rendered_logs.append(rendered) rendered_logs.append(rendered)
confrepl["version"] = changelog[0]["version"] confrepl["version"] = changelog[0]["version"]
changelog_out = op.join(basepath, "changelog.rst") changelog_out = op.join(basepath, "changelog.rst")
@@ -73,26 +67,8 @@ def gen(
if op.exists(confpath): if op.exists(confpath):
conf_out = op.join(basepath, "conf.py") conf_out = op.join(basepath, "conf.py")
filereplace(confpath, conf_out, **confrepl) filereplace(confpath, conf_out, **confrepl)
if LooseVersion(get_distribution("sphinx").version) >= LooseVersion("1.7.0"):
from sphinx.cmd.build import build_main as sphinx_build
# Call the sphinx_build function, which is the same as doing sphinx-build from cli # Call the sphinx_build function, which is the same as doing sphinx-build from cli
try: try:
sphinx_build([basepath, destpath]) sphinx_build([basepath, destpath])
except SystemExit: except SystemExit:
print( print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")
"Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit"
)
else:
# We used to call sphinx-build with print_and_do(), but the problem was that the virtualenv
# of the calling python wasn't correctly considered and caused problems with documentation
# relying on autodoc (which tries to import the module to auto-document, but fail because of
# missing dependencies which are in the virtualenv). Here, we do exactly what is done when
# calling the command from bash.
cmd = load_entry_point("Sphinx", "console_scripts", "sphinx-build")
try:
cmd(["sphinx-build", basepath, destpath])
except SystemExit:
print(
"Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit"
)

View File

@@ -80,9 +80,7 @@ class TestCase_move_copy:
assert self.path["baz"].exists() assert self.path["baz"].exists()
assert not self.path["foo"].exists() assert not self.path["foo"].exists()
def test_copy_no_conflict( def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move
self, do_setup
): # No need to duplicate the rest of the tests... Let's just test on move
smart_copy(self.path + "foo", self.path + "baz") smart_copy(self.path + "foo", self.path + "baz")
assert self.path["baz"].exists() assert self.path["baz"].exists()
assert self.path["foo"].exists() assert self.path["foo"].exists()

View File

@@ -128,9 +128,7 @@ def test_repeater_with_repeated_notifications():
r.connect() r.connect()
listener.connect() listener.connect()
b.notify("hello") b.notify("hello")
b.notify( b.notify("foo") # if the repeater repeated this notif, we'd get a crash on HelloListener
"foo"
) # if the repeater repeated this notif, we'd get a crash on HelloListener
eq_(r.hello_count, 1) eq_(r.hello_count, 1)
eq_(listener.hello_count, 1) eq_(listener.hello_count, 1)
eq_(r.foo_count, 1) eq_(r.foo_count, 1)

View File

@@ -87,8 +87,7 @@ def test_filename(force_ossep):
def test_deal_with_empty_components(force_ossep): def test_deal_with_empty_components(force_ossep):
"""Keep ONLY a leading space, which means we want a leading slash. """Keep ONLY a leading space, which means we want a leading slash."""
"""
eq_("foo//bar", str(Path(("foo", "", "bar")))) eq_("foo//bar", str(Path(("foo", "", "bar"))))
eq_("/foo/bar", str(Path(("", "foo", "bar")))) eq_("/foo/bar", str(Path(("", "foo", "bar"))))
eq_("foo/bar", str(Path("foo/bar/"))) eq_("foo/bar", str(Path("foo/bar/")))
@@ -154,8 +153,7 @@ def test_path_slice(force_ossep):
def test_add_with_root_path(force_ossep): def test_add_with_root_path(force_ossep):
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f """if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f"""
"""
eq_("/foo/bar", str(Path("/foo") + Path("/bar"))) eq_("/foo/bar", str(Path("/foo") + Path("/bar")))
@@ -166,8 +164,7 @@ def test_create_with_tuple_that_have_slash_inside(force_ossep, monkeypatch):
def test_auto_decode_os_sep(force_ossep, monkeypatch): def test_auto_decode_os_sep(force_ossep, monkeypatch):
"""Path should decode any either / or os.sep, but always encode in os.sep. """Path should decode any either / or os.sep, but always encode in os.sep."""
"""
eq_(("foo\\bar", "bleh"), Path("foo\\bar/bleh")) eq_(("foo\\bar", "bleh"), Path("foo\\bar/bleh"))
monkeypatch.setattr(os, "sep", "\\") monkeypatch.setattr(os, "sep", "\\")
eq_(("foo", "bar/bleh"), Path("foo\\bar/bleh")) eq_(("foo", "bar/bleh"), Path("foo\\bar/bleh"))

View File

@@ -44,9 +44,7 @@ def test_guicalls():
# A GUISelectableList appropriately calls its view. # A GUISelectableList appropriately calls its view.
sl = GUISelectableList(["foo", "bar"]) sl = GUISelectableList(["foo", "bar"])
sl.view = CallLogger() sl.view = CallLogger()
sl.view.check_gui_calls( sl.view.check_gui_calls(["refresh"]) # Upon setting the view, we get a call to refresh()
["refresh"]
) # Upon setting the view, we get a call to refresh()
sl[1] = "baz" sl[1] = "baz"
sl.view.check_gui_calls(["refresh"]) sl.view.check_gui_calls(["refresh"])
sl.append("foo") sl.append("foo")

View File

@@ -105,9 +105,7 @@ def test_findall_dont_include_self():
# When calling findall with include_self=False, the node itself is never evaluated. # When calling findall with include_self=False, the node itself is never evaluated.
t = tree_with_some_nodes() t = tree_with_some_nodes()
del t._name # so that if the predicate is called on `t`, we crash del t._name # so that if the predicate is called on `t`, we crash
r = t.findall( r = t.findall(lambda n: not n.name.startswith("sub"), include_self=False) # no crash
lambda n: not n.name.startswith("sub"), include_self=False
) # no crash
eq_(set(r), set([t[0], t[1], t[2]])) eq_(set(r), set([t[0], t[1], t[2]]))

View File

@@ -105,9 +105,7 @@ def test_iterconsume():
# We just want to make sure that we return *all* items and that we're not mistakenly skipping # We just want to make sure that we return *all* items and that we're not mistakenly skipping
# one. # one.
eq_(list(range(2500)), list(iterconsume(list(range(2500))))) eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
eq_( eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))
list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False))
)
# --- String # --- String

View File

@@ -86,9 +86,7 @@ class CallLogger:
eq_(set(self.calls), set(expected)) eq_(set(self.calls), set(expected))
self.clear_calls() self.clear_calls()
def check_gui_calls_partial( def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):
self, expected=None, not_expected=None, verify_order=False
):
"""Checks that the expected calls have been made to 'self', then clears the log. """Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter. `expected` is an iterable of strings representing method names. Order doesn't matter.
@@ -99,25 +97,17 @@ class CallLogger:
__tracebackhide__ = True __tracebackhide__ = True
if expected is not None: if expected is not None:
not_called = set(expected) - set(self.calls) not_called = set(expected) - set(self.calls)
assert not not_called, "These calls haven't been made: {0}".format( assert not not_called, "These calls haven't been made: {0}".format(not_called)
not_called
)
if verify_order: if verify_order:
max_index = 0 max_index = 0
for call in expected: for call in expected:
index = self.calls.index(call) index = self.calls.index(call)
if index < max_index: if index < max_index:
raise AssertionError( raise AssertionError("The call {0} hasn't been made in the correct order".format(call))
"The call {0} hasn't been made in the correct order".format(
call
)
)
max_index = index max_index = index
if not_expected is not None: if not_expected is not None:
called = set(not_expected) & set(self.calls) called = set(not_expected) & set(self.calls)
assert not called, "These calls shouldn't have been made: {0}".format( assert not called, "These calls shouldn't have been made: {0}".format(called)
called
)
self.clear_calls() self.clear_calls()
@@ -211,9 +201,7 @@ def _unify_args(func, args, kwargs, args_to_ignore=None):
result = kwargs.copy() result = kwargs.copy()
if hasattr(func, "__code__"): # built-in functions don't have func_code if hasattr(func, "__code__"): # built-in functions don't have func_code
args = list(args) args = list(args)
if ( if getattr(func, "__self__", None) is not None: # bound method, we have to add self to args list
getattr(func, "__self__", None) is not None
): # bound method, we have to add self to args list
args = [func.__self__] + args args = [func.__self__] + args
defaults = list(func.__defaults__) if func.__defaults__ is not None else [] defaults = list(func.__defaults__) if func.__defaults__ is not None else []
arg_count = func.__code__.co_argcount arg_count = func.__code__.co_argcount

View File

@@ -13,7 +13,7 @@ import locale
import logging import logging
import os.path as op import os.path as op
from .plat import ISWINDOWS, ISLINUX from .plat import ISLINUX
_trfunc = None _trfunc = None
_trget = None _trget = None
@@ -46,36 +46,23 @@ def set_tr(new_tr, new_trget=None):
def get_locale_name(lang): def get_locale_name(lang):
if ISWINDOWS: # Removed old conversion code as windows seems to support these
# http://msdn.microsoft.com/en-us/library/39cwe7zf(vs.71).aspx
LANG2LOCALENAME = {
"cs": "czy",
"de": "deu",
"el": "grc",
"es": "esn",
"fr": "fra",
"it": "ita",
"ko": "korean",
"nl": "nld",
"pl_PL": "polish_poland",
"pt_BR": "ptb",
"ru": "rus",
"zh_CN": "chs",
}
else:
LANG2LOCALENAME = { LANG2LOCALENAME = {
"cs": "cs_CZ", "cs": "cs_CZ",
"de": "de_DE", "de": "de_DE",
"el": "el_GR", "el": "el_GR",
"en": "en",
"es": "es_ES", "es": "es_ES",
"fr": "fr_FR", "fr": "fr_FR",
"it": "it_IT",
"nl": "nl_NL",
"hy": "hy_AM", "hy": "hy_AM",
"it": "it_IT",
"ja": "ja_JP",
"ko": "ko_KR", "ko": "ko_KR",
"nl": "nl_NL",
"pl_PL": "pl_PL", "pl_PL": "pl_PL",
"pt_BR": "pt_BR", "pt_BR": "pt_BR",
"ru": "ru_RU", "ru": "ru_RU",
"tr": "tr_TR",
"uk": "uk_UA", "uk": "uk_UA",
"vi": "vi_VN", "vi": "vi_VN",
"zh_CN": "zh_CN", "zh_CN": "zh_CN",
@@ -123,9 +110,7 @@ def install_gettext_trans(base_folder, lang):
if not lang: if not lang:
return lambda s: s return lambda s: s
try: try:
return gettext.translation( return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
domain, localedir=base_folder, languages=[lang]
).gettext
except IOError: except IOError:
return lambda s: s return lambda s: s

View File

@@ -19,8 +19,7 @@ from .path import Path, pathify, log_io_error
def nonone(value, replace_value): def nonone(value, replace_value):
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise. """Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise."""
"""
if value is None: if value is None:
return replace_value return replace_value
else: else:
@@ -28,8 +27,7 @@ def nonone(value, replace_value):
def tryint(value, default=0): def tryint(value, default=0):
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails. """Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails."""
"""
try: try:
return int(value) return int(value)
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -37,8 +35,7 @@ def tryint(value, default=0):
def minmax(value, min_value, max_value): def minmax(value, min_value, max_value):
"""Returns `value` or one of the min/max bounds if `value` is not between them. """Returns `value` or one of the min/max bounds if `value` is not between them."""
"""
return min(max(value, min_value), max_value) return min(max(value, min_value), max_value)
@@ -75,8 +72,7 @@ def flatten(iterables, start_with=None):
def first(iterable): def first(iterable):
"""Returns the first item of ``iterable``. """Returns the first item of ``iterable``."""
"""
try: try:
return next(iter(iterable)) return next(iter(iterable))
except StopIteration: except StopIteration:
@@ -84,14 +80,12 @@ def first(iterable):
def stripfalse(seq): def stripfalse(seq):
"""Returns a sequence with all false elements stripped out of seq. """Returns a sequence with all false elements stripped out of seq."""
"""
return [x for x in seq if x] return [x for x in seq if x]
def extract(predicate, iterable): def extract(predicate, iterable):
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both. """Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both."""
"""
wheat = [] wheat = []
shaft = [] shaft = []
for item in iterable: for item in iterable:
@@ -103,8 +97,7 @@ def extract(predicate, iterable):
def allsame(iterable): def allsame(iterable):
"""Returns whether all elements of 'iterable' are the same. """Returns whether all elements of 'iterable' are the same."""
"""
it = iter(iterable) it = iter(iterable)
try: try:
first_item = next(it) first_item = next(it)
@@ -152,14 +145,12 @@ def iterconsume(seq, reverse=True):
def escape(s, to_escape, escape_with="\\"): def escape(s, to_escape, escape_with="\\"):
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``. """Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``."""
"""
return "".join((escape_with + c if c in to_escape else c) for c in s) return "".join((escape_with + c if c in to_escape else c) for c in s)
def get_file_ext(filename): def get_file_ext(filename):
"""Returns the lowercase extension part of filename, without the dot. """Returns the lowercase extension part of filename, without the dot."""
"""
pos = filename.rfind(".") pos = filename.rfind(".")
if pos > -1: if pos > -1:
return filename[pos + 1 :].lower() return filename[pos + 1 :].lower()
@@ -168,8 +159,7 @@ def get_file_ext(filename):
def rem_file_ext(filename): def rem_file_ext(filename):
"""Returns the filename without extension. """Returns the filename without extension."""
"""
pos = filename.rfind(".") pos = filename.rfind(".")
if pos > -1: if pos > -1:
return filename[:pos] return filename[:pos]
@@ -217,8 +207,7 @@ def format_time(seconds, with_hours=True):
def format_time_decimal(seconds): def format_time_decimal(seconds):
"""Transforms seconds in a strings like '3.4 minutes'. """Transforms seconds in a strings like '3.4 minutes'."""
"""
minus = seconds < 0 minus = seconds < 0
if minus: if minus:
seconds *= -1 seconds *= -1
@@ -320,8 +309,7 @@ ONE_DAY = timedelta(1)
def iterdaterange(start, end): def iterdaterange(start, end):
"""Yields every day between ``start`` and ``end``. """Yields every day between ``start`` and ``end``."""
"""
date = start date = start
while date <= end: while date <= end:
yield date yield date
@@ -365,8 +353,7 @@ def find_in_path(name, paths=None):
@log_io_error @log_io_error
@pathify @pathify
def delete_if_empty(path: Path, files_to_delete=[]): def delete_if_empty(path: Path, files_to_delete=[]):
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete. """Deletes the directory at 'path' if it is empty or if it only contains files_to_delete."""
"""
if not path.exists() or not path.isdir(): if not path.exists() or not path.isdir():
return return
contents = path.listdir() contents = path.listdir()
@@ -411,8 +398,7 @@ def ensure_file(path):
def delete_files_with_pattern(folder_path, pattern, recursive=True): def delete_files_with_pattern(folder_path, pattern, recursive=True):
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`. """Delete all files (or folders) in `folder_path` that match the glob `pattern`."""
"""
to_delete = glob.glob(op.join(folder_path, pattern)) to_delete = glob.glob(op.join(folder_path, pattern))
for fn in to_delete: for fn in to_delete:
if op.isdir(fn): if op.isdir(fn):

View File

@@ -84,7 +84,7 @@ msgstr ""
msgid "Select a destination for your exported CSV" msgid "Select a destination for your exported CSV"
msgstr "" msgstr ""
#: core\app.py:534 core\app.py:801 core\app.py:811 #: core\app.py:534 core\app.py:803 core\app.py:813
msgid "Couldn't write to file: {}" msgid "Couldn't write to file: {}"
msgstr "" msgstr ""
@@ -96,27 +96,27 @@ msgstr ""
msgid "You are about to remove %d files from results. Continue?" msgid "You are about to remove %d files from results. Continue?"
msgstr "" msgstr ""
#: core\app.py:774 #: core\app.py:776
msgid "{} duplicate groups were changed by the re-prioritization." msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "" msgstr ""
#: core\app.py:821 #: core\app.py:823
msgid "The selected directories contain no scannable file." msgid "The selected directories contain no scannable file."
msgstr "" msgstr ""
#: core\app.py:835 #: core\app.py:837
msgid "Collecting files to scan" msgid "Collecting files to scan"
msgstr "" msgstr ""
#: core\app.py:891 #: core\app.py:893
msgid "%s (%d discarded)" msgid "%s (%d discarded)"
msgstr "" msgstr ""
#: core\engine.py:244 core\engine.py:288 #: core\engine.py:255 core\engine.py:299
msgid "0 matches found" msgid "0 matches found"
msgstr "" msgstr ""
#: core\engine.py:262 core\engine.py:296 #: core\engine.py:273 core\engine.py:307
msgid "%d matches found" msgid "%d matches found"
msgstr "" msgstr ""
@@ -212,11 +212,11 @@ msgstr ""
msgid "Oldest" msgid "Oldest"
msgstr "" msgstr ""
#: core\results.py:142 #: core\results.py:144
msgid "%d / %d (%s / %s) duplicates marked." msgid "%d / %d (%s / %s) duplicates marked."
msgstr "" msgstr ""
#: core\results.py:149 #: core\results.py:151
msgid " filter: %s" msgid " filter: %s"
msgstr "" msgstr ""

View File

@@ -308,7 +308,7 @@ msgstr "Rimuovi le cartelle vuote dopo aver cancellato o spostato"
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file" msgid "Ignore duplicates hardlinking to the same file"
msgstr "Non creare gli hardlink per i duplicati verso il medesimo file" msgstr "Non considerare gli hardlink come duplicati"
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0

View File

@@ -1,9 +1,10 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Bas <duvel3@gmail.com>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\n" "Last-Translator: Bas <duvel3@gmail.com>, 2021\n"
"Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n"
"Language: nl\n" "Language: nl\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -13,11 +14,11 @@ msgstr ""
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20 #: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
#: core\gui\problem_table.py:18 #: core\gui\problem_table.py:18
msgid "File Path" msgid "File Path"
msgstr "Bestand locatie" msgstr "Bestandspad"
#: core\gui\problem_table.py:19 #: core\gui\problem_table.py:19
msgid "Error Message" msgid "Error Message"
msgstr "Fout Melding" msgstr "Foutmelding"
#: core\me\prioritize.py:23 #: core\me\prioritize.py:23
msgid "Duration" msgid "Duration"
@@ -39,11 +40,11 @@ msgstr "Bestandsnaam"
#: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75 #: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75
#: core\se\result_table.py:20 #: core\se\result_table.py:20
msgid "Folder" msgid "Folder"
msgstr "Folder" msgstr "Map"
#: core\me\result_table.py:21 #: core\me\result_table.py:21
msgid "Size (MB)" msgid "Size (MB)"
msgstr "Grote (MB)" msgstr "Grootte (MB)"
#: core\me\result_table.py:22 #: core\me\result_table.py:22
msgid "Time" msgid "Time"
@@ -56,7 +57,7 @@ msgstr "Sample Frequentie"
#: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65 #: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65
#: core\se\result_table.py:22 #: core\se\result_table.py:22
msgid "Kind" msgid "Kind"
msgstr "Kind" msgstr "Soort"
#: core\me\result_table.py:26 core\pe\result_table.py:25 #: core\me\result_table.py:26 core\pe\result_table.py:25
#: core\prioritize.py:163 core\se\result_table.py:23 #: core\prioritize.py:163 core\se\result_table.py:23
@@ -111,7 +112,7 @@ msgstr "Afmetingen"
#: core\pe\result_table.py:21 core\se\result_table.py:21 #: core\pe\result_table.py:21 core\se\result_table.py:21
msgid "Size (KB)" msgid "Size (KB)"
msgstr "Grote (KB)" msgstr "Grootte (KB)"
#: core\pe\result_table.py:24 #: core\pe\result_table.py:24
msgid "EXIF Timestamp" msgid "EXIF Timestamp"
@@ -119,4 +120,4 @@ msgstr "EXIF Tijdstip"
#: core\prioritize.py:156 #: core\prioritize.py:156
msgid "Size" msgid "Size"
msgstr "Grote" msgstr "Grootte"

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Bas <duvel3@gmail.com>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Bas <duvel3@gmail.com>, 2021\n"
"Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n"
"Language: nl\n" "Language: nl\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -42,7 +43,7 @@ msgstr "Verplaatsen"
#: core\app.py:74 #: core\app.py:74
msgid "Copying" msgid "Copying"
msgstr "Kopieeren" msgstr "Kopiëren"
#: core\app.py:75 #: core\app.py:75
msgid "Sending to Trash" msgid "Sending to Trash"
@@ -151,7 +152,7 @@ msgstr "%d overeenkomsten gevonden"
#: core\gui\deletion_options.py:73 #: core\gui\deletion_options.py:73
msgid "You are sending {} file(s) to the Trash." msgid "You are sending {} file(s) to the Trash."
msgstr "Je verplaatst {} bestanden naar de prullenbak" msgstr "Je verplaatst {} bestand(en) naar de prullenbak"
#: core\gui\exclude_list_table.py:15 #: core\gui\exclude_list_table.py:15
msgid "Regular Expressions" msgid "Regular Expressions"
@@ -160,7 +161,7 @@ msgstr "Normale Uitdrukkingen"
#: core\gui\ignore_list_dialog.py:25 #: core\gui\ignore_list_dialog.py:25
msgid "Do you really want to remove all %d items from the ignore list?" msgid "Do you really want to remove all %d items from the ignore list?"
msgstr "" msgstr ""
"Weet je zeker dat je all %d regels uit de overslaan lijst wilt verwijderen?" "Weet je zeker dat je alle %d regels uit de overslaan lijst wilt verwijderen?"
#: core\me\scanner.py:20 core\se\scanner.py:16 #: core\me\scanner.py:20 core\se\scanner.py:16
msgid "Filename" msgid "Filename"
@@ -252,7 +253,7 @@ msgstr "filter: %s"
#: core\scanner.py:85 #: core\scanner.py:85
msgid "Read size of %d/%d files" msgid "Read size of %d/%d files"
msgstr "Bestands grote van %d/%d bestanden aan het lezen." msgstr "Bestandsgrootte van %d/%d bestanden aan het lezen."
#: core\scanner.py:109 #: core\scanner.py:109
msgid "Read metadata of %d/%d files" msgid "Read metadata of %d/%d files"

View File

@@ -1,10 +1,11 @@
# Translators: # Translators:
# Andrew Senetar <arsenetar@gmail.com>, 2021 # Andrew Senetar <arsenetar@gmail.com>, 2021
# Fuan <jcfrt@posteo.net>, 2021 # Fuan <jcfrt@posteo.net>, 2021
# Bas <duvel3@gmail.com>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n" "Last-Translator: Bas <duvel3@gmail.com>, 2021\n"
"Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n" "Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\n"
"Language: nl\n" "Language: nl\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -47,7 +48,7 @@ msgstr "Weet je zeker dat je de afbeeldings-analyse cache wilt verwijderen"
#: qt/app.py:184 #: qt/app.py:184
msgid "Picture cache cleared." msgid "Picture cache cleared."
msgstr "Afbeelding cache leeggemaakt" msgstr "Afbeelding cache leeggemaakt."
#: qt/app.py:251 #: qt/app.py:251
msgid "{} file (*.{})" msgid "{} file (*.{})"
@@ -55,7 +56,7 @@ msgstr "{} bestand (*.{})"
#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0 #: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0
msgid "Deletion Options" msgid "Deletion Options"
msgstr "verwijder opties" msgstr "Verwijderopties"
#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0 #: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0
msgid "Link deleted files" msgid "Link deleted files"
@@ -125,7 +126,7 @@ msgstr "Resultaten venster"
#: qt/directories_dialog.py:66 #: qt/directories_dialog.py:66
msgid "Add Folder..." msgid "Add Folder..."
msgstr "Folder toevoegen" msgstr "Folder toevoegen..."
#: qt/directories_dialog.py:74 qt/result_window.py:100 #: qt/directories_dialog.py:74 qt/result_window.py:100
#: cocoa/en.lproj/Localizable.strings:0 #: cocoa/en.lproj/Localizable.strings:0
@@ -287,7 +288,7 @@ msgstr "Word gewicht"
#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32 #: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32
#: cocoa/en.lproj/Localizable.strings:0 #: cocoa/en.lproj/Localizable.strings:0
msgid "Match similar words" msgid "Match similar words"
msgstr "vergelijk gelijkwaardige woorden" msgstr "Vergelijk gelijkwaardige woorden"
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21 #: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
@@ -307,7 +308,7 @@ msgstr "Verwijder lege folders tijdens weggooien of verplaatsen"
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27 #: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file" msgid "Ignore duplicates hardlinking to the same file"
msgstr "negeer dubbelingen die hard gelinkt zijn aan het zelfde bestand" msgstr "Negeer dubbelingen die hard gelinkt zijn aan het zelfde bestand"
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29 #: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0 #: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0
@@ -633,7 +634,7 @@ msgstr "Folder selectie venster"
#: cocoa/en.lproj/Localizable.strings:0 #: cocoa/en.lproj/Localizable.strings:0
msgid "Font Size:" msgid "Font Size:"
msgstr "Font grote:" msgstr "Grootte lettertype:"
#: cocoa/en.lproj/Localizable.strings:0 #: cocoa/en.lproj/Localizable.strings:0
msgid "Hide dupeGuru" msgid "Hide dupeGuru"

View File

@@ -0,0 +1,122 @@
# Translators:
# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021\n"
"Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n"
"Language: tr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: core\gui\ignore_list_table.py:19 core\gui\ignore_list_table.py:20
#: core\gui\problem_table.py:18
msgid "File Path"
msgstr "Dosya Yolu"
#: core\gui\problem_table.py:19
msgid "Error Message"
msgstr "Hata Mesajı"
#: core\me\prioritize.py:23
msgid "Duration"
msgstr "Süre"
#: core\me\prioritize.py:30 core\me\result_table.py:23
msgid "Bitrate"
msgstr "Bit hızı"
#: core\me\prioritize.py:37
msgid "Samplerate"
msgstr "Örnekleme hızı"
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
#: core\se\result_table.py:19
msgid "Filename"
msgstr "Dosya adı"
#: core\me\result_table.py:20 core\pe\result_table.py:20 core\prioritize.py:75
#: core\se\result_table.py:20
msgid "Folder"
msgstr "Dizin"
#: core\me\result_table.py:21
msgid "Size (MB)"
msgstr "Boyut (MB)"
#: core\me\result_table.py:22
msgid "Time"
msgstr "Zaman"
#: core\me\result_table.py:24
msgid "Sample Rate"
msgstr "Örnekleme Hızı"
#: core\me\result_table.py:25 core\pe\result_table.py:22 core\prioritize.py:65
#: core\se\result_table.py:22
msgid "Kind"
msgstr "Tür"
#: core\me\result_table.py:26 core\pe\result_table.py:25
#: core\prioritize.py:163 core\se\result_table.py:23
msgid "Modification"
msgstr "Düzenleme"
#: core\me\result_table.py:27
msgid "Title"
msgstr "Başlık"
#: core\me\result_table.py:28
msgid "Artist"
msgstr "Sanatçı"
#: core\me\result_table.py:29
msgid "Album"
msgstr "Albüm"
#: core\me\result_table.py:30
msgid "Genre"
msgstr "Tarz"
#: core\me\result_table.py:31
msgid "Year"
msgstr "Yıl"
#: core\me\result_table.py:32
msgid "Track Number"
msgstr "Parça Numarası"
#: core\me\result_table.py:33
msgid "Comment"
msgstr "Yorum"
#: core\me\result_table.py:34 core\pe\result_table.py:26
#: core\se\result_table.py:24
msgid "Match %"
msgstr "Eşleşme oranı %"
#: core\me\result_table.py:35 core\se\result_table.py:25
msgid "Words Used"
msgstr "Kullanılan Kelimeler"
#: core\me\result_table.py:36 core\pe\result_table.py:27
#: core\se\result_table.py:26
msgid "Dupe Count"
msgstr "Kopya Sayısı"
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
msgid "Dimensions"
msgstr "Boyutlar"
#: core\pe\result_table.py:21 core\se\result_table.py:21
msgid "Size (KB)"
msgstr "Boyut (KB)"
#: core\pe\result_table.py:24
msgid "EXIF Timestamp"
msgstr "EXIF Zaman damgası"
#: core\prioritize.py:156
msgid "Size"
msgstr "Boyut"

View File

@@ -0,0 +1,258 @@
# Translators:
# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021\n"
"Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n"
"Language: tr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: core\app.py:42
msgid "There are no marked duplicates. Nothing has been done."
msgstr "İşaretlenmiş kopya yok. Hiçbir işlem yapılmadı."
#: core\app.py:43
msgid "There are no selected duplicates. Nothing has been done."
msgstr "Seçilmiş kopya yok. Hiçbir işlem yapılmadı."
#: core\app.py:44
msgid ""
"You're about to open many files at once. Depending on what those files are "
"opened with, doing so can create quite a mess. Continue?"
msgstr ""
"Aynı anda birçok dosyayı açmak üzeresiniz. Bu dosyaların neyle açıldığına "
"bağlı olarak, bunu yapmak büyük karışıklık yaratabilir. Yine de devam "
"edilsin mi?"
#: core\app.py:71
msgid "Scanning for duplicates"
msgstr "Kopyalar için taranıyor"
#: core\app.py:72
msgid "Loading"
msgstr "Yükleniyor"
#: core\app.py:73
msgid "Moving"
msgstr "Taşınıyor"
#: core\app.py:74
msgid "Copying"
msgstr "Kopyalanıyor"
#: core\app.py:75
msgid "Sending to Trash"
msgstr "Geri Dönüşüm Kutusuna gönderiliyor"
#: core\app.py:308
msgid ""
"A previous action is still hanging in there. You can't start a new one yet. "
"Wait a few seconds, then try again."
msgstr ""
"Önceki eylem hala tamamlanmadı. Henüz yeni bir eylem başlatamazsınız. Birkaç"
" saniye bekleyin, ardından tekrar deneyin."
#: core\app.py:318
msgid "No duplicates found."
msgstr "Hiç kopya bulunamadı."
#: core\app.py:333
msgid "All marked files were copied successfully."
msgstr "İşaretlenmiş tüm dosyalar başarıyla kopyalandı."
#: core\app.py:334
msgid "All marked files were moved successfully."
msgstr "İşaretlenmiş tüm dosyalar başarıyla taşındı."
#: core\app.py:335
msgid "All marked files were successfully sent to Trash."
msgstr "İşaretlenmiş tüm dosyalar başarıyla Geri Dönüşüm Kutusuna gönderildi."
#: core\app.py:343
msgid "Could not load file: {}"
msgstr "Dosya yüklenemedi: {}"
#: core\app.py:399
msgid "'{}' already is in the list."
msgstr "'{}' zaten listede."
#: core\app.py:401
msgid "'{}' does not exist."
msgstr "'{}' mevcut değil."
#: core\app.py:410
msgid ""
"All selected %d matches are going to be ignored in all subsequent scans. "
"Continue?"
msgstr ""
"Tüm seçili %deşleşmeleri sonraki taramalarda yok sayılacaktır. Devam edilsin"
" mi?"
#: core\app.py:486
msgid "Select a directory to copy marked files to"
msgstr "İşaretlenmiş dosyaları kopyalamak için bir dizin seçin"
#: core\app.py:487
msgid "Select a directory to move marked files to"
msgstr "İşaretlenmiş dosyaları taşımak için bir dizin seçin"
#: core\app.py:527
msgid "Select a destination for your exported CSV"
msgstr "Dışa aktarılacak CSV dosyası için bir hedef seçin"
#: core\app.py:534 core\app.py:801 core\app.py:811
msgid "Couldn't write to file: {}"
msgstr "Dosyaya yazılamadı: {}"
#: core\app.py:559
msgid "You have no custom command set up. Set it up in your preferences."
msgstr "Özel bir komut ayarınız yok. Tercihlerinizden ayarlayabilirsiniz."
#: core\app.py:727 core\app.py:740
msgid "You are about to remove %d files from results. Continue?"
msgstr "Sonuçlardan%ddosyaları çıkarmak üzeresiniz. Devam edilsin mi?"
#: core\app.py:774
msgid "{} duplicate groups were changed by the re-prioritization."
msgstr "{} yinelenen gruplar, yeniden önceliklendirme ile değiştirildi."
#: core\app.py:821
msgid "The selected directories contain no scannable file."
msgstr "Seçili dizinler taranabilir dosya içermiyor."
#: core\app.py:835
msgid "Collecting files to scan"
msgstr "Taranacak dosyalar toplanıyor"
#: core\app.py:891
msgid "%s (%d discarded)"
msgstr "%s(%d atıldı)"
#: core\engine.py:244 core\engine.py:288
msgid "0 matches found"
msgstr "0 eşleşme bulundu"
#: core\engine.py:262 core\engine.py:296
msgid "%d matches found"
msgstr "%deşleşme bulundu"
#: core\gui\deletion_options.py:73
msgid "You are sending {} file(s) to the Trash."
msgstr "{} dosyayı/dosyaları Geri Dönüşüm Kutusuna gönderiyorsunuz."
#: core\gui\exclude_list_table.py:15
msgid "Regular Expressions"
msgstr "Düzenli İfadeler"
#: core\gui\ignore_list_dialog.py:25
msgid "Do you really want to remove all %d items from the ignore list?"
msgstr ""
"Yok sayılanlar listesinden %d öğelerin tümünü çıkarmak istediğinize emin "
"misiniz?"
#: core\me\scanner.py:20 core\se\scanner.py:16
msgid "Filename"
msgstr "Dosya adı"
#: core\me\scanner.py:21
msgid "Filename - Fields"
msgstr "Dosya adı - Alanlar"
#: core\me\scanner.py:22
msgid "Filename - Fields (No Order)"
msgstr "Dosya Adı - Alanlar (Düzen Yok)"
#: core\me\scanner.py:23
msgid "Tags"
msgstr "Etiketler"
#: core\me\scanner.py:24 core\pe\scanner.py:21 core\se\scanner.py:17
msgid "Contents"
msgstr "İçindekiler"
#: core\pe\matchblock.py:72
msgid "Analyzed %d/%d pictures"
msgstr " %d/%d resim analiz edildi."
#: core\pe\matchblock.py:181
msgid "Performed %d/%d chunk matches"
msgstr "%d/%d öbek eşleştirme gerçekleştirildi"
#: core\pe\matchblock.py:191
msgid "Preparing for matching"
msgstr "Eşleştirmek için hazırlanılıyor"
#: core\pe\matchblock.py:244
msgid "Verified %d/%d matches"
msgstr "%d/%d eşleşmeler doğrulandı"
#: core\pe\matchexif.py:19
msgid "Read EXIF of %d/%d pictures"
msgstr "%d/%d resimlerin EXIF'i okunuyor"
#: core\pe\scanner.py:22
msgid "EXIF Timestamp"
msgstr "EXIF Zaman damgası"
#: core\prioritize.py:70
msgid "None"
msgstr "Hiçbiri"
#: core\prioritize.py:100
msgid "Ends with number"
msgstr "Sayıyla biter"
#: core\prioritize.py:101
msgid "Doesn't end with number"
msgstr "Sayıyla bitmez"
#: core\prioritize.py:102
msgid "Longest"
msgstr "En uzun"
#: core\prioritize.py:103
msgid "Shortest"
msgstr "En kısa"
#: core\prioritize.py:140
msgid "Highest"
msgstr "En yüksek"
#: core\prioritize.py:140
msgid "Lowest"
msgstr "En düşük"
#: core\prioritize.py:169
msgid "Newest"
msgstr "En yeni"
#: core\prioritize.py:169
msgid "Oldest"
msgstr "En eski"
#: core\results.py:142
msgid "%d / %d (%s / %s) duplicates marked."
msgstr "%d/%d(%s/%s) kopyalar işaretlendi."
#: core\results.py:149
msgid " filter: %s"
msgstr "filtrele: %s"
#: core\scanner.py:85
msgid "Read size of %d/%d files"
msgstr "%d/%d dosyaların boyutunu oku"
#: core\scanner.py:109
msgid "Read metadata of %d/%d files"
msgstr "%d/%d dosyaların üst verisini(metadata) oku"
#: core\scanner.py:147
msgid "Almost done! Fiddling with results..."
msgstr "Neredeyse bitti! Sonuçlarla uğraşılıyor..."
#: core\se\scanner.py:18
msgid "Folders"
msgstr "Dizinler"

947
locale/tr/LC_MESSAGES/ui.po Normal file
View File

@@ -0,0 +1,947 @@
# Translators:
# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021
#
msgid ""
msgstr ""
"Last-Translator: Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021\n"
"Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\n"
"Language: tr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: qt/app.py:81
msgid "Quit"
msgstr "Çık"
#: qt/app.py:82 qt/preferences_dialog.py:116
#: cocoa/en.lproj/Localizable.strings:0
msgid "Options"
msgstr "Seçenekler"
#: qt/app.py:83 qt/ignore_list_dialog.py:32
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ignore List"
msgstr "Yoksayılanlar Listesi"
#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0
msgid "Clear Picture Cache"
msgstr "Resim Önbelliğini Temizle"
#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Help"
msgstr "dupeGuru Yardımı"
#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0
msgid "About dupeGuru"
msgstr "dupeGuru Hakkında"
#: qt/app.py:87
msgid "Open Debug Log"
msgstr "Hata Ayıklama Günlüğünü Aç"
#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0
msgid "Do you really want to remove all your cached picture analysis?"
msgstr ""
"Önbelleğe alınmış tüm resim analizlerinizi gerçekten kaldırmak istiyor "
"musunuz?"
#: qt/app.py:184
msgid "Picture cache cleared."
msgstr "Resim önbelleği temizlendi."
#: qt/app.py:251
msgid "{} file (*.{})"
msgstr "{} dosya (*.{})"
#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0
msgid "Deletion Options"
msgstr "Silme Seçenekleri"
#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0
msgid "Link deleted files"
msgstr "Silinen dosyaları bağla"
#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0
msgid ""
"After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file."
msgstr ""
"Bir kopyayı sildikten sonra, silinen dosyayı değiştirmek için referans "
"dosyayı hedefleyen bir bağlantı yerleştirin."
#: qt/deletion_options.py:44
msgid "Hardlink"
msgstr "Hard link"
#: qt/deletion_options.py:44
msgid "Symlink"
msgstr "Sembolik link"
#: qt/deletion_options.py:48
msgid " (unsupported)"
msgstr "(desteklenmiyor)"
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
msgid "Directly delete files"
msgstr "Doğrudan dosyaları sil"
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
msgid ""
"Instead of sending files to trash, delete them directly. This option is "
"usually used as a workaround when the normal deletion method doesn't work."
msgstr ""
"Dosyaları Geri Dönüşüm Kutusuna göndermek yerine, onları doğrudan sil. Bu "
"seçenek genellikle normal silme yöntemi çalışmadığında geçici bir çözüm "
"olarak kullanılır."
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Proceed"
msgstr "Devam Et"
#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0
msgid "Cancel"
msgstr "İptal Et"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Attribute"
msgstr "Özellik"
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
msgid "Selected"
msgstr "Seçili"
#: qt/details_table.py:16 qt/directories_model.py:24
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reference"
msgstr "Referans"
#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0
msgid "Load Results..."
msgstr "Sonuçları Yükle..."
#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0
msgid "Results Window"
msgstr "Sonuç Penceresi"
#: qt/directories_dialog.py:66
msgid "Add Folder..."
msgstr "Dizini Ekle..."
#: qt/directories_dialog.py:74 qt/result_window.py:100
#: cocoa/en.lproj/Localizable.strings:0
msgid "File"
msgstr "Dosya"
#: qt/directories_dialog.py:76 qt/result_window.py:108
msgid "View"
msgstr "Görüntüle"
#: qt/directories_dialog.py:78 qt/result_window.py:110
#: cocoa/en.lproj/Localizable.strings:0
msgid "Help"
msgstr "Yardım"
#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0
msgid "Load Recent Results"
msgstr "Son Sonuçları Yükle"
#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0
msgid "Application Mode:"
msgstr "Uygulama Modu:"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Music"
msgstr "Müzik"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Picture"
msgstr "Resim"
#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0
msgid "Standard"
msgstr "Standart"
#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0
msgid "Scan Type:"
msgstr "Tarama Türü:"
#: qt/directories_dialog.py:135
msgid "More Options"
msgstr "Daha Fazla Seçenek"
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
msgid "Select folders to scan and press \"Scan\"."
msgstr "Taranacak dizinleri seçin ve \"Tara\"ya basın."
#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0
msgid "Load Results"
msgstr "Sonuçları Yükle"
#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0
msgid "Scan"
msgstr "Tara"
#: qt/directories_dialog.py:230
msgid "Unsaved results"
msgstr "Kaydedilmeyen sonuçlar"
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to quit?"
msgstr "Kaydedilmeyen sonuçlarınız var, gerçekten çıkmak istiyor musunuz?"
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
msgid "Select a folder to add to the scanning list"
msgstr "Tarama listesine eklemek için bir klasör seçin"
#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0
msgid "Select a results file to load"
msgstr "Yüklenecek bir sonuç dosyası seçin"
#: qt/directories_dialog.py:267
msgid "All Files (*.*)"
msgstr "Tüm Dosyalar (*.*)"
#: qt/directories_dialog.py:267 qt/result_window.py:311
msgid "dupeGuru Results (*.dupeguru)"
msgstr "dupeGuru Sonuçları (*.dupeguru)"
#: qt/directories_dialog.py:278
msgid "Start a new scan"
msgstr "Yeni bir tarama başlat"
#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0
msgid "You have unsaved results, do you really want to continue?"
msgstr ""
"Kaydedilmeyen sonuçlarınız var, gerçekten devam etmek istiyor musunuz?"
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
msgid "Name"
msgstr "İsim"
#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0
msgid "State"
msgstr "Durum"
#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0
msgid "Excluded"
msgstr "Hariç Tutulan"
#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0
msgid "Normal"
msgstr "Normal"
#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Selected"
msgstr "Seçili Öğeyi Kaldır"
#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0
msgid "Clear"
msgstr "Temizle"
#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61
#: cocoa/en.lproj/Localizable.strings:0
msgid "Close"
msgstr "Kapat"
#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24
#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details"
msgstr "Ayrıntılar"
#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0
msgid "Tags to scan:"
msgstr "Taranacak etiketler:"
#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
msgid "Track"
msgstr "Parça"
#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Artist"
msgstr "Sanatçı"
#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0
msgid "Album"
msgstr "Albüm"
#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0
msgid "Title"
msgstr "Başlık"
#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0
msgid "Genre"
msgstr "Tarz"
#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0
msgid "Year"
msgstr "Yıl"
#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30
#: cocoa/en.lproj/Localizable.strings:0
msgid "Word weighting"
msgstr "Kelimeleri tartma"
#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32
#: cocoa/en.lproj/Localizable.strings:0
msgid "Match similar words"
msgstr "Benzer kelimeleri eşleştir"
#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21
#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0
msgid "Can mix file kind"
msgstr "Dosya türünü karıştırabilir"
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
msgid "Use regular expressions when filtering"
msgstr "Filtrelerken normal ifadeler kullan"
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
msgid "Remove empty folders on delete or move"
msgstr "Silme veya taşıma sırasında boş klasörleri kaldır"
#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27
#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Ignore duplicates hardlinking to the same file"
msgstr "Aynı dosyaya sabit bağlantı(hardlink) yapan kopyaları yoksay"
#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29
#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0
msgid "Debug mode (restart required)"
msgstr "Hata ayıklama modu (yeniden başlatma gerektirir)"
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
msgid "Match pictures of different dimensions"
msgstr "Farklı boyutlardaki resimleri eşleştir"
#: qt/preferences_dialog.py:43
msgid "Filter Hardness:"
msgstr "Filtre Sertliği:"
#: qt/preferences_dialog.py:69
msgid "More Results"
msgstr "Daha Fazla Sonuç"
#: qt/preferences_dialog.py:74
msgid "Fewer Results"
msgstr "Daha Az Sonuç"
#: qt/preferences_dialog.py:81
msgid "Font size:"
msgstr "Yazı boyutu:"
#: qt/preferences_dialog.py:85
msgid "Language:"
msgstr "Dil:"
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
msgid "Copy and Move:"
msgstr "Kopyala ve Taşı:"
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
msgid "Right in destination"
msgstr "Doğrudan hedef klasöre"
#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0
msgid "Recreate relative path"
msgstr "Göreli yolu yeniden oluştur"
#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0
msgid "Recreate absolute path"
msgstr "Mutlak yolu yeniden oluştur"
#: qt/preferences_dialog.py:99
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
msgstr "Özel Komut (argümanlar: %d kopya için, %r referans için):"
#: qt/preferences_dialog.py:174
msgid "dupeGuru has to restart for language changes to take effect."
msgstr ""
"Dil değişiklerinin etkili olması için dupeGuru'nun yeniden başlatılması "
"gerekir."
#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0
msgid "Re-Prioritize duplicates"
msgstr "Kopyaları yeniden önceliklendirin"
#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0
msgid ""
"Add criteria to the right box and click OK to send the dupes that correspond"
" the best to these criteria to their respective group's reference position. "
"Read the help file for more information."
msgstr ""
"Sağdaki kutuya kriter ekleyin ve bu kriterlere en iyi uyan kopyaları ilgili "
"grubun referans konumuna göndermek için Tamam'a tıklayın. Daha fazla bilgi "
"için yardım dosyasını okuyun."
#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0
msgid "Problems!"
msgstr "Problemler!"
#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0
msgid ""
"There were problems processing some (or all) of the files. The cause of "
"these problems are described in the table below. Those files were not "
"removed from your results."
msgstr ""
"Dosyaların bazılarını (veya tümünü) işlerken sorunlar oluştu. Bu sorunların "
"nedeni aşağıdaki tabloda açıklanmıştır. Bu dosyalar sonuçlarınızdan "
"kaldırılmadı."
#: qt/problem_dialog.py:56
msgid "Reveal Selected"
msgstr "Seçili Öğeyi Göster"
#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167
#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0
msgid "Actions"
msgstr "Eylemler"
#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0
msgid "Show Dupes Only"
msgstr "Sadece Kopyaları Göster"
#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0
msgid "Show Delta Values"
msgstr "Delta Değerlerini Göster"
#: qt/result_window.py:60
msgid "Send Marked to Recycle Bin..."
msgstr "İşaretlileri Geri Dönüşüm Kutusuna Gönder..."
#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0
msgid "Move Marked to..."
msgstr "İşaretlileri Şuraya Taşı..."
#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0
msgid "Copy Marked to..."
msgstr "İşaretlileri Şuraya Kopyala..."
#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Marked from Results"
msgstr "Sonuçlardan İşaretlileri Kaldır"
#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0
msgid "Re-Prioritize Results..."
msgstr "Sonuçları yeniden önceliklendirin..."
#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0
msgid "Remove Selected from Results"
msgstr "Sonuçlardan Seçili Öğeyi Kaldır"
#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0
msgid "Add Selected to Ignore List"
msgstr "Seçili Öğeyi Yok Sayılanlar Listesine Ekle"
#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0
msgid "Make Selected into Reference"
msgstr "Seçili Öğeyi Referans Yap"
#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0
msgid "Open Selected with Default Application"
msgstr "Seçili Öğeyi Varsayılan Uygulama ile Aç"
#: qt/result_window.py:80
msgid "Open Containing Folder of Selected"
msgstr "Seçili Öğenin Bulunduğu Klasörü Aç"
#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0
msgid "Rename Selected"
msgstr "Seçili Öğeyi Yeniden Adlandır"
#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0
msgid "Mark All"
msgstr "Tümünü İşaretle"
#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0
msgid "Mark None"
msgstr "Hiçbirini İşaretleme"
#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0
msgid "Invert Marking"
msgstr "İşaretleri Tersine Çevir"
#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0
msgid "Mark Selected"
msgstr "Seçili Öğeyi İşaretle"
#: qt/result_window.py:87
msgid "Export To HTML"
msgstr "HTML'ye Aktar"
#: qt/result_window.py:88
msgid "Export To CSV"
msgstr "CSV'ye Aktar"
#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0
msgid "Save Results..."
msgstr "Sonuçları Kaydet..."
#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0
msgid "Invoke Custom Command"
msgstr "Özel Komutu Çağır"
#: qt/result_window.py:102
msgid "Mark"
msgstr "İşaret"
#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0
msgid "Columns"
msgstr "Sütunlar"
#: qt/result_window.py:163
msgid "Reset to Defaults"
msgstr "Varsayılanlara Dön"
#: qt/result_window.py:185
msgid "{} Results"
msgstr "{} Sonuçlar"
#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0
msgid "Dupes Only"
msgstr "Sadece Kopyalar"
#: qt/result_window.py:194
msgid "Delta Values"
msgstr "Delta Değerleri"
#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0
msgid "Select a file to save your results to"
msgstr "Sonuçlarınızı kaydetmek için bir dosya seçin"
#: qt/se/preferences_dialog.py:41
msgid "Ignore files smaller than"
msgstr "Şu boyuttan küçük dosyaları yoksay"
#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0
msgid "KB"
msgstr "KB"
#: cocoa/en.lproj/Localizable.strings:0
msgid "%@ Results"
msgstr "%@ Sonuçlar"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Action"
msgstr "Eylem"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Add New Folder..."
msgstr "Yeni Klasör Ekle..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Advanced"
msgstr "Gelişmiş"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Automatically check for updates"
msgstr "Güncellemeleri otomatik olarak kontrol et"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Basic"
msgstr "Temel"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Bring All to Front"
msgstr "Tümünü Öne Getir"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Check for update..."
msgstr "Güncellemeleri kontrol et..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Close Window"
msgstr "Pencereyi Kapat"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Copy"
msgstr "Kopyala"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Custom command (arguments: %d for dupe, %r for ref):"
msgstr "Özel komutlar (argümanlar: %d kopya için, %r referans için):"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Cut"
msgstr "Kes"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Delta"
msgstr "Delta"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details of Selected File"
msgstr "Seçili Dosyanın Ayrıntıları"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Details Panel"
msgstr "Ayrıntılar Paneli"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Directories"
msgstr "Dizinler"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru"
msgstr "dupeGuru"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Preferences"
msgstr "dupeGuru Tercihleri"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Results"
msgstr "dupeGuru Sonuçları"
#: cocoa/en.lproj/Localizable.strings:0
msgid "dupeGuru Website"
msgstr "dupeGuru Websitesi"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Edit"
msgstr "Düzenle"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Export Results to CSV"
msgstr "Sonuçları CSV'ye Aktar"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Export Results to XHTML"
msgstr "Sonuçları XHTML'ye Aktar"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Fewer results"
msgstr "Daha az sonuç"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter"
msgstr "Filtrele"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter hardness:"
msgstr "Filtre sertliği:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Filter Results..."
msgstr "Sonuçları Filtrele..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Folder Selection Window"
msgstr "Klasör Seçim Penceresi"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Font Size:"
msgstr "Yazı Tipi Boyutu:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Hide dupeGuru"
msgstr "dupeGuru'yu Gizle"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Hide Others"
msgstr "Diğerlerini Gizle"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ignore files smaller than:"
msgstr "Şu boyuttan küçük dosyaları yoksay:"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Load from file..."
msgstr "Dosyadan yükle..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Minimize"
msgstr "Küçült"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Mode"
msgstr "Mod"
#: cocoa/en.lproj/Localizable.strings:0
msgid "More results"
msgstr "Daha fazla sonuç"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Ok"
msgstr "Tamam"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Paste"
msgstr "Yapıştır"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Preferences..."
msgstr "Tercihler..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Quick Look"
msgstr "Hızlı Bakış"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Quit dupeGuru"
msgstr "dupeGuru'dan Çık"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reset to Default"
msgstr "Varsayılana Dön"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reset To Defaults"
msgstr "Varsayılanlara Dön"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reveal"
msgstr "Ortaya Çıkar"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Reveal Selected in Finder"
msgstr "Seçili Öğeyi Finder'da Göster"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Select All"
msgstr "Tümünü Seç"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Send Marked to Trash..."
msgstr "İşaretlileri Geri Dönüşüm Kutusuna Gönder..."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Services"
msgstr "Hizmetler"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Show All"
msgstr "Tümünü Göster"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Start Duplicate Scan"
msgstr "Kopyaları Taramayı Başlat"
#: cocoa/en.lproj/Localizable.strings:0
msgid "The name '%@' already exists."
msgstr "'%@' adı zaten var."
#: cocoa/en.lproj/Localizable.strings:0
msgid "Window"
msgstr "Pencere"
#: cocoa/en.lproj/Localizable.strings:0
msgid "Zoom"
msgstr "Yakınlaştır"
#: qt\app.py:158
msgid "Exclusion Filters"
msgstr "Harici Bırakma Filtreleri"
#: qt\directories_dialog.py:91
msgid "Scan Results"
msgstr "Tarama Sonuçları"
#: qt\directories_dialog.py:95
msgid "Load Directories..."
msgstr "Dizinleri Yükle..."
#: qt\directories_dialog.py:96
msgid "Save Directories..."
msgstr "Dizinleri Kaydet..."
#: qt\directories_dialog.py:337
msgid "Select a directories file to load"
msgstr "Yüklenecek bir dizin dosyası seçin"
#: qt\directories_dialog.py:338
msgid "dupeGuru Results (*.dupegurudirs)"
msgstr "dupeGuru Sonuçları (*.dupegurudirs)"
#: qt\directories_dialog.py:347
msgid "Select a file to save your directories to"
msgstr "Dizinlerinizi kaydetmek için bir dosya seçin"
#: qt\directories_dialog.py:348
msgid "dupeGuru Directories (*.dupegurudirs)"
msgstr "dupeGuru Dizinleri (*.dupegurudirs)"
#: qt\exclude_list_dialog.py:44
msgid "Add"
msgstr "Ekle"
#: qt\exclude_list_dialog.py:46
msgid "Restore defaults"
msgstr "Varsayılanları geri yükle"
#: qt\exclude_list_dialog.py:47
msgid "Test string"
msgstr "Test dizisi"
#: qt\exclude_list_dialog.py:83
msgid "Type a python regular expression here..."
msgstr "Buraya bir Python düzenli ifadesi yazın..."
#: qt\exclude_list_dialog.py:85
msgid "Type a file system path or filename here..."
msgstr "Buraya bir dosya sistemi yolu veya dosya adı yazın..."
#: qt\exclude_list_dialog.py:152
msgid ""
"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\n"
"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\n"
"Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>"
msgstr ""
"Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri, taramalar sırasında dosyaları filtreleyecektir.<br>Dizinler ayrıca, adları seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesinde<strong>varsayılan durumları</strong>Hariç Tutuldu olarak ayarlanır.<br>Toplanan her dosya için, tamamen göz ardı edilip edilmeyeceğini belirlemek için dosyaların her birinin üzerinde iki test yapılır:<br><li>1. İçinde yol ayırıcı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılır.</li>\n"
"<li>2. İçinde yol ayırıcı olmayan normal ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.</li><br>\n"
"Örnek: .PNG dosyalarını yalnızca \"My Pictures\" dizininden filtrelemek istiyorsanız:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>Düzenli ifadeyi, test dizesi özelliğinin içine sahte bir yol yapıştırarak test edebilirsiniz:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Eşleşen normal ifadeler vurgulanacaktır.<br>En az bir vurgu varsa, test edilen yol taramalar sırasında yok sayılır<br><br>Nokta '.' ile başlayan dizinler ve dosyalar varsayılan olarak filtrelenir.<br><br>"
#: qt\exclude_list_table.py:36
msgid "Compilation error: "
msgstr "Derleme hatası:"
#: qt\pe\image_viewer.py:56
msgid "Increase zoom"
msgstr "Yakınlaştırmayı arttır"
#: qt\pe\image_viewer.py:66
msgid "Decrease zoom"
msgstr "Yakınlaştırmayı azalt"
#: qt\pe\image_viewer.py:71
msgid "Ctrl+/"
msgstr "Ctrl+/"
#: qt\pe\image_viewer.py:76
msgid "Normal size"
msgstr "Normal boyut"
#: qt\pe\image_viewer.py:81
msgid "Ctrl+*"
msgstr "Ctrl+*"
#: qt\pe\image_viewer.py:86
msgid "Best fit"
msgstr "En uygun"
#: qt\pe\preferences_dialog.py:49
msgid "Picture cache mode:"
msgstr "Resim önbellek modu:"
#: qt\pe\preferences_dialog.py:56
msgid "Override theme icons in viewer toolbar"
msgstr "Görüntüleyici araç çubuğundaki tema simgelerini geçersiz kıl"
#: qt\pe\preferences_dialog.py:58
msgid ""
"Use our own internal icons instead of those provided by the theme engine"
msgstr ""
"Tema motoru tarafından sağlananlar yerine kendi dahili simgelerimizi "
"kullanın"
#: qt\pe\preferences_dialog.py:66
msgid "Show scrollbars in image viewers"
msgstr "Resim görüntüleyicilerde kaydırma çubuklarını göster"
#: qt\pe\preferences_dialog.py:68
msgid ""
"When the image displayed doesn't fit the viewport, show scrollbars to span "
"the view around"
msgstr ""
"Görüntülenen görüntü görünüm alanına sığmadığında, görünümü etrafa yaymak "
"için kaydırma çubuklarını göster"
#: qt\preferences_dialog.py:156
msgid "Use default position for tab bar (requires restart)"
msgstr ""
"Sekme çubuğu için varsayılan konumu kullan (yeniden başlatma gerektirir)"
#: qt\preferences_dialog.py:158
msgid ""
"Place the tab bar below the main menu instead of next to it\n"
"On MacOS, the tab bar will fill up the window's width instead."
msgstr ""
"Sekme çubuğunu ana menünün yanına değil altına yerleştirin\n"
"MacOS'ta sekme çubuğu bunun yerine pencerenin genişliğini dolduracaktır."
#: qt\preferences_dialog.py:172
msgid "Use bold font for references"
msgstr "Referanslar için kalın yazı tipi kullanın"
#: qt\preferences_dialog.py:176
msgid "Reference foreground color:"
msgstr "Referans ön plan rengi:"
#: qt\preferences_dialog.py:179
msgid "Reference background color:"
msgstr "Referans arka plan rengi:"
#: qt\preferences_dialog.py:182 qt\preferences_dialog.py:216
msgid "Delta foreground color:"
msgstr "Delta ön plan rengi:"
#: qt\preferences_dialog.py:195
msgid "Show the title bar and can be docked"
msgstr "Başlık çubuğunu görüntüleyebilir ve sabitleyebilirsiniz."
#: qt\preferences_dialog.py:197
msgid ""
"While the title bar is hidden, use the modifier key to drag the floating "
"window around"
msgstr ""
"Başlık çubuğu gizliyken, kayan pencereyi etrafında sürüklemek için "
"değiştirici tuşu kullanın."
#: qt\preferences_dialog.py:199
msgid "The title bar can only be disabled while the window is docked"
msgstr ""
"Başlık çubuğu yalnızca pencere sabitlendiğinde devre dışı bırakılabilir"
#: qt\preferences_dialog.py:202
msgid "Vertical title bar"
msgstr "Dikey başlık çubuğu"
#: qt\preferences_dialog.py:204
msgid ""
"Change the title bar from horizontal on top, to vertical on the left side"
msgstr "Başlık çubuğunu üstte yataydan sol tarafta dikey olarak değiştirin"
#: qt\tabbed_window.py:44
msgid "Show tab bar"
msgstr "Sekme çubuğunu göster"
#: qt\exclude_list_dialog.py:152
msgid ""
"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\n"
"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\n"
"Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:<br><code>.*My\\sPictures\\\\.*\\.png</code><br><br>You can test the regular expression with the \"test string\" button after pasting a fake path in the test field:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>"
msgstr ""
"Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri, taramalar sırasında dosyaları filtreleyecektir.<br>Dizinler ayrıca, adları seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesinde <strong>varsayılan durumları</strong> Hariç Tutuldu olarak ayarlanır.<br>Toplanan her dosya için, tamamen göz ardı edilip edilmeyeceğini belirlemek için iki test yapılır:<br><li>1. İçinde yol ayırıcı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılacaktır.</li>\n"
"<li>2. İçinde en az bir yol ayırıcı bulunan düzenli ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.</li><br>\n"
"<br>Örnek: .PNG dosyalarını yalnızca \"My Pictures\" dizininden filtrelemek istiyorsanız:<code>.*My\\sPictures\\\\.*\\.png</code><br><br>Test alanına sahte bir yol yapıştırdıktan sonra normal ifadeyi \"test dizesi\" düğmesiyle test edebilirsiniz:<br><code>C:\\\\User\\My Pictures\\test.png</code><br><br>\n"
"Eşleşen normal ifadeler vurgulanacaktır.<br>En az bir vurgu varsa, test edilen yol veya dosya adı taramalar sırasında yok sayılır.<br><br>Nokta '.' ile başlayan dizinler ve dosyalar varsayılan olarak filtrelenir.<br><br>"
#: qt\app.py:256
msgid "Results"
msgstr "Sonuçlar"
#: qt\preferences_dialog.py:150
msgid "General Interface"
msgstr "Genel Arayüz"
#: qt\preferences_dialog.py:176
msgid "Result Table"
msgstr "Sonuç Tablosu"
#: qt\preferences_dialog.py:205
msgid "Details Window"
msgstr "Ayrıntı Penceresi"
#: qt\preferences_dialog.py:285
msgid "General"
msgstr "Genel"
#: qt\preferences_dialog.py:286
msgid "Display"
msgstr "Görüntüle"

View File

@@ -11,7 +11,7 @@
1. Install Xcode if desired 1. Install Xcode if desired
2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc` 2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`
with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take
affect. effect.
3. Install qt5 with `brew`. If you are using a version of macos without system python 3.6+ then you will 3. Install qt5 with `brew`. If you are using a version of macos without system python 3.6+ then you will
also need to install that via brew or with pyenv. also need to install that via brew or with pyenv.

View File

@@ -82,11 +82,7 @@ def package_debian_distribution(distribution):
copy(op.join(debskel, fn), op.join(debdest, fn)) copy(op.join(debskel, fn), op.join(debdest, fn))
filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts) filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts)
filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts) filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts)
filereplace( filereplace(op.join(debskel, "dupeguru.desktop"), op.join(debdest, "dupeguru.desktop"), **debopts)
op.join(debskel, "dupeguru.desktop"),
op.join(debdest, "dupeguru.desktop"),
**debopts
)
changelogpath = op.join("help", "changelog") changelogpath = op.join("help", "changelog")
changelog_dest = op.join(debdest, "changelog") changelog_dest = op.join(debdest, "changelog")
project_name = debopts["pkgname"] project_name = debopts["pkgname"]
@@ -128,11 +124,7 @@ def package_arch():
copy_files_to_package(srcpath, packages, with_so=True) copy_files_to_package(srcpath, packages, with_so=True)
shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath) shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath)
debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json"))) debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json")))
filereplace( filereplace(op.join("pkg", "arch", "dupeguru.desktop"), op.join(srcpath, "dupeguru.desktop"), **debopts)
op.join("pkg", "arch", "dupeguru.desktop"),
op.join(srcpath, "dupeguru.desktop"),
**debopts
)
def package_source_txz(): def package_source_txz():
@@ -173,11 +165,7 @@ def package_windows():
version_info = version_template.read() version_info = version_template.read()
version_template.close() version_template.close()
version_info_file = open("win_version_info.txt", "w") version_info_file = open("win_version_info.txt", "w")
version_info_file.write( version_info_file.write(version_info.format(version_array[0], version_array[1], version_array[2], bits))
version_info.format(
version_array[0], version_array[1], version_array[2], bits
)
)
version_info_file.close() version_info_file.close()
except Exception: except Exception:
print("Error creating version info file, exiting...") print("Error creating version info file, exiting...")
@@ -195,9 +183,7 @@ def package_windows():
"--add-data=build/locale;locale", "--add-data=build/locale;locale",
"--add-data=build/help;help", "--add-data=build/help;help",
"--version-file=win_version_info.txt", "--version-file=win_version_info.txt",
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format( "--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(arch),
arch
),
"run.py", "run.py",
] ]
) )

View File

@@ -6,19 +6,19 @@ import importlib
from setuptools import setup, Extension from setuptools import setup, Extension
sys.path.insert(1, op.abspath('src')) sys.path.insert(1, op.abspath("src"))
from hscommon.build import move_all from hscommon.build import move_all
exts = [ exts = [
Extension("_block", [op.join('modules', 'block.c'), op.join('modules', 'common.c')]), Extension("_block", [op.join("modules", "block.c"), op.join("modules", "common.c")]),
Extension("_cache", [op.join('modules', 'cache.c'), op.join('modules', 'common.c')]), Extension("_cache", [op.join("modules", "cache.c"), op.join("modules", "common.c")]),
Extension("_block_qt", [op.join('modules', 'block_qt.c')]), Extension("_block_qt", [op.join("modules", "block_qt.c")]),
] ]
setup( setup(
script_args = ['build_ext', '--inplace'], script_args=["build_ext", "--inplace"],
ext_modules=exts, ext_modules=exts,
) )
move_all('_block_qt*', op.join('src', 'qt', 'pe')) move_all("_block_qt*", op.join("src", "qt", "pe"))
move_all('_cache*', op.join('src', 'core/pe')) move_all("_cache*", op.join("src", "core/pe"))
move_all('_block*', op.join('src', 'core/pe')) move_all("_block*", op.join("src", "core/pe"))

5
pyproject.toml Normal file
View File

@@ -0,0 +1,5 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 120

View File

@@ -65,18 +65,10 @@ class DupeGuru(QObject):
self.recentResults.mustOpenItem.connect(self.model.load_from) self.recentResults.mustOpenItem.connect(self.model.load_from)
self.resultWindow = None self.resultWindow = None
if self.use_tabs: if self.use_tabs:
self.main_window = ( self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self)
TabBarWindow(self)
if not self.prefs.tabs_default_pos
else TabWindow(self)
)
parent_window = self.main_window parent_window = self.main_window
self.directories_dialog = self.main_window.createPage( self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self)
"DirectoriesDialog", app=self self.main_window.addTab(self.directories_dialog, tr("Directories"), switch=False)
)
self.main_window.addTab(
self.directories_dialog, tr("Directories"), switch=False
)
self.actionDirectoriesWindow.setEnabled(False) self.actionDirectoriesWindow.setEnabled(False)
else: # floating windows only else: # floating windows only
self.main_window = None self.main_window = None
@@ -84,9 +76,7 @@ class DupeGuru(QObject):
parent_window = self.directories_dialog parent_window = self.directories_dialog
self.progress_window = ProgressWindow(parent_window, self.model.progress_window) self.progress_window = ProgressWindow(parent_window, self.model.progress_window)
self.problemDialog = ProblemDialog( self.problemDialog = ProblemDialog(parent=parent_window, model=self.model.problem_dialog)
parent=parent_window, model=self.model.problem_dialog
)
if self.use_tabs: if self.use_tabs:
self.ignoreListDialog = self.main_window.createPage( self.ignoreListDialog = self.main_window.createPage(
"IgnoreListDialog", "IgnoreListDialog",
@@ -101,16 +91,10 @@ class DupeGuru(QObject):
model=self.model.exclude_list_dialog, model=self.model.exclude_list_dialog,
) )
else: else:
self.ignoreListDialog = IgnoreListDialog( self.ignoreListDialog = IgnoreListDialog(parent=parent_window, model=self.model.ignore_list_dialog)
parent=parent_window, model=self.model.ignore_list_dialog self.excludeDialog = ExcludeListDialog(app=self, parent=parent_window, model=self.model.exclude_list_dialog)
)
self.excludeDialog = ExcludeListDialog(
app=self, parent=parent_window, model=self.model.exclude_list_dialog
)
self.deletionOptions = DeletionOptions( self.deletionOptions = DeletionOptions(parent=parent_window, model=self.model.deletion_options)
parent=parent_window, model=self.model.deletion_options
)
self.about_box = AboutBox(parent_window, self) self.about_box = AboutBox(parent_window, self)
parent_window.show() parent_window.show()
@@ -174,20 +158,21 @@ class DupeGuru(QObject):
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
self.model.options[ self.model.options["ignore_hardlink_matches"] = self.prefs.ignore_hardlink_matches
"ignore_hardlink_matches"
] = self.prefs.ignore_hardlink_matches
self.model.options["copymove_dest_type"] = self.prefs.destination_type 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["scan_type"] = self.prefs.get_scan_type(self.model.app_mode)
self.model.options["min_match_percentage"] = self.prefs.filter_hardness self.model.options["min_match_percentage"] = self.prefs.filter_hardness
self.model.options["word_weighting"] = self.prefs.word_weighting self.model.options["word_weighting"] = self.prefs.word_weighting
self.model.options["match_similar_words"] = self.prefs.match_similar self.model.options["match_similar_words"] = self.prefs.match_similar
threshold = ( threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
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
big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
self.model.options["big_file_size_threshold"] = (
big_file_size_threshold
* 1024
* 1024
# threshold is in MiB. The scanner wants bytes
) )
self.model.options["size_threshold"] = (
threshold * 1024
) # threshold is in KB. the scanner wants bytes
scanned_tags = set() scanned_tags = set()
if self.prefs.scan_tag_track: if self.prefs.scan_tag_track:
scanned_tags.add("track") scanned_tags.add("track")
@@ -252,9 +237,7 @@ class DupeGuru(QObject):
if self.resultWindow is not None: if self.resultWindow is not None:
if self.use_tabs: if self.use_tabs:
if self.main_window.indexOfWidget(self.resultWindow) < 0: if self.main_window.indexOfWidget(self.resultWindow) < 0:
self.main_window.addTab( self.main_window.addTab(self.resultWindow, tr("Results"), switch=True)
self.resultWindow, tr("Results"), switch=True
)
return return
self.main_window.showTab(self.resultWindow) self.main_window.showTab(self.resultWindow)
else: else:
@@ -271,6 +254,9 @@ class DupeGuru(QObject):
self.willSavePrefs.emit() self.willSavePrefs.emit()
self.prefs.save() self.prefs.save()
self.model.save() self.model.save()
# Workaround for #857, hide() or close().
if self.details_dialog is not None:
self.details_dialog.close()
QApplication.quit() QApplication.quit()
# --- Signals # --- Signals
@@ -308,9 +294,7 @@ class DupeGuru(QObject):
def excludeListTriggered(self): def excludeListTriggered(self):
if self.use_tabs: if self.use_tabs:
self.showTriggeredTabbedDialog( self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
self.excludeListDialog, tr("Exclusion Filters")
)
else: # floating windows else: # floating windows
self.model.exclude_list_dialog.show() self.model.exclude_list_dialog.show()
@@ -318,9 +302,7 @@ class DupeGuru(QObject):
"""Add tab for dialog, name the tab with desc_string, then show it.""" """Add tab for dialog, name the tab with desc_string, then show it."""
index = self.main_window.indexOfWidget(dialog) index = self.main_window.indexOfWidget(dialog)
# Create the tab if it doesn't exist already # Create the tab if it doesn't exist already
if ( if index < 0: # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
index < 0
): # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
index = self.main_window.addTab(dialog, desc_string, switch=True) index = self.main_window.addTab(dialog, desc_string, switch=True)
# Show the tab for that widget # Show the tab for that widget
self.main_window.setCurrentIndex(index) self.main_window.setCurrentIndex(index)
@@ -392,13 +374,9 @@ class DupeGuru(QObject):
if self.resultWindow is not None: if self.resultWindow is not None:
self.resultWindow.close() self.resultWindow.close()
# This is better for tabs, as it takes care of duplicate items in menu bar # This is better for tabs, as it takes care of duplicate items in menu bar
self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent( self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(None)
None
)
if self.use_tabs: if self.use_tabs:
self.resultWindow = self.main_window.createPage( self.resultWindow = self.main_window.createPage("ResultWindow", parent=self.main_window, app=self)
"ResultWindow", parent=self.main_window, app=self
)
else: # We don't use a tab widget, regular floating QMainWindow else: # We don't use a tab widget, regular floating QMainWindow
self.resultWindow = ResultWindow(self.directories_dialog, self) self.resultWindow = ResultWindow(self.directories_dialog, self)
self.directories_dialog._updateActionsState() self.directories_dialog._updateActionsState()
@@ -416,9 +394,7 @@ class DupeGuru(QObject):
def select_dest_file(self, prompt, extension): def select_dest_file(self, prompt, extension):
files = tr("{} file (*.{})").format(extension.upper(), extension) files = tr("{} file (*.{})").format(extension.upper(), extension)
destination, chosen_filter = QFileDialog.getSaveFileName( destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
self.resultWindow, prompt, "", files
)
if not destination.endswith(".{}".format(extension)): if not destination.endswith(".{}".format(extension)):
destination = "{}.{}".format(destination, extension) destination = "{}.{}".format(destination, extension)
return destination return destination

View File

@@ -42,9 +42,7 @@ class DeletionOptions(QDialog):
self.linkMessageLabel = QLabel(text) self.linkMessageLabel = QLabel(text)
self.linkMessageLabel.setWordWrap(True) self.linkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.linkMessageLabel) self.verticalLayout.addWidget(self.linkMessageLabel)
self.linkTypeRadio = RadioBox( self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False)
items=[tr("Symlink"), tr("Hardlink")], spread=False
)
self.verticalLayout.addWidget(self.linkTypeRadio) self.verticalLayout.addWidget(self.linkTypeRadio)
if not self.model.supports_links(): if not self.model.supports_links():
self.linkCheckbox.setEnabled(False) self.linkCheckbox.setEnabled(False)

View File

@@ -31,8 +31,7 @@ class DetailsDialog(QDockWidget):
self.model.view = self self.model.view = self
self.app.willSavePrefs.connect(self.appWillSavePrefs) self.app.willSavePrefs.connect(self.appWillSavePrefs)
# self.setAttribute(Qt.WA_DeleteOnClose) # self.setAttribute(Qt.WA_DeleteOnClose)
parent.addDockWidget( parent.addDockWidget(area if self._wasDocked else Qt.BottomDockWidgetArea, self)
area if self._wasDocked else Qt.BottomDockWidgetArea, self)
def _setupUi(self): # Virtual def _setupUi(self): # Virtual
pass pass
@@ -51,7 +50,7 @@ class DetailsDialog(QDockWidget):
if not self.titleBarWidget(): # default title bar if not self.titleBarWidget(): # default title bar
self.setTitleBarWidget(QWidget()) # disables title bar self.setTitleBarWidget(QWidget()) # disables title bar
# Windows (and MacOS?) users cannot move a floating window which # Windows (and MacOS?) users cannot move a floating window which
# has not native decoration so we force it to dock for now # has no native decoration so we force it to dock for now
if not ISLINUX: if not ISLINUX:
self.setFloating(False) self.setFloating(False)
elif self.titleBarWidget() is not None: # title bar is disabled elif self.titleBarWidget() is not None: # title bar is disabled

View File

@@ -34,9 +34,11 @@ class DetailsModel(QAbstractTableModel):
row = index.row() row = index.row()
ignored_fields = ["Dupe Count"] ignored_fields = ["Dupe Count"]
if (self.model.row(row)[0] in ignored_fields if (
self.model.row(row)[0] in ignored_fields
or self.model.row(row)[1] == "---" or self.model.row(row)[1] == "---"
or self.model.row(row)[2] == "---"): or self.model.row(row)[2] == "---"
):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return None return None
return self.model.row(row)[column] return self.model.row(row)[column]
@@ -52,17 +54,9 @@ class DetailsModel(QAbstractTableModel):
return None # QVariant() return None # QVariant()
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if ( if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):
orientation == Qt.Horizontal
and role == Qt.DisplayRole
and section < len(HEADER)
):
return HEADER[section] return HEADER[section]
elif ( elif orientation == Qt.Vertical and role == Qt.DisplayRole and section < self.model.row_count():
orientation == Qt.Vertical
and role == Qt.DisplayRole
and section < self.model.row_count()
):
# Read "Attribute" cell for horizontal header # Read "Attribute" cell for horizontal header
return self.model.row(section)[0] return self.model.row(section)[0]
return None return None

View File

@@ -45,9 +45,7 @@ class DirectoriesDialog(QMainWindow):
self.recentFolders = Recent(self.app, "recentFolders") self.recentFolders = Recent(self.app, "recentFolders")
self._setupUi() self._setupUi()
self._updateScanTypeList() self._updateScanTypeList()
self.directoriesModel = DirectoriesModel( self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)
self.app.model.directory_tree, view=self.treeView
)
self.directoriesDelegate = DirectoriesDelegate() self.directoriesDelegate = DirectoriesDelegate()
self.treeView.setItemDelegate(self.directoriesDelegate) self.treeView.setItemDelegate(self.directoriesDelegate)
self._setupColumns() self._setupColumns()
@@ -170,9 +168,7 @@ class DirectoriesDialog(QMainWindow):
label = QLabel(tr("Application Mode:"), self) label = QLabel(tr("Application Mode:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label) hl.addWidget(label)
self.appModeRadioBox = RadioBox( self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False)
self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False
)
hl.addWidget(self.appModeRadioBox) hl.addWidget(self.appModeRadioBox)
self.verticalLayout.addLayout(hl) self.verticalLayout.addLayout(hl)
hl = QHBoxLayout() hl = QHBoxLayout()
@@ -181,27 +177,21 @@ class DirectoriesDialog(QMainWindow):
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label) hl.addWidget(label)
self.scanTypeComboBox = QComboBox(self) self.scanTypeComboBox = QComboBox(self)
self.scanTypeComboBox.setSizePolicy( self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
)
self.scanTypeComboBox.setMaximumWidth(400) self.scanTypeComboBox.setMaximumWidth(400)
hl.addWidget(self.scanTypeComboBox) hl.addWidget(self.scanTypeComboBox)
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget) self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(self.showPreferencesButton) hl.addWidget(self.showPreferencesButton)
self.verticalLayout.addLayout(hl) self.verticalLayout.addLayout(hl)
self.promptLabel = QLabel( self.promptLabel = QLabel(tr('Select folders to scan and press "Scan".'), self.centralwidget)
tr('Select folders to scan and press "Scan".'), self.centralwidget
)
self.verticalLayout.addWidget(self.promptLabel) self.verticalLayout.addWidget(self.promptLabel)
self.treeView = QTreeView(self.centralwidget) self.treeView = QTreeView(self.centralwidget)
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.treeView.setAcceptDrops(True) self.treeView.setAcceptDrops(True)
triggers = ( triggers = (
QAbstractItemView.DoubleClicked QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
| QAbstractItemView.EditKeyPressed
| QAbstractItemView.SelectedClicked
) )
self.treeView.setEditTriggers(triggers) self.treeView.setEditTriggers(triggers)
self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropOverwriteMode(True)
@@ -267,9 +257,7 @@ class DirectoriesDialog(QMainWindow):
def _updateScanTypeList(self): def _updateScanTypeList(self):
try: try:
self.scanTypeComboBox.currentIndexChanged[int].disconnect( self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)
self.scanTypeChanged
)
except TypeError: except TypeError:
# Not connected, ignore # Not connected, ignore
pass pass
@@ -299,9 +287,7 @@ class DirectoriesDialog(QMainWindow):
def addFolderTriggered(self): def addFolderTriggered(self):
title = tr("Select a folder to add to the scanning list") title = tr("Select a folder to add to the scanning list")
flags = QFileDialog.ShowDirsOnly flags = QFileDialog.ShowDirsOnly
dirpath = str( dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)
)
if not dirpath: if not dirpath:
return return
self.lastAddedFolder = dirpath self.lastAddedFolder = dirpath
@@ -362,9 +348,7 @@ class DirectoriesDialog(QMainWindow):
def scanTypeChanged(self, index): def scanTypeChanged(self, index):
scan_options = self.app.model.SCANNER_CLASS.get_scan_options() scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
self.app.prefs.set_scan_type( self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)
self.app.model.app_mode, scan_options[index].scan_type
)
self.app._update_options() self.app._update_options()
def selectionChanged(self, selected, deselected): def selectionChanged(self, selected, deselected):

View File

@@ -44,9 +44,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
# On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to # On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to
# fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright. # fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.
cboption.state |= QStyle.State_Enabled cboption.state |= QStyle.State_Enabled
QApplication.style().drawComplexControl( QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter)
QStyle.CC_ComboBox, cboption, painter
)
painter.setBrush(option.palette.text()) painter.setBrush(option.palette.text())
rect = QRect(option.rect) rect = QRect(option.rect)
rect.setLeft(rect.left() + 4) rect.setLeft(rect.left() + 4)
@@ -75,9 +73,7 @@ class DirectoriesModel(TreeModel):
self.view = view self.view = view
self.view.setModel(self) self.view.setModel(self)
self.view.selectionModel().selectionChanged[ self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
(QItemSelection, QItemSelection)
].connect(self.selectionChanged)
def _createNode(self, ref, row): def _createNode(self, ref, row):
return RefNode(self, None, ref, row) return RefNode(self, None, ref, row)
@@ -155,10 +151,7 @@ class DirectoriesModel(TreeModel):
# --- Events # --- Events
def selectionChanged(self, selected, deselected): def selectionChanged(self, selected, deselected):
newNodes = [ newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
modelIndex.internalPointer().ref
for modelIndex in self.view.selectionModel().selectedRows()
]
self.model.selected_nodes = newNodes self.model.selected_nodes = newNodes
# --- Signals # --- Signals

View File

@@ -5,13 +5,22 @@
import re import re
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QPushButton, QLineEdit, QVBoxLayout, QGridLayout, QDialog, QPushButton,
QTableView, QAbstractItemView, QSpacerItem, QSizePolicy, QHeaderView QLineEdit,
QVBoxLayout,
QGridLayout,
QDialog,
QTableView,
QAbstractItemView,
QSpacerItem,
QSizePolicy,
QHeaderView,
) )
from .exclude_list_table import ExcludeListTable from .exclude_list_table import ExcludeListTable
from core.exclude import AlreadyThereException from core.exclude import AlreadyThereException
from hscommon.trans import trget from hscommon.trans import trget
tr = trget("ui") tr = trget("ui")
@@ -51,9 +60,7 @@ class ExcludeListDialog(QDialog):
self.testLine = QLineEdit() self.testLine = QLineEdit()
self.tableView = QTableView() self.tableView = QTableView()
triggers = ( triggers = (
QAbstractItemView.DoubleClicked QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
| QAbstractItemView.EditKeyPressed
| QAbstractItemView.SelectedClicked
) )
self.tableView.setEditTriggers(triggers) self.tableView.setEditTriggers(triggers)
self.tableView.setSelectionMode(QTableView.ExtendedSelection) self.tableView.setSelectionMode(QTableView.ExtendedSelection)
@@ -116,31 +123,32 @@ class ExcludeListDialog(QDialog):
if not input_text: if not input_text:
self.reset_input_style() self.reset_input_style()
return return
# if at least one row matched, we know whether table is highlighted or not # If at least one row matched, we know whether table is highlighted or not
self._row_matched = self.model.test_string(input_text) self._row_matched = self.model.test_string(input_text)
self.table.refresh() self.table.refresh()
# Test the string currently in the input text box as well
input_regex = self.inputLine.text() input_regex = self.inputLine.text()
if not input_regex: if not input_regex:
self.reset_input_style() self.reset_input_style()
return return
compiled = None
try: try:
compiled = re.compile(input_regex) compiled = re.compile(input_regex)
except re.error: except re.error:
self.reset_input_style() self.reset_input_style()
return return
match = compiled.match(input_text) if self.model.is_match(input_text, compiled):
if match:
self._input_styled = True
self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);") self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);")
self._input_styled = True
else: else:
self.reset_input_style() self.reset_input_style()
def reset_input_style(self): def reset_input_style(self):
"""Reset regex input line background""" """Reset regex input line background"""
if self._input_styled: if self._input_styled:
self._input_styled = False
self.inputLine.setStyleSheet(self.styleSheet()) self.inputLine.setStyleSheet(self.styleSheet())
self._input_styled = False
def reset_table_style(self): def reset_table_style(self):
if self._row_matched: if self._row_matched:
@@ -149,7 +157,9 @@ class ExcludeListDialog(QDialog):
self.table.refresh() self.table.refresh()
def display_help_message(self): def display_help_message(self):
self.app.show_message(tr("""\ self.app.show_message(
tr(
"""\
These (case sensitive) python regular expressions will filter out files during scans.<br>\ These (case sensitive) python regular expressions will filter out files during scans.<br>\
Directores will also have their <strong>default state</strong> set to Excluded \ Directores will also have their <strong>default state</strong> set to Excluded \
in the Directories tab if their name happens to match one of the selected regular expressions.<br>\ in the Directories tab if their name happens to match one of the selected regular expressions.<br>\
@@ -162,4 +172,6 @@ You can test the regular expression with the "test string" button after pasting
<code>C:\\\\User\\My Pictures\\test.png</code><br><br> <code>C:\\\\User\\My Pictures\\test.png</code><br><br>
Matching regular expressions will be highlighted.<br>\ Matching regular expressions will be highlighted.<br>\
If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\ If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\
Directories and files starting with a period '.' are filtered out by default.<br><br>""")) Directories and files starting with a period '.' are filtered out by default.<br><br>"""
)
)

View File

@@ -8,15 +8,14 @@ from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor
from qtlib.column import Column from qtlib.column import Column
from qtlib.table import Table from qtlib.table import Table
from hscommon.trans import trget from hscommon.trans import trget
tr = trget("ui") tr = trget("ui")
class ExcludeListTable(Table): class ExcludeListTable(Table):
"""Model for exclude list""" """Model for exclude list"""
COLUMNS = [
Column("marked", defaultWidth=15), COLUMNS = [Column("marked", defaultWidth=15), Column("regex", defaultWidth=230)]
Column("regex", defaultWidth=230)
]
def __init__(self, app, view, **kwargs): def __init__(self, app, view, **kwargs):
model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable

View File

@@ -56,9 +56,7 @@ class IgnoreListDialog(QDialog):
self.clearButton = QPushButton(tr("Clear")) self.clearButton = QPushButton(tr("Clear"))
self.closeButton = QPushButton(tr("Close")) self.closeButton = QPushButton(tr("Close"))
self.verticalLayout.addLayout( self.verticalLayout.addLayout(
horizontalWrap( horizontalWrap([self.removeSelectedButton, self.clearButton, None, self.closeButton])
[self.removeSelectedButton, self.clearButton, None, self.closeButton]
)
) )
# --- model --> view # --- model --> view

View File

@@ -59,13 +59,9 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.matchSimilarBox) self.widgetsVLayout.addWidget(self.matchSimilarBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind")) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
self.widgetsVLayout.addWidget(self.mixFileKindBox) self.widgetsVLayout.addWidget(self.mixFileKindBox)
self._setupAddCheckbox( self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
"useRegexpBox", tr("Use regular expressions when filtering")
)
self.widgetsVLayout.addWidget(self.useRegexpBox) self.widgetsVLayout.addWidget(self.useRegexpBox)
self._setupAddCheckbox( self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
)
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox) self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
self._setupAddCheckbox( self._setupAddCheckbox(
"ignoreHardlinkMatches", "ignoreHardlinkMatches",

View File

@@ -5,21 +5,19 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame)
from PyQt5.QtGui import QResizeEvent from PyQt5.QtGui import QResizeEvent
from hscommon.trans import trget from hscommon.trans import trget
from ..details_dialog import DetailsDialog as DetailsDialogBase from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable from ..details_table import DetailsTable
from .image_viewer import ( from .image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController)
tr = trget("ui") tr = trget("ui")
class DetailsDialog(DetailsDialogBase): class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app): def __init__(self, parent, app):
self.vController = None self.vController = None
self.app = app
super().__init__(parent, app) super().__init__(parent, app)
def _setupUi(self): def _setupUi(self):
@@ -71,8 +69,7 @@ class DetailsDialog(DetailsDialogBase):
self.splitter.addWidget(self.tableView) self.splitter.addWidget(self.tableView)
self.splitter.setStretchFactor(1, 1) self.splitter.setStretchFactor(1, 1)
# Late population needed here for connections to the toolbar # Late population needed here for connections to the toolbar
self.vController.setupViewers( self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer)
self.selectedImageViewer, self.referenceImageViewer)
# self.setCentralWidget(self.splitter) # only as QMainWindow # self.setCentralWidget(self.splitter) # only as QMainWindow
self.setWidget(self.splitter) # only as QDockWidget self.setWidget(self.splitter) # only as QDockWidget
@@ -104,11 +101,11 @@ class DetailsDialog(DetailsDialogBase):
# Give the splitter a maximum height to reach. This is assuming that # Give the splitter a maximum height to reach. This is assuming that
# all rows below their headers have the same height # all rows below their headers have the same height
self.tableView.setMaximumHeight( self.tableView.setMaximumHeight(
self.tableView.rowHeight(1) self.tableView.rowHeight(1) * self.tableModel.model.row_count()
* self.tableModel.model.row_count()
+ self.tableView.verticalHeader().sectionSize(0) + self.tableView.verticalHeader().sectionSize(0)
# looks like the handle is taken into account by the splitter # looks like the handle is taken into account by the splitter
+ self.splitter.handle(1).size().height()) + self.splitter.handle(1).size().height()
)
DetailsDialogBase.show(self) DetailsDialogBase.show(self)
self.ensure_same_sizes() self.ensure_same_sizes()
self._update() self._update()
@@ -139,6 +136,7 @@ class DetailsDialog(DetailsDialogBase):
class EmittingFrame(QFrame): class EmittingFrame(QFrame):
"""Emits a signal whenever is resized""" """Emits a signal whenever is resized"""
resized = pyqtSignal(QResizeEvent) resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event): def resizeEvent(self, event):

View File

@@ -2,15 +2,24 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import ( from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent)
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsView,
QToolBar, QToolButton, QAction, QWidget, QScrollArea, QGraphicsScene,
QApplication, QAbstractScrollArea, QStyle) QGraphicsPixmapItem,
QToolBar,
QToolButton,
QAction,
QWidget,
QScrollArea,
QApplication,
QAbstractScrollArea,
QStyle,
)
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.plat import ISLINUX from hscommon.plat import ISLINUX
tr = trget("ui") tr = trget("ui")
MAX_SCALE = 12.0 MAX_SCALE = 12.0
@@ -50,8 +59,7 @@ class ViewerToolBar(QToolBar):
"actionZoomIn", "actionZoomIn",
QKeySequence.ZoomIn, QKeySequence.ZoomIn,
QIcon.fromTheme("zoom-in") QIcon.fromTheme("zoom-in")
if ISLINUX if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_in")), else QIcon(QPixmap(":/" + "zoom_in")),
tr("Increase zoom"), tr("Increase zoom"),
controller.zoomIn, controller.zoomIn,
@@ -60,8 +68,7 @@ class ViewerToolBar(QToolBar):
"actionZoomOut", "actionZoomOut",
QKeySequence.ZoomOut, QKeySequence.ZoomOut,
QIcon.fromTheme("zoom-out") QIcon.fromTheme("zoom-out")
if ISLINUX if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_out")), else QIcon(QPixmap(":/" + "zoom_out")),
tr("Decrease zoom"), tr("Decrease zoom"),
controller.zoomOut, controller.zoomOut,
@@ -70,8 +77,7 @@ class ViewerToolBar(QToolBar):
"actionNormalSize", "actionNormalSize",
tr("Ctrl+/"), tr("Ctrl+/"),
QIcon.fromTheme("zoom-original") QIcon.fromTheme("zoom-original")
if ISLINUX if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_original")), else QIcon(QPixmap(":/" + "zoom_original")),
tr("Normal size"), tr("Normal size"),
controller.zoomNormalSize, controller.zoomNormalSize,
@@ -80,12 +86,11 @@ class ViewerToolBar(QToolBar):
"actionBestFit", "actionBestFit",
tr("Ctrl+*"), tr("Ctrl+*"),
QIcon.fromTheme("zoom-best-fit") QIcon.fromTheme("zoom-best-fit")
if ISLINUX if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_best_fit")), else QIcon(QPixmap(":/" + "zoom_best_fit")),
tr("Best fit"), tr("Best fit"),
controller.zoomBestFit, controller.zoomBestFit,
) ),
] ]
# TODO try with QWidgetAction() instead in order to have # TODO try with QWidgetAction() instead in order to have
# the popup menu work in the toolbar (if resized below minimum height) # the popup menu work in the toolbar (if resized below minimum height)
@@ -95,13 +100,12 @@ class ViewerToolBar(QToolBar):
self.buttonImgSwap = QToolButton(self) self.buttonImgSwap = QToolButton(self)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonImgSwap.setIcon( self.buttonImgSwap.setIcon(
QIcon.fromTheme('view-refresh', QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload))
self.style().standardIcon(QStyle.SP_BrowserReload)) if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX else QIcon(QPixmap(":/" + "exchange"))
and not self.parent.app.prefs.details_dialog_override_theme_icons )
else QIcon(QPixmap(":/" + "exchange"))) self.buttonImgSwap.setText("Swap images")
self.buttonImgSwap.setText('Swap images') self.buttonImgSwap.setToolTip("Swap images")
self.buttonImgSwap.setToolTip('Swap images')
self.buttonImgSwap.pressed.connect(self.controller.swapImages) self.buttonImgSwap.pressed.connect(self.controller.swapImages)
self.buttonImgSwap.released.connect(self.controller.swapImages) self.buttonImgSwap.released.connect(self.controller.swapImages)
@@ -207,11 +211,11 @@ class BaseController(QObject):
# than the ReferenceImageViewer by one pixel, which distorts the # than the ReferenceImageViewer by one pixel, which distorts the
# scaled down pixmap for the reference, hence we'll reuse its size here. # scaled down pixmap for the reference, hence we'll reuse its size here.
selected_size = self._updateImage( selected_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap, self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, same_group
self.selectedViewer, None, same_group) )
self._updateImage( self._updateImage(
self.referencePixmap, self.scaledReferencePixmap, self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, selected_size, same_group
self.referenceViewer, selected_size, same_group) )
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = False self.selectedViewer.ignore_signal = False
@@ -229,12 +233,10 @@ class BaseController(QObject):
return target_size return target_size
# zoomed in state, expand # zoomed in state, expand
# only if not same_group, we need full update # only if not same_group, we need full update
scaledpixmap = pixmap.scaled( scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
else: else:
# best fit, keep ratio always # best fit, keep ratio always
scaledpixmap = pixmap.scaled( scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
viewer.setImage(scaledpixmap) viewer.setImage(scaledpixmap)
return target_size return target_size
@@ -347,12 +349,8 @@ class BaseController(QObject):
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
target_size = self._updateImage( target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True)
self.selectedPixmap, self.scaledSelectedPixmap, self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True)
self.selectedViewer, None, True)
self._updateImage(
self.referencePixmap, self.scaledReferencePixmap,
self.referenceViewer, target_size, True)
self.centerViews() self.centerViews()
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
@@ -402,6 +400,7 @@ class BaseController(QObject):
class QWidgetController(BaseController): class QWidgetController(BaseController):
"""Specialized version for QWidget-based viewers.""" """Specialized version for QWidget-based viewers."""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@@ -430,6 +429,7 @@ class QWidgetController(BaseController):
class ScrollAreaController(BaseController): class ScrollAreaController(BaseController):
"""Specialized version fro QLabel-based viewers.""" """Specialized version fro QLabel-based viewers."""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@@ -442,10 +442,8 @@ class ScrollAreaController(BaseController):
super().updateBothImages(same_group) super().updateBothImages(same_group)
if not self.referenceViewer.isEnabled(): if not self.referenceViewer.isEnabled():
return return
self.referenceViewer._horizontalScrollBar.setValue( self.referenceViewer._horizontalScrollBar.setValue(self.selectedViewer._horizontalScrollBar.value())
self.selectedViewer._horizontalScrollBar.value()) self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())
self.referenceViewer._verticalScrollBar.setValue(
self.selectedViewer._verticalScrollBar.value())
@pyqtSlot(QPoint) @pyqtSlot(QPoint)
def onDraggedMouse(self, delta): def onDraggedMouse(self, delta):
@@ -518,6 +516,7 @@ class ScrollAreaController(BaseController):
class GraphicsViewController(BaseController): class GraphicsViewController(BaseController):
"""Specialized version fro QGraphicsView-based viewers.""" """Specialized version fro QGraphicsView-based viewers."""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
@@ -625,10 +624,8 @@ class GraphicsViewController(BaseController):
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = True self.selectedViewer.ignore_signal = True
self._updateFitImage( self._updateFitImage(self.selectedPixmap, self.selectedViewer)
self.selectedPixmap, self.selectedViewer) self._updateFitImage(self.referencePixmap, self.referenceViewer)
self._updateFitImage(
self.referencePixmap, self.referenceViewer)
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = False self.selectedViewer.ignore_signal = False
@@ -699,6 +696,7 @@ class GraphicsViewController(BaseController):
class QWidgetImageViewer(QWidget): class QWidgetImageViewer(QWidget):
"""Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation.""" """Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation."""
# FIXME: panning while zoomed-in is broken (due to delta not interpolated right? # FIXME: panning while zoomed-in is broken (due to delta not interpolated right?
mouseDragged = pyqtSignal(QPointF) mouseDragged = pyqtSignal(QPointF)
mouseWheeled = pyqtSignal(float) mouseWheeled = pyqtSignal(float)
@@ -720,15 +718,13 @@ class QWidgetImageViewer(QWidget):
self.setMouseTracking(False) self.setMouseTracking(False)
def __repr__(self): def __repr__(self):
return f'{self._instance_name}' return f"{self._instance_name}"
def connectMouseSignals(self): def connectMouseSignals(self):
if not self._dragConnection: if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect( self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
self.controller.onDraggedMouse)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(self.controller.scaleImagesBy)
self.controller.scaleImagesBy)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@@ -783,8 +779,7 @@ class QWidgetImageViewer(QWidget):
event.ignore() event.ignore()
return return
self._mousePanningDelta += ( self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
if self._drag: if self._drag:
self.mouseDragged.emit(self._mousePanningDelta) self.mouseDragged.emit(self._mousePanningDelta)
@@ -860,6 +855,7 @@ class QWidgetImageViewer(QWidget):
class ScalablePixmap(QWidget): class ScalablePixmap(QWidget):
"""Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer.""" """Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer."""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self._pixmap = QPixmap() self._pixmap = QPixmap()
@@ -881,6 +877,7 @@ class ScalablePixmap(QWidget):
class ScrollAreaImageViewer(QScrollArea): class ScrollAreaImageViewer(QScrollArea):
"""Implementation using a pixmap container in a simple scroll area.""" """Implementation using a pixmap container in a simple scroll area."""
mouseDragged = pyqtSignal(QPoint) mouseDragged = pyqtSignal(QPoint)
mouseWheeled = pyqtSignal(float, QPointF) mouseWheeled = pyqtSignal(float, QPointF)
@@ -921,7 +918,7 @@ class ScrollAreaImageViewer(QScrollArea):
self.setVisible(True) self.setVisible(True)
def __repr__(self): def __repr__(self):
return f'{self._instance_name}' return f"{self._instance_name}"
def toggleScrollBars(self, forceOn=False): def toggleScrollBars(self, forceOn=False):
if not self.prefs.details_dialog_viewers_show_scrollbars: if not self.prefs.details_dialog_viewers_show_scrollbars:
@@ -938,11 +935,9 @@ class ScrollAreaImageViewer(QScrollArea):
def connectMouseSignals(self): def connectMouseSignals(self):
if not self._dragConnection: if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect( self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
self.controller.onDraggedMouse)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
self.controller.onMouseWheel)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@@ -955,10 +950,8 @@ class ScrollAreaImageViewer(QScrollArea):
def connectScrollBars(self): def connectScrollBars(self):
"""Only call once controller is connected.""" """Only call once controller is connected."""
# Cyclic connections are handled by Qt # Cyclic connections are handled by Qt
self._verticalScrollBar.valueChanged.connect( self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self.controller.onVScrollBarChanged, Qt.UniqueConnection) self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
"""Block parent's (main window) context menu on right click.""" """Block parent's (main window) context menu on right click."""
@@ -987,7 +980,7 @@ class ScrollAreaImageViewer(QScrollArea):
event.ignore() event.ignore()
return return
if self._drag: if self._drag:
delta = (event.pos() - self._lastMouseClickPoint) delta = event.pos() - self._lastMouseClickPoint
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
self.mouseDragged.emit(delta) self.mouseDragged.emit(delta)
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
@@ -1064,32 +1057,26 @@ class ScrollAreaImageViewer(QScrollArea):
"""After scaling, no mouse position, default to center.""" """After scaling, no mouse position, default to center."""
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep()) # scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
self._horizontalScrollBar.setValue( self._horizontalScrollBar.setValue(
int(factor * self._horizontalScrollBar.value() int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))) )
self._verticalScrollBar.setValue( self._verticalScrollBar.setValue(
int(factor * self._verticalScrollBar.value() int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2))) )
def adjustScrollBarsScaled(self, delta): def adjustScrollBarsScaled(self, delta):
"""After scaling with the mouse, update relative to mouse position.""" """After scaling with the mouse, update relative to mouse position."""
self._horizontalScrollBar.setValue( self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
self._horizontalScrollBar.value() + delta.x()) self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
self._verticalScrollBar.setValue(
self._verticalScrollBar.value() + delta.y())
def adjustScrollBarsAuto(self): def adjustScrollBarsAuto(self):
"""After panning, update accordingly.""" """After panning, update accordingly."""
self.horizontalScrollBar().setValue( self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.horizontalScrollBar().value() - self._mousePanningDelta.x()) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y())
def adjustScrollBarCentered(self): def adjustScrollBarCentered(self):
"""Just center in the middle.""" """Just center in the middle."""
self._horizontalScrollBar.setValue( self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.maximum() / 2))
int(self._horizontalScrollBar.maximum() / 2)) self._verticalScrollBar.setValue(int(self._verticalScrollBar.maximum() / 2))
self._verticalScrollBar.setValue(
int(self._verticalScrollBar.maximum() / 2))
def resetCenter(self): def resetCenter(self):
"""Resets origin""" """Resets origin"""
@@ -1127,6 +1114,7 @@ class ScrollAreaImageViewer(QScrollArea):
class GraphicsViewViewer(QGraphicsView): class GraphicsViewViewer(QGraphicsView):
"""Re-Implementation a full-fledged GraphicsView but is a bit buggy.""" """Re-Implementation a full-fledged GraphicsView but is a bit buggy."""
mouseDragged = pyqtSignal() mouseDragged = pyqtSignal()
mouseWheeled = pyqtSignal(float, QPointF) mouseWheeled = pyqtSignal(float, QPointF)
@@ -1178,11 +1166,9 @@ class GraphicsViewViewer(QGraphicsView):
def connectMouseSignals(self): def connectMouseSignals(self):
if not self._dragConnection: if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect( self._dragConnection = self.mouseDragged.connect(self.controller.syncCenters)
self.controller.syncCenters)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
self.controller.onMouseWheel)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@@ -1195,10 +1181,8 @@ class GraphicsViewViewer(QGraphicsView):
def connectScrollBars(self): def connectScrollBars(self):
"""Only call once controller is connected.""" """Only call once controller is connected."""
# Cyclic connections are handled by Qt # Cyclic connections are handled by Qt
self._verticalScrollBar.valueChanged.connect( self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self.controller.onVScrollBarChanged, Qt.UniqueConnection) self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
def toggleScrollBars(self, forceOn=False): def toggleScrollBars(self, forceOn=False):
if not self.prefs.details_dialog_viewers_show_scrollbars: if not self.prefs.details_dialog_viewers_show_scrollbars:
@@ -1345,10 +1329,8 @@ class GraphicsViewViewer(QGraphicsView):
def adjustScrollBarsScaled(self, delta): def adjustScrollBarsScaled(self, delta):
"""After scaling with the mouse, update relative to mouse position.""" """After scaling with the mouse, update relative to mouse position."""
self._horizontalScrollBar.setValue( self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
self._horizontalScrollBar.value() + delta.x()) self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
self._verticalScrollBar.setValue(
self._verticalScrollBar.value() + delta.y())
def sizeHint(self): def sizeHint(self):
return self.viewport().rect().size() return self.viewport().rect().size()
@@ -1356,15 +1338,13 @@ class GraphicsViewViewer(QGraphicsView):
def adjustScrollBarsFactor(self, factor): def adjustScrollBarsFactor(self, factor):
"""After scaling, no mouse position, default to center.""" """After scaling, no mouse position, default to center."""
self._horizontalScrollBar.setValue( self._horizontalScrollBar.setValue(
int(factor * self._horizontalScrollBar.value() int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))) )
self._verticalScrollBar.setValue( self._verticalScrollBar.setValue(
int(factor * self._verticalScrollBar.value() int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2))) )
def adjustScrollBarsAuto(self): def adjustScrollBarsAuto(self):
"""After panning, update accordingly.""" """After panning, update accordingly."""
self.horizontalScrollBar().setValue( self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.horizontalScrollBar().value() - self._mousePanningDelta.x()) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y())

View File

@@ -30,13 +30,15 @@ class File(PhotoBase):
image = QImage(str(self.path)) image = QImage(str(self.path))
image = image.convertToFormat(QImage.Format_RGB888) image = image.convertToFormat(QImage.Format_RGB888)
if type(orientation) == str: if type(orientation) == str:
logging.warning("Orientation for file '%s' was a str '%s', not an int.", logging.warning("Orientation for file '%s' was a str '%s', not an int.", str(self.path), orientation)
str(self.path), orientation)
try: try:
orientation = int(orientation) orientation = int(orientation)
except Exception as e: except Exception as e:
logging.exception("Skipping transformation because could not \ logging.exception(
convert str to int. %s", e) "Skipping transformation because could not \
convert str to int. %s",
e,
)
return getblocks(image, block_count_per_side) return getblocks(image, block_count_per_side)
# MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for # MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for
# duplicate scanning. The transforms seems to work fine (if I try to save the image after # duplicate scanning. The transforms seems to work fine (if I try to save the image after

View File

@@ -21,19 +21,13 @@ class PreferencesDialog(PreferencesDialogBase):
def _setupPreferenceWidgets(self): def _setupPreferenceWidgets(self):
self._setupFilterHardnessBox() self._setupFilterHardnessBox()
self.widgetsVLayout.addLayout(self.filterHardnessHLayout) self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
self._setupAddCheckbox( self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
"matchScaledBox", tr("Match pictures of different dimensions")
)
self.widgetsVLayout.addWidget(self.matchScaledBox) self.widgetsVLayout.addWidget(self.matchScaledBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind")) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
self.widgetsVLayout.addWidget(self.mixFileKindBox) self.widgetsVLayout.addWidget(self.mixFileKindBox)
self._setupAddCheckbox( self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
"useRegexpBox", tr("Use regular expressions when filtering")
)
self.widgetsVLayout.addWidget(self.useRegexpBox) self.widgetsVLayout.addWidget(self.useRegexpBox)
self._setupAddCheckbox( self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
)
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox) self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
self._setupAddCheckbox( self._setupAddCheckbox(
"ignoreHardlinkMatches", "ignoreHardlinkMatches",
@@ -52,45 +46,37 @@ class PreferencesDialog(PreferencesDialogBase):
def _setupDisplayPage(self): def _setupDisplayPage(self):
super()._setupDisplayPage() super()._setupDisplayPage()
self._setupAddCheckbox("details_dialog_override_theme_icons", self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
tr("Override theme icons in viewer toolbar"))
self.details_dialog_override_theme_icons.setToolTip( self.details_dialog_override_theme_icons.setToolTip(
tr("Use our own internal icons instead of those provided by the theme engine")) tr("Use our own internal icons instead of those provided by the theme engine")
)
# Prevent changing this on platforms where themes are unpredictable # Prevent changing this on platforms where themes are unpredictable
self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True) self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)
# Insert this right after the vertical title bar option # Insert this right after the vertical title bar option
index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar) index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)
self.details_groupbox_layout.insertWidget( self.details_groupbox_layout.insertWidget(index + 1, self.details_dialog_override_theme_icons)
index + 1, self.details_dialog_override_theme_icons) self._setupAddCheckbox("details_dialog_viewers_show_scrollbars", tr("Show scrollbars in image viewers"))
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars",
tr("Show scrollbars in image viewers"))
self.details_dialog_viewers_show_scrollbars.setToolTip( self.details_dialog_viewers_show_scrollbars.setToolTip(
tr("When the image displayed doesn't fit the viewport, \ tr(
show scrollbars to span the view around")) "When the image displayed doesn't fit the viewport, \
self.details_groupbox_layout.insertWidget( show scrollbars to span the view around"
index + 2, self.details_dialog_viewers_show_scrollbars) )
)
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
def _load(self, prefs, setchecked, section): def _load(self, prefs, setchecked, section):
setchecked(self.matchScaledBox, prefs.match_scaled) setchecked(self.matchScaledBox, prefs.match_scaled)
self.cacheTypeRadio.selected_index = ( self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
1 if prefs.picture_cache_type == "shelve" else 0
)
# Update UI state based on selected scan type # Update UI state based on selected scan type
scan_type = prefs.get_scan_type(AppMode.Picture) scan_type = prefs.get_scan_type(AppMode.Picture)
fuzzy_scan = scan_type == ScanType.FuzzyBlock fuzzy_scan = scan_type == ScanType.FuzzyBlock
self.filterHardnessSlider.setEnabled(fuzzy_scan) self.filterHardnessSlider.setEnabled(fuzzy_scan)
setchecked(self.details_dialog_override_theme_icons, setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)
prefs.details_dialog_override_theme_icons) setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
setchecked(self.details_dialog_viewers_show_scrollbars,
prefs.details_dialog_viewers_show_scrollbars)
def _save(self, prefs, ischecked): def _save(self, prefs, ischecked):
prefs.match_scaled = ischecked(self.matchScaledBox) prefs.match_scaled = ischecked(self.matchScaledBox)
prefs.picture_cache_type = ( prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
"shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite" prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)
) prefs.details_dialog_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars)
prefs.details_dialog_override_theme_icons =\
ischecked(self.details_dialog_override_theme_icons)
prefs.details_dialog_viewers_show_scrollbars =\
ischecked(self.details_dialog_viewers_show_scrollbars)

View File

@@ -20,9 +20,7 @@ class Preferences(PreferencesBase):
get = self.get_value get = self.get_value
self.filter_hardness = get("FilterHardness", self.filter_hardness) self.filter_hardness = get("FilterHardness", self.filter_hardness)
self.mix_file_kind = get("MixFileKind", self.mix_file_kind) self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
self.ignore_hardlink_matches = get( self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
"IgnoreHardlinkMatches", self.ignore_hardlink_matches
)
self.use_regexp = get("UseRegexp", self.use_regexp) self.use_regexp = get("UseRegexp", self.use_regexp)
self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders) self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders)
self.debug_mode = get("DebugMode", self.debug_mode) self.debug_mode = get("DebugMode", self.debug_mode)
@@ -34,37 +32,36 @@ class Preferences(PreferencesBase):
self.tableFontSize = get("TableFontSize", self.tableFontSize) self.tableFontSize = get("TableFontSize", self.tableFontSize)
self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font) self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font)
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled)
self.details_dialog_titlebar_enabled) self.details_dialog_vertical_titlebar = get(
self.details_dialog_vertical_titlebar = get("DetailsDialogVerticalTitleBar", "DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar
self.details_dialog_vertical_titlebar) )
# On Windows and MacOS, use internal icons by default # On Windows and MacOS, use internal icons by default
self.details_dialog_override_theme_icons =\ self.details_dialog_override_theme_icons = (
get("DetailsDialogOverrideThemeIcons", get("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons) if ISLINUX else True
self.details_dialog_override_theme_icons) if ISLINUX else True
self.details_table_delta_foreground_color =\
get("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
self.details_dialog_viewers_show_scrollbars =\
get("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
self.result_table_ref_foreground_color =\
get("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
self.result_table_ref_background_color =\
get("ResultTableRefBackgroundColor", self.result_table_ref_background_color)
self.result_table_delta_foreground_color =\
get("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
self.resultWindowIsMaximized = get(
"ResultWindowIsMaximized", self.resultWindowIsMaximized
) )
self.details_table_delta_foreground_color = get(
"DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color
)
self.details_dialog_viewers_show_scrollbars = get(
"DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars
)
self.result_table_ref_foreground_color = get(
"ResultTableRefForegroundColor", self.result_table_ref_foreground_color
)
self.result_table_ref_background_color = get(
"ResultTableRefBackgroundColor", self.result_table_ref_background_color
)
self.result_table_delta_foreground_color = get(
"ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color
)
self.resultWindowIsMaximized = get("ResultWindowIsMaximized", self.resultWindowIsMaximized)
self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect) self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect)
self.mainWindowIsMaximized = get( self.mainWindowIsMaximized = get("MainWindowIsMaximized", self.mainWindowIsMaximized)
"MainWindowIsMaximized", self.mainWindowIsMaximized
)
self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect) self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect)
self.directoriesWindowRect = self.get_rect( self.directoriesWindowRect = self.get_rect("DirectoriesWindowRect", self.directoriesWindowRect)
"DirectoriesWindowRect", self.directoriesWindowRect
)
self.recentResults = get("RecentResults", self.recentResults) self.recentResults = get("RecentResults", self.recentResults)
self.recentFolders = get("RecentFolders", self.recentFolders) self.recentFolders = get("RecentFolders", self.recentFolders)
@@ -73,6 +70,8 @@ class Preferences(PreferencesBase):
self.match_similar = get("MatchSimilar", self.match_similar) self.match_similar = get("MatchSimilar", self.match_similar)
self.ignore_small_files = get("IgnoreSmallFiles", self.ignore_small_files) self.ignore_small_files = get("IgnoreSmallFiles", self.ignore_small_files)
self.small_file_threshold = get("SmallFileThreshold", self.small_file_threshold) self.small_file_threshold = get("SmallFileThreshold", self.small_file_threshold)
self.big_file_partial_hashes = get("BigFilePartialHashes", self.big_file_partial_hashes)
self.big_file_size_threshold = get("BigFileSizeThreshold", self.big_file_size_threshold)
self.scan_tag_track = get("ScanTagTrack", self.scan_tag_track) self.scan_tag_track = get("ScanTagTrack", self.scan_tag_track)
self.scan_tag_artist = get("ScanTagArtist", self.scan_tag_artist) self.scan_tag_artist = get("ScanTagArtist", self.scan_tag_artist)
self.scan_tag_album = get("ScanTagAlbum", self.scan_tag_album) self.scan_tag_album = get("ScanTagAlbum", self.scan_tag_album)
@@ -102,7 +101,7 @@ class Preferences(PreferencesBase):
self.details_dialog_override_theme_icons = False if not ISLINUX else True self.details_dialog_override_theme_icons = False if not ISLINUX else True
self.details_dialog_viewers_show_scrollbars = True self.details_dialog_viewers_show_scrollbars = True
self.result_table_ref_foreground_color = QColor(Qt.blue) self.result_table_ref_foreground_color = QColor(Qt.blue)
self.result_table_ref_background_color = QColor(Qt.darkGray) self.result_table_ref_background_color = QColor(Qt.lightGray)
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
self.resultWindowIsMaximized = False self.resultWindowIsMaximized = False
self.resultWindowRect = None self.resultWindowRect = None
@@ -117,6 +116,8 @@ class Preferences(PreferencesBase):
self.match_similar = False self.match_similar = False
self.ignore_small_files = True self.ignore_small_files = True
self.small_file_threshold = 10 # KB self.small_file_threshold = 10 # KB
self.big_file_partial_hashes = False
self.big_file_size_threshold = 100 # MB
self.scan_tag_track = False self.scan_tag_track = False
self.scan_tag_artist = True self.scan_tag_artist = True
self.scan_tag_album = True self.scan_tag_album = True
@@ -161,6 +162,8 @@ class Preferences(PreferencesBase):
set_("MatchSimilar", self.match_similar) set_("MatchSimilar", self.match_similar)
set_("IgnoreSmallFiles", self.ignore_small_files) set_("IgnoreSmallFiles", self.ignore_small_files)
set_("SmallFileThreshold", self.small_file_threshold) set_("SmallFileThreshold", self.small_file_threshold)
set_("BigFilePartialHashes", self.big_file_partial_hashes)
set_("BigFileSizeThreshold", self.big_file_size_threshold)
set_("ScanTagTrack", self.scan_tag_track) set_("ScanTagTrack", self.scan_tag_track)
set_("ScanTagArtist", self.scan_tag_artist) set_("ScanTagArtist", self.scan_tag_artist)
set_("ScanTagAlbum", self.scan_tag_album) set_("ScanTagAlbum", self.scan_tag_album)

View File

@@ -41,23 +41,24 @@ from .preferences import Preferences
tr = trget("ui") tr = trget("ui")
SUPPORTED_LANGUAGES = [ SUPPORTED_LANGUAGES = [
"en", "cs",
"fr",
"de", "de",
"el", "el",
"zh_CN", "en",
"cs",
"it",
"hy",
"ru",
"uk",
"pt_BR",
"vi",
"pl_PL",
"ko",
"es", "es",
"nl", "fr",
"hy",
"it",
"ja", "ja",
"ko",
"nl",
"pl_PL",
"pt_BR",
"ru",
"tr",
"uk",
"vi",
"zh_CN",
] ]
@@ -75,14 +76,10 @@ class PreferencesDialogBase(QDialog):
super().__init__(parent, flags, **kwargs) super().__init__(parent, flags, **kwargs)
self.app = app self.app = app
all_languages = get_langnames() all_languages = get_langnames()
self.supportedLanguages = sorted( self.supportedLanguages = sorted(SUPPORTED_LANGUAGES, key=lambda lang: all_languages[lang])
SUPPORTED_LANGUAGES, key=lambda lang: all_languages[lang]
)
self._setupUi() self._setupUi()
self.filterHardnessSlider.valueChanged["int"].connect( self.filterHardnessSlider.valueChanged["int"].connect(self.filterHardnessLabel.setNum)
self.filterHardnessLabel.setNum
)
self.buttonBox.clicked.connect(self.buttonClicked) self.buttonBox.clicked.connect(self.buttonClicked)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
@@ -101,9 +98,7 @@ class PreferencesDialogBase(QDialog):
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth())
self.filterHardnessSlider.sizePolicy().hasHeightForWidth()
)
self.filterHardnessSlider.setSizePolicy(sizePolicy) self.filterHardnessSlider.setSizePolicy(sizePolicy)
self.filterHardnessSlider.setMinimum(1) self.filterHardnessSlider.setMinimum(1)
self.filterHardnessSlider.setMaximum(100) self.filterHardnessSlider.setMaximum(100)
@@ -139,9 +134,7 @@ class PreferencesDialogBase(QDialog):
self.copyMoveDestinationComboBox.addItem(tr("Recreate absolute path")) self.copyMoveDestinationComboBox.addItem(tr("Recreate absolute path"))
self.widgetsVLayout.addWidget(self.copyMoveDestinationComboBox) self.widgetsVLayout.addWidget(self.copyMoveDestinationComboBox)
self.customCommandLabel = QLabel(self) self.customCommandLabel = QLabel(self)
self.customCommandLabel.setText( self.customCommandLabel.setText(tr("Custom Command (arguments: %d for dupe, %r for ref):"))
tr("Custom Command (arguments: %d for dupe, %r for ref):")
)
self.widgetsVLayout.addWidget(self.customCommandLabel) self.widgetsVLayout.addWidget(self.customCommandLabel)
self.customCommandEdit = QLineEdit(self) self.customCommandEdit = QLineEdit(self)
self.widgetsVLayout.addWidget(self.customCommandEdit) self.widgetsVLayout.addWidget(self.customCommandEdit)
@@ -153,9 +146,7 @@ class PreferencesDialogBase(QDialog):
self.languageComboBox = QComboBox(self) self.languageComboBox = QComboBox(self)
for lang in self.supportedLanguages: for lang in self.supportedLanguages:
self.languageComboBox.addItem(get_langnames()[lang]) self.languageComboBox.addItem(get_langnames()[lang])
layout.addLayout( layout.addLayout(horizontalWrap([self.languageLabel, self.languageComboBox, None]))
horizontalWrap([self.languageLabel, self.languageComboBox, None])
)
self._setupAddCheckbox( self._setupAddCheckbox(
"tabs_default_pos", "tabs_default_pos",
tr("Use default position for tab bar (requires restart)"), tr("Use default position for tab bar (requires restart)"),
@@ -177,23 +168,15 @@ On MacOS, the tab bar will fill up the window's width instead."
self.fontSizeSpinBox = QSpinBox() self.fontSizeSpinBox = QSpinBox()
self.fontSizeSpinBox.setMinimum(5) self.fontSizeSpinBox.setMinimum(5)
formlayout.addRow(tr("Font size:"), self.fontSizeSpinBox) formlayout.addRow(tr("Font size:"), self.fontSizeSpinBox)
self._setupAddCheckbox( self._setupAddCheckbox("reference_bold_font", tr("Use bold font for references"))
"reference_bold_font", tr("Use bold font for references")
)
formlayout.addRow(self.reference_bold_font) formlayout.addRow(self.reference_bold_font)
self.result_table_ref_foreground_color = ColorPickerButton(self) self.result_table_ref_foreground_color = ColorPickerButton(self)
formlayout.addRow( formlayout.addRow(tr("Reference foreground color:"), self.result_table_ref_foreground_color)
tr("Reference foreground color:"), self.result_table_ref_foreground_color
)
self.result_table_ref_background_color = ColorPickerButton(self) self.result_table_ref_background_color = ColorPickerButton(self)
formlayout.addRow( formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color)
tr("Reference background color:"), self.result_table_ref_background_color
)
self.result_table_delta_foreground_color = ColorPickerButton(self) self.result_table_delta_foreground_color = ColorPickerButton(self)
formlayout.addRow( formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color)
tr("Delta foreground color:"), self.result_table_delta_foreground_color
)
formlayout.setLabelAlignment(Qt.AlignLeft) formlayout.setLabelAlignment(Qt.AlignLeft)
# Keep same vertical spacing as parent layout for consistency # Keep same vertical spacing as parent layout for consistency
@@ -217,30 +200,20 @@ use the modifier key to drag the floating window around"
else tr("The title bar can only be disabled while the window is docked") else tr("The title bar can only be disabled while the window is docked")
) )
self.details_groupbox_layout.addWidget(self.details_dialog_titlebar_enabled) self.details_groupbox_layout.addWidget(self.details_dialog_titlebar_enabled)
self._setupAddCheckbox( self._setupAddCheckbox("details_dialog_vertical_titlebar", tr("Vertical title bar"))
"details_dialog_vertical_titlebar", tr("Vertical title bar")
)
self.details_dialog_vertical_titlebar.setToolTip( self.details_dialog_vertical_titlebar.setToolTip(
tr( tr("Change the title bar from horizontal on top, to vertical on the left side")
"Change the title bar from horizontal on top, to vertical on the left side"
)
) )
self.details_groupbox_layout.addWidget(self.details_dialog_vertical_titlebar) self.details_groupbox_layout.addWidget(self.details_dialog_vertical_titlebar)
self.details_dialog_vertical_titlebar.setEnabled( self.details_dialog_vertical_titlebar.setEnabled(self.details_dialog_titlebar_enabled.isChecked())
self.details_dialog_titlebar_enabled.isChecked() self.details_dialog_titlebar_enabled.stateChanged.connect(self.details_dialog_vertical_titlebar.setEnabled)
)
self.details_dialog_titlebar_enabled.stateChanged.connect(
self.details_dialog_vertical_titlebar.setEnabled
)
gridlayout = QGridLayout() gridlayout = QGridLayout()
formlayout = QFormLayout() formlayout = QFormLayout()
self.details_table_delta_foreground_color = ColorPickerButton(self) self.details_table_delta_foreground_color = ColorPickerButton(self)
# Padding on the right side and space between label and widget to keep it somewhat consistent across themes # Padding on the right side and space between label and widget to keep it somewhat consistent across themes
gridlayout.setColumnStretch(1, 1) gridlayout.setColumnStretch(1, 1)
formlayout.setHorizontalSpacing(50) formlayout.setHorizontalSpacing(50)
formlayout.addRow( formlayout.addRow(tr("Delta foreground color:"), self.details_table_delta_foreground_color)
tr("Delta foreground color:"), self.details_table_delta_foreground_color
)
gridlayout.addLayout(formlayout, 0, 0) gridlayout.addLayout(formlayout, 0, 0)
self.details_groupbox_layout.addLayout(gridlayout) self.details_groupbox_layout.addLayout(gridlayout)
details_groupbox.setLayout(self.details_groupbox_layout) details_groupbox.setLayout(self.details_groupbox_layout)
@@ -275,9 +248,7 @@ use the modifier key to drag the floating window around"
# self.mainVLayout.addLayout(self.widgetsVLayout) # self.mainVLayout.addLayout(self.widgetsVLayout)
self.buttonBox = QDialogButtonBox(self) self.buttonBox = QDialogButtonBox(self)
self.buttonBox.setStandardButtons( self.buttonBox.setStandardButtons(
QDialogButtonBox.Cancel QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.RestoreDefaults
| QDialogButtonBox.Ok
| QDialogButtonBox.RestoreDefaults
) )
self.mainVLayout.addWidget(self.tabwidget) self.mainVLayout.addWidget(self.tabwidget)
self.mainVLayout.addWidget(self.buttonBox) self.mainVLayout.addWidget(self.buttonBox)
@@ -298,7 +269,10 @@ use the modifier key to drag the floating window around"
def load(self, prefs=None, section=Sections.ALL): def load(self, prefs=None, section=Sections.ALL):
if prefs is None: if prefs is None:
prefs = self.app.prefs prefs = self.app.prefs
setchecked = lambda cb, b: cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
def setchecked(cb, b):
cb.setCheckState(Qt.Checked if b else Qt.Unchecked)
if section & Sections.GENERAL: if section & Sections.GENERAL:
self.filterHardnessSlider.setValue(prefs.filter_hardness) self.filterHardnessSlider.setValue(prefs.filter_hardness)
self.filterHardnessLabel.setNum(prefs.filter_hardness) self.filterHardnessLabel.setNum(prefs.filter_hardness)
@@ -321,18 +295,10 @@ use the modifier key to drag the floating window around"
prefs.details_dialog_vertical_titlebar, prefs.details_dialog_vertical_titlebar,
) )
self.fontSizeSpinBox.setValue(prefs.tableFontSize) self.fontSizeSpinBox.setValue(prefs.tableFontSize)
self.details_table_delta_foreground_color.setColor( self.details_table_delta_foreground_color.setColor(prefs.details_table_delta_foreground_color)
prefs.details_table_delta_foreground_color self.result_table_ref_foreground_color.setColor(prefs.result_table_ref_foreground_color)
) self.result_table_ref_background_color.setColor(prefs.result_table_ref_background_color)
self.result_table_ref_foreground_color.setColor( self.result_table_delta_foreground_color.setColor(prefs.result_table_delta_foreground_color)
prefs.result_table_ref_foreground_color
)
self.result_table_ref_background_color.setColor(
prefs.result_table_ref_background_color
)
self.result_table_delta_foreground_color.setColor(
prefs.result_table_delta_foreground_color
)
try: try:
langindex = self.supportedLanguages.index(self.app.prefs.language) langindex = self.supportedLanguages.index(self.app.prefs.language)
except ValueError: except ValueError:
@@ -343,31 +309,22 @@ use the modifier key to drag the floating window around"
def save(self): def save(self):
prefs = self.app.prefs prefs = self.app.prefs
prefs.filter_hardness = self.filterHardnessSlider.value() prefs.filter_hardness = self.filterHardnessSlider.value()
ischecked = lambda cb: cb.checkState() == Qt.Checked
def ischecked(cb):
return cb.checkState() == Qt.Checked
prefs.mix_file_kind = ischecked(self.mixFileKindBox) prefs.mix_file_kind = ischecked(self.mixFileKindBox)
prefs.use_regexp = ischecked(self.useRegexpBox) prefs.use_regexp = ischecked(self.useRegexpBox)
prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox) prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox)
prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches) prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches)
prefs.debug_mode = ischecked(self.debugModeBox) prefs.debug_mode = ischecked(self.debugModeBox)
prefs.reference_bold_font = ischecked(self.reference_bold_font) prefs.reference_bold_font = ischecked(self.reference_bold_font)
prefs.details_dialog_titlebar_enabled = ischecked( prefs.details_dialog_titlebar_enabled = ischecked(self.details_dialog_titlebar_enabled)
self.details_dialog_titlebar_enabled prefs.details_dialog_vertical_titlebar = ischecked(self.details_dialog_vertical_titlebar)
) prefs.details_table_delta_foreground_color = self.details_table_delta_foreground_color.color
prefs.details_dialog_vertical_titlebar = ischecked( prefs.result_table_ref_foreground_color = self.result_table_ref_foreground_color.color
self.details_dialog_vertical_titlebar prefs.result_table_ref_background_color = self.result_table_ref_background_color.color
) prefs.result_table_delta_foreground_color = self.result_table_delta_foreground_color.color
prefs.details_table_delta_foreground_color = (
self.details_table_delta_foreground_color.color
)
prefs.result_table_ref_foreground_color = (
self.result_table_ref_foreground_color.color
)
prefs.result_table_ref_background_color = (
self.result_table_ref_background_color.color
)
prefs.result_table_delta_foreground_color = (
self.result_table_delta_foreground_color.color
)
prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex() prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()
prefs.custom_command = str(self.customCommandEdit.text()) prefs.custom_command = str(self.customCommandEdit.text())
prefs.tableFontSize = self.fontSizeSpinBox.value() prefs.tableFontSize = self.fontSizeSpinBox.value()
@@ -410,9 +367,7 @@ class ColorPickerButton(QPushButton):
@pyqtSlot() @pyqtSlot()
def onClicked(self): def onClicked(self):
color = QColorDialog.getColor( color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent)
self.color if self.color is not None else Qt.white, self.parent
)
self.setColor(color) self.setColor(color)
def setColor(self, color): def setColor(self, color):

View File

@@ -79,12 +79,8 @@ class PrioritizeDialog(QDialog):
super().__init__(parent, flags, **kwargs) super().__init__(parent, flags, **kwargs)
self._setupUi() self._setupUi()
self.model = PrioritizeDialogModel(app=app.model) self.model = PrioritizeDialogModel(app=app.model)
self.categoryList = ComboboxModel( self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
model=self.model.category_list, view=self.categoryCombobox self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView)
)
self.criteriaList = ListviewModel(
model=self.model.criteria_list, view=self.criteriaListView
)
self.prioritizationList = PrioritizationList( self.prioritizationList = PrioritizationList(
model=self.model.prioritization_list, view=self.prioritizationListView model=self.model.prioritization_list, view=self.prioritizationListView
) )
@@ -112,12 +108,8 @@ class PrioritizeDialog(QDialog):
self.categoryCombobox = QComboBox() self.categoryCombobox = QComboBox()
self.criteriaListView = QListView() self.criteriaListView = QListView()
self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.addCriteriaButton = QPushButton( self.addCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowRight), "")
self.style().standardIcon(QStyle.SP_ArrowRight), "" self.removeCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowLeft), "")
)
self.removeCriteriaButton = QPushButton(
self.style().standardIcon(QStyle.SP_ArrowLeft), ""
)
self.prioritizationListView = QListView() self.prioritizationListView = QListView()
self.prioritizationListView.setAcceptDrops(True) self.prioritizationListView.setAcceptDrops(True)
self.prioritizationListView.setDragEnabled(True) self.prioritizationListView.setDragEnabled(True)

View File

@@ -295,9 +295,7 @@ class ResultWindow(QMainWindow):
if menu.actions(): if menu.actions():
menu.clear() menu.clear()
self._column_actions = [] self._column_actions = []
for index, (display, visible) in enumerate( for index, (display, visible) in enumerate(self.app.model.result_table.columns.menu_items()):
self.app.model.result_table.columns.menu_items()
):
action = menu.addAction(display) action = menu.addAction(display)
action.setCheckable(True) action.setCheckable(True)
action.setChecked(visible) action.setChecked(visible)

View File

@@ -6,17 +6,16 @@
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QSpinBox,
QVBoxLayout, QVBoxLayout,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QSizePolicy, QSizePolicy,
QSpacerItem, QSpacerItem,
QWidget, QWidget,
QLineEdit,
) )
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.util import tryint
from core.app import AppMode from core.app import AppMode
from core.scanner import ScanType from core.scanner import ScanType
@@ -35,15 +34,11 @@ class PreferencesDialog(PreferencesDialogBase):
self.verticalLayout_4 = QVBoxLayout(self.widget) self.verticalLayout_4 = QVBoxLayout(self.widget)
self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget) self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget)
self.verticalLayout_4.addWidget(self.wordWeightingBox) self.verticalLayout_4.addWidget(self.wordWeightingBox)
self._setupAddCheckbox( self._setupAddCheckbox("matchSimilarBox", tr("Match similar words"), self.widget)
"matchSimilarBox", tr("Match similar words"), self.widget
)
self.verticalLayout_4.addWidget(self.matchSimilarBox) self.verticalLayout_4.addWidget(self.matchSimilarBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget) self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget)
self.verticalLayout_4.addWidget(self.mixFileKindBox) self.verticalLayout_4.addWidget(self.mixFileKindBox)
self._setupAddCheckbox( self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"), self.widget)
"useRegexpBox", tr("Use regular expressions when filtering"), self.widget
)
self.verticalLayout_4.addWidget(self.useRegexpBox) self.verticalLayout_4.addWidget(self.useRegexpBox)
self._setupAddCheckbox( self._setupAddCheckbox(
"removeEmptyFoldersBox", "removeEmptyFoldersBox",
@@ -52,35 +47,48 @@ class PreferencesDialog(PreferencesDialogBase):
) )
self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox) self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox)
self.horizontalLayout_2 = QHBoxLayout() self.horizontalLayout_2 = QHBoxLayout()
self._setupAddCheckbox( self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
"ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget
)
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox) self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
self.sizeThresholdEdit = QLineEdit(self.widget) self.sizeThresholdSpinBox = QSpinBox(self.widget)
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
self.sizeThresholdEdit.sizePolicy().hasHeightForWidth() self.sizeThresholdSpinBox.setSizePolicy(sizePolicy)
) self.sizeThresholdSpinBox.setMaximumSize(QSize(100, 16777215))
self.sizeThresholdEdit.setSizePolicy(sizePolicy) self.sizeThresholdSpinBox.setRange(0, 1000000)
self.sizeThresholdEdit.setMaximumSize(QSize(50, 16777215)) self.horizontalLayout_2.addWidget(self.sizeThresholdSpinBox)
self.horizontalLayout_2.addWidget(self.sizeThresholdEdit)
self.label_6 = QLabel(self.widget) self.label_6 = QLabel(self.widget)
self.label_6.setText(tr("KB")) self.label_6.setText(tr("KB"))
self.horizontalLayout_2.addWidget(self.label_6) self.horizontalLayout_2.addWidget(self.label_6)
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1) self.horizontalLayout_2.addItem(spacerItem1)
self.verticalLayout_4.addLayout(self.horizontalLayout_2) self.verticalLayout_4.addLayout(self.horizontalLayout_2)
self.horizontalLayout_2b = QHBoxLayout()
self._setupAddCheckbox(
"bigFilePartialHashesBox",
tr("Partially hash files bigger than"),
self.widget,
)
self.horizontalLayout_2b.addWidget(self.bigFilePartialHashesBox)
self.bigSizeThresholdSpinBox = QSpinBox(self.widget)
self.bigSizeThresholdSpinBox.setSizePolicy(sizePolicy)
self.bigSizeThresholdSpinBox.setMaximumSize(QSize(100, 16777215))
self.bigSizeThresholdSpinBox.setRange(0, 1000000)
self.horizontalLayout_2b.addWidget(self.bigSizeThresholdSpinBox)
self.label_6b = QLabel(self.widget)
self.label_6b.setText(tr("MB"))
self.horizontalLayout_2b.addWidget(self.label_6b)
spacerItem2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2b.addItem(spacerItem2)
self.verticalLayout_4.addLayout(self.horizontalLayout_2b)
self._setupAddCheckbox( self._setupAddCheckbox(
"ignoreHardlinkMatches", "ignoreHardlinkMatches",
tr("Ignore duplicates hardlinking to the same file"), tr("Ignore duplicates hardlinking to the same file"),
self.widget, self.widget,
) )
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches) self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
self._setupAddCheckbox( self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"), self.widget)
"debugModeBox", tr("Debug mode (restart required)"), self.widget
)
self.verticalLayout_4.addWidget(self.debugModeBox) self.verticalLayout_4.addWidget(self.debugModeBox)
self.widgetsVLayout.addWidget(self.widget) self.widgetsVLayout.addWidget(self.widget)
self._setupBottomPart() self._setupBottomPart()
@@ -89,7 +97,9 @@ class PreferencesDialog(PreferencesDialogBase):
setchecked(self.matchSimilarBox, prefs.match_similar) setchecked(self.matchSimilarBox, prefs.match_similar)
setchecked(self.wordWeightingBox, prefs.word_weighting) setchecked(self.wordWeightingBox, prefs.word_weighting)
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files) setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold)) self.sizeThresholdSpinBox.setValue(prefs.small_file_threshold)
setchecked(self.bigFilePartialHashesBox, prefs.big_file_partial_hashes)
self.bigSizeThresholdSpinBox.setValue(prefs.big_file_size_threshold)
# Update UI state based on selected scan type # Update UI state based on selected scan type
scan_type = prefs.get_scan_type(AppMode.Standard) scan_type = prefs.get_scan_type(AppMode.Standard)
@@ -102,4 +112,6 @@ class PreferencesDialog(PreferencesDialogBase):
prefs.match_similar = ischecked(self.matchSimilarBox) prefs.match_similar = ischecked(self.matchSimilarBox)
prefs.word_weighting = ischecked(self.wordWeightingBox) prefs.word_weighting = ischecked(self.wordWeightingBox)
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox) prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text()) prefs.small_file_threshold = self.sizeThresholdSpinBox.value()
prefs.big_file_partial_hashes = ischecked(self.bigFilePartialHashesBox)
prefs.big_file_size_threshold = self.bigSizeThresholdSpinBox.value()

View File

@@ -19,6 +19,7 @@ from .directories_dialog import DirectoriesDialog
from .result_window import ResultWindow from .result_window import ResultWindow
from .ignore_list_dialog import IgnoreListDialog from .ignore_list_dialog import IgnoreListDialog
from .exclude_list_dialog import ExcludeListDialog from .exclude_list_dialog import ExcludeListDialog
tr = trget("ui") tr = trget("ui")
@@ -135,16 +136,15 @@ class TabWindow(QMainWindow):
action.setEnabled(True) action.setEnabled(True)
self.app.directories_dialog.actionShowResultsWindow.setEnabled( self.app.directories_dialog.actionShowResultsWindow.setEnabled(
False if page_type == "ResultWindow" False if page_type == "ResultWindow" else self.app.resultWindow is not None
else self.app.resultWindow is not None) )
self.app.actionIgnoreList.setEnabled( self.app.actionIgnoreList.setEnabled(
True if self.app.ignoreListDialog is not None True if self.app.ignoreListDialog is not None and not page_type == "IgnoreListDialog" else False
and not page_type == "IgnoreListDialog" else False) )
self.app.actionDirectoriesWindow.setEnabled( self.app.actionDirectoriesWindow.setEnabled(False if page_type == "DirectoriesDialog" else True)
False if page_type == "DirectoriesDialog" else True)
self.app.actionExcludeList.setEnabled( self.app.actionExcludeList.setEnabled(
True if self.app.excludeListDialog is not None True if self.app.excludeListDialog is not None and not page_type == "ExcludeListDialog" else False
and not page_type == "ExcludeListDialog" else False) )
self.previous_widget_actions = active_widget.specific_actions self.previous_widget_actions = active_widget.specific_actions
self.last_index = current_index self.last_index = current_index
@@ -176,8 +176,7 @@ class TabWindow(QMainWindow):
index = self.tabWidget.addTab(page, title) index = self.tabWidget.addTab(page, title)
# index = self.tabWidget.insertTab(-1, page, title) # index = self.tabWidget.insertTab(-1, page, title)
if isinstance(page, DirectoriesDialog): if isinstance(page, DirectoriesDialog):
self.tabWidget.tabBar().setTabButton( self.tabWidget.tabBar().setTabButton(index, QTabBar.RightSide, None)
index, QTabBar.RightSide, None)
if switch: if switch:
self.setCurrentIndex(index) self.setCurrentIndex(index)
return index return index
@@ -250,6 +249,7 @@ class TabWindow(QMainWindow):
class TabBarWindow(TabWindow): class TabBarWindow(TabWindow):
"""Implementation which uses a separate QTabBar and QStackedWidget. """Implementation which uses a separate QTabBar and QStackedWidget.
The Tab bar is placed next to the menu bar to save real estate.""" The Tab bar is placed next to the menu bar to save real estate."""
def __init__(self, app, **kwargs): def __init__(self, app, **kwargs):
super().__init__(app, **kwargs) super().__init__(app, **kwargs)
@@ -286,8 +286,7 @@ class TabBarWindow(TabWindow):
self.tabBar.insertTab(stack_index, title) self.tabBar.insertTab(stack_index, title)
if isinstance(page, DirectoriesDialog): if isinstance(page, DirectoriesDialog):
self.tabBar.setTabButton( self.tabBar.setTabButton(stack_index, QTabBar.RightSide, None)
stack_index, QTabBar.RightSide, None)
if switch: # switch to the added tab immediately upon creation if switch: # switch to the added tab immediately upon creation
self.setTabIndex(stack_index) self.setTabIndex(stack_index)
return stack_index return stack_index

View File

@@ -25,12 +25,7 @@ tr = trget("qtlib")
class AboutBox(QDialog): class AboutBox(QDialog):
def __init__(self, parent, app, **kwargs): def __init__(self, parent, app, **kwargs):
flags = ( flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
Qt.CustomizeWindowHint
| Qt.WindowTitleHint
| Qt.WindowSystemMenuHint
| Qt.MSWindowsFixedSizeDialogHint
)
super().__init__(parent, flags, **kwargs) super().__init__(parent, flags, **kwargs)
self.app = app self.app = app
self._setupUi() self._setupUi()
@@ -39,9 +34,7 @@ class AboutBox(QDialog):
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
def _setupUi(self): def _setupUi(self):
self.setWindowTitle( self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
tr("About {}").format(QCoreApplication.instance().applicationName())
)
self.resize(400, 290) self.resize(400, 290)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@@ -61,9 +54,7 @@ class AboutBox(QDialog):
self.nameLabel.setText(QCoreApplication.instance().applicationName()) self.nameLabel.setText(QCoreApplication.instance().applicationName())
self.verticalLayout.addWidget(self.nameLabel) self.verticalLayout.addWidget(self.nameLabel)
self.versionLabel = QLabel(self) self.versionLabel = QLabel(self)
self.versionLabel.setText( self.versionLabel.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
tr("Version {}").format(QCoreApplication.instance().applicationVersion())
)
self.verticalLayout.addWidget(self.versionLabel) self.verticalLayout.addWidget(self.versionLabel)
self.label_3 = QLabel(self) self.label_3 = QLabel(self)
self.verticalLayout.addWidget(self.label_3) self.verticalLayout.addWidget(self.label_3)

View File

@@ -62,9 +62,7 @@ class Columns:
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns. # See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
for column in self.model.column_list: for column in self.model.column_list:
if column.resizeToFit: if column.resizeToFit:
self._headerView.setSectionResizeMode( self._headerView.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
column.logical_index, QHeaderView.ResizeToContents
)
# --- Public # --- Public
def setColumnsWidth(self, widths): def setColumnsWidth(self, widths):

View File

@@ -34,9 +34,7 @@ class ErrorReportDialog(QDialog):
self._setupUi() self._setupUi()
name = QCoreApplication.applicationName() name = QCoreApplication.applicationName()
version = QCoreApplication.applicationVersion() version = QCoreApplication.applicationVersion()
errorText = "Application Name: {}\nVersion: {}\n\n{}".format( errorText = "Application Name: {}\nVersion: {}\n\n{}".format(name, version, error)
name, version, error
)
# Under windows, we end up with an error report without linesep if we don't mangle it # Under windows, we end up with an error report without linesep if we don't mangle it
errorText = errorText.replace("\n", os.linesep) errorText = errorText.replace("\n", os.linesep)
self.errorTextEdit.setPlainText(errorText) self.errorTextEdit.setPlainText(errorText)

View File

@@ -57,72 +57,76 @@ msgid "Go to Github"
msgstr "Přejít na Github" msgstr "Přejít na Github"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Anglicky."
#: qtlib\preferences.py:24
msgid "French"
msgstr "Francouzsky"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Německy"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "řecky"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "čínsky (zjednodušeně)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "česky" msgstr "česky"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "italsky" msgstr "Německy"
#: qtlib\preferences.py:30 #: qtlib\preferences.py:25
msgid "Armenian" msgid "Greek"
msgstr "arménsky" msgstr "řecky"
#: qtlib\preferences.py:31 #: qtlib\preferences.py:26
msgid "Korean" msgid "English"
msgstr "korejsky" msgstr "Anglicky."
#: qtlib\preferences.py:32 #: qtlib\preferences.py:27
msgid "Russian"
msgstr "rusky"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "ukrajinsky"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "holandsky"
#: qtlib\preferences.py:35
msgid "Polish"
msgstr "polsky"
#: qtlib\preferences.py:36
msgid "Brazilian"
msgstr "brazilsky"
#: qtlib\preferences.py:37
msgid "Spanish" msgid "Spanish"
msgstr "španělsky" msgstr "španělsky"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Francouzsky"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "arménsky"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "italsky"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "Japonština"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "korejsky"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "holandsky"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "polsky"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "brazilsky"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "rusky"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "ukrajinsky"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "vietnamsky" msgstr "vietnamsky"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "Japonština" msgstr "čínsky (zjednodušeně)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

View File

@@ -58,72 +58,76 @@ msgid "Go to Github"
msgstr "Geh zu Github" msgstr "Geh zu Github"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Englisch"
#: qtlib\preferences.py:24
msgid "French"
msgstr "Französisch"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Deutsch"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "Griechisch"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "Chinesisch (Vereinfachtes)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "Tschechisch" msgstr "Tschechisch"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "Italienisch" msgstr "Deutsch"
#: qtlib\preferences.py:30 #: qtlib\preferences.py:25
msgid "Armenian" msgid "Greek"
msgstr "Armenisch" msgstr "Griechisch"
#: qtlib\preferences.py:31 #: qtlib\preferences.py:26
msgid "Korean" msgid "English"
msgstr "Koreanisch" msgstr "Englisch"
#: qtlib\preferences.py:32 #: qtlib\preferences.py:27
msgid "Russian"
msgstr "Russisch"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "Ukrainisch"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "Niederländisch"
#: qtlib\preferences.py:35
msgid "Polish"
msgstr "Polnisch"
#: qtlib\preferences.py:36
msgid "Brazilian"
msgstr "Brasilianisch"
#: qtlib\preferences.py:37
msgid "Spanish" msgid "Spanish"
msgstr "Spanisch" msgstr "Spanisch"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Französisch"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "Armenisch"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "Italienisch"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "Japanisch"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "Koreanisch"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "Niederländisch"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "Polnisch"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "Brasilianisch"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "Russisch"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "Ukrainisch"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "Vietnamesisch" msgstr "Vietnamesisch"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "Japanisch" msgstr "Chinesisch (Vereinfachtes)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

View File

@@ -58,72 +58,76 @@ msgid "Go to Github"
msgstr "Επίσκεψη Github" msgstr "Επίσκεψη Github"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Αγγλικά"
#: qtlib\preferences.py:24
msgid "French"
msgstr "Γαλλικά"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Γερμανικά"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "Ελληνικά"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "Κινέζικα (Απλοποιημένα)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "Τσέχικα" msgstr "Τσέχικα"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "Ιταλικά"
#: qtlib\preferences.py:30
msgid "Armenian"
msgstr "Αρμένικα"
#: qtlib\preferences.py:31
msgid "Korean"
msgstr "Κορεάτικα"
#: qtlib\preferences.py:32
msgid "Russian"
msgstr "Ρώσικα"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "Ουκρανέζικα"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "Γερμανικά" msgstr "Γερμανικά"
#: qtlib\preferences.py:35 #: qtlib\preferences.py:25
msgid "Polish" msgid "Greek"
msgstr "Πολωνικά" msgstr "Ελληνικά"
#: qtlib\preferences.py:36 #: qtlib\preferences.py:26
msgid "Brazilian" msgid "English"
msgstr "Βραζιλιάνικα" msgstr "Αγγλικά"
#: qtlib\preferences.py:37 #: qtlib\preferences.py:27
msgid "Spanish" msgid "Spanish"
msgstr "Ισπανικά" msgstr "Ισπανικά"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Γαλλικά"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "Αρμένικα"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "Ιταλικά"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "Ιαπωνικά"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "Κορεάτικα"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "Γερμανικά"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "Πολωνικά"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "Βραζιλιάνικα"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "Ρώσικα"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "Ουκρανέζικα"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "Βιετναμέζικα" msgstr "Βιετναμέζικα"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "Ιαπωνικά" msgstr "Κινέζικα (Απλοποιημένα)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

View File

@@ -58,72 +58,76 @@ msgid "Go to Github"
msgstr "Ir a Github" msgstr "Ir a Github"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Inglés"
#: qtlib\preferences.py:24
msgid "French"
msgstr "Francés"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Alemán"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "Griego"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "Chino (simplificado)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "Checo" msgstr "Checo"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "Italiano" msgstr "Alemán"
#: qtlib\preferences.py:30 #: qtlib\preferences.py:25
msgid "Armenian" msgid "Greek"
msgstr "Armenio" msgstr "Griego"
#: qtlib\preferences.py:31 #: qtlib\preferences.py:26
msgid "Korean" msgid "English"
msgstr "Coreano" msgstr "Inglés"
#: qtlib\preferences.py:32 #: qtlib\preferences.py:27
msgid "Russian"
msgstr "Ruso"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "Ucraniano"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "Holandés"
#: qtlib\preferences.py:35
msgid "Polish"
msgstr "Polaco"
#: qtlib\preferences.py:36
msgid "Brazilian"
msgstr "Brasileño"
#: qtlib\preferences.py:37
msgid "Spanish" msgid "Spanish"
msgstr "Español" msgstr "Español"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Francés"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "Armenio"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "Italiano"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "Japonés"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "Coreano"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "Holandés"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "Polaco"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "Brasileño"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "Ruso"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "Ucraniano"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "Vietnamita" msgstr "Vietnamita"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "Japonés" msgstr "Chino (simplificado)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

View File

@@ -58,72 +58,76 @@ msgid "Go to Github"
msgstr "Aller sur Github" msgstr "Aller sur Github"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Anglais"
#: qtlib\preferences.py:24
msgid "French"
msgstr "Français"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Allemand"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "Grecque"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "Chinois (Simplifié)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "Tchèque" msgstr "Tchèque"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "Italien" msgstr "Allemand"
#: qtlib\preferences.py:30 #: qtlib\preferences.py:25
msgid "Armenian" msgid "Greek"
msgstr "Arménien" msgstr "Grecque"
#: qtlib\preferences.py:31 #: qtlib\preferences.py:26
msgid "Korean" msgid "English"
msgstr "Coréen" msgstr "Anglais"
#: qtlib\preferences.py:32 #: qtlib\preferences.py:27
msgid "Russian"
msgstr "Russe"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "Ukrainien"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "Néerlandais"
#: qtlib\preferences.py:35
msgid "Polish"
msgstr "Polonais"
#: qtlib\preferences.py:36
msgid "Brazilian"
msgstr "Brésilien"
#: qtlib\preferences.py:37
msgid "Spanish" msgid "Spanish"
msgstr "Espagnol" msgstr "Espagnol"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Français"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "Arménien"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "Italien"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "Japonais"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "Coréen"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "Néerlandais"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "Polonais"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "Brésilien"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "Russe"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "Ukrainien"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "Vietnamien" msgstr "Vietnamien"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "Japonais" msgstr "Chinois (Simplifié)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

View File

@@ -58,72 +58,76 @@ msgid "Go to Github"
msgstr "Գնացեք Գիթուբ" msgstr "Գնացեք Գիթուբ"
#: qtlib\preferences.py:23 #: qtlib\preferences.py:23
msgid "English"
msgstr "Անգլերեն"
#: qtlib\preferences.py:24
msgid "French"
msgstr "Ֆրանսերեն"
#: qtlib\preferences.py:25
msgid "German"
msgstr "Գերմաներեն"
#: qtlib\preferences.py:26
msgid "Greek"
msgstr "հունարեն"
#: qtlib\preferences.py:27
msgid "Chinese (Simplified)"
msgstr "Չինարեն (Պարզեցված)"
#: qtlib\preferences.py:28
msgid "Czech" msgid "Czech"
msgstr "Չեխերեն" msgstr "Չեխերեն"
#: qtlib\preferences.py:29 #: qtlib\preferences.py:24
msgid "Italian" msgid "German"
msgstr "Իտալերեն" msgstr "Գերմաներեն"
#: qtlib\preferences.py:30 #: qtlib\preferences.py:25
msgid "Armenian" msgid "Greek"
msgstr "հայերեն" msgstr "հունարեն"
#: qtlib\preferences.py:31 #: qtlib\preferences.py:26
msgid "Korean" msgid "English"
msgstr "կորեերեն" msgstr "Անգլերեն"
#: qtlib\preferences.py:32 #: qtlib\preferences.py:27
msgid "Russian"
msgstr "ռուսերեն"
#: qtlib\preferences.py:33
msgid "Ukrainian"
msgstr "ուկրաիներեն"
#: qtlib\preferences.py:34
msgid "Dutch"
msgstr "հոլանդերեն"
#: qtlib\preferences.py:35
msgid "Polish"
msgstr "լեհերեն"
#: qtlib\preferences.py:36
msgid "Brazilian"
msgstr "բրազիլական"
#: qtlib\preferences.py:37
msgid "Spanish" msgid "Spanish"
msgstr "Իսպաներեն" msgstr "Իսպաներեն"
#: qtlib\preferences.py:28
msgid "French"
msgstr "Ֆրանսերեն"
#: qtlib\preferences.py:29
msgid "Armenian"
msgstr "հայերեն"
#: qtlib\preferences.py:30
msgid "Italian"
msgstr "Իտալերեն"
#: qtlib\preferences.py:31
msgid "Japanese"
msgstr "ճապոներեն"
#: qtlib\preferences.py:32
msgid "Korean"
msgstr "կորեերեն"
#: qtlib\preferences.py:33
msgid "Dutch"
msgstr "հոլանդերեն"
#: qtlib\preferences.py:34
msgid "Polish"
msgstr "լեհերեն"
#: qtlib\preferences.py:35
msgid "Brazilian"
msgstr "բրազիլական"
#: qtlib\preferences.py:36
msgid "Russian"
msgstr "ռուսերեն"
#: qtlib\preferences.py:37
msgid "Turkish"
msgstr ""
#: qtlib\preferences.py:38 #: qtlib\preferences.py:38
msgid "Ukrainian"
msgstr "ուկրաիներեն"
#: qtlib\preferences.py:39
msgid "Vietnamese" msgid "Vietnamese"
msgstr "վիետնամերեն" msgstr "վիետնամերեն"
#: qtlib\preferences.py:39 #: qtlib\preferences.py:40
msgid "Japanese" msgid "Chinese (Simplified)"
msgstr "ճապոներեն" msgstr "Չինարեն (Պարզեցված)"
#: qtlib\recent.py:54 #: qtlib\recent.py:54
msgid "Clear List" msgid "Clear List"

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