mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-03-12 06:24:36 +00:00
Merge remote-tracking branch 'upstream/master' into colors-bytes
This commit is contained in:
commit
8c5e18b980
45
.github/workflows/default.yml
vendored
45
.github/workflows/default.yml
vendored
@ -9,43 +9,22 @@ on:
|
|||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
pre-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python 3.10
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
- name: Install dependencies
|
- uses: pre-commit/action@v3.0.0
|
||||||
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.10
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: "3.10"
|
|
||||||
- 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:
|
test:
|
||||||
needs: [lint, format]
|
needs: [pre-commit]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
python-version: [3.7, 3.8, 3.9, "3.10"]
|
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
|
||||||
exclude:
|
exclude:
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
@ -53,17 +32,21 @@ jobs:
|
|||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
|
- os: macos-latest
|
||||||
|
python-version: "3.10"
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: 3.7
|
python-version: 3.7
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
24
.pre-commit-config.yaml
Normal file
24
.pre-commit-config.yaml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-toml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: ".*.json"
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 22.10.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
exclude: ^(.tox|env|build|dist|help|qt/dg_rc.py|pkg).*
|
||||||
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
|
rev: v9.3.0
|
||||||
|
hooks:
|
||||||
|
- id: commitlint
|
||||||
|
stages: [commit-msg]
|
||||||
|
additional_dependencies: ["@commitlint/config-conventional"]
|
@ -1 +1 @@
|
|||||||
sonar.python.version=3.7, 3.8, 3.9, 3.10
|
sonar.python.version=3.7, 3.8, 3.9, 3.10, 3.11
|
||||||
|
@ -18,4 +18,3 @@ 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
|
||||||
|
|
||||||
|
1
LICENSE
1
LICENSE
@ -619,4 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
|
|||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
17
commitlint.config.js
Normal file
17
commitlint.config.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const Configuration = {
|
||||||
|
/*
|
||||||
|
* Resolve and load @commitlint/config-conventional from node_modules.
|
||||||
|
* Referenced packages must be installed
|
||||||
|
*/
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
/*
|
||||||
|
* Any rules defined here will override rules from @commitlint/config-conventional
|
||||||
|
*/
|
||||||
|
rules: {
|
||||||
|
'header-max-length': [2, 'always', 72],
|
||||||
|
'subject-case': [2, 'always', 'sentence-case'],
|
||||||
|
'scope-enum': [2, 'always'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Configuration;
|
13
core/app.py
13
core/app.py
@ -126,8 +126,6 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
NAME = PROMPT_NAME = "dupeGuru"
|
NAME = PROMPT_NAME = "dupeGuru"
|
||||||
|
|
||||||
PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache
|
|
||||||
|
|
||||||
def __init__(self, view, portable=False):
|
def __init__(self, view, portable=False):
|
||||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
@ -153,7 +151,8 @@ class DupeGuru(Broadcaster):
|
|||||||
"clean_empty_dirs": False,
|
"clean_empty_dirs": False,
|
||||||
"ignore_hardlink_matches": False,
|
"ignore_hardlink_matches": False,
|
||||||
"copymove_dest_type": DestType.RELATIVE,
|
"copymove_dest_type": DestType.RELATIVE,
|
||||||
"picture_cache_type": self.PICTURE_CACHE_TYPE,
|
"include_exists_check": True,
|
||||||
|
"rehash_ignore_mtime": False,
|
||||||
}
|
}
|
||||||
self.selected_dupes = []
|
self.selected_dupes = []
|
||||||
self.details_panel = DetailsPanel(self)
|
self.details_panel = DetailsPanel(self)
|
||||||
@ -183,8 +182,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.view.create_results_window()
|
self.view.create_results_window()
|
||||||
|
|
||||||
def _get_picture_cache_path(self):
|
def _get_picture_cache_path(self):
|
||||||
cache_type = self.options["picture_cache_type"]
|
cache_name = "cached_pictures.db"
|
||||||
cache_name = "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):
|
||||||
@ -555,7 +553,9 @@ class DupeGuru(Broadcaster):
|
|||||||
# a workaround to make the damn thing work.
|
# a workaround to make the damn thing work.
|
||||||
exepath, args = match.groups()
|
exepath, args = match.groups()
|
||||||
path, exename = op.split(exepath)
|
path, exename = op.split(exepath)
|
||||||
p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
p = subprocess.Popen(
|
||||||
|
exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||||
|
)
|
||||||
output = p.stdout.read()
|
output = p.stdout.read()
|
||||||
logging.info("Custom command %s %s: %s", exename, args, output)
|
logging.info("Custom command %s %s: %s", exename, args, output)
|
||||||
else:
|
else:
|
||||||
@ -792,6 +792,7 @@ class DupeGuru(Broadcaster):
|
|||||||
Scans folders selected in :attr:`directories` and put the results in :attr:`results`
|
Scans folders selected in :attr:`directories` and put the results in :attr:`results`
|
||||||
"""
|
"""
|
||||||
scanner = self.SCANNER_CLASS()
|
scanner = self.SCANNER_CLASS()
|
||||||
|
fs.filesdb.ignore_mtime = self.options["rehash_ignore_mtime"] is True
|
||||||
if not self.directories.has_any_file():
|
if not self.directories.has_any_file():
|
||||||
self.view.show_message(tr("The selected directories contain no scannable file."))
|
self.view.show_message(tr("The selected directories contain no scannable file."))
|
||||||
return
|
return
|
||||||
|
60
core/fs.py
60
core/fs.py
@ -97,59 +97,68 @@ class FilesDB:
|
|||||||
schema_version = 1
|
schema_version = 1
|
||||||
schema_version_description = "Changed from md5 to xxhash if available."
|
schema_version_description = "Changed from md5 to xxhash if available."
|
||||||
|
|
||||||
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)"
|
create_table_query = """CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER,
|
||||||
|
entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)"""
|
||||||
drop_table_query = "DROP TABLE IF EXISTS files;"
|
drop_table_query = "DROP TABLE IF EXISTS files;"
|
||||||
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
|
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
|
||||||
|
select_query_ignore_mtime = "SELECT {key} FROM files WHERE path=:path AND size=:size"
|
||||||
insert_query = """
|
insert_query = """
|
||||||
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
|
INSERT INTO files (path, size, mtime_ns, entry_dt, {key})
|
||||||
|
VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
|
||||||
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
|
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ignore_mtime = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.conn = None
|
self.conn = None
|
||||||
self.cur = None
|
|
||||||
self.lock = None
|
self.lock = None
|
||||||
|
|
||||||
def connect(self, path: Union[AnyStr, os.PathLike]) -> None:
|
def connect(self, path: Union[AnyStr, os.PathLike]) -> None:
|
||||||
self.conn = sqlite3.connect(path, check_same_thread=False)
|
self.conn = sqlite3.connect(path, check_same_thread=False)
|
||||||
self.cur = self.conn.cursor()
|
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self._check_upgrade()
|
self._check_upgrade()
|
||||||
|
|
||||||
def _check_upgrade(self) -> None:
|
def _check_upgrade(self) -> None:
|
||||||
with self.lock:
|
with self.lock, self.conn as conn:
|
||||||
has_schema = self.cur.execute(
|
has_schema = conn.execute(
|
||||||
"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'"
|
"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
version = None
|
version = None
|
||||||
if has_schema:
|
if has_schema:
|
||||||
version = self.cur.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0]
|
version = conn.execute("SELECT version FROM schema_version ORDER BY version DESC").fetchone()[0]
|
||||||
else:
|
else:
|
||||||
self.cur.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)")
|
conn.execute("CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)")
|
||||||
if version != self.schema_version:
|
if version != self.schema_version:
|
||||||
self.cur.execute(self.drop_table_query)
|
conn.execute(self.drop_table_query)
|
||||||
self.cur.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)",
|
"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)",
|
||||||
{"version": self.schema_version, "description": self.schema_version_description},
|
{"version": self.schema_version, "description": self.schema_version_description},
|
||||||
)
|
)
|
||||||
self.cur.execute(self.create_table_query)
|
conn.execute(self.create_table_query)
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
with self.lock:
|
with self.lock, self.conn as conn:
|
||||||
self.cur.execute(self.drop_table_query)
|
conn.execute(self.drop_table_query)
|
||||||
self.cur.execute(self.create_table_query)
|
conn.execute(self.create_table_query)
|
||||||
|
|
||||||
def get(self, path: Path, key: str) -> Union[bytes, None]:
|
def get(self, path: Path, key: str) -> Union[bytes, None]:
|
||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
size = stat.st_size
|
size = stat.st_size
|
||||||
mtime_ns = stat.st_mtime_ns
|
mtime_ns = stat.st_mtime_ns
|
||||||
try:
|
try:
|
||||||
with self.lock:
|
with self.conn as conn:
|
||||||
self.cur.execute(
|
if self.ignore_mtime:
|
||||||
self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns}
|
cursor = conn.execute(
|
||||||
|
self.select_query_ignore_mtime.format(key=key), {"path": str(path), "size": size}
|
||||||
)
|
)
|
||||||
result = self.cur.fetchone()
|
else:
|
||||||
|
cursor = conn.execute(
|
||||||
|
self.select_query.format(key=key),
|
||||||
|
{"path": str(path), "size": size, "mtime_ns": mtime_ns},
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return result[0]
|
return result[0]
|
||||||
@ -163,8 +172,8 @@ class FilesDB:
|
|||||||
size = stat.st_size
|
size = stat.st_size
|
||||||
mtime_ns = stat.st_mtime_ns
|
mtime_ns = stat.st_mtime_ns
|
||||||
try:
|
try:
|
||||||
with self.lock:
|
with self.lock, self.conn as conn:
|
||||||
self.cur.execute(
|
conn.execute(
|
||||||
self.insert_query.format(key=key),
|
self.insert_query.format(key=key),
|
||||||
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
||||||
)
|
)
|
||||||
@ -177,7 +186,6 @@ class FilesDB:
|
|||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.cur.close()
|
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
|
|
||||||
@ -307,6 +315,14 @@ class File:
|
|||||||
"""Returns whether this file wrapper class can handle ``path``."""
|
"""Returns whether this file wrapper class can handle ``path``."""
|
||||||
return not path.is_symlink() and path.is_file()
|
return not path.is_symlink() and path.is_file()
|
||||||
|
|
||||||
|
def exists(self) -> bool:
|
||||||
|
"""Safely check if the underlying file exists, treat error as non-existent"""
|
||||||
|
try:
|
||||||
|
return self.path.exists()
|
||||||
|
except OSError as ex:
|
||||||
|
logging.warning(f"Checking {self.path} raised: {ex}")
|
||||||
|
return False
|
||||||
|
|
||||||
def rename(self, newname):
|
def rename(self, newname):
|
||||||
if newname == self.name:
|
if newname == self.name:
|
||||||
return
|
return
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
# Copyright 2016 Virgil Dupras
|
|
||||||
#
|
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
||||||
# which should be included with this package. The terms are also available at
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import os
|
|
||||||
import os.path as op
|
|
||||||
import shelve
|
|
||||||
import tempfile
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from core.pe.cache import bytes_to_colors, colors_to_bytes
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_path(path):
|
|
||||||
return f"path:{path}"
|
|
||||||
|
|
||||||
|
|
||||||
def unwrap_path(key):
|
|
||||||
return key[5:]
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_id(path):
|
|
||||||
return f"id:{path}"
|
|
||||||
|
|
||||||
|
|
||||||
def unwrap_id(key):
|
|
||||||
return int(key[3:])
|
|
||||||
|
|
||||||
|
|
||||||
CacheRow = namedtuple("CacheRow", "id path blocks mtime")
|
|
||||||
|
|
||||||
|
|
||||||
class ShelveCache:
|
|
||||||
"""A class to cache picture blocks in a shelve backend."""
|
|
||||||
|
|
||||||
def __init__(self, db=None, readonly=False):
|
|
||||||
self.istmp = db is None
|
|
||||||
if self.istmp:
|
|
||||||
self.dtmp = tempfile.mkdtemp()
|
|
||||||
self.ftmp = db = op.join(self.dtmp, "tmpdb")
|
|
||||||
flag = "r" if readonly else "c"
|
|
||||||
self.shelve = shelve.open(db, flag)
|
|
||||||
self.maxid = self._compute_maxid()
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
return wrap_path(key) in self.shelve
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
row = self.shelve[wrap_path(key)]
|
|
||||||
del self.shelve[wrap_path(key)]
|
|
||||||
del self.shelve[wrap_id(row.id)]
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if isinstance(key, int):
|
|
||||||
skey = self.shelve[wrap_id(key)]
|
|
||||||
else:
|
|
||||||
skey = wrap_path(key)
|
|
||||||
return bytes_to_colors(self.shelve[skey].blocks)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return (unwrap_path(k) for k in self.shelve if k.startswith("path:"))
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return sum(1 for k in self.shelve if k.startswith("path:"))
|
|
||||||
|
|
||||||
def __setitem__(self, path_str, blocks):
|
|
||||||
blocks = colors_to_bytes(blocks)
|
|
||||||
if op.exists(path_str):
|
|
||||||
mtime = int(os.stat(path_str).st_mtime)
|
|
||||||
else:
|
|
||||||
mtime = 0
|
|
||||||
if path_str in self:
|
|
||||||
rowid = self.shelve[wrap_path(path_str)].id
|
|
||||||
else:
|
|
||||||
rowid = self._get_new_id()
|
|
||||||
row = CacheRow(rowid, path_str, blocks, mtime)
|
|
||||||
self.shelve[wrap_path(path_str)] = row
|
|
||||||
self.shelve[wrap_id(rowid)] = wrap_path(path_str)
|
|
||||||
|
|
||||||
def _compute_maxid(self):
|
|
||||||
return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1)
|
|
||||||
|
|
||||||
def _get_new_id(self):
|
|
||||||
self.maxid += 1
|
|
||||||
return self.maxid
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.shelve.clear()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.shelve is not None:
|
|
||||||
self.shelve.close()
|
|
||||||
if self.istmp:
|
|
||||||
os.remove(self.ftmp)
|
|
||||||
os.rmdir(self.dtmp)
|
|
||||||
self.shelve = None
|
|
||||||
|
|
||||||
def filter(self, func):
|
|
||||||
to_delete = [key for key in self if not func(key)]
|
|
||||||
for key in to_delete:
|
|
||||||
del self[key]
|
|
||||||
|
|
||||||
def get_id(self, path):
|
|
||||||
if path in self:
|
|
||||||
return self.shelve[wrap_path(path)].id
|
|
||||||
else:
|
|
||||||
raise ValueError(path)
|
|
||||||
|
|
||||||
def get_multiple(self, rowids):
|
|
||||||
for rowid in rowids:
|
|
||||||
try:
|
|
||||||
skey = self.shelve[wrap_id(rowid)]
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
yield (rowid, bytes_to_colors(self.shelve[skey].blocks))
|
|
||||||
|
|
||||||
def purge_outdated(self):
|
|
||||||
"""Go through the cache and purge outdated records.
|
|
||||||
|
|
||||||
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
|
|
||||||
the db.
|
|
||||||
"""
|
|
||||||
todelete = []
|
|
||||||
for path in self:
|
|
||||||
row = self.shelve[wrap_path(path)]
|
|
||||||
if row.mtime and op.exists(path):
|
|
||||||
picture_mtime = os.stat(path).st_mtime
|
|
||||||
if int(picture_mtime) <= row.mtime:
|
|
||||||
# not outdated
|
|
||||||
continue
|
|
||||||
todelete.append(path)
|
|
||||||
for path in todelete:
|
|
||||||
try:
|
|
||||||
del self[path]
|
|
||||||
except KeyError:
|
|
||||||
# I have no idea why a KeyError sometimes happen, but it does, as we can see in
|
|
||||||
# #402 and #439. I don't think it hurts to silently ignore the error, so that's
|
|
||||||
# what we do
|
|
||||||
pass
|
|
@ -16,6 +16,7 @@ from hscommon.jobprogress import job
|
|||||||
|
|
||||||
from core.engine import Match
|
from core.engine import Match
|
||||||
from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError
|
from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError
|
||||||
|
from core.pe.cache_sqlite import SqliteCache
|
||||||
|
|
||||||
# OPTIMIZATION NOTES:
|
# OPTIMIZATION NOTES:
|
||||||
# The bottleneck of the matching phase is CPU, which is why we use multiprocessing. However, another
|
# The bottleneck of the matching phase is CPU, which is why we use multiprocessing. However, another
|
||||||
@ -50,13 +51,6 @@ except Exception:
|
|||||||
|
|
||||||
|
|
||||||
def get_cache(cache_path, readonly=False):
|
def get_cache(cache_path, readonly=False):
|
||||||
if cache_path.endswith("shelve"):
|
|
||||||
from core.pe.cache_shelve import ShelveCache
|
|
||||||
|
|
||||||
return ShelveCache(cache_path, readonly=readonly)
|
|
||||||
else:
|
|
||||||
from core.pe.cache_sqlite import SqliteCache
|
|
||||||
|
|
||||||
return SqliteCache(cache_path, readonly=readonly)
|
return SqliteCache(cache_path, readonly=readonly)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,16 +2,14 @@
|
|||||||
* Created On: 2010-01-30
|
* Created On: 2010-01-30
|
||||||
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
* Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
*
|
*
|
||||||
* This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
* This software is licensed under the "BSD" License as described in the
|
||||||
* which should be included with this package. The terms are also available at
|
* "LICENSE" file, which should be included with this package. The terms are
|
||||||
* http://www.hardcoded.net/licenses/bsd_license
|
* also available at http://www.hardcoded.net/licenses/bsd_license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
static PyObject*
|
static PyObject *cache_bytes_to_colors(PyObject *self, PyObject *args) {
|
||||||
cache_bytes_to_colors(PyObject *self, PyObject *args)
|
|
||||||
{
|
|
||||||
char *y;
|
char *y;
|
||||||
Py_ssize_t char_count, i, color_count;
|
Py_ssize_t char_count, i, color_count;
|
||||||
PyObject *result;
|
PyObject *result;
|
||||||
@ -47,12 +45,12 @@ cache_bytes_to_colors(PyObject *self, PyObject *args)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PyMethodDef CacheMethods[] = {
|
static PyMethodDef CacheMethods[] = {
|
||||||
{"bytes_to_colors", cache_bytes_to_colors, METH_VARARGS, "Transform the bytes 's' into a list of 3 sized tuples."},
|
{"bytes_to_colors", cache_bytes_to_colors, METH_VARARGS,
|
||||||
|
"Transform the bytes 's' into a list of 3 sized tuples."},
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
{NULL, NULL, 0, NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct PyModuleDef CacheDef = {
|
static struct PyModuleDef CacheDef = {PyModuleDef_HEAD_INIT,
|
||||||
PyModuleDef_HEAD_INIT,
|
|
||||||
"_cache",
|
"_cache",
|
||||||
NULL,
|
NULL,
|
||||||
-1,
|
-1,
|
||||||
@ -60,12 +58,9 @@ static struct PyModuleDef CacheDef = {
|
|||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
NULL,
|
NULL,
|
||||||
NULL
|
NULL};
|
||||||
};
|
|
||||||
|
|
||||||
PyObject *
|
PyObject *PyInit__cache(void) {
|
||||||
PyInit__cache(void)
|
|
||||||
{
|
|
||||||
PyObject *m = PyModule_Create(&CacheDef);
|
PyObject *m = PyModule_Create(&CacheDef);
|
||||||
if (m == NULL) {
|
if (m == NULL) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -29,7 +29,7 @@ class Photo(fs.File):
|
|||||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
# These extensions are supported on all platforms
|
# These extensions are supported on all platforms
|
||||||
HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif"}
|
HANDLED_EXTS = {"png", "jpg", "jpeg", "gif", "bmp", "tiff", "tif", "webp"}
|
||||||
|
|
||||||
def _plat_get_dimensions(self):
|
def _plat_get_dimensions(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -171,7 +171,8 @@ class Scanner:
|
|||||||
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
|
matches = [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 = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
|
matches = [m 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()]
|
if self.include_exists_check:
|
||||||
|
matches = [m for m in matches if m.first.exists() and m.second.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 = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
|
matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]
|
||||||
@ -212,3 +213,4 @@ class Scanner:
|
|||||||
large_size_threshold = 0
|
large_size_threshold = 0
|
||||||
big_file_size_threshold = 0
|
big_file_size_threshold = 0
|
||||||
word_weighting = False
|
word_weighting = False
|
||||||
|
include_exists_check = True
|
||||||
|
@ -12,7 +12,6 @@ from hscommon.testutil import eq_
|
|||||||
try:
|
try:
|
||||||
from core.pe.cache import colors_to_bytes, bytes_to_colors
|
from core.pe.cache import colors_to_bytes, bytes_to_colors
|
||||||
from core.pe.cache_sqlite import SqliteCache
|
from core.pe.cache_sqlite import SqliteCache
|
||||||
from core.pe.cache_shelve import ShelveCache
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
skip("Can't import the cache module, probably hasn't been compiled.")
|
skip("Can't import the cache module, probably hasn't been compiled.")
|
||||||
|
|
||||||
@ -134,11 +133,6 @@ class TestCaseSqliteCache(BaseTestCaseCache):
|
|||||||
eq_(c["foo"], [(1, 2, 3)])
|
eq_(c["foo"], [(1, 2, 3)])
|
||||||
|
|
||||||
|
|
||||||
class TestCaseShelveCache(BaseTestCaseCache):
|
|
||||||
def get_cache(self, dbname=None):
|
|
||||||
return ShelveCache(dbname)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseCacheSQLEscape:
|
class TestCaseCacheSQLEscape:
|
||||||
def get_cache(self):
|
def get_cache(self):
|
||||||
return SqliteCache()
|
return SqliteCache()
|
||||||
|
@ -17,6 +17,7 @@ from core.scanner import Scanner, ScanType
|
|||||||
from core.me.scanner import ScannerME
|
from core.me.scanner import ScannerME
|
||||||
|
|
||||||
|
|
||||||
|
# TODO update this to be able to inherit from fs.File
|
||||||
class NamedObject:
|
class NamedObject:
|
||||||
def __init__(self, name="foobar", size=1, path=None):
|
def __init__(self, name="foobar", size=1, path=None):
|
||||||
if path is None:
|
if path is None:
|
||||||
@ -31,6 +32,9 @@ class NamedObject:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<NamedObject {!r} {!r}>".format(self.name, self.path)
|
return "<NamedObject {!r} {!r}>".format(self.name, self.path)
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return self.path.exists()
|
||||||
|
|
||||||
|
|
||||||
no = NamedObject
|
no = NamedObject
|
||||||
|
|
||||||
|
@ -15,4 +15,3 @@ hscommon.gui.progress_window
|
|||||||
.. autoclass:: ProgressWindowView
|
.. autoclass:: ProgressWindowView
|
||||||
:members:
|
:members:
|
||||||
:private-members:
|
:private-members:
|
||||||
|
|
||||||
|
@ -15,4 +15,3 @@ hscommon.gui.tree
|
|||||||
.. autoclass:: Node
|
.. autoclass:: Node
|
||||||
:members:
|
:members:
|
||||||
:private-members:
|
:private-members:
|
||||||
|
|
||||||
|
@ -13,4 +13,3 @@ hscommon
|
|||||||
util
|
util
|
||||||
jobprogress/*
|
jobprogress/*
|
||||||
gui/*
|
gui/*
|
||||||
|
|
||||||
|
@ -14,4 +14,3 @@ hscommon.jobprogress.job
|
|||||||
|
|
||||||
.. autoclass:: NullJob
|
.. autoclass:: NullJob
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
@ -9,4 +9,3 @@ hscommon.jobprogress.performer
|
|||||||
|
|
||||||
.. autoclass:: ThreadedJobPerformer
|
.. autoclass:: ThreadedJobPerformer
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
@ -178,4 +178,3 @@ Preferences are stored elsewhere:
|
|||||||
|
|
||||||
.. _Github: https://github.com/arsenetar/dupeguru
|
.. _Github: https://github.com/arsenetar/dupeguru
|
||||||
.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels
|
.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels
|
||||||
|
|
||||||
|
@ -12,4 +12,3 @@
|
|||||||
* Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**:
|
* Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**:
|
||||||
|
|
||||||
Սա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը:
|
Սա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը:
|
||||||
|
|
||||||
|
@ -23,4 +23,3 @@ dupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակներ
|
|||||||
մեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի ֆայլի անուն, որը
|
մեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի ֆայլի անուն, որը
|
||||||
չի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները
|
չի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները
|
||||||
նախկինում էին, խումբը պետք է օգտագործվի:
|
նախկինում էին, խումբը պետք է օգտագործվի:
|
||||||
|
|
||||||
|
@ -114,4 +114,3 @@
|
|||||||
Якщо все це не так, `контакт УГ підтримки <http://www.hardcoded.net/support>`_, ми зрозуміти це.
|
Якщо все це не так, `контакт УГ підтримки <http://www.hardcoded.net/support>`_, ми зрозуміти це.
|
||||||
|
|
||||||
.. todo:: This FAQ qestion is outdated, see english version.
|
.. todo:: This FAQ qestion is outdated, see english version.
|
||||||
|
|
||||||
|
@ -41,7 +41,8 @@ def trget(domain: str) -> Callable[[str], str]:
|
|||||||
|
|
||||||
|
|
||||||
def set_tr(
|
def set_tr(
|
||||||
new_tr: Callable[[str, Union[str, None]], str], new_trget: Union[Callable[[str], Callable[[str], str]], None] = None
|
new_tr: Callable[[str, Union[str, None]], str],
|
||||||
|
new_trget: Union[Callable[[str], Callable[[str], str]], None] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
global _trfunc, _trget
|
global _trfunc, _trget
|
||||||
_trfunc = new_tr
|
_trfunc = new_tr
|
||||||
|
@ -10,110 +10,110 @@ 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 ""
|
msgstr "مسار الملف"
|
||||||
|
|
||||||
#: core\gui\problem_table.py:19
|
#: core\gui\problem_table.py:19
|
||||||
msgid "Error Message"
|
msgid "Error Message"
|
||||||
msgstr ""
|
msgstr "رسالة خطأ"
|
||||||
|
|
||||||
#: core\me\prioritize.py:23
|
#: core\me\prioritize.py:23
|
||||||
msgid "Duration"
|
msgid "Duration"
|
||||||
msgstr ""
|
msgstr "مدة"
|
||||||
|
|
||||||
#: core\me\prioritize.py:30 core\me\result_table.py:23
|
#: core\me\prioritize.py:30 core\me\result_table.py:23
|
||||||
msgid "Bitrate"
|
msgid "Bitrate"
|
||||||
msgstr ""
|
msgstr "معدل البت"
|
||||||
|
|
||||||
#: core\me\prioritize.py:37
|
#: core\me\prioritize.py:37
|
||||||
msgid "Samplerate"
|
msgid "Samplerate"
|
||||||
msgstr ""
|
msgstr "معدل العينة"
|
||||||
|
|
||||||
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
|
#: core\me\result_table.py:19 core\pe\result_table.py:19 core\prioritize.py:92
|
||||||
#: core\se\result_table.py:19
|
#: core\se\result_table.py:19
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr ""
|
msgstr "اسم الملف"
|
||||||
|
|
||||||
#: 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 ""
|
msgstr "مجلد"
|
||||||
|
|
||||||
#: core\me\result_table.py:21
|
#: core\me\result_table.py:21
|
||||||
msgid "Size (MB)"
|
msgid "Size (MB)"
|
||||||
msgstr ""
|
msgstr "الحجم (ميغا بايت)"
|
||||||
|
|
||||||
#: core\me\result_table.py:22
|
#: core\me\result_table.py:22
|
||||||
msgid "Time"
|
msgid "Time"
|
||||||
msgstr ""
|
msgstr "زمن"
|
||||||
|
|
||||||
#: core\me\result_table.py:24
|
#: core\me\result_table.py:24
|
||||||
msgid "Sample Rate"
|
msgid "Sample Rate"
|
||||||
msgstr ""
|
msgstr "معدل العينة"
|
||||||
|
|
||||||
#: 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 ""
|
msgstr "طيب القلب"
|
||||||
|
|
||||||
#: 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
|
||||||
msgid "Modification"
|
msgid "Modification"
|
||||||
msgstr ""
|
msgstr "تعديل"
|
||||||
|
|
||||||
#: core\me\result_table.py:27
|
#: core\me\result_table.py:27
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr ""
|
msgstr "عنوان"
|
||||||
|
|
||||||
#: core\me\result_table.py:28
|
#: core\me\result_table.py:28
|
||||||
msgid "Artist"
|
msgid "Artist"
|
||||||
msgstr ""
|
msgstr "فنان"
|
||||||
|
|
||||||
#: core\me\result_table.py:29
|
#: core\me\result_table.py:29
|
||||||
msgid "Album"
|
msgid "Album"
|
||||||
msgstr ""
|
msgstr "البوم"
|
||||||
|
|
||||||
#: core\me\result_table.py:30
|
#: core\me\result_table.py:30
|
||||||
msgid "Genre"
|
msgid "Genre"
|
||||||
msgstr ""
|
msgstr "النوع"
|
||||||
|
|
||||||
#: core\me\result_table.py:31
|
#: core\me\result_table.py:31
|
||||||
msgid "Year"
|
msgid "Year"
|
||||||
msgstr ""
|
msgstr "سنة"
|
||||||
|
|
||||||
#: core\me\result_table.py:32
|
#: core\me\result_table.py:32
|
||||||
msgid "Track Number"
|
msgid "Track Number"
|
||||||
msgstr ""
|
msgstr "رقم الشاحنة"
|
||||||
|
|
||||||
#: core\me\result_table.py:33
|
#: core\me\result_table.py:33
|
||||||
msgid "Comment"
|
msgid "Comment"
|
||||||
msgstr ""
|
msgstr "تعليق"
|
||||||
|
|
||||||
#: core\me\result_table.py:34 core\pe\result_table.py:26
|
#: core\me\result_table.py:34 core\pe\result_table.py:26
|
||||||
#: core\se\result_table.py:24
|
#: core\se\result_table.py:24
|
||||||
msgid "Match %"
|
msgid "Match %"
|
||||||
msgstr ""
|
msgstr "مباراة ٪"
|
||||||
|
|
||||||
#: core\me\result_table.py:35 core\se\result_table.py:25
|
#: core\me\result_table.py:35 core\se\result_table.py:25
|
||||||
msgid "Words Used"
|
msgid "Words Used"
|
||||||
msgstr ""
|
msgstr "الكلمات المستخدمة"
|
||||||
|
|
||||||
#: core\me\result_table.py:36 core\pe\result_table.py:27
|
#: core\me\result_table.py:36 core\pe\result_table.py:27
|
||||||
#: core\se\result_table.py:26
|
#: core\se\result_table.py:26
|
||||||
msgid "Dupe Count"
|
msgid "Dupe Count"
|
||||||
msgstr ""
|
msgstr "عدد المخادعين"
|
||||||
|
|
||||||
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
|
#: core\pe\prioritize.py:23 core\pe\result_table.py:23
|
||||||
msgid "Dimensions"
|
msgid "Dimensions"
|
||||||
msgstr ""
|
msgstr "أبعاد"
|
||||||
|
|
||||||
#: 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 ""
|
msgstr "الحجم (كيلو بايت)"
|
||||||
|
|
||||||
#: core\pe\result_table.py:24
|
#: core\pe\result_table.py:24
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
msgstr ""
|
msgstr "الطابع الزمني EXIF"
|
||||||
|
|
||||||
#: core\prioritize.py:156
|
#: core\prioritize.py:156
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr "بحجم"
|
||||||
|
@ -114,4 +114,3 @@ msgstr ""
|
|||||||
#: core\prioritize.py:158
|
#: core\prioritize.py:158
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -36,83 +36,83 @@ msgstr ""
|
|||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:291
|
#: core\app.py:293
|
||||||
msgid "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:302
|
#: core\app.py:304
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:319
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:319
|
#: core\app.py:321
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:323
|
||||||
msgid "All marked files were deleted successfully."
|
msgid "All marked files were deleted successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:323
|
#: core\app.py:325
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:328
|
#: core\app.py:330
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:386
|
#: core\app.py:388
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:394
|
#: core\app.py:396
|
||||||
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
msgid "All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:473
|
#: core\app.py:475
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:512
|
#: core\app.py:514
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:518 core\app.py:773 core\app.py:783
|
#: core\app.py:520 core\app.py:781 core\app.py:791
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:541
|
#: core\app.py:543
|
||||||
msgid "You have no custom command set up. Set it up in your preferences."
|
msgid "You have no custom command set up. Set it up in your preferences."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:697 core\app.py:709
|
#: core\app.py:705 core\app.py:717
|
||||||
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:745
|
#: core\app.py:753
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:792
|
#: core\app.py:801
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:808
|
#: core\app.py:817
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:858
|
#: core\app.py:867
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -243,4 +243,3 @@ msgstr ""
|
|||||||
#: core\se\scanner.py:18
|
#: core\se\scanner.py:18
|
||||||
msgid "Folders"
|
msgid "Folders"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ msgstr "Raccolte {} cartelle da scansionare"
|
|||||||
|
|
||||||
#: core\engine.py:27
|
#: core\engine.py:27
|
||||||
msgid "%d matches found from %d groups"
|
msgid "%d matches found from %d groups"
|
||||||
msgstr "%d corrispondeze trovate da %d gruppi"
|
msgstr "%d corrispondenze trovate da %d gruppi"
|
||||||
|
|
||||||
#: core\gui\deletion_options.py:71
|
#: core\gui\deletion_options.py:71
|
||||||
msgid "You are sending {} file(s) to the Trash."
|
msgid "You are sending {} file(s) to the Trash."
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
|
||||||
|
|
@ -1092,3 +1092,32 @@ msgstr ""
|
|||||||
#: qt\search_edit.py:78
|
#: qt\search_edit.py:78
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:219
|
||||||
|
msgid ""
|
||||||
|
"These options are for advanced users or for very specific situations, most "
|
||||||
|
"users should not have to modify these."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:225
|
||||||
|
msgid "Include existence check after scan completion"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:227
|
||||||
|
msgid "Ignore difference in mtime when loading cached digests"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\progress_window.py:64
|
||||||
|
msgid "Cancel?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\progress_window.py:65
|
||||||
|
msgid "Are you sure you want to cancel? All progress will be lost."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\exclude_list_dialog.py:161
|
||||||
|
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>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 ""
|
||||||
|
@ -348,4 +348,3 @@ dupeguru (2.9.2-1) unstable; urgency=low
|
|||||||
* Fixed selection glitches, especially while renaming. (#93)
|
* Fixed selection glitches, especially while renaming. (#93)
|
||||||
|
|
||||||
-- Virgil Dupras <hsoft@hardcoded.net> Wed, 10 Feb 2010 00:00:00 +0000
|
-- Virgil Dupras <hsoft@hardcoded.net> Wed, 10 Feb 2010 00:00:00 +0000
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Vcs-Git: https://github.com/arsenetar/dupeguru.git
|
|||||||
|
|
||||||
Package: {pkgname}
|
Package: {pkgname}
|
||||||
Architecture: {arch}
|
Architecture: {arch}
|
||||||
Depends: ${shlibs:Depends}, python3 (>=3.7), python3-pyqt5, python3-mutagen, python3-semantic-version
|
Depends: ${shlibs:Depends}, python3 (>=3.7), python3 (<<3.12), python3-pyqt5, python3-mutagen, python3-semantic-version
|
||||||
Provides: dupeguru-se, dupeguru-me, dupeguru-pe
|
Provides: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
Replaces: dupeguru-se, dupeguru-me, dupeguru-pe
|
Replaces: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe
|
Conflicts: dupeguru-se, dupeguru-me, dupeguru-pe
|
||||||
|
@ -6,3 +6,4 @@ Icon=dupeguru
|
|||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
|
Keywords=file manager;gui;
|
||||||
|
@ -192,7 +192,8 @@ class DupeGuru(QObject):
|
|||||||
scanned_tags.add("year")
|
scanned_tags.add("year")
|
||||||
self.model.options["scanned_tags"] = scanned_tags
|
self.model.options["scanned_tags"] = scanned_tags
|
||||||
self.model.options["match_scaled"] = self.prefs.match_scaled
|
self.model.options["match_scaled"] = self.prefs.match_scaled
|
||||||
self.model.options["picture_cache_type"] = self.prefs.picture_cache_type
|
self.model.options["include_exists_check"] = self.prefs.include_exists_check
|
||||||
|
self.model.options["rehash_ignore_mtime"] = self.prefs.rehash_ignore_mtime
|
||||||
|
|
||||||
if self.details_dialog:
|
if self.details_dialog:
|
||||||
self.details_dialog.update_options()
|
self.details_dialog.update_options()
|
||||||
|
@ -165,8 +165,8 @@ 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>\
|
||||||
For each file collected, two tests are performed to determine whether or not to completely ignore it:<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>
|
<li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>
|
||||||
<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>
|
<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li>\
|
||||||
Example: if you want to filter out .PNG files from the "My Pictures" directory only:<br>\
|
<br>Example: if you want to filter out .PNG files from the "My Pictures" directory only:<br>\
|
||||||
<code>.*My\\sPictures\\\\.*\\.png</code><br><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>\
|
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>
|
<code>C:\\\\User\\My Pictures\\test.png</code><br><br>
|
||||||
|
@ -31,7 +31,10 @@ class File(PhotoBase):
|
|||||||
image = image.convertToFormat(QImage.Format_RGB888)
|
image = image.convertToFormat(QImage.Format_RGB888)
|
||||||
if type(orientation) != int:
|
if type(orientation) != int:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"Orientation for file '%s' was a %s '%s', not an int.", str(self.path), type(orientation), orientation
|
"Orientation for file '%s' was a %s '%s', not an int.",
|
||||||
|
str(self.path),
|
||||||
|
type(orientation),
|
||||||
|
orientation,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
orientation = int(orientation)
|
orientation = int(orientation)
|
||||||
|
@ -4,11 +4,9 @@
|
|||||||
# 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.QtWidgets import QFormLayout
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon.plat import ISLINUX
|
from hscommon.plat import ISLINUX
|
||||||
from qt.radio_box import RadioBox
|
|
||||||
from core.scanner import ScanType
|
from core.scanner import ScanType
|
||||||
from core.app import AppMode
|
from core.app import AppMode
|
||||||
|
|
||||||
@ -35,11 +33,6 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
)
|
)
|
||||||
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
|
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
|
||||||
|
|
||||||
self.cacheTypeRadio = RadioBox(self, items=["Sqlite", "Shelve"], spread=False)
|
|
||||||
cache_form = QFormLayout()
|
|
||||||
cache_form.setLabelAlignment(Qt.AlignLeft)
|
|
||||||
cache_form.addRow(tr("Picture cache mode:"), self.cacheTypeRadio)
|
|
||||||
self.widgetsVLayout.addLayout(cache_form)
|
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _setupDisplayPage(self):
|
def _setupDisplayPage(self):
|
||||||
@ -64,7 +57,6 @@ show scrollbars to span the view around"
|
|||||||
|
|
||||||
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 = 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)
|
||||||
@ -75,6 +67,5 @@ show scrollbars to span the view around"
|
|||||||
|
|
||||||
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 = "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_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_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars)
|
||||||
|
@ -161,6 +161,8 @@ class Preferences(PreferencesBase):
|
|||||||
self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
|
self.ignore_hardlink_matches = get("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.rehash_ignore_mtime = get("RehashIgnoreMTime", self.rehash_ignore_mtime)
|
||||||
|
self.include_exists_check = get("IncludeExistsCheck", self.include_exists_check)
|
||||||
self.debug_mode = get("DebugMode", self.debug_mode)
|
self.debug_mode = get("DebugMode", self.debug_mode)
|
||||||
self.profile_scan = get("ProfileScan", self.profile_scan)
|
self.profile_scan = get("ProfileScan", self.profile_scan)
|
||||||
self.destination_type = get("DestinationType", self.destination_type)
|
self.destination_type = get("DestinationType", self.destination_type)
|
||||||
@ -223,7 +225,6 @@ class Preferences(PreferencesBase):
|
|||||||
self.scan_tag_genre = get("ScanTagGenre", self.scan_tag_genre)
|
self.scan_tag_genre = get("ScanTagGenre", self.scan_tag_genre)
|
||||||
self.scan_tag_year = get("ScanTagYear", self.scan_tag_year)
|
self.scan_tag_year = get("ScanTagYear", self.scan_tag_year)
|
||||||
self.match_scaled = get("MatchScaled", self.match_scaled)
|
self.match_scaled = get("MatchScaled", self.match_scaled)
|
||||||
self.picture_cache_type = get("PictureCacheType", self.picture_cache_type)
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.filter_hardness = 95
|
self.filter_hardness = 95
|
||||||
@ -231,6 +232,8 @@ class Preferences(PreferencesBase):
|
|||||||
self.use_regexp = False
|
self.use_regexp = False
|
||||||
self.ignore_hardlink_matches = False
|
self.ignore_hardlink_matches = False
|
||||||
self.remove_empty_folders = False
|
self.remove_empty_folders = False
|
||||||
|
self.rehash_ignore_mtime = False
|
||||||
|
self.include_exists_check = True
|
||||||
self.debug_mode = False
|
self.debug_mode = False
|
||||||
self.profile_scan = False
|
self.profile_scan = False
|
||||||
self.destination_type = 1
|
self.destination_type = 1
|
||||||
@ -274,7 +277,6 @@ class Preferences(PreferencesBase):
|
|||||||
self.scan_tag_genre = False
|
self.scan_tag_genre = False
|
||||||
self.scan_tag_year = False
|
self.scan_tag_year = False
|
||||||
self.match_scaled = False
|
self.match_scaled = False
|
||||||
self.picture_cache_type = "sqlite"
|
|
||||||
|
|
||||||
def _save_values(self, settings):
|
def _save_values(self, settings):
|
||||||
set_ = self.set_value
|
set_ = self.set_value
|
||||||
@ -283,6 +285,8 @@ class Preferences(PreferencesBase):
|
|||||||
set_("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
|
set_("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
|
||||||
set_("UseRegexp", self.use_regexp)
|
set_("UseRegexp", self.use_regexp)
|
||||||
set_("RemoveEmptyFolders", self.remove_empty_folders)
|
set_("RemoveEmptyFolders", self.remove_empty_folders)
|
||||||
|
set_("RehashIgnoreMTime", self.rehash_ignore_mtime)
|
||||||
|
set_("IncludeExistsCheck", self.include_exists_check)
|
||||||
set_("DebugMode", self.debug_mode)
|
set_("DebugMode", self.debug_mode)
|
||||||
set_("ProfileScan", self.profile_scan)
|
set_("ProfileScan", self.profile_scan)
|
||||||
set_("DestinationType", self.destination_type)
|
set_("DestinationType", self.destination_type)
|
||||||
@ -326,7 +330,6 @@ class Preferences(PreferencesBase):
|
|||||||
set_("ScanTagGenre", self.scan_tag_genre)
|
set_("ScanTagGenre", self.scan_tag_genre)
|
||||||
set_("ScanTagYear", self.scan_tag_year)
|
set_("ScanTagYear", self.scan_tag_year)
|
||||||
set_("MatchScaled", self.match_scaled)
|
set_("MatchScaled", self.match_scaled)
|
||||||
set_("PictureCacheType", self.picture_cache_type)
|
|
||||||
|
|
||||||
# scan_type is special because we save it immediately when we set it.
|
# scan_type is special because we save it immediately when we set it.
|
||||||
def get_scan_type(self, app_mode):
|
def get_scan_type(self, app_mode):
|
||||||
|
@ -47,8 +47,9 @@ class Sections(Flag):
|
|||||||
|
|
||||||
GENERAL = auto()
|
GENERAL = auto()
|
||||||
DISPLAY = auto()
|
DISPLAY = auto()
|
||||||
|
ADVANCED = auto()
|
||||||
DEBUG = auto()
|
DEBUG = auto()
|
||||||
ALL = GENERAL | DISPLAY | DEBUG
|
ALL = GENERAL | DISPLAY | ADVANCED | DEBUG
|
||||||
|
|
||||||
|
|
||||||
class PreferencesDialogBase(QDialog):
|
class PreferencesDialogBase(QDialog):
|
||||||
@ -145,7 +146,8 @@ On MacOS, the tab bar will fill up the window's width instead."
|
|||||||
)
|
)
|
||||||
self.use_native_dialogs.setToolTip(
|
self.use_native_dialogs.setToolTip(
|
||||||
tr(
|
tr(
|
||||||
"For actions such as file/folder selection use the OS native dialogs.\nSome native dialogs have limited functionality."
|
"For actions such as file/folder selection use the OS native dialogs.\n\
|
||||||
|
Some native dialogs have limited functionality."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
layout.addWidget(self.use_native_dialogs)
|
layout.addWidget(self.use_native_dialogs)
|
||||||
@ -213,6 +215,20 @@ use the modifier key to drag the floating window around"
|
|||||||
details_groupbox.setLayout(self.details_groupbox_layout)
|
details_groupbox.setLayout(self.details_groupbox_layout)
|
||||||
self.displayVLayout.addWidget(details_groupbox)
|
self.displayVLayout.addWidget(details_groupbox)
|
||||||
|
|
||||||
|
def _setup_advanced_page(self):
|
||||||
|
tab_label = QLabel(
|
||||||
|
tr(
|
||||||
|
"These options are for advanced users or for very specific situations, \
|
||||||
|
most users should not have to modify these."
|
||||||
|
),
|
||||||
|
wordWrap=True,
|
||||||
|
)
|
||||||
|
self.advanced_vlayout.addWidget(tab_label)
|
||||||
|
self._setupAddCheckbox("include_exists_check_box", tr("Include existence check after scan completion"))
|
||||||
|
self.advanced_vlayout.addWidget(self.include_exists_check_box)
|
||||||
|
self._setupAddCheckbox("rehash_ignore_mtime_box", tr("Ignore difference in mtime when loading cached digests"))
|
||||||
|
self.advanced_vlayout.addWidget(self.rehash_ignore_mtime_box)
|
||||||
|
|
||||||
def _setupDebugPage(self):
|
def _setupDebugPage(self):
|
||||||
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
|
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
|
||||||
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
|
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
|
||||||
@ -244,16 +260,20 @@ use the modifier key to drag the floating window around"
|
|||||||
self.tabwidget = QTabWidget()
|
self.tabwidget = QTabWidget()
|
||||||
self.page_general = QWidget()
|
self.page_general = QWidget()
|
||||||
self.page_display = QWidget()
|
self.page_display = QWidget()
|
||||||
|
self.page_advanced = QWidget()
|
||||||
self.page_debug = QWidget()
|
self.page_debug = QWidget()
|
||||||
self.widgetsVLayout = QVBoxLayout()
|
self.widgetsVLayout = QVBoxLayout()
|
||||||
self.page_general.setLayout(self.widgetsVLayout)
|
self.page_general.setLayout(self.widgetsVLayout)
|
||||||
self.displayVLayout = QVBoxLayout()
|
self.displayVLayout = QVBoxLayout()
|
||||||
self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style
|
self.displayVLayout.setSpacing(5) # arbitrary value, might conflict with style
|
||||||
self.page_display.setLayout(self.displayVLayout)
|
self.page_display.setLayout(self.displayVLayout)
|
||||||
|
self.advanced_vlayout = QVBoxLayout()
|
||||||
|
self.page_advanced.setLayout(self.advanced_vlayout)
|
||||||
self.debugVLayout = QVBoxLayout()
|
self.debugVLayout = QVBoxLayout()
|
||||||
self.page_debug.setLayout(self.debugVLayout)
|
self.page_debug.setLayout(self.debugVLayout)
|
||||||
self._setupPreferenceWidgets()
|
self._setupPreferenceWidgets()
|
||||||
self._setupDisplayPage()
|
self._setupDisplayPage()
|
||||||
|
self._setup_advanced_page()
|
||||||
self._setupDebugPage()
|
self._setupDebugPage()
|
||||||
# self.mainVLayout.addLayout(self.widgetsVLayout)
|
# self.mainVLayout.addLayout(self.widgetsVLayout)
|
||||||
self.buttonBox = QDialogButtonBox(self)
|
self.buttonBox = QDialogButtonBox(self)
|
||||||
@ -265,9 +285,11 @@ use the modifier key to drag the floating window around"
|
|||||||
self.layout().setSizeConstraint(QLayout.SetFixedSize)
|
self.layout().setSizeConstraint(QLayout.SetFixedSize)
|
||||||
self.tabwidget.addTab(self.page_general, tr("General"))
|
self.tabwidget.addTab(self.page_general, tr("General"))
|
||||||
self.tabwidget.addTab(self.page_display, tr("Display"))
|
self.tabwidget.addTab(self.page_display, tr("Display"))
|
||||||
|
self.tabwidget.addTab(self.page_advanced, tr("Advanced"))
|
||||||
self.tabwidget.addTab(self.page_debug, tr("Debug"))
|
self.tabwidget.addTab(self.page_debug, tr("Debug"))
|
||||||
self.displayVLayout.addStretch(0)
|
self.displayVLayout.addStretch(0)
|
||||||
self.widgetsVLayout.addStretch(0)
|
self.widgetsVLayout.addStretch(0)
|
||||||
|
self.advanced_vlayout.addStretch(0)
|
||||||
self.debugVLayout.addStretch(0)
|
self.debugVLayout.addStretch(0)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked, section):
|
def _load(self, prefs, setchecked, section):
|
||||||
@ -318,6 +340,9 @@ use the modifier key to drag the floating window around"
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
selected_lang = self.supportedLanguages["en"]
|
selected_lang = self.supportedLanguages["en"]
|
||||||
self.languageComboBox.setCurrentText(selected_lang)
|
self.languageComboBox.setCurrentText(selected_lang)
|
||||||
|
if section & Sections.ADVANCED:
|
||||||
|
setchecked(self.rehash_ignore_mtime_box, prefs.rehash_ignore_mtime)
|
||||||
|
setchecked(self.include_exists_check_box, prefs.include_exists_check)
|
||||||
if section & Sections.DEBUG:
|
if section & Sections.DEBUG:
|
||||||
setchecked(self.debugModeBox, prefs.debug_mode)
|
setchecked(self.debugModeBox, prefs.debug_mode)
|
||||||
setchecked(self.profile_scan_box, prefs.profile_scan)
|
setchecked(self.profile_scan_box, prefs.profile_scan)
|
||||||
@ -334,6 +359,8 @@ use the modifier key to drag the floating window around"
|
|||||||
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.rehash_ignore_mtime = ischecked(self.rehash_ignore_mtime_box)
|
||||||
|
prefs.include_exists_check = ischecked(self.include_exists_check_box)
|
||||||
prefs.debug_mode = ischecked(self.debugModeBox)
|
prefs.debug_mode = ischecked(self.debugModeBox)
|
||||||
prefs.profile_scan = ischecked(self.profile_scan_box)
|
prefs.profile_scan = ischecked(self.profile_scan_box)
|
||||||
prefs.reference_bold_font = ischecked(self.reference_bold_font)
|
prefs.reference_bold_font = ischecked(self.reference_bold_font)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtWidgets import QProgressDialog
|
from PyQt5.QtWidgets import QDialog, QMessageBox, QVBoxLayout, QLabel, QProgressBar, QPushButton
|
||||||
|
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
|
|
||||||
@ -25,37 +25,60 @@ class ProgressWindow:
|
|||||||
def refresh(self): # Labels
|
def refresh(self): # Labels
|
||||||
if self._window is not None:
|
if self._window is not None:
|
||||||
self._window.setWindowTitle(self.model.jobdesc_textfield.text)
|
self._window.setWindowTitle(self.model.jobdesc_textfield.text)
|
||||||
self._window.setLabelText(self.model.progressdesc_textfield.text)
|
self._label.setText(self.model.progressdesc_textfield.text)
|
||||||
|
|
||||||
def set_progress(self, last_progress):
|
def set_progress(self, last_progress):
|
||||||
if self._window is not None:
|
if self._window is not None:
|
||||||
if last_progress < 0:
|
if last_progress < 0:
|
||||||
self._window.setRange(0, 0)
|
self._progress_bar.setRange(0, 0)
|
||||||
else:
|
else:
|
||||||
self._window.setRange(0, 100)
|
self._progress_bar.setRange(0, 100)
|
||||||
self._window.setValue(last_progress)
|
self._progress_bar.setValue(last_progress)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||||
self._window = QProgressDialog("", tr("Cancel"), 0, 100, self.parent, flags)
|
self._window = QDialog(self.parent, flags)
|
||||||
|
self._setup_ui()
|
||||||
self._window.setModal(True)
|
self._window.setModal(True)
|
||||||
self._window.setAutoReset(False)
|
|
||||||
self._window.setAutoClose(False)
|
|
||||||
self._timer = QTimer(self._window)
|
self._timer = QTimer(self._window)
|
||||||
self._timer.timeout.connect(self.model.pulse)
|
self._timer.timeout.connect(self.model.pulse)
|
||||||
self._window.show()
|
self._window.show()
|
||||||
self._window.canceled.connect(self.model.cancel)
|
|
||||||
self._timer.start(500)
|
self._timer.start(500)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
self._window.setWindowTitle(tr("Cancel"))
|
||||||
|
vertical_layout = QVBoxLayout(self._window)
|
||||||
|
self._label = QLabel("", self._window)
|
||||||
|
vertical_layout.addWidget(self._label)
|
||||||
|
self._progress_bar = QProgressBar(self._window)
|
||||||
|
self._progress_bar.setRange(0, 100)
|
||||||
|
vertical_layout.addWidget(self._progress_bar)
|
||||||
|
self._cancel_button = QPushButton(tr("Cancel"), self._window)
|
||||||
|
self._cancel_button.clicked.connect(self.cancel)
|
||||||
|
vertical_layout.addWidget(self._cancel_button)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
if self._window is not None:
|
||||||
|
confirm_dialog = QMessageBox(
|
||||||
|
QMessageBox.Icon.Question,
|
||||||
|
tr("Cancel?"),
|
||||||
|
tr("Are you sure you want to cancel? All progress will be lost."),
|
||||||
|
QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes,
|
||||||
|
self._window,
|
||||||
|
)
|
||||||
|
confirm_dialog.setDefaultButton(QMessageBox.StandardButton.No)
|
||||||
|
result = confirm_dialog.exec_()
|
||||||
|
if result != QMessageBox.StandardButton.Yes:
|
||||||
|
return
|
||||||
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
# it seems it is possible for close to be called without a corresponding
|
# it seems it is possible for close to be called without a corresponding
|
||||||
# show, only perform a close if there is a window to close
|
# show, only perform a close if there is a window to close
|
||||||
if self._window is not None:
|
if self._window is not None:
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
del self._timer
|
del self._timer
|
||||||
# For some weird reason, canceled() signal is sent upon close, whether the user canceled
|
|
||||||
# or not. If we don't want a false cancellation, we have to disconnect it.
|
|
||||||
self._window.canceled.disconnect()
|
|
||||||
self._window.close()
|
self._window.close()
|
||||||
self._window.setParent(None)
|
self._window.setParent(None)
|
||||||
self._window = None
|
self._window = None
|
||||||
|
self.model.cancel()
|
||||||
|
4
tox.ini
4
tox.ini
@ -16,7 +16,7 @@ deps =
|
|||||||
-r{toxinidir}/requirements-extra.txt
|
-r{toxinidir}/requirements-extra.txt
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg
|
exclude = .tox,env*,build,help,qt/dg_rc.py,pkg
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
select = C,E,F,W,B,B950
|
select = C,E,F,W,B,B950
|
||||||
extend-ignore = E203, E501
|
extend-ignore = E203,W503
|
||||||
|
Loading…
x
Reference in New Issue
Block a user