mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-02-01 19:11:38 +00:00
Compare commits
33 Commits
143147cb8e
...
4.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
1f1dfa88dc
|
|||
|
916c5204cf
|
|||
|
71af825b37
|
|||
|
97f490b8b7
|
|||
|
d369bcddd7
|
|||
|
360dceca7b
|
|||
|
92b27801c3
|
|||
|
|
b9aabb8545 | ||
|
d5eeab4a17
|
|||
|
7865e4aeac
|
|||
|
58863b1728
|
|||
|
e382683f66
|
|||
|
f7ed1c801c
|
|||
|
f587c7b5d8
|
|||
|
40ff40bea8
|
|||
|
7a44c72a0a
|
|||
|
66aff9f74e
|
|||
|
5451f55219
|
|||
|
36280b01e6
|
|||
|
18359c3ea6
|
|||
|
0a4e61edf5
|
|||
|
d73a85b82e
|
|||
|
81c593399e
|
|||
|
6a732a79a8
|
|||
|
63dd4d4561
|
|||
|
e0061d7bc1
|
|||
|
c5818b1d1f
|
|||
|
a470a8de25
|
|||
|
a37b5b0eeb
|
|||
|
efd500ecc1
|
|||
|
43fcc52291
|
|||
|
50f5db1543
|
|||
|
a5b0ccdd02
|
@@ -13,12 +13,6 @@ source_file = locale/core.pot
|
|||||||
source_lang = en
|
source_lang = en
|
||||||
type = PO
|
type = PO
|
||||||
|
|
||||||
[o:voltaicideas:p:dupeguru-1:r:qtlib]
|
|
||||||
file_filter = qtlib/locale/<lang>/LC_MESSAGES/qtlib.po
|
|
||||||
source_file = qtlib/locale/qtlib.pot
|
|
||||||
source_lang = en
|
|
||||||
type = PO
|
|
||||||
|
|
||||||
[o:voltaicideas:p:dupeguru-1:r:ui]
|
[o:voltaicideas:p:dupeguru-1:r: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
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ recursive-include core *.m
|
|||||||
include run.py
|
include run.py
|
||||||
graft locale
|
graft locale
|
||||||
graft help
|
graft help
|
||||||
graft qtlib/locale
|
|
||||||
2
Makefile
2
Makefile
@@ -35,7 +35,7 @@ endif
|
|||||||
# Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we
|
# Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we
|
||||||
# use one of each file to act as a representative, a target, of these groups.
|
# use one of each file to act as a representative, a target, of these groups.
|
||||||
|
|
||||||
packages = hscommon qtlib core qt
|
packages = hscommon core qt
|
||||||
localedirs = $(wildcard locale/*/LC_MESSAGES)
|
localedirs = $(wildcard locale/*/LC_MESSAGES)
|
||||||
pofiles = $(wildcard locale/*/LC_MESSAGES/*.po)
|
pofiles = $(wildcard locale/*/LC_MESSAGES/*.po)
|
||||||
mofiles = $(patsubst %.po,%.mo,$(pofiles))
|
mofiles = $(patsubst %.po,%.mo,$(pofiles))
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ This folder contains the source for dupeGuru. Its documentation is in `help`, bu
|
|||||||
* help: Help document, written for Sphinx.
|
* help: Help document, written for Sphinx.
|
||||||
* locale: .po files for localization.
|
* locale: .po files for localization.
|
||||||
* hscommon: A collection of helpers used across HS applications.
|
* hscommon: A collection of helpers used across HS applications.
|
||||||
* qtlib: A collection of helpers used across Qt UI codebases of HS applications.
|
|
||||||
|
|
||||||
## How to build dupeGuru from source
|
## How to build dupeGuru from source
|
||||||
|
|
||||||
|
|||||||
14
build.py
14
build.py
@@ -61,7 +61,7 @@ def parse_args():
|
|||||||
|
|
||||||
|
|
||||||
def build_one_help(language):
|
def build_one_help(language):
|
||||||
print("Generating Help in {}".format(language))
|
print(f"Generating Help in {language}")
|
||||||
current_path = Path(".").absolute()
|
current_path = Path(".").absolute()
|
||||||
changelog_path = current_path.joinpath("help", "changelog")
|
changelog_path = current_path.joinpath("help", "changelog")
|
||||||
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
|
tixurl = "https://github.com/arsenetar/dupeguru/issues/{}"
|
||||||
@@ -88,14 +88,8 @@ def build_help():
|
|||||||
p.map(build_one_help, languages)
|
p.map(build_one_help, languages)
|
||||||
|
|
||||||
|
|
||||||
def build_qt_localizations():
|
|
||||||
loc.compile_all_po(Path("qtlib", "locale"))
|
|
||||||
loc.merge_locale_dir(Path("qtlib", "locale"), "locale")
|
|
||||||
|
|
||||||
|
|
||||||
def build_localizations():
|
def build_localizations():
|
||||||
loc.compile_all_po("locale")
|
loc.compile_all_po("locale")
|
||||||
build_qt_localizations()
|
|
||||||
locale_dest = Path("build", "locale")
|
locale_dest = Path("build", "locale")
|
||||||
if locale_dest.exists():
|
if locale_dest.exists():
|
||||||
shutil.rmtree(locale_dest)
|
shutil.rmtree(locale_dest)
|
||||||
@@ -110,19 +104,15 @@ def build_updatepot():
|
|||||||
loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"])
|
loc.generate_pot(["core"], Path("locale", "columns.pot"), ["coltr"])
|
||||||
print("Building ui.pot")
|
print("Building ui.pot")
|
||||||
loc.generate_pot(["qt"], Path("locale", "ui.pot"), ["tr"], merge=True)
|
loc.generate_pot(["qt"], Path("locale", "ui.pot"), ["tr"], merge=True)
|
||||||
print("Building qtlib.pot")
|
|
||||||
loc.generate_pot(["qtlib"], Path("qtlib", "locale", "qtlib.pot"), ["tr"])
|
|
||||||
|
|
||||||
|
|
||||||
def build_mergepot():
|
def build_mergepot():
|
||||||
print("Updating .po files using .pot files")
|
print("Updating .po files using .pot files")
|
||||||
loc.merge_pots_into_pos("locale")
|
loc.merge_pots_into_pos("locale")
|
||||||
loc.merge_pots_into_pos(Path("qtlib", "locale"))
|
|
||||||
|
|
||||||
|
|
||||||
def build_normpo():
|
def build_normpo():
|
||||||
loc.normalize_all_pos("locale")
|
loc.normalize_all_pos("locale")
|
||||||
loc.normalize_all_pos(Path("qtlib", "locale"))
|
|
||||||
|
|
||||||
|
|
||||||
def build_pe_modules():
|
def build_pe_modules():
|
||||||
@@ -139,7 +129,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("pyrcc5 {0} > {1}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
|
print_and_do("pyrcc5 {} > {}".format(Path("qt", "dg.qrc"), Path("qt", "dg_rc.py")))
|
||||||
fix_qt_resource_file(Path("qt", "dg_rc.py"))
|
fix_qt_resource_file(Path("qt", "dg_rc.py"))
|
||||||
build_help()
|
build_help()
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "4.2.1"
|
__version__ = "4.3.1"
|
||||||
__appname__ = "dupeGuru"
|
__appname__ = "dupeGuru"
|
||||||
|
|||||||
52
core/app.py
52
core/app.py
@@ -4,6 +4,8 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
import cProfile
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import os.path as op
|
import os.path as op
|
||||||
import logging
|
import logging
|
||||||
@@ -21,20 +23,20 @@ from hscommon.util import delete_if_empty, first, escape, nonone, allsame
|
|||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from hscommon import desktop
|
from hscommon import desktop
|
||||||
|
|
||||||
from . import se, me, pe
|
from core import se, me, pe
|
||||||
from .pe.photo import get_delta_dimensions
|
from core.pe.photo import get_delta_dimensions
|
||||||
from .util import cmp_value, fix_surrogate_encoding
|
from core.util import cmp_value, fix_surrogate_encoding
|
||||||
from . import directories, results, export, fs, prioritize
|
from core import directories, results, export, fs, prioritize
|
||||||
from .ignore import IgnoreList
|
from core.ignore import IgnoreList
|
||||||
from .exclude import ExcludeDict as ExcludeList
|
from core.exclude import ExcludeDict as ExcludeList
|
||||||
from .scanner import ScanType
|
from core.scanner import ScanType
|
||||||
from .gui.deletion_options import DeletionOptions
|
from core.gui.deletion_options import DeletionOptions
|
||||||
from .gui.details_panel import DetailsPanel
|
from core.gui.details_panel import DetailsPanel
|
||||||
from .gui.directory_tree import DirectoryTree
|
from core.gui.directory_tree import DirectoryTree
|
||||||
from .gui.ignore_list_dialog import IgnoreListDialog
|
from core.gui.ignore_list_dialog import IgnoreListDialog
|
||||||
from .gui.exclude_list_dialog import ExcludeListDialogCore
|
from core.gui.exclude_list_dialog import ExcludeListDialogCore
|
||||||
from .gui.problem_dialog import ProblemDialog
|
from core.gui.problem_dialog import ProblemDialog
|
||||||
from .gui.stats_label import StatsLabel
|
from core.gui.stats_label import StatsLabel
|
||||||
|
|
||||||
HAD_FIRST_LAUNCH_PREFERENCE = "HadFirstLaunch"
|
HAD_FIRST_LAUNCH_PREFERENCE = "HadFirstLaunch"
|
||||||
DEBUG_MODE_PREFERENCE = "DebugMode"
|
DEBUG_MODE_PREFERENCE = "DebugMode"
|
||||||
@@ -132,7 +134,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(desktop.SpecialFolder.APPDATA, appname=self.NAME, portable=portable)
|
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, portable=portable)
|
||||||
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
|
||||||
@@ -248,7 +250,7 @@ class DupeGuru(Broadcaster):
|
|||||||
ref = group.ref
|
ref = group.ref
|
||||||
linkfunc = os.link if use_hardlinks else os.symlink
|
linkfunc = os.link if use_hardlinks else os.symlink
|
||||||
linkfunc(str(ref.path), str_path)
|
linkfunc(str(ref.path), str_path)
|
||||||
self.clean_empty_dirs(dupe.path.parent())
|
self.clean_empty_dirs(dupe.path.parent)
|
||||||
|
|
||||||
def _create_file(self, path):
|
def _create_file(self, path):
|
||||||
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
|
# We add fs.Folder to fileclasses in case the file we're loading contains folder paths.
|
||||||
@@ -262,7 +264,7 @@ class DupeGuru(Broadcaster):
|
|||||||
try:
|
try:
|
||||||
f._read_all_info(attrnames=self.METADATA_TO_READ)
|
f._read_all_info(attrnames=self.METADATA_TO_READ)
|
||||||
return f
|
return f
|
||||||
except EnvironmentError:
|
except OSError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_export_data(self):
|
def _get_export_data(self):
|
||||||
@@ -553,9 +555,13 @@ 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)
|
||||||
subprocess.Popen(exename + args, shell=True, cwd=path)
|
p = subprocess.Popen(exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
output = p.stdout.read()
|
||||||
|
logging.info("Custom command %s %s: %s", exename, args, output)
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(dupe_cmd, shell=True)
|
p = subprocess.Popen(dupe_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
output = p.stdout.read()
|
||||||
|
logging.info("Custom command %s: %s", dupe_cmd, output)
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load directory selection and ignore list from files in appdata.
|
"""Load directory selection and ignore list from files in appdata.
|
||||||
@@ -780,7 +786,7 @@ class DupeGuru(Broadcaster):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
|
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
|
||||||
|
|
||||||
def start_scanning(self):
|
def start_scanning(self, profile_scan=False):
|
||||||
"""Starts an async job to scan for duplicates.
|
"""Starts an async job to scan for duplicates.
|
||||||
|
|
||||||
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`
|
||||||
@@ -800,6 +806,9 @@ class DupeGuru(Broadcaster):
|
|||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def do(j):
|
def do(j):
|
||||||
|
if profile_scan:
|
||||||
|
pr = cProfile.Profile()
|
||||||
|
pr.enable()
|
||||||
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(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
||||||
@@ -810,6 +819,9 @@ class DupeGuru(Broadcaster):
|
|||||||
logging.info("Scanning %d files" % len(files))
|
logging.info("Scanning %d files" % len(files))
|
||||||
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
|
self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)
|
||||||
self.discarded_file_count = scanner.discarded_file_count
|
self.discarded_file_count = scanner.discarded_file_count
|
||||||
|
if profile_scan:
|
||||||
|
pr.disable()
|
||||||
|
pr.dump_stats(op.join(self.appdata, f"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}.profile"))
|
||||||
|
|
||||||
self._start_job(JobType.SCAN, do)
|
self._start_job(JobType.SCAN, do)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from hscommon.jobprogress import job
|
|||||||
from hscommon.util import FileOrPath
|
from hscommon.util import FileOrPath
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
|
|
||||||
from . import fs
|
from core import fs
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Directories",
|
"Directories",
|
||||||
@@ -90,60 +90,57 @@ class Directories:
|
|||||||
return DirectoryState.EXCLUDED
|
return DirectoryState.EXCLUDED
|
||||||
|
|
||||||
def _get_files(self, from_path, fileclasses, j):
|
def _get_files(self, from_path, fileclasses, j):
|
||||||
for root, dirs, files in os.walk(str(from_path)):
|
try:
|
||||||
j.check_if_cancelled()
|
with os.scandir(from_path) as iter:
|
||||||
root_path = Path(root)
|
root_path = Path(from_path)
|
||||||
state = self.get_state(root_path)
|
state = self.get_state(root_path)
|
||||||
if state == DirectoryState.EXCLUDED and not any(
|
# if we have no un-excluded dirs under this directory skip going deeper
|
||||||
p.parts[: len(root_path.parts)] == root_path.parts for p in self.states
|
skip_dirs = state == DirectoryState.EXCLUDED and not any(
|
||||||
):
|
p.parts[: len(root_path.parts)] == root_path.parts for p in self.states
|
||||||
# Recursively get files from folders with lots of subfolder is expensive. However, there
|
)
|
||||||
# might be a subfolder in this path that is not excluded. What we want to do is to skim
|
count = 0
|
||||||
# through self.states and see if we must continue, or we can stop right here to save time
|
for item in iter:
|
||||||
del dirs[:]
|
j.check_if_cancelled()
|
||||||
try:
|
try:
|
||||||
if state != DirectoryState.EXCLUDED:
|
if item.is_dir():
|
||||||
# Old logic
|
if skip_dirs:
|
||||||
if self._exclude_list is None or not self._exclude_list.mark_count:
|
continue
|
||||||
found_files = [fs.get_file(root_path.joinpath(f), fileclasses=fileclasses) for f in files]
|
yield from self._get_files(item.path, fileclasses, j)
|
||||||
else:
|
continue
|
||||||
found_files = []
|
elif state == DirectoryState.EXCLUDED:
|
||||||
# print(f"len of files: {len(files)} {files}")
|
continue
|
||||||
for f in files:
|
# File excluding or not
|
||||||
if not self._exclude_list.is_excluded(root, f):
|
if (
|
||||||
found_files.append(fs.get_file(root_path.joinpath(f), fileclasses=fileclasses))
|
self._exclude_list is None
|
||||||
found_files = [f for f in found_files if f is not None]
|
or not self._exclude_list.mark_count
|
||||||
# In some cases, directories can be considered as files by dupeGuru, which is
|
or not self._exclude_list.is_excluded(str(from_path), item.name)
|
||||||
# why we have this line below. In fact, there only one case: Bundle files under
|
):
|
||||||
# OS X... In other situations, this forloop will do nothing.
|
file = fs.get_file(item, fileclasses=fileclasses)
|
||||||
for d in dirs[:]:
|
if file:
|
||||||
f = fs.get_file(root_path.joinpath(d), fileclasses=fileclasses)
|
file.is_ref = state == DirectoryState.REFERENCE
|
||||||
if f is not None:
|
count += 1
|
||||||
found_files.append(f)
|
yield file
|
||||||
dirs.remove(d)
|
except (OSError, fs.InvalidPath):
|
||||||
logging.debug(
|
pass
|
||||||
"Collected %d files in folder %s",
|
logging.debug(
|
||||||
len(found_files),
|
"Collected %d files in folder %s",
|
||||||
str(root_path),
|
count,
|
||||||
)
|
str(root_path),
|
||||||
for file in found_files:
|
)
|
||||||
file.is_ref = state == DirectoryState.REFERENCE
|
except OSError:
|
||||||
yield file
|
pass
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_folders(self, from_folder, j):
|
def _get_folders(self, from_folder, j):
|
||||||
j.check_if_cancelled()
|
j.check_if_cancelled()
|
||||||
try:
|
try:
|
||||||
for subfolder in from_folder.subfolders:
|
for subfolder in from_folder.subfolders:
|
||||||
for folder in self._get_folders(subfolder, j):
|
yield from self._get_folders(subfolder, j)
|
||||||
yield folder
|
|
||||||
state = self.get_state(from_folder.path)
|
state = self.get_state(from_folder.path)
|
||||||
if state != DirectoryState.EXCLUDED:
|
if state != DirectoryState.EXCLUDED:
|
||||||
from_folder.is_ref = state == DirectoryState.REFERENCE
|
from_folder.is_ref = state == DirectoryState.REFERENCE
|
||||||
logging.debug("Yielding Folder %r state: %d", from_folder, state)
|
logging.debug("Yielding Folder %r state: %d", from_folder, state)
|
||||||
yield from_folder
|
yield from_folder
|
||||||
except (EnvironmentError, fs.InvalidPath):
|
except (OSError, fs.InvalidPath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ---Public
|
# ---Public
|
||||||
@@ -175,7 +172,7 @@ class Directories:
|
|||||||
subpaths = [p for p in path.glob("*") if p.is_dir()]
|
subpaths = [p for p in path.glob("*") if p.is_dir()]
|
||||||
subpaths.sort(key=lambda x: x.name.lower())
|
subpaths.sort(key=lambda x: x.name.lower())
|
||||||
return subpaths
|
return subpaths
|
||||||
except EnvironmentError:
|
except OSError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_files(self, fileclasses=None, j=job.nulljob):
|
def get_files(self, fileclasses=None, j=job.nulljob):
|
||||||
@@ -222,14 +219,11 @@ class Directories:
|
|||||||
if state != DirectoryState.NORMAL:
|
if state != DirectoryState.NORMAL:
|
||||||
self.states[path] = state
|
self.states[path] = state
|
||||||
return state
|
return state
|
||||||
|
# find the longest parent path that is in states and return that state if found
|
||||||
prevlen = 0
|
# NOTE: path.parents is ordered longest to shortest
|
||||||
# we loop through the states to find the longest matching prefix
|
for parent_path in path.parents:
|
||||||
# if the parent has a state in cache, return that state
|
if parent_path in self.states:
|
||||||
for p, s in self.states.items():
|
return self.states[parent_path]
|
||||||
if p in path.parents and len(p.parts) > prevlen:
|
|
||||||
prevlen = len(p.parts)
|
|
||||||
state = s
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def has_any_file(self):
|
def has_any_file(self):
|
||||||
|
|||||||
@@ -166,7 +166,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(word for word, objects in word_dict.items() if len(objects) < threshold)
|
uncommon_words = {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
|
||||||
@@ -303,12 +303,13 @@ def getmatches_by_contents(files, bigsize=0, j=job.nulljob):
|
|||||||
# skip hashing for zero length files
|
# skip hashing for zero length files
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
continue
|
continue
|
||||||
if first.digest_partial == second.digest_partial:
|
# if digests are the same (and not None) then files match
|
||||||
|
if first.digest_partial == second.digest_partial and first.digest_partial is not None:
|
||||||
if bigsize > 0 and first.size > bigsize:
|
if bigsize > 0 and first.size > bigsize:
|
||||||
if first.digest_samples == second.digest_samples:
|
if first.digest_samples == second.digest_samples and first.digest_samples is not None:
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
else:
|
else:
|
||||||
if first.digest == second.digest:
|
if first.digest == second.digest and first.digest is not None:
|
||||||
result.append(Match(first, second, 100))
|
result.append(Match(first, second, 100))
|
||||||
group_count += 1
|
group_count += 1
|
||||||
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))
|
||||||
@@ -409,7 +410,7 @@ 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(m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second]))
|
discarded = {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
|
||||||
@@ -456,7 +457,7 @@ class Group:
|
|||||||
self._matches_for_ref = None
|
self._matches_for_ref = None
|
||||||
if (len(self) > 1) and any(not getattr(item, "is_ref", False) for item in self):
|
if (len(self) > 1) and any(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 = {m for m in self.matches if item not in m}
|
||||||
else:
|
else:
|
||||||
self._clear()
|
self._clear()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -529,7 +530,7 @@ def get_groups(matches):
|
|||||||
del dupe2group
|
del dupe2group
|
||||||
del matches
|
del matches
|
||||||
# should free enough memory to continue
|
# should free enough memory to continue
|
||||||
logging.warning("Memory Overflow. Groups: {0}".format(len(groups)))
|
logging.warning(f"Memory Overflow. Groups: {len(groups)}")
|
||||||
# Now that we have a group, we have to discard groups' matches and see if there're any "orphan"
|
# Now that we have a group, we have to discard groups' matches and see if there're any "orphan"
|
||||||
# matches, that is, matches that were candidate in a group but that none of their 2 files were
|
# matches, that is, matches that were candidate in a group but that none of their 2 files were
|
||||||
# accepted in the group. With these orphan groups, it's safe to build additional groups
|
# accepted in the group. With these orphan groups, it's safe to build additional groups
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# 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 .markable import Markable
|
from core.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/
|
||||||
|
|||||||
108
core/fs.py
108
core/fs.py
@@ -13,6 +13,16 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from math import floor
|
||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
from threading import Lock
|
||||||
|
from typing import Any, AnyStr, Union, Callable
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from hscommon.util import nonone, get_file_ext
|
||||||
|
|
||||||
|
hasher: Callable
|
||||||
try:
|
try:
|
||||||
import xxhash
|
import xxhash
|
||||||
|
|
||||||
@@ -22,15 +32,6 @@ except ImportError:
|
|||||||
|
|
||||||
hasher = hashlib.md5
|
hasher = hashlib.md5
|
||||||
|
|
||||||
from math import floor
|
|
||||||
import logging
|
|
||||||
import sqlite3
|
|
||||||
from threading import Lock
|
|
||||||
from typing import Any, AnyStr, Union
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from hscommon.util import nonone, get_file_ext
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"File",
|
"File",
|
||||||
"Folder",
|
"Folder",
|
||||||
@@ -143,13 +144,17 @@ class FilesDB:
|
|||||||
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:
|
||||||
|
with self.lock:
|
||||||
|
self.cur.execute(
|
||||||
|
self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns}
|
||||||
|
)
|
||||||
|
result = self.cur.fetchone()
|
||||||
|
|
||||||
with self.lock:
|
if result:
|
||||||
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
|
return result[0]
|
||||||
result = self.cur.fetchone()
|
except Exception as ex:
|
||||||
|
logging.warning(f"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}")
|
||||||
if result:
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -157,12 +162,14 @@ class FilesDB:
|
|||||||
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:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.cur.execute(
|
self.cur.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},
|
||||||
)
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logging.warning(f"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}")
|
||||||
|
|
||||||
def commit(self) -> None:
|
def commit(self) -> None:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -187,12 +194,17 @@ class File:
|
|||||||
__slots__ = ("path", "is_ref", "words") + tuple(INITIAL_INFO.keys())
|
__slots__ = ("path", "is_ref", "words") + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self.path = path
|
|
||||||
for attrname in self.INITIAL_INFO:
|
for attrname in self.INITIAL_INFO:
|
||||||
setattr(self, attrname, NOT_SET)
|
setattr(self, attrname, NOT_SET)
|
||||||
|
if type(path) is os.DirEntry:
|
||||||
|
self.path = Path(path.path)
|
||||||
|
self.size = nonone(path.stat().st_size, 0)
|
||||||
|
self.mtime = nonone(path.stat().st_mtime, 0)
|
||||||
|
else:
|
||||||
|
self.path = path
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
return f"<{self.__class__.__name__} {str(self.path)}>"
|
||||||
|
|
||||||
def __getattribute__(self, attrname):
|
def __getattribute__(self, attrname):
|
||||||
result = object.__getattribute__(self, attrname)
|
result = object.__getattribute__(self, attrname)
|
||||||
@@ -259,34 +271,25 @@ class File:
|
|||||||
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 == "digest_partial":
|
elif field == "digest_partial":
|
||||||
try:
|
self.digest_partial = filesdb.get(self.path, "digest_partial")
|
||||||
self.digest_partial = filesdb.get(self.path, "digest_partial")
|
if self.digest_partial is None:
|
||||||
if self.digest_partial is None:
|
self.digest_partial = self._calc_digest_partial()
|
||||||
self.digest_partial = self._calc_digest_partial()
|
filesdb.put(self.path, "digest_partial", self.digest_partial)
|
||||||
filesdb.put(self.path, "digest_partial", self.digest_partial)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning("Couldn't get digest_partial for %s: %s", self.path, e)
|
|
||||||
elif field == "digest":
|
elif field == "digest":
|
||||||
try:
|
self.digest = filesdb.get(self.path, "digest")
|
||||||
self.digest = filesdb.get(self.path, "digest")
|
if self.digest is None:
|
||||||
if self.digest is None:
|
self.digest = self._calc_digest()
|
||||||
self.digest = self._calc_digest()
|
filesdb.put(self.path, "digest", self.digest)
|
||||||
filesdb.put(self.path, "digest", self.digest)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning("Couldn't get digest for %s: %s", self.path, e)
|
|
||||||
elif field == "digest_samples":
|
elif field == "digest_samples":
|
||||||
size = self.size
|
size = self.size
|
||||||
# Might as well hash such small files entirely.
|
# Might as well hash such small files entirely.
|
||||||
if size <= MIN_FILE_SIZE:
|
if size <= MIN_FILE_SIZE:
|
||||||
setattr(self, field, self.digest)
|
setattr(self, field, self.digest)
|
||||||
return
|
return
|
||||||
try:
|
self.digest_samples = filesdb.get(self.path, "digest_samples")
|
||||||
self.digest_samples = filesdb.get(self.path, "digest_samples")
|
if self.digest_samples is None:
|
||||||
if self.digest_samples is None:
|
self.digest_samples = self._calc_digest_samples()
|
||||||
self.digest_samples = self._calc_digest_samples()
|
filesdb.put(self.path, "digest_samples", self.digest_samples)
|
||||||
filesdb.put(self.path, "digest_samples", self.digest_samples)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Couldn't get digest_samples for {self.path}: {e}")
|
|
||||||
|
|
||||||
def _read_all_info(self, attrnames=None):
|
def _read_all_info(self, attrnames=None):
|
||||||
"""Cache all possible info.
|
"""Cache all possible info.
|
||||||
@@ -312,7 +315,7 @@ class File:
|
|||||||
raise AlreadyExistsError(newname, self.path.parent)
|
raise AlreadyExistsError(newname, self.path.parent)
|
||||||
try:
|
try:
|
||||||
self.path.rename(destpath)
|
self.path.rename(destpath)
|
||||||
except EnvironmentError:
|
except OSError:
|
||||||
raise OperationError(self)
|
raise OperationError(self)
|
||||||
if not destpath.exists():
|
if not destpath.exists():
|
||||||
raise OperationError(self)
|
raise OperationError(self)
|
||||||
@@ -346,6 +349,7 @@ class Folder(File):
|
|||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
File.__init__(self, path)
|
File.__init__(self, path)
|
||||||
|
self.size = NOT_SET
|
||||||
self._subfolders = None
|
self._subfolders = None
|
||||||
|
|
||||||
def _all_items(self):
|
def _all_items(self):
|
||||||
@@ -377,7 +381,8 @@ class Folder(File):
|
|||||||
@property
|
@property
|
||||||
def subfolders(self):
|
def subfolders(self):
|
||||||
if self._subfolders is None:
|
if self._subfolders is None:
|
||||||
subfolders = [p for p in self.path.glob("*") if not p.is_symlink() and p.is_dir()]
|
with os.scandir(self.path) as iter:
|
||||||
|
subfolders = [p for p in iter if not p.is_symlink() and p.is_dir()]
|
||||||
self._subfolders = [self.__class__(p) for p in subfolders]
|
self._subfolders = [self.__class__(p) for p in subfolders]
|
||||||
return self._subfolders
|
return self._subfolders
|
||||||
|
|
||||||
@@ -408,10 +413,11 @@ def get_files(path, fileclasses=[File]):
|
|||||||
assert all(issubclass(fileclass, File) for fileclass in fileclasses)
|
assert all(issubclass(fileclass, File) for fileclass in fileclasses)
|
||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
for path in path.glob("*"):
|
with os.scandir(path) as iter:
|
||||||
file = get_file(path, fileclasses=fileclasses)
|
for item in iter:
|
||||||
if file is not None:
|
file = get_file(item, fileclasses=fileclasses)
|
||||||
result.append(file)
|
if file is not None:
|
||||||
|
result.append(file)
|
||||||
return result
|
return result
|
||||||
except EnvironmentError:
|
except OSError:
|
||||||
raise InvalidPath(path)
|
raise InvalidPath(path)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
from .base import DupeGuruGUIObject
|
from core.gui.base import DupeGuruGUIObject
|
||||||
|
|
||||||
|
|
||||||
class DetailsPanel(GUIObject, DupeGuruGUIObject):
|
class DetailsPanel(GUIObject, DupeGuruGUIObject):
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
from hscommon.gui.tree import Tree, Node
|
from hscommon.gui.tree import Tree, Node
|
||||||
|
|
||||||
from ..directories import DirectoryState
|
from core.directories import DirectoryState
|
||||||
from .base import DupeGuruGUIObject
|
from core.gui.base import DupeGuruGUIObject
|
||||||
|
|
||||||
STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]
|
STATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# 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 .exclude_list_table import ExcludeListTable
|
from core.gui.exclude_list_table import ExcludeListTable
|
||||||
from core.exclude import has_sep
|
from core.exclude import has_sep
|
||||||
from os import sep
|
from os import sep
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# 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 .base import DupeGuruGUIObject
|
from core.gui.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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from .ignore_list_table import IgnoreListTable
|
from core.gui.ignore_list_table import IgnoreListTable
|
||||||
|
|
||||||
|
|
||||||
class IgnoreListDialog:
|
class IgnoreListDialog:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
from hscommon import desktop
|
from hscommon import desktop
|
||||||
|
|
||||||
from .problem_table import ProblemTable
|
from core.gui.problem_table import ProblemTable
|
||||||
|
|
||||||
|
|
||||||
class ProblemDialog:
|
class ProblemDialog:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from operator import attrgetter
|
|||||||
from hscommon.gui.table import GUITable, Row
|
from hscommon.gui.table import GUITable, Row
|
||||||
from hscommon.gui.column import Columns
|
from hscommon.gui.column import Columns
|
||||||
|
|
||||||
from .base import DupeGuruGUIObject
|
from core.gui.base import DupeGuruGUIObject
|
||||||
|
|
||||||
|
|
||||||
class DupeRow(Row):
|
class DupeRow(Row):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 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 .base import DupeGuruGUIObject
|
from core.gui.base import DupeGuruGUIObject
|
||||||
|
|
||||||
|
|
||||||
class StatsLabel(DupeGuruGUIObject):
|
class StatsLabel(DupeGuruGUIObject):
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from . import fs, prioritize, result_table, scanner # noqa
|
from core.me import fs, prioritize, result_table, scanner # noqa
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from . import ( # noqa
|
from core.pe import ( # noqa
|
||||||
block,
|
block,
|
||||||
cache,
|
cache,
|
||||||
exif,
|
exif,
|
||||||
iphoto_plist,
|
|
||||||
matchblock,
|
matchblock,
|
||||||
matchexif,
|
matchexif,
|
||||||
photo,
|
photo,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 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 ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA
|
from core.pe._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA
|
||||||
|
|
||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblock(image):
|
# def getblock(image):
|
||||||
|
|||||||
13
core/pe/block.pyi
Normal file
13
core/pe/block.pyi
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import Tuple, List, Union, Sequence
|
||||||
|
|
||||||
|
_block = Tuple[int, int, int]
|
||||||
|
|
||||||
|
class NoBlocksError(Exception): ... # noqa: E302, E701
|
||||||
|
class DifferentBlockCountError(Exception): ... # noqa E701
|
||||||
|
|
||||||
|
def getblock(image: object) -> Union[_block, None]: ... # noqa: E302
|
||||||
|
def getblocks2(image: object, block_count_per_side: int) -> Union[List[_block], None]: ...
|
||||||
|
def diff(first: _block, second: _block) -> int: ...
|
||||||
|
def avgdiff( # noqa: E302
|
||||||
|
first: Sequence[_block], second: Sequence[_block], limit: int = 768, min_iterations: int = 1
|
||||||
|
) -> Union[int, None]: ...
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
# 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 ._cache import string_to_colors # noqa
|
from core.pe._cache import string_to_colors # noqa
|
||||||
|
|
||||||
|
|
||||||
def colors_to_string(colors):
|
def colors_to_string(colors):
|
||||||
@@ -13,7 +13,7 @@ def colors_to_string(colors):
|
|||||||
[(0,100,255)] --> 0064ff
|
[(0,100,255)] --> 0064ff
|
||||||
[(1,2,3),(4,5,6)] --> 010203040506
|
[(1,2,3),(4,5,6)] --> 010203040506
|
||||||
"""
|
"""
|
||||||
return "".join("%02x%02x%02x" % (r, g, b) for r, g, b in colors)
|
return "".join("{:02x}{:02x}{:02x}".format(r, g, b) for r, g, b in colors)
|
||||||
|
|
||||||
|
|
||||||
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
||||||
|
|||||||
6
core/pe/cache.pyi
Normal file
6
core/pe/cache.pyi
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from typing import Union, Tuple, List
|
||||||
|
|
||||||
|
_block = Tuple[int, int, int]
|
||||||
|
|
||||||
|
def colors_to_string(colors: List[_block]) -> str: ... # noqa: E302
|
||||||
|
def string_to_colors(s: str) -> Union[List[_block], None]: ...
|
||||||
@@ -10,11 +10,11 @@ import shelve
|
|||||||
import tempfile
|
import tempfile
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from .cache import string_to_colors, colors_to_string
|
from core.pe.cache import string_to_colors, colors_to_string
|
||||||
|
|
||||||
|
|
||||||
def wrap_path(path):
|
def wrap_path(path):
|
||||||
return "path:{}".format(path)
|
return f"path:{path}"
|
||||||
|
|
||||||
|
|
||||||
def unwrap_path(key):
|
def unwrap_path(key):
|
||||||
@@ -22,7 +22,7 @@ def unwrap_path(key):
|
|||||||
|
|
||||||
|
|
||||||
def wrap_id(path):
|
def wrap_id(path):
|
||||||
return "id:{}".format(path)
|
return f"id:{path}"
|
||||||
|
|
||||||
|
|
||||||
def unwrap_id(key):
|
def unwrap_id(key):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import os.path as op
|
|||||||
import logging
|
import logging
|
||||||
import sqlite3 as sqlite
|
import sqlite3 as sqlite
|
||||||
|
|
||||||
from .cache import string_to_colors, colors_to_string
|
from core.pe.cache import string_to_colors, colors_to_string
|
||||||
|
|
||||||
|
|
||||||
class SqliteCache:
|
class SqliteCache:
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2014-03-15
|
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
#
|
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
||||||
# which should be included with this package. The terms are also available at
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import plistlib
|
|
||||||
|
|
||||||
|
|
||||||
class IPhotoPlistParser(plistlib._PlistParser):
|
|
||||||
"""A parser for iPhoto plists.
|
|
||||||
|
|
||||||
iPhoto plists tend to be malformed, so we have to subclass the built-in parser to be a bit more
|
|
||||||
lenient.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
plistlib._PlistParser.__init__(self, use_builtin_types=True, dict_type=dict)
|
|
||||||
# For debugging purposes, we remember the last bit of data to be analyzed so that we can
|
|
||||||
# log it in case of an exception
|
|
||||||
self.lastdata = ""
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
self.lastdata = plistlib._PlistParser.get_data(self)
|
|
||||||
return self.lastdata
|
|
||||||
|
|
||||||
def end_integer(self):
|
|
||||||
try:
|
|
||||||
self.add_object(int(self.get_data()))
|
|
||||||
except ValueError:
|
|
||||||
self.add_object(0)
|
|
||||||
@@ -15,7 +15,7 @@ from hscommon.trans import tr
|
|||||||
from hscommon.jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
|
|
||||||
from core.engine import Match
|
from core.engine import Match
|
||||||
from .block import avgdiff, DifferentBlockCountError, NoBlocksError
|
from core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError
|
||||||
|
|
||||||
# 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
|
||||||
@@ -51,11 +51,11 @@ except Exception:
|
|||||||
|
|
||||||
def get_cache(cache_path, readonly=False):
|
def get_cache(cache_path, readonly=False):
|
||||||
if cache_path.endswith("shelve"):
|
if cache_path.endswith("shelve"):
|
||||||
from .cache_shelve import ShelveCache
|
from core.pe.cache_shelve import ShelveCache
|
||||||
|
|
||||||
return ShelveCache(cache_path, readonly=readonly)
|
return ShelveCache(cache_path, readonly=readonly)
|
||||||
else:
|
else:
|
||||||
from .cache_sqlite import SqliteCache
|
from core.pe.cache_sqlite import SqliteCache
|
||||||
|
|
||||||
return SqliteCache(cache_path, readonly=readonly)
|
return SqliteCache(cache_path, readonly=readonly)
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
|
|||||||
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
|
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
|
||||||
cache[picture.unicode_path] = blocks
|
cache[picture.unicode_path] = blocks
|
||||||
prepared.append(picture)
|
prepared.append(picture)
|
||||||
except (IOError, ValueError) as e:
|
except (OSError, ValueError) as e:
|
||||||
logging.warning(str(e))
|
logging.warning(str(e))
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from hscommon.util import get_file_ext, format_size
|
|||||||
|
|
||||||
from core.util import format_timestamp, format_perc, format_dupe_count
|
from core.util import format_timestamp, format_perc, format_dupe_count
|
||||||
from core import fs
|
from core import fs
|
||||||
from . import exif
|
from core.pe import exif
|
||||||
|
|
||||||
# This global value is set by the platform-specific subclasser of the Photo base class
|
# This global value is set by the platform-specific subclasser of the Photo base class
|
||||||
PLAT_SPECIFIC_PHOTO_CLASS = None
|
PLAT_SPECIFIC_PHOTO_CLASS = None
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from hscommon.trans import tr
|
|||||||
|
|
||||||
from core.scanner import Scanner, ScanType, ScanOption
|
from core.scanner import Scanner, ScanType, ScanOption
|
||||||
|
|
||||||
from . import matchblock, matchexif
|
from core.pe import matchblock, matchexif
|
||||||
|
|
||||||
|
|
||||||
class ScannerPE(Scanner):
|
class ScannerPE(Scanner):
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class Criterion:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def display(self):
|
def display(self):
|
||||||
return "{} ({})".format(self.category.NAME, self.display_value)
|
return f"{self.category.NAME} ({self.display_value})"
|
||||||
|
|
||||||
|
|
||||||
class ValueListCategory(CriterionCategory):
|
class ValueListCategory(CriterionCategory):
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from hscommon.conflict import get_conflicted_name
|
|||||||
from hscommon.util import flatten, nonone, FileOrPath, format_size
|
from hscommon.util import flatten, nonone, FileOrPath, format_size
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
|
|
||||||
from . import engine
|
from core import engine
|
||||||
from .markable import Markable
|
from core.markable import Markable
|
||||||
|
|
||||||
|
|
||||||
class Results(Markable):
|
class Results(Markable):
|
||||||
@@ -191,7 +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(dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path)))
|
self.__filtered_dupes = {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))
|
||||||
@@ -301,7 +301,7 @@ class Results(Markable):
|
|||||||
try:
|
try:
|
||||||
func(dupe)
|
func(dupe)
|
||||||
to_remove.append(dupe)
|
to_remove.append(dupe)
|
||||||
except (EnvironmentError, UnicodeEncodeError) as e:
|
except (OSError, UnicodeEncodeError) as e:
|
||||||
self.problems.append((dupe, str(e)))
|
self.problems.append((dupe, str(e)))
|
||||||
if remove_from_results:
|
if remove_from_results:
|
||||||
self.remove_duplicates(to_remove)
|
self.remove_duplicates(to_remove)
|
||||||
@@ -374,8 +374,8 @@ class Results(Markable):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
do_write(outfile)
|
do_write(outfile)
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
# If our IOError is because dest is already a directory, we want to handle that. 21 is
|
# If our OSError is because dest is already a directory, we want to handle that. 21 is
|
||||||
# the code we get on OS X and Linux, 13 is what we get on Windows.
|
# the code we get on OS X and Linux, 13 is what we get on Windows.
|
||||||
if e.errno in {21, 13}:
|
if e.errno in {21, 13}:
|
||||||
p = str(outfile)
|
p = str(outfile)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from hscommon.jobprogress import job
|
|||||||
from hscommon.util import dedupe, rem_file_ext, get_file_ext
|
from hscommon.util import dedupe, rem_file_ext, get_file_ext
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
|
|
||||||
from . import engine
|
from core import engine
|
||||||
|
|
||||||
# It's quite ugly to have scan types from all editions all put in the same class, but because there's
|
# It's quite ugly to have scan types from all editions all put in the same class, but because there's
|
||||||
# there will be some nasty bugs popping up (ScanType is used in core when in should exclusively be
|
# there will be some nasty bugs popping up (ScanType is used in core when in should exclusively be
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from . import fs, result_table, scanner # noqa
|
from core.se import fs, result_table, scanner # noqa
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import os
|
import os
|
||||||
import os.path as op
|
import os.path as op
|
||||||
import logging
|
import logging
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -15,10 +16,10 @@ import hscommon.util
|
|||||||
from hscommon.testutil import eq_, log_calls
|
from hscommon.testutil import eq_, log_calls
|
||||||
from hscommon.jobprogress.job import Job
|
from hscommon.jobprogress.job import Job
|
||||||
|
|
||||||
from .base import TestApp
|
from core.tests.base import TestApp
|
||||||
from .results_test import GetTestGroups
|
from core.tests.results_test import GetTestGroups
|
||||||
from .. import app, fs, engine
|
from core import app, fs, engine
|
||||||
from ..scanner import ScanType
|
from core.scanner import ScanType
|
||||||
|
|
||||||
|
|
||||||
def add_fake_files_to_directories(directories, files):
|
def add_fake_files_to_directories(directories, files):
|
||||||
@@ -68,11 +69,12 @@ class TestCaseDupeGuru:
|
|||||||
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()
|
||||||
dgapp.copy_or_move(f, True, "some_destination", 0)
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
eq_(1, len(hscommon.conflict.smart_copy.calls))
|
dgapp.copy_or_move(f, True, tmp_dir, 0)
|
||||||
call = hscommon.conflict.smart_copy.calls[0]
|
eq_(1, len(hscommon.conflict.smart_copy.calls))
|
||||||
eq_(call["dest_path"], Path("some_destination", "foo"))
|
call = hscommon.conflict.smart_copy.calls[0]
|
||||||
eq_(call["source_path"], f.path)
|
eq_(call["dest_path"], Path(tmp_dir, "foo"))
|
||||||
|
eq_(call["source_path"], f.path)
|
||||||
|
|
||||||
def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):
|
def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):
|
||||||
tmppath = Path(str(tmpdir))
|
tmppath = Path(str(tmpdir))
|
||||||
@@ -95,7 +97,7 @@ class TestCaseDupeGuru:
|
|||||||
|
|
||||||
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
||||||
app = TestApp().app
|
app = TestApp().app
|
||||||
f1, f2 = [FakeFile("foo") for _ in range(2)]
|
f1, f2 = (FakeFile("foo") for _ in range(2))
|
||||||
f1.is_ref, f2.is_ref = (False, False)
|
f1.is_ref, f2.is_ref = (False, False)
|
||||||
assert not (bool(f1) and bool(f2))
|
assert not (bool(f1) and bool(f2))
|
||||||
add_fake_files_to_directories(app.directories, [f1, f2])
|
add_fake_files_to_directories(app.directories, [f1, f2])
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ from hscommon.util import get_file_ext, format_size
|
|||||||
from hscommon.gui.column import Column
|
from hscommon.gui.column import Column
|
||||||
from hscommon.jobprogress.job import nulljob, JobCancelled
|
from hscommon.jobprogress.job import nulljob, JobCancelled
|
||||||
|
|
||||||
from .. import engine
|
from core import engine, prioritize
|
||||||
from .. import prioritize
|
from core.engine import getwords
|
||||||
from ..engine import getwords
|
from core.app import DupeGuru as DupeGuruBase
|
||||||
from ..app import DupeGuru as DupeGuruBase
|
from core.gui.result_table import ResultTable as ResultTableBase
|
||||||
from ..gui.result_table import ResultTable as ResultTableBase
|
from core.gui.prioritize_dialog import PrioritizeDialog
|
||||||
from ..gui.prioritize_dialog import PrioritizeDialog
|
|
||||||
|
|
||||||
|
|
||||||
class DupeGuruView:
|
class DupeGuruView:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pytest import raises, skip
|
|||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ..pe.block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError
|
from core.pe.block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError
|
||||||
except ImportError:
|
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.")
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ from pytest import raises, skip
|
|||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ..pe.cache import colors_to_string, string_to_colors
|
from core.pe.cache import colors_to_string, string_to_colors
|
||||||
from ..pe.cache_sqlite import SqliteCache
|
from core.pe.cache_sqlite import SqliteCache
|
||||||
from ..pe.cache_shelve import ShelveCache
|
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.")
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ from pathlib import Path
|
|||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from hscommon.plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
|
|
||||||
from ..fs import File
|
from core.fs import File
|
||||||
from ..directories import (
|
from core.directories import (
|
||||||
Directories,
|
Directories,
|
||||||
DirectoryState,
|
DirectoryState,
|
||||||
AlreadyThereError,
|
AlreadyThereError,
|
||||||
InvalidPathError,
|
InvalidPathError,
|
||||||
)
|
)
|
||||||
from ..exclude import ExcludeList, ExcludeDict
|
from core.exclude import ExcludeList, ExcludeDict
|
||||||
|
|
||||||
|
|
||||||
def create_fake_fs(rootpath):
|
def create_fake_fs(rootpath):
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ from hscommon.jobprogress import job
|
|||||||
from hscommon.util import first
|
from hscommon.util import first
|
||||||
from hscommon.testutil import eq_, log_calls
|
from hscommon.testutil import eq_, log_calls
|
||||||
|
|
||||||
from .base import NamedObject
|
from core.tests.base import NamedObject
|
||||||
from .. import engine
|
from core import engine
|
||||||
from ..engine import (
|
from core.engine import (
|
||||||
get_match,
|
get_match,
|
||||||
getwords,
|
getwords,
|
||||||
Group,
|
Group,
|
||||||
@@ -271,9 +271,9 @@ class TestCaseBuildWordDict:
|
|||||||
class TestCaseMergeSimilarWords:
|
class TestCaseMergeSimilarWords:
|
||||||
def test_some_similar_words(self):
|
def test_some_similar_words(self):
|
||||||
d = {
|
d = {
|
||||||
"foobar": set([1]),
|
"foobar": {1},
|
||||||
"foobar1": set([2]),
|
"foobar1": {2},
|
||||||
"foobar2": set([3]),
|
"foobar2": {3},
|
||||||
}
|
}
|
||||||
merge_similar_words(d)
|
merge_similar_words(d)
|
||||||
eq_(1, len(d))
|
eq_(1, len(d))
|
||||||
@@ -283,8 +283,8 @@ class TestCaseMergeSimilarWords:
|
|||||||
class TestCaseReduceCommonWords:
|
class TestCaseReduceCommonWords:
|
||||||
def test_typical(self):
|
def test_typical(self):
|
||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar", True) for _ in range(50)]),
|
"foo": {NamedObject("foo bar", True) for _ in range(50)},
|
||||||
"bar": set([NamedObject("foo bar", True) for _ in range(49)]),
|
"bar": {NamedObject("foo bar", True) for _ in range(49)},
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert "foo" not in d
|
assert "foo" not in d
|
||||||
@@ -293,7 +293,7 @@ class TestCaseReduceCommonWords:
|
|||||||
def test_dont_remove_objects_with_only_common_words(self):
|
def test_dont_remove_objects_with_only_common_words(self):
|
||||||
d = {
|
d = {
|
||||||
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
"uncommon": {NamedObject("common uncommon", True)},
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
eq_(1, len(d["common"]))
|
eq_(1, len(d["common"]))
|
||||||
@@ -302,7 +302,7 @@ class TestCaseReduceCommonWords:
|
|||||||
def test_values_still_are_set_instances(self):
|
def test_values_still_are_set_instances(self):
|
||||||
d = {
|
d = {
|
||||||
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
"common": set([NamedObject("common uncommon", True) for _ in range(50)] + [NamedObject("common", True)]),
|
||||||
"uncommon": set([NamedObject("common uncommon", True)]),
|
"uncommon": {NamedObject("common uncommon", True)},
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
assert isinstance(d["common"], set)
|
assert isinstance(d["common"], set)
|
||||||
@@ -312,9 +312,9 @@ class TestCaseReduceCommonWords:
|
|||||||
# If a word has been removed by the reduce, an object in a subsequent common word that
|
# If a word has been removed by the reduce, an object in a subsequent common word that
|
||||||
# contains the word that has been removed would cause a KeyError.
|
# contains the word that has been removed would cause a KeyError.
|
||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
"foo": {NamedObject("foo bar baz", True) for _ in range(50)},
|
||||||
"bar": set([NamedObject("foo bar baz", True) for _ in range(50)]),
|
"bar": {NamedObject("foo bar baz", True) for _ in range(50)},
|
||||||
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
"baz": {NamedObject("foo bar baz", True) for _ in range(49)},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
@@ -328,7 +328,7 @@ class TestCaseReduceCommonWords:
|
|||||||
o.words = [["foo", "bar"], ["baz"]]
|
o.words = [["foo", "bar"], ["baz"]]
|
||||||
return o
|
return o
|
||||||
|
|
||||||
d = {"foo": set([create_it() for _ in range(50)])}
|
d = {"foo": {create_it() for _ in range(50)}}
|
||||||
try:
|
try:
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -343,7 +343,7 @@ class TestCaseReduceCommonWords:
|
|||||||
d = {
|
d = {
|
||||||
"foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
"foo": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||||
"bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
"bar": set([NamedObject("foo bar baz", True) for _ in range(49)] + [only_common]),
|
||||||
"baz": set([NamedObject("foo bar baz", True) for _ in range(49)]),
|
"baz": {NamedObject("foo bar baz", True) for _ in range(49)},
|
||||||
}
|
}
|
||||||
reduce_common_words(d, 50)
|
reduce_common_words(d, 50)
|
||||||
eq_(1, len(d["foo"]))
|
eq_(1, len(d["foo"]))
|
||||||
@@ -884,7 +884,7 @@ class TestCaseGetGroups:
|
|||||||
# If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the
|
# If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the
|
||||||
# (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D
|
# (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D
|
||||||
# in a separate group instead of discarding them.
|
# in a separate group instead of discarding them.
|
||||||
A, B, C, D = [NamedObject() for _ in range(4)]
|
A, B, C, D = (NamedObject() for _ in range(4))
|
||||||
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
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from xml.etree import ElementTree as ET
|
|||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from hscommon.plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
|
|
||||||
from .base import DupeGuru
|
from core.tests.base import DupeGuru
|
||||||
from ..exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException
|
from core.exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException
|
||||||
|
|
||||||
from re import error
|
from re import error
|
||||||
|
|
||||||
@@ -289,8 +289,8 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
|
|||||||
compiled = [x for x in self.exclude_list.compiled]
|
compiled = [x for x in self.exclude_list.compiled]
|
||||||
assert regex not in compiled
|
assert regex not in compiled
|
||||||
# Need to escape both to get the same strings after compilation
|
# 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("|")])
|
compiled_escaped = {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])
|
default_escaped = {x.encode("unicode-escape").decode() for x in default_regexes}
|
||||||
assert compiled_escaped == default_escaped
|
assert compiled_escaped == default_escaped
|
||||||
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
@@ -366,8 +366,8 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
|||||||
compiled = [x for x in self.exclude_list.compiled]
|
compiled = [x for x in self.exclude_list.compiled]
|
||||||
assert regex not in compiled
|
assert regex not in compiled
|
||||||
# Need to escape both to get the same strings after compilation
|
# 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("|")])
|
compiled_escaped = {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])
|
default_escaped = {x.encode("unicode-escape").decode() for x in default_regexes}
|
||||||
assert compiled_escaped == default_escaped
|
assert compiled_escaped == default_escaped
|
||||||
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,16 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
import typing
|
||||||
|
from os import urandom
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from hscommon.testutil import eq_
|
||||||
|
from core.tests.directories_test import create_fake_fs
|
||||||
|
|
||||||
|
from core import fs
|
||||||
|
|
||||||
|
hasher: typing.Callable
|
||||||
try:
|
try:
|
||||||
import xxhash
|
import xxhash
|
||||||
|
|
||||||
@@ -15,14 +25,6 @@ except ImportError:
|
|||||||
|
|
||||||
hasher = hashlib.md5
|
hasher = hashlib.md5
|
||||||
|
|
||||||
from os import urandom
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from hscommon.testutil import eq_
|
|
||||||
from core.tests.directories_test import create_fake_fs
|
|
||||||
|
|
||||||
from .. import fs
|
|
||||||
|
|
||||||
|
|
||||||
def create_fake_fs_with_random_data(rootpath):
|
def create_fake_fs_with_random_data(rootpath):
|
||||||
rootpath = rootpath.joinpath("fs")
|
rootpath = rootpath.joinpath("fs")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from xml.etree import ElementTree as ET
|
|||||||
from pytest import raises
|
from pytest import raises
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
from ..ignore import IgnoreList
|
from core.ignore import IgnoreList
|
||||||
|
|
||||||
|
|
||||||
def test_empty():
|
def test_empty():
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
from ..markable import MarkableList, Markable
|
from core.markable import MarkableList, Markable
|
||||||
|
|
||||||
|
|
||||||
def gen():
|
def gen():
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
|
|
||||||
from .base import TestApp, NamedObject, with_app, eq_
|
from core.tests.base import TestApp, NamedObject, with_app, eq_
|
||||||
from ..engine import Group, Match
|
from core.engine import Group, Match
|
||||||
|
|
||||||
no = NamedObject
|
no = NamedObject
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 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 .base import TestApp, GetTestGroups
|
from core.tests.base import TestApp, GetTestGroups
|
||||||
|
|
||||||
|
|
||||||
def app_with_results():
|
def app_with_results():
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ from xml.etree import ElementTree as ET
|
|||||||
from pytest import raises
|
from pytest import raises
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from hscommon.util import first
|
from hscommon.util import first
|
||||||
|
from core import engine
|
||||||
from .. import engine
|
from core.tests.base import NamedObject, GetTestGroups, DupeGuru
|
||||||
from .base import NamedObject, GetTestGroups, DupeGuru
|
from core.results import Results
|
||||||
from ..results import Results
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseResultsEmpty:
|
class TestCaseResultsEmpty:
|
||||||
@@ -337,7 +336,7 @@ class TestCaseResultsMarkings:
|
|||||||
def log_object(o):
|
def log_object(o):
|
||||||
log.append(o)
|
log.append(o)
|
||||||
if o is self.objects[1]:
|
if o is self.objects[1]:
|
||||||
raise EnvironmentError("foobar")
|
raise OSError("foobar")
|
||||||
|
|
||||||
log = []
|
log = []
|
||||||
self.results.mark_all()
|
self.results.mark_all()
|
||||||
@@ -464,7 +463,7 @@ class TestCaseResultsXML:
|
|||||||
eq_(6, len(g1))
|
eq_(6, len(g1))
|
||||||
eq_(3, len([c for c in g1 if c.tag == "file"]))
|
eq_(3, len([c for c in g1 if c.tag == "file"]))
|
||||||
eq_(3, len([c for c in g1 if c.tag == "match"]))
|
eq_(3, len([c for c in g1 if c.tag == "match"]))
|
||||||
d1, d2, d3 = [c for c in g1 if c.tag == "file"]
|
d1, d2, d3 = (c for c in g1 if c.tag == "file")
|
||||||
eq_(op.join("basepath", "foo bar"), d1.get("path"))
|
eq_(op.join("basepath", "foo bar"), d1.get("path"))
|
||||||
eq_(op.join("basepath", "bar bleh"), d2.get("path"))
|
eq_(op.join("basepath", "bar bleh"), d2.get("path"))
|
||||||
eq_(op.join("basepath", "foo bleh"), d3.get("path"))
|
eq_(op.join("basepath", "foo bleh"), d3.get("path"))
|
||||||
@@ -477,7 +476,7 @@ class TestCaseResultsXML:
|
|||||||
eq_(3, len(g2))
|
eq_(3, len(g2))
|
||||||
eq_(2, len([c for c in g2 if c.tag == "file"]))
|
eq_(2, len([c for c in g2 if c.tag == "file"]))
|
||||||
eq_(1, len([c for c in g2 if c.tag == "match"]))
|
eq_(1, len([c for c in g2 if c.tag == "match"]))
|
||||||
d1, d2 = [c for c in g2 if c.tag == "file"]
|
d1, d2 = (c for c in g2 if c.tag == "file")
|
||||||
eq_(op.join("basepath", "ibabtu"), d1.get("path"))
|
eq_(op.join("basepath", "ibabtu"), d1.get("path"))
|
||||||
eq_(op.join("basepath", "ibabtu"), d2.get("path"))
|
eq_(op.join("basepath", "ibabtu"), d2.get("path"))
|
||||||
eq_("n", d1.get("is_ref"))
|
eq_("n", d1.get("is_ref"))
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ from hscommon.jobprogress import job
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from hscommon.testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
from .. import fs
|
from core import fs
|
||||||
from ..engine import getwords, Match
|
from core.engine import getwords, Match
|
||||||
from ..ignore import IgnoreList
|
from core.ignore import IgnoreList
|
||||||
from ..scanner import Scanner, ScanType
|
from core.scanner import Scanner, ScanType
|
||||||
from ..me.scanner import ScannerME
|
from core.me.scanner import ScannerME
|
||||||
|
|
||||||
|
|
||||||
class NamedObject:
|
class NamedObject:
|
||||||
@@ -29,7 +29,7 @@ class NamedObject:
|
|||||||
self.words = getwords(name)
|
self.words = getwords(name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<NamedObject %r %r>" % (self.name, self.path)
|
return "<NamedObject {!r} {!r}>".format(self.name, self.path)
|
||||||
|
|
||||||
|
|
||||||
no = NamedObject
|
no = NamedObject
|
||||||
@@ -336,7 +336,7 @@ def test_tag_scan(fake_fileexists):
|
|||||||
def test_tag_with_album_scan(fake_fileexists):
|
def test_tag_with_album_scan(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "album", "title"])
|
s.scanned_tags = {"artist", "album", "title"}
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o3 = no("bleh")
|
o3 = no("bleh")
|
||||||
@@ -356,7 +356,7 @@ def test_tag_with_album_scan(fake_fileexists):
|
|||||||
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "album", "title"])
|
s.scanned_tags = {"artist", "album", "title"}
|
||||||
s.min_match_percentage = 50
|
s.min_match_percentage = 50
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
@@ -373,7 +373,7 @@ def test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):
|
|||||||
def test_tag_scan_with_different_scanned(fake_fileexists):
|
def test_tag_scan_with_different_scanned(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["track", "year"])
|
s.scanned_tags = {"track", "year"}
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o1.artist = "The White Stripes"
|
o1.artist = "The White Stripes"
|
||||||
@@ -391,7 +391,7 @@ def test_tag_scan_with_different_scanned(fake_fileexists):
|
|||||||
def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["artist", "foo"])
|
s.scanned_tags = {"artist", "foo"}
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o1.artist = "The White Stripes"
|
o1.artist = "The White Stripes"
|
||||||
@@ -405,7 +405,7 @@ def test_tag_scan_only_scans_existing_tags(fake_fileexists):
|
|||||||
def test_tag_scan_converts_to_str(fake_fileexists):
|
def test_tag_scan_converts_to_str(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["track"])
|
s.scanned_tags = {"track"}
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o1.track = 42
|
o1.track = 42
|
||||||
@@ -420,7 +420,7 @@ def test_tag_scan_converts_to_str(fake_fileexists):
|
|||||||
def test_tag_scan_non_ascii(fake_fileexists):
|
def test_tag_scan_non_ascii(fake_fileexists):
|
||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.TAG
|
s.scan_type = ScanType.TAG
|
||||||
s.scanned_tags = set(["title"])
|
s.scanned_tags = {"title"}
|
||||||
o1 = no("foo")
|
o1 = no("foo")
|
||||||
o2 = no("bar")
|
o2 = no("bar")
|
||||||
o1.title = "foobar\u00e9"
|
o1.title = "foobar\u00e9"
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
=== 4.3.1 (2022-07-08)
|
||||||
|
* Fix issue where cache db exceptions could prevent files being hashed (#1015)
|
||||||
|
* Add extra guard for non-zero length files without digests to prevent false duplicates
|
||||||
|
* Update Italian translations
|
||||||
|
|
||||||
|
=== 4.3.0 (2022-07-01)
|
||||||
|
* Redirect stdout from custom command to the log files (#1008)
|
||||||
|
* Update translations
|
||||||
|
* Fix typo in debian control file (#989)
|
||||||
|
* Add option to profile scans
|
||||||
|
* Update fs.py to optimize stat() calls
|
||||||
|
* Fix Error when delete after scan (#988)
|
||||||
|
* Update directory scanning to use os.scandir() and DirEntry objects
|
||||||
|
* Improve performance of Directories.get_state()
|
||||||
|
* Migrate from hscommon.path to pathlib
|
||||||
|
* Switch file hashing to xxhash with fallback to md5
|
||||||
|
* Add update check feature to about box
|
||||||
|
|
||||||
=== 4.2.1 (2022-03-25)
|
=== 4.2.1 (2022-03-25)
|
||||||
* Default to English on unsupported system language (#976)
|
* Default to English on unsupported system language (#976)
|
||||||
* Fix image viewer zoom datatype issue (#978)
|
* Fix image viewer zoom datatype issue (#978)
|
||||||
|
|||||||
5
hscommon/.gitignore
vendored
5
hscommon/.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
*.pyc
|
|
||||||
*.mo
|
|
||||||
*.so
|
|
||||||
.DS_Store
|
|
||||||
/docs_html
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"""This module is a collection of function to help in HS apps build process.
|
"""This module is a collection of function to help in HS apps build process.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import os.path as op
|
import os.path as op
|
||||||
@@ -20,18 +21,19 @@ import re
|
|||||||
import importlib
|
import importlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import glob
|
import glob
|
||||||
|
from typing import Any, AnyStr, Callable, Dict, List, Union
|
||||||
|
|
||||||
from .plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
|
|
||||||
|
|
||||||
def print_and_do(cmd):
|
def print_and_do(cmd: str) -> int:
|
||||||
"""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()
|
||||||
|
|
||||||
|
|
||||||
def _perform(src, dst, action, actionname):
|
def _perform(src: os.PathLike, dst: os.PathLike, action: Callable, actionname: str) -> None:
|
||||||
if not op.lexists(src):
|
if not op.lexists(src):
|
||||||
print("Copying %s failed: it doesn't exist." % src)
|
print("Copying %s failed: it doesn't exist." % src)
|
||||||
return
|
return
|
||||||
@@ -40,34 +42,26 @@ def _perform(src, dst, action, actionname):
|
|||||||
shutil.rmtree(dst)
|
shutil.rmtree(dst)
|
||||||
else:
|
else:
|
||||||
os.remove(dst)
|
os.remove(dst)
|
||||||
print("%s %s --> %s" % (actionname, src, dst))
|
print("{} {} --> {}".format(actionname, src, dst))
|
||||||
action(src, dst)
|
action(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def copy_file_or_folder(src, dst):
|
def copy_file_or_folder(src: os.PathLike, dst: os.PathLike) -> None:
|
||||||
if op.isdir(src):
|
if op.isdir(src):
|
||||||
shutil.copytree(src, dst, symlinks=True)
|
shutil.copytree(src, dst, symlinks=True)
|
||||||
else:
|
else:
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def move(src, dst):
|
def move(src: os.PathLike, dst: os.PathLike) -> None:
|
||||||
_perform(src, dst, os.rename, "Moving")
|
_perform(src, dst, os.rename, "Moving")
|
||||||
|
|
||||||
|
|
||||||
def copy(src, dst):
|
def copy(src: os.PathLike, dst: os.PathLike) -> None:
|
||||||
_perform(src, dst, copy_file_or_folder, "Copying")
|
_perform(src, dst, copy_file_or_folder, "Copying")
|
||||||
|
|
||||||
|
|
||||||
def symlink(src, dst):
|
def _perform_on_all(pattern: AnyStr, dst: os.PathLike, action: Callable) -> None:
|
||||||
_perform(src, dst, os.symlink, "Symlinking")
|
|
||||||
|
|
||||||
|
|
||||||
def hardlink(src, dst):
|
|
||||||
_perform(src, dst, os.link, "Hardlinking")
|
|
||||||
|
|
||||||
|
|
||||||
def _perform_on_all(pattern, dst, action):
|
|
||||||
# pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder
|
# pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder
|
||||||
# structure from src is kept.
|
# structure from src is kept.
|
||||||
filenames = glob.glob(pattern)
|
filenames = glob.glob(pattern)
|
||||||
@@ -76,42 +70,35 @@ def _perform_on_all(pattern, dst, action):
|
|||||||
action(fn, destpath)
|
action(fn, destpath)
|
||||||
|
|
||||||
|
|
||||||
def move_all(pattern, dst):
|
def move_all(pattern: AnyStr, dst: os.PathLike) -> None:
|
||||||
_perform_on_all(pattern, dst, move)
|
_perform_on_all(pattern, dst, move)
|
||||||
|
|
||||||
|
|
||||||
def copy_all(pattern, dst):
|
def copy_all(pattern: AnyStr, dst: os.PathLike) -> None:
|
||||||
_perform_on_all(pattern, dst, copy)
|
_perform_on_all(pattern, dst, copy)
|
||||||
|
|
||||||
|
|
||||||
def ensure_empty_folder(path):
|
def filereplace(filename: os.PathLike, outfilename: Union[os.PathLike, None] = None, **kwargs) -> None:
|
||||||
"""Make sure that the path exists and that it's an empty folder."""
|
|
||||||
if op.exists(path):
|
|
||||||
shutil.rmtree(path)
|
|
||||||
os.mkdir(path)
|
|
||||||
|
|
||||||
|
|
||||||
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, encoding="utf-8")
|
||||||
contents = fp.read()
|
contents = fp.read()
|
||||||
fp.close()
|
fp.close()
|
||||||
# We can't use str.format() because in some files, there might be {} characters that mess with it.
|
# We can't use str.format() because in some files, there might be {} characters that mess with it.
|
||||||
for key, item in kwargs.items():
|
for key, item in kwargs.items():
|
||||||
contents = contents.replace("{{{}}}".format(key), item)
|
contents = contents.replace(f"{{{key}}}", item)
|
||||||
fp = open(outfilename, "wt", encoding="utf-8")
|
fp = open(outfilename, "wt", encoding="utf-8")
|
||||||
fp.write(contents)
|
fp.write(contents)
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
|
|
||||||
def get_module_version(modulename):
|
def get_module_version(modulename: str) -> str:
|
||||||
mod = importlib.import_module(modulename)
|
mod = importlib.import_module(modulename)
|
||||||
return mod.__version__
|
return mod.__version__
|
||||||
|
|
||||||
|
|
||||||
def setup_package_argparser(parser):
|
def setup_package_argparser(parser: ArgumentParser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--sign",
|
"--sign",
|
||||||
dest="sign_identity",
|
dest="sign_identity",
|
||||||
@@ -138,13 +125,13 @@ def setup_package_argparser(parser):
|
|||||||
|
|
||||||
|
|
||||||
# `args` come from an ArgumentParser updated with setup_package_argparser()
|
# `args` come from an ArgumentParser updated with setup_package_argparser()
|
||||||
def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
def package_cocoa_app_in_dmg(app_path: os.PathLike, destfolder: os.PathLike, args) -> None:
|
||||||
# Rather than signing our app in XCode during the build phase, we sign it during the package
|
# Rather than signing our app in XCode during the build phase, we sign it during the package
|
||||||
# phase because running the app before packaging can modify it and we want to be sure to have
|
# phase because running the app before packaging can modify it and we want to be sure to have
|
||||||
# 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 = f"Developer ID Application: {args.sign_identity}"
|
||||||
result = print_and_do('codesign --force --deep --sign "{}" "{}"'.format(sign_identity, app_path))
|
result = print_and_do(f'codesign --force --deep --sign "{sign_identity}" "{app_path}"')
|
||||||
if result != 0:
|
if result != 0:
|
||||||
print("ERROR: Signing failed. Aborting packaging.")
|
print("ERROR: Signing failed. Aborting packaging.")
|
||||||
return
|
return
|
||||||
@@ -154,29 +141,32 @@ def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
|||||||
build_dmg(app_path, destfolder)
|
build_dmg(app_path, destfolder)
|
||||||
|
|
||||||
|
|
||||||
def build_dmg(app_path, destfolder):
|
def build_dmg(app_path: os.PathLike, destfolder: os.PathLike) -> None:
|
||||||
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
|
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
|
||||||
|
|
||||||
The name of the resulting DMG volume is determined by the app's name and version.
|
The name of the resulting DMG volume is determined by the app's name and version.
|
||||||
"""
|
"""
|
||||||
print(repr(op.join(app_path, "Contents", "Info.plist")))
|
print(repr(op.join(app_path, "Contents", "Info.plist")))
|
||||||
plist = plistlib.readPlist(op.join(app_path, "Contents", "Info.plist"))
|
with open(op.join(app_path, "Contents", "Info.plist"), "rb") as fp:
|
||||||
|
plist = plistlib.load(fp)
|
||||||
workpath = tempfile.mkdtemp()
|
workpath = tempfile.mkdtemp()
|
||||||
dmgpath = op.join(workpath, plist["CFBundleName"])
|
dmgpath = op.join(workpath, plist["CFBundleName"])
|
||||||
os.mkdir(dmgpath)
|
os.mkdir(dmgpath)
|
||||||
print_and_do('cp -R "%s" "%s"' % (app_path, dmgpath))
|
print_and_do('cp -R "{}" "{}"'.format(app_path, dmgpath))
|
||||||
print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, "Applications"))
|
print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, "Applications"))
|
||||||
dmgname = "%s_osx_%s.dmg" % (
|
dmgname = "{}_osx_{}.dmg".format(
|
||||||
plist["CFBundleName"].lower().replace(" ", "_"),
|
plist["CFBundleName"].lower().replace(" ", "_"),
|
||||||
plist["CFBundleVersion"].replace(".", "_"),
|
plist["CFBundleVersion"].replace(".", "_"),
|
||||||
)
|
)
|
||||||
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('hdiutil create "%s" -format UDBZ -nocrossdev -srcdir "%s"' % (op.join(destfolder, dmgname), dmgpath))
|
print_and_do(
|
||||||
|
'hdiutil create "{}" -format UDBZ -nocrossdev -srcdir "{}"'.format(op.join(destfolder, dmgname), dmgpath)
|
||||||
|
)
|
||||||
print("Build Complete")
|
print("Build Complete")
|
||||||
|
|
||||||
|
|
||||||
def add_to_pythonpath(path):
|
def add_to_pythonpath(path: os.PathLike) -> None:
|
||||||
"""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", "")
|
||||||
@@ -189,7 +179,12 @@ def add_to_pythonpath(path):
|
|||||||
# This is a method to hack around those freakingly tricky data inclusion/exlusion rules
|
# This is a method to hack around those freakingly tricky data inclusion/exlusion rules
|
||||||
# in setuptools. We copy the packages *without data* in a build folder and then build the plugin
|
# in setuptools. We copy the packages *without data* in a build folder and then build the plugin
|
||||||
# from there.
|
# from there.
|
||||||
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
def copy_packages(
|
||||||
|
packages_names: List[str],
|
||||||
|
dest: os.PathLike,
|
||||||
|
create_links: bool = False,
|
||||||
|
extra_ignores: Union[List[str], None] = None,
|
||||||
|
) -> None:
|
||||||
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
|
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
|
||||||
|
|
||||||
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
|
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
|
||||||
@@ -216,7 +211,7 @@ def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
|||||||
os.unlink(dest_path)
|
os.unlink(dest_path)
|
||||||
else:
|
else:
|
||||||
shutil.rmtree(dest_path)
|
shutil.rmtree(dest_path)
|
||||||
print("Copying package at {0} to {1}".format(source_path, dest_path))
|
print(f"Copying package at {source_path} to {dest_path}")
|
||||||
if create_links:
|
if create_links:
|
||||||
os.symlink(op.abspath(source_path), dest_path)
|
os.symlink(op.abspath(source_path), dest_path)
|
||||||
else:
|
else:
|
||||||
@@ -227,13 +222,13 @@ def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
|||||||
|
|
||||||
|
|
||||||
def build_debian_changelog(
|
def build_debian_changelog(
|
||||||
changelogpath,
|
changelogpath: os.PathLike,
|
||||||
destfile,
|
destfile: os.PathLike,
|
||||||
pkgname,
|
pkgname: str,
|
||||||
from_version=None,
|
from_version: Union[str, None] = None,
|
||||||
distribution="precise",
|
distribution: str = "precise",
|
||||||
fix_version=None,
|
fix_version: Union[str, None] = None,
|
||||||
):
|
) -> None:
|
||||||
"""Builds a debian changelog out of a YAML changelog.
|
"""Builds a debian changelog out of a YAML changelog.
|
||||||
|
|
||||||
Use fix_version to patch the top changelog to that version (if, for example, there was a
|
Use fix_version to patch the top changelog to that version (if, for example, there was a
|
||||||
@@ -286,7 +281,7 @@ def build_debian_changelog(
|
|||||||
re_changelog_header = re.compile(r"=== ([\d.b]*) \(([\d\-]*)\)")
|
re_changelog_header = re.compile(r"=== ([\d.b]*) \(([\d\-]*)\)")
|
||||||
|
|
||||||
|
|
||||||
def read_changelog_file(filename):
|
def read_changelog_file(filename: os.PathLike) -> List[Dict[str, Any]]:
|
||||||
def iter_by_three(it):
|
def iter_by_three(it):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -297,7 +292,7 @@ def read_changelog_file(filename):
|
|||||||
return
|
return
|
||||||
yield version, date, description
|
yield version, date, description
|
||||||
|
|
||||||
with open(filename, "rt", encoding="utf-8") as fp:
|
with open(filename, encoding="utf-8") as fp:
|
||||||
contents = fp.read()
|
contents = fp.read()
|
||||||
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
||||||
result = []
|
result = []
|
||||||
@@ -313,7 +308,7 @@ def read_changelog_file(filename):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def fix_qt_resource_file(path):
|
def fix_qt_resource_file(path: os.PathLike) -> None:
|
||||||
# pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date
|
# pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date
|
||||||
# containing accented characters. If it does, the encoding is wrong and it prevents the file
|
# containing accented characters. If it does, the encoding is wrong and it prevents the file
|
||||||
# from being correctly frozen by cx_freeze. To work around that, we open the file, strip all
|
# from being correctly frozen by cx_freeze. To work around that, we open the file, strip all
|
||||||
|
|||||||
@@ -1,30 +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 argparse
|
|
||||||
|
|
||||||
from setuptools import setup, Extension
|
|
||||||
|
|
||||||
|
|
||||||
def get_parser():
|
|
||||||
parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.")
|
|
||||||
parser.add_argument("source_files", nargs="+", help="List of source files to compile")
|
|
||||||
parser.add_argument("name", nargs=1, help="Name of the resulting extension")
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = get_parser().parse_args()
|
|
||||||
print("Building {}...".format(args.name[0]))
|
|
||||||
ext = Extension(args.name[0], args.source_files)
|
|
||||||
setup(
|
|
||||||
script_args=["build_ext", "--inplace"],
|
|
||||||
ext_modules=[ext],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -15,6 +15,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Callable, List
|
||||||
|
|
||||||
# This matches [123], but not [12] (3 digits being the minimum).
|
# This matches [123], but not [12] (3 digits being the minimum).
|
||||||
# It also matches [1234] [12345] etc..
|
# It also matches [1234] [12345] etc..
|
||||||
@@ -22,7 +23,7 @@ from pathlib import Path
|
|||||||
re_conflict = re.compile(r"^\[\d{3}\d*\] ")
|
re_conflict = re.compile(r"^\[\d{3}\d*\] ")
|
||||||
|
|
||||||
|
|
||||||
def get_conflicted_name(other_names, name):
|
def get_conflicted_name(other_names: List[str], name: str) -> str:
|
||||||
"""Returns name with a ``[000]`` number in front of it.
|
"""Returns name with a ``[000]`` number in front of it.
|
||||||
|
|
||||||
The number between brackets depends on how many conlicted filenames
|
The number between brackets depends on how many conlicted filenames
|
||||||
@@ -39,7 +40,7 @@ def get_conflicted_name(other_names, name):
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
def get_unconflicted_name(name):
|
def get_unconflicted_name(name: str) -> str:
|
||||||
"""Returns ``name`` without ``[]`` brackets.
|
"""Returns ``name`` without ``[]`` brackets.
|
||||||
|
|
||||||
Brackets which, of course, might have been added by func:`get_conflicted_name`.
|
Brackets which, of course, might have been added by func:`get_conflicted_name`.
|
||||||
@@ -47,12 +48,12 @@ def get_unconflicted_name(name):
|
|||||||
return re_conflict.sub("", name, 1)
|
return re_conflict.sub("", name, 1)
|
||||||
|
|
||||||
|
|
||||||
def is_conflicted(name):
|
def is_conflicted(name: str) -> bool:
|
||||||
"""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
|
||||||
|
|
||||||
|
|
||||||
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
|
def _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None:
|
||||||
"""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.is_dir() and not source_path.is_dir():
|
if dest_path.is_dir() and not source_path.is_dir():
|
||||||
dest_path = dest_path.joinpath(source_path.name)
|
dest_path = dest_path.joinpath(source_path.name)
|
||||||
@@ -64,16 +65,16 @@ def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
|
|||||||
operation(str(source_path), str(dest_path))
|
operation(str(source_path), str(dest_path))
|
||||||
|
|
||||||
|
|
||||||
def smart_move(source_path, dest_path):
|
def smart_move(source_path: Path, dest_path: Path) -> None:
|
||||||
"""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: Path, dest_path: Path) -> None:
|
||||||
"""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 OSError as e:
|
||||||
if e.errno in {
|
if e.errno in {
|
||||||
21,
|
21,
|
||||||
13,
|
13,
|
||||||
|
|||||||
@@ -6,31 +6,33 @@
|
|||||||
# 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 enum import Enum
|
||||||
|
from os import PathLike
|
||||||
import os.path as op
|
import os.path as op
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class SpecialFolder:
|
class SpecialFolder(Enum):
|
||||||
APPDATA = 1
|
APPDATA = 1
|
||||||
CACHE = 2
|
CACHE = 2
|
||||||
|
|
||||||
|
|
||||||
def open_url(url):
|
def open_url(url: str) -> None:
|
||||||
"""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: PathLike) -> None:
|
||||||
"""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: PathLike) -> None:
|
||||||
"""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))
|
||||||
|
|
||||||
|
|
||||||
def special_folder_path(special_folder, appname=None, portable=False):
|
def special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:
|
||||||
"""Returns the path of ``special_folder``.
|
"""Returns the path of ``special_folder``.
|
||||||
|
|
||||||
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current
|
||||||
@@ -38,25 +40,25 @@ def special_folder_path(special_folder, appname=None, portable=False):
|
|||||||
|
|
||||||
You can override the application name with ``appname``. This argument is ingored under Qt.
|
You can override the application name with ``appname``. This argument is ingored under Qt.
|
||||||
"""
|
"""
|
||||||
return _special_folder_path(special_folder, appname, portable=portable)
|
return _special_folder_path(special_folder, portable=portable)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtCore import QUrl, QStandardPaths
|
from PyQt5.QtCore import QUrl, QStandardPaths
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from qtlib.util import get_appdata
|
from qt.util import get_appdata
|
||||||
from core.util import executable_folder
|
from core.util import executable_folder
|
||||||
from hscommon.plat import ISWINDOWS, ISOSX
|
from hscommon.plat import ISWINDOWS, ISOSX
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
def _open_url(url):
|
def _open_url(url: str) -> None:
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
QDesktopServices.openUrl(QUrl(url))
|
||||||
|
|
||||||
def _open_path(path):
|
def _open_path(path: str) -> None:
|
||||||
url = QUrl.fromLocalFile(str(path))
|
url = QUrl.fromLocalFile(str(path))
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def _reveal_path(path):
|
def _reveal_path(path: str) -> None:
|
||||||
if ISWINDOWS:
|
if ISWINDOWS:
|
||||||
subprocess.run(["explorer", "/select,", op.abspath(path)])
|
subprocess.run(["explorer", "/select,", op.abspath(path)])
|
||||||
elif ISOSX:
|
elif ISOSX:
|
||||||
@@ -64,7 +66,7 @@ try:
|
|||||||
else:
|
else:
|
||||||
_open_path(op.dirname(str(path)))
|
_open_path(op.dirname(str(path)))
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None, portable=False):
|
def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:
|
||||||
if special_folder == SpecialFolder.CACHE:
|
if special_folder == SpecialFolder.CACHE:
|
||||||
if ISWINDOWS and portable:
|
if ISWINDOWS and portable:
|
||||||
folder = op.join(executable_folder(), "cache")
|
folder = op.join(executable_folder(), "cache")
|
||||||
@@ -79,13 +81,17 @@ except ImportError:
|
|||||||
# weird situation. Let's just have dummy fallbacks.
|
# weird situation. Let's just have dummy fallbacks.
|
||||||
logging.warning("Can't setup desktop functions!")
|
logging.warning("Can't setup desktop functions!")
|
||||||
|
|
||||||
def _open_path(path):
|
def _open_url(url: str) -> None:
|
||||||
# Dummy for tests
|
# Dummy for tests
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _reveal_path(path):
|
def _open_path(path: str) -> None:
|
||||||
# Dummy for tests
|
# Dummy for tests
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _special_folder_path(special_folder, appname=None, portable=False):
|
def _reveal_path(path: str) -> None:
|
||||||
|
# Dummy for tests
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:
|
||||||
return "/tmp"
|
return "/tmp"
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ class GUIObject:
|
|||||||
``multibind`` flag to ``True`` and the safeguard will be disabled.
|
``multibind`` flag to ``True`` and the safeguard will be disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, multibind=False):
|
def __init__(self, multibind: bool = False) -> None:
|
||||||
self._view = None
|
self._view = None
|
||||||
self._multibind = multibind
|
self._multibind = multibind
|
||||||
|
|
||||||
def _view_updated(self):
|
def _view_updated(self) -> None:
|
||||||
"""(Virtual) Called after :attr:`view` has been set.
|
"""(Virtual) Called after :attr:`view` has been set.
|
||||||
|
|
||||||
Doing nothing by default, this method is called after :attr:`view` has been set (it isn't
|
Doing nothing by default, this method is called after :attr:`view` has been set (it isn't
|
||||||
@@ -48,7 +48,7 @@ class GUIObject:
|
|||||||
(which is often the whole of the initialization code).
|
(which is often the whole of the initialization code).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_view(self):
|
def has_view(self) -> bool:
|
||||||
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
|
return (self._view is not None) and (not isinstance(self._view, NoopGUI))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -67,7 +67,7 @@ class GUIObject:
|
|||||||
return self._view
|
return self._view
|
||||||
|
|
||||||
@view.setter
|
@view.setter
|
||||||
def view(self, value):
|
def view(self, value) -> None:
|
||||||
if self._view is None and value is None:
|
if self._view is None and value is None:
|
||||||
# Initial view assignment
|
# Initial view assignment
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
from .base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
|
from hscommon.gui.table import GUITable
|
||||||
|
|
||||||
|
|
||||||
class Column:
|
class Column:
|
||||||
@@ -17,7 +19,7 @@ class Column:
|
|||||||
These attributes are then used to correctly configure the column on the "view" side.
|
These attributes are then used to correctly configure the column on the "view" side.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, display="", visible=True, optional=False):
|
def __init__(self, name: str, display: str = "", visible: bool = True, optional: bool = False) -> None:
|
||||||
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
|
#: "programmatical" (not for display) name. Used as a reference in a couple of place, such
|
||||||
#: as :meth:`Columns.column_by_name`.
|
#: as :meth:`Columns.column_by_name`.
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -52,14 +54,14 @@ class ColumnsView:
|
|||||||
callbacks.
|
callbacks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def restore_columns(self):
|
def restore_columns(self) -> None:
|
||||||
"""Update all columns according to the model.
|
"""Update all columns according to the model.
|
||||||
|
|
||||||
When this is called, our view has to update the columns title, order and visibility of all
|
When this is called, our view has to update the columns title, order and visibility of all
|
||||||
columns.
|
columns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_column_visible(self, colname, visible):
|
def set_column_visible(self, colname: str, visible: bool) -> None:
|
||||||
"""Update visibility of column ``colname``.
|
"""Update visibility of column ``colname``.
|
||||||
|
|
||||||
Called when the user toggles the visibility of a column, we must update the column
|
Called when the user toggles the visibility of a column, we must update the column
|
||||||
@@ -73,13 +75,13 @@ class PrefAccessInterface:
|
|||||||
*Not actually used in the code. For documentation purposes only.*
|
*Not actually used in the code. For documentation purposes only.*
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_default(self, key, fallback_value):
|
def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any:
|
||||||
"""Retrieve the value for ``key`` in the currently running app's preference store.
|
"""Retrieve the value for ``key`` in the currently running app's preference store.
|
||||||
|
|
||||||
If the key doesn't exist, return ``fallback_value``.
|
If the key doesn't exist, return ``fallback_value``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_default(self, key, value):
|
def set_default(self, key: str, value: Any) -> None:
|
||||||
"""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."""
|
||||||
|
|
||||||
|
|
||||||
@@ -104,65 +106,65 @@ class Columns(GUIObject):
|
|||||||
have that same prefix.
|
have that same prefix.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, table, prefaccess=None, savename=None):
|
def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None):
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
self.table = table
|
self.table = table
|
||||||
self.prefaccess = prefaccess
|
self.prefaccess = prefaccess
|
||||||
self.savename = savename
|
self.savename = savename
|
||||||
# We use copy here for test isolation. If we don't, changing a column affects all tests.
|
# We use copy here for test isolation. If we don't, changing a column affects all tests.
|
||||||
self.column_list = list(map(copy.copy, table.COLUMNS))
|
self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS))
|
||||||
for i, column in enumerate(self.column_list):
|
for i, column in enumerate(self.column_list):
|
||||||
column.logical_index = i
|
column.logical_index = i
|
||||||
column.ordered_index = i
|
column.ordered_index = i
|
||||||
self.coldata = {col.name: col for col in self.column_list}
|
self.coldata = {col.name: col for col in self.column_list}
|
||||||
|
|
||||||
# --- Private
|
# --- Private
|
||||||
def _get_colname_attr(self, colname, attrname, default):
|
def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:
|
||||||
try:
|
try:
|
||||||
return getattr(self.coldata[colname], attrname)
|
return getattr(self.coldata[colname], attrname)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _set_colname_attr(self, colname, attrname, value):
|
def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:
|
||||||
try:
|
try:
|
||||||
col = self.coldata[colname]
|
col = self.coldata[colname]
|
||||||
setattr(col, attrname, value)
|
setattr(col, attrname, value)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _optional_columns(self):
|
def _optional_columns(self) -> List[Column]:
|
||||||
return [c for c in self.column_list if c.optional]
|
return [c for c in self.column_list if c.optional]
|
||||||
|
|
||||||
# --- Override
|
# --- Override
|
||||||
def _view_updated(self):
|
def _view_updated(self) -> None:
|
||||||
self.restore_columns()
|
self.restore_columns()
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def column_by_index(self, index):
|
def column_by_index(self, index: int):
|
||||||
"""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: str):
|
||||||
"""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) -> int:
|
||||||
"""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: str) -> str:
|
||||||
"""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: str) -> bool:
|
||||||
"""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: str) -> int:
|
||||||
"""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: str) -> List[str]:
|
||||||
"""Returns the list of all columns to the right of ``colname``.
|
"""Returns the list of all columns to the right of ``colname``.
|
||||||
|
|
||||||
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
|
"right" meaning "having a higher :attr:`Column.ordered_index`" in our left-to-right
|
||||||
@@ -172,7 +174,7 @@ class Columns(GUIObject):
|
|||||||
index = column.ordered_index
|
index = column.ordered_index
|
||||||
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
||||||
|
|
||||||
def menu_items(self):
|
def menu_items(self) -> List[Tuple[str, bool]]:
|
||||||
"""Returns a list of items convenient for quick visibility menu generation.
|
"""Returns a list of items convenient for quick visibility menu generation.
|
||||||
|
|
||||||
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
|
Returns a list of ``(display_name, is_marked)`` items for each optional column in the
|
||||||
@@ -184,7 +186,7 @@ class Columns(GUIObject):
|
|||||||
"""
|
"""
|
||||||
return [(c.display, c.visible) for c in self._optional_columns()]
|
return [(c.display, c.visible) for c in self._optional_columns()]
|
||||||
|
|
||||||
def move_column(self, colname, index):
|
def move_column(self, colname: str, index: int) -> None:
|
||||||
"""Moves column ``colname`` to ``index``.
|
"""Moves column ``colname`` to ``index``.
|
||||||
|
|
||||||
The column will be placed just in front of the column currently having that index, or to the
|
The column will be placed just in front of the column currently having that index, or to the
|
||||||
@@ -195,7 +197,7 @@ class Columns(GUIObject):
|
|||||||
colnames.insert(index, colname)
|
colnames.insert(index, colname)
|
||||||
self.set_column_order(colnames)
|
self.set_column_order(colnames)
|
||||||
|
|
||||||
def reset_to_defaults(self):
|
def reset_to_defaults(self) -> None:
|
||||||
"""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():
|
||||||
@@ -203,11 +205,11 @@ class Columns(GUIObject):
|
|||||||
col.width = col.default_width
|
col.width = col.default_width
|
||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def resize_column(self, colname, newwidth):
|
def resize_column(self, colname: str, newwidth: int) -> None:
|
||||||
"""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) -> None:
|
||||||
"""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):
|
||||||
@@ -216,7 +218,7 @@ class Columns(GUIObject):
|
|||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
return
|
return
|
||||||
for col in self.column_list:
|
for col in self.column_list:
|
||||||
pref_name = "{}.Columns.{}".format(self.savename, col.name)
|
pref_name = f"{self.savename}.Columns.{col.name}"
|
||||||
coldata = self.prefaccess.get_default(pref_name, fallback_value={})
|
coldata = self.prefaccess.get_default(pref_name, fallback_value={})
|
||||||
if "index" in coldata:
|
if "index" in coldata:
|
||||||
col.ordered_index = coldata["index"]
|
col.ordered_index = coldata["index"]
|
||||||
@@ -226,18 +228,19 @@ class Columns(GUIObject):
|
|||||||
col.visible = coldata["visible"]
|
col.visible = coldata["visible"]
|
||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def save_columns(self):
|
def save_columns(self) -> None:
|
||||||
"""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:
|
||||||
pref_name = "{}.Columns.{}".format(self.savename, col.name)
|
pref_name = f"{self.savename}.Columns.{col.name}"
|
||||||
coldata = {"index": col.ordered_index, "width": col.width}
|
coldata = {"index": col.ordered_index, "width": col.width}
|
||||||
if col.optional:
|
if col.optional:
|
||||||
coldata["visible"] = col.visible
|
coldata["visible"] = col.visible
|
||||||
self.prefaccess.set_default(pref_name, coldata)
|
self.prefaccess.set_default(pref_name, coldata)
|
||||||
|
|
||||||
def set_column_order(self, colnames):
|
# TODO annotate colnames
|
||||||
|
def set_column_order(self, colnames) -> None:
|
||||||
"""Change the columns order so it matches the order in ``colnames``.
|
"""Change the columns order so it matches the order in ``colnames``.
|
||||||
|
|
||||||
:param colnames: A list of column names in the desired order.
|
:param colnames: A list of column names in the desired order.
|
||||||
@@ -247,17 +250,17 @@ class Columns(GUIObject):
|
|||||||
col = self.coldata[colname]
|
col = self.coldata[colname]
|
||||||
col.ordered_index = i
|
col.ordered_index = i
|
||||||
|
|
||||||
def set_column_visible(self, colname, visible):
|
def set_column_visible(self, colname: str, visible: bool) -> None:
|
||||||
"""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: str, width: int) -> None:
|
||||||
"""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: int) -> bool:
|
||||||
"""Toggles the visibility of an optional column.
|
"""Toggles the visibility of an optional column.
|
||||||
|
|
||||||
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
|
You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``
|
||||||
@@ -271,11 +274,11 @@ class Columns(GUIObject):
|
|||||||
|
|
||||||
# --- Properties
|
# --- Properties
|
||||||
@property
|
@property
|
||||||
def ordered_columns(self):
|
def ordered_columns(self) -> List[Column]:
|
||||||
"""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[str]:
|
||||||
"""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]
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
# 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 ..jobprogress.performer import ThreadedJobPerformer
|
from typing import Callable, Tuple, Union
|
||||||
from .base import GUIObject
|
from hscommon.jobprogress.performer import ThreadedJobPerformer
|
||||||
from .text_field import TextField
|
from hscommon.gui.base import GUIObject
|
||||||
|
from hscommon.gui.text_field import TextField
|
||||||
|
|
||||||
|
|
||||||
class ProgressWindowView:
|
class ProgressWindowView:
|
||||||
@@ -20,13 +21,13 @@ class ProgressWindowView:
|
|||||||
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
"""Show the dialog."""
|
"""Show the dialog."""
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""Close the dialog."""
|
"""Close the dialog."""
|
||||||
|
|
||||||
def set_progress(self, progress):
|
def set_progress(self, progress: int) -> None:
|
||||||
"""Set the progress of the progress bar to ``progress``.
|
"""Set the progress of the progress bar to ``progress``.
|
||||||
|
|
||||||
Not all jobs are equally responsive on their job progress report and it is recommended that
|
Not all jobs are equally responsive on their job progress report and it is recommended that
|
||||||
@@ -60,7 +61,11 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
called as if the job terminated normally.
|
called as if the job terminated normally.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, finish_func, error_func=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
finish_func: Callable[[Union[str, None]], None],
|
||||||
|
error_func: Callable[[Union[str, None], Exception], bool] = None,
|
||||||
|
) -> None:
|
||||||
# finish_func(jobid) is the function that is called when a job is completed.
|
# finish_func(jobid) is the function that is called when a job is completed.
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
ThreadedJobPerformer.__init__(self)
|
ThreadedJobPerformer.__init__(self)
|
||||||
@@ -71,9 +76,9 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
#: :class:`.TextField`. It contains the job textual update that the function might yield
|
#: :class:`.TextField`. It contains the job textual update that the function might yield
|
||||||
#: during its course.
|
#: during its course.
|
||||||
self.progressdesc_textfield = TextField()
|
self.progressdesc_textfield = TextField()
|
||||||
self.jobid = None
|
self.jobid: Union[str, None] = None
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
"""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
|
||||||
@@ -81,7 +86,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
if self._job_running:
|
if self._job_running:
|
||||||
self.job_cancelled = True
|
self.job_cancelled = True
|
||||||
|
|
||||||
def pulse(self):
|
def pulse(self) -> None:
|
||||||
"""Update progress reports in the GUI.
|
"""Update progress reports in the GUI.
|
||||||
|
|
||||||
Call this regularly from the GUI main run loop. The values might change before
|
Call this regularly from the GUI main run loop. The values might change before
|
||||||
@@ -111,7 +116,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
self.progressdesc_textfield.text = last_desc
|
self.progressdesc_textfield.text = last_desc
|
||||||
self.view.set_progress(last_progress)
|
self.view.set_progress(last_progress)
|
||||||
|
|
||||||
def run(self, jobid, title, target, args=()):
|
def run(self, jobid: str, title: str, target: Callable, args: Tuple = ()):
|
||||||
"""Starts a threaded job.
|
"""Starts a threaded job.
|
||||||
|
|
||||||
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
from collections.abc import Sequence, MutableSequence
|
from collections.abc import Sequence, MutableSequence
|
||||||
|
|
||||||
from .base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
|
|
||||||
|
|
||||||
class Selectable(Sequence):
|
class Selectable(Sequence):
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
|
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import MutableSequence
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
from .base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
from .selectable_list import Selectable
|
from hscommon.gui.selectable_list import Selectable
|
||||||
|
|
||||||
|
|
||||||
# We used to directly subclass list, but it caused problems at some point with deepcopy
|
# We used to directly subclass list, but it caused problems at some point with deepcopy
|
||||||
@@ -27,12 +28,16 @@ class Table(MutableSequence, Selectable):
|
|||||||
Subclasses :class:`.Selectable`.
|
Subclasses :class:`.Selectable`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
# Should be List[Column], but have circular import...
|
||||||
Selectable.__init__(self)
|
COLUMNS: List = []
|
||||||
self._rows = []
|
|
||||||
self._header = None
|
|
||||||
self._footer = None
|
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
Selectable.__init__(self)
|
||||||
|
self._rows: List["Row"] = []
|
||||||
|
self._header: Union["Row", None] = None
|
||||||
|
self._footer: Union["Row", None] = None
|
||||||
|
|
||||||
|
# TODO type hint for key
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
self._rows.__delitem__(key)
|
self._rows.__delitem__(key)
|
||||||
if self._header is not None and ((not self) or (self[0] is not self._header)):
|
if self._header is not None and ((not self) or (self[0] is not self._header)):
|
||||||
@@ -41,16 +46,18 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._footer = None
|
self._footer = None
|
||||||
self._check_selection_range()
|
self._check_selection_range()
|
||||||
|
|
||||||
def __getitem__(self, key):
|
# TODO type hint for key
|
||||||
|
def __getitem__(self, key) -> Any:
|
||||||
return self._rows.__getitem__(key)
|
return self._rows.__getitem__(key)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self._rows)
|
return len(self._rows)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
# TODO type hint for key
|
||||||
|
def __setitem__(self, key, value: Any) -> None:
|
||||||
self._rows.__setitem__(key, value)
|
self._rows.__setitem__(key, value)
|
||||||
|
|
||||||
def append(self, item):
|
def append(self, item: "Row") -> None:
|
||||||
"""Appends ``item`` at the end of the table.
|
"""Appends ``item`` at the end of the table.
|
||||||
|
|
||||||
If there's a footer, the item is inserted before it.
|
If there's a footer, the item is inserted before it.
|
||||||
@@ -60,7 +67,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
else:
|
else:
|
||||||
self._rows.append(item)
|
self._rows.append(item)
|
||||||
|
|
||||||
def insert(self, index, item):
|
def insert(self, index: int, item: "Row") -> None:
|
||||||
"""Inserts ``item`` at ``index`` in the table.
|
"""Inserts ``item`` at ``index`` in the table.
|
||||||
|
|
||||||
If there's a header, will make sure we don't insert before it, and if there's a footer, will
|
If there's a header, will make sure we don't insert before it, and if there's a footer, will
|
||||||
@@ -72,7 +79,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
index = len(self) - 1
|
index = len(self) - 1
|
||||||
self._rows.insert(index, item)
|
self._rows.insert(index, item)
|
||||||
|
|
||||||
def remove(self, row):
|
def remove(self, row: "Row") -> None:
|
||||||
"""Removes ``row`` from table.
|
"""Removes ``row`` from table.
|
||||||
|
|
||||||
If ``row`` is a header or footer, that header or footer will be set to ``None``.
|
If ``row`` is a header or footer, that header or footer will be set to ``None``.
|
||||||
@@ -84,7 +91,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._rows.remove(row)
|
self._rows.remove(row)
|
||||||
self._check_selection_range()
|
self._check_selection_range()
|
||||||
|
|
||||||
def sort_by(self, column_name, desc=False):
|
def sort_by(self, column_name: str, desc: bool = False) -> None:
|
||||||
"""Sort table by ``column_name``.
|
"""Sort table by ``column_name``.
|
||||||
|
|
||||||
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
Sort key for each row is computed from :meth:`Row.sort_key_for_column`.
|
||||||
@@ -105,7 +112,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
|
|
||||||
# --- Properties
|
# --- Properties
|
||||||
@property
|
@property
|
||||||
def footer(self):
|
def footer(self) -> Union["Row", None]:
|
||||||
"""If set, a row that always stay at the bottom of the table.
|
"""If set, a row that always stay at the bottom of the table.
|
||||||
|
|
||||||
:class:`Row`. *get/set*.
|
:class:`Row`. *get/set*.
|
||||||
@@ -128,7 +135,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self._footer
|
return self._footer
|
||||||
|
|
||||||
@footer.setter
|
@footer.setter
|
||||||
def footer(self, value):
|
def footer(self, value: Union["Row", None]) -> None:
|
||||||
if self._footer is not None:
|
if self._footer is not None:
|
||||||
self._rows.pop()
|
self._rows.pop()
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -136,7 +143,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._footer = value
|
self._footer = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def header(self):
|
def header(self) -> Union["Row", None]:
|
||||||
"""If set, a row that always stay at the bottom of the table.
|
"""If set, a row that always stay at the bottom of the table.
|
||||||
|
|
||||||
See :attr:`footer` for details.
|
See :attr:`footer` for details.
|
||||||
@@ -144,7 +151,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self._header
|
return self._header
|
||||||
|
|
||||||
@header.setter
|
@header.setter
|
||||||
def header(self, value):
|
def header(self, value: Union["Row", None]) -> None:
|
||||||
if self._header is not None:
|
if self._header is not None:
|
||||||
self._rows.pop(0)
|
self._rows.pop(0)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -152,7 +159,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
self._header = value
|
self._header = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def row_count(self):
|
def row_count(self) -> int:
|
||||||
"""Number or rows in the table (without counting header and footer).
|
"""Number or rows in the table (without counting header and footer).
|
||||||
|
|
||||||
*int*. *read-only*.
|
*int*. *read-only*.
|
||||||
@@ -165,7 +172,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rows(self):
|
def rows(self) -> List["Row"]:
|
||||||
"""List of rows in the table, excluding header and footer.
|
"""List of rows in the table, excluding header and footer.
|
||||||
|
|
||||||
List of :class:`Row`. *read-only*.
|
List of :class:`Row`. *read-only*.
|
||||||
@@ -179,7 +186,7 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self[start:end]
|
return self[start:end]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_row(self):
|
def selected_row(self) -> "Row":
|
||||||
"""Selected row according to :attr:`Selectable.selected_index`.
|
"""Selected row according to :attr:`Selectable.selected_index`.
|
||||||
|
|
||||||
:class:`Row`. *get/set*.
|
:class:`Row`. *get/set*.
|
||||||
@@ -190,14 +197,14 @@ class Table(MutableSequence, Selectable):
|
|||||||
return self[self.selected_index] if self.selected_index is not None else None
|
return self[self.selected_index] if self.selected_index is not None else None
|
||||||
|
|
||||||
@selected_row.setter
|
@selected_row.setter
|
||||||
def selected_row(self, value):
|
def selected_row(self, value: int) -> None:
|
||||||
try:
|
try:
|
||||||
self.selected_index = self.index(value)
|
self.selected_index = self.index(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_rows(self):
|
def selected_rows(self) -> List["Row"]:
|
||||||
"""List of selected rows based on :attr:`.selected_indexes`.
|
"""List of selected rows based on :attr:`.selected_indexes`.
|
||||||
|
|
||||||
List of :class:`Row`. *read-only*.
|
List of :class:`Row`. *read-only*.
|
||||||
@@ -219,20 +226,20 @@ class GUITableView:
|
|||||||
Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.
|
Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self) -> None:
|
||||||
"""Refreshes the contents of the table widget.
|
"""Refreshes the contents of the table widget.
|
||||||
|
|
||||||
Ensures that the contents of the table widget is synced with the model. This includes
|
Ensures that the contents of the table widget is synced with the model. This includes
|
||||||
selection.
|
selection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def start_editing(self):
|
def start_editing(self) -> None:
|
||||||
"""Start editing the currently selected row.
|
"""Start editing the currently selected row.
|
||||||
|
|
||||||
Begin whatever inline editing support that the view supports.
|
Begin whatever inline editing support that the view supports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def stop_editing(self):
|
def stop_editing(self) -> None:
|
||||||
"""Stop editing if there's an inline editing in effect.
|
"""Stop editing if there's an inline editing in effect.
|
||||||
|
|
||||||
There's no "aborting" implied in this call, so it's appropriate to send whatever the user
|
There's no "aborting" implied in this call, so it's appropriate to send whatever the user
|
||||||
@@ -260,33 +267,33 @@ class GUITable(Table, GUIObject):
|
|||||||
:class:`GUITableView`.
|
:class:`GUITableView`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
GUIObject.__init__(self)
|
GUIObject.__init__(self)
|
||||||
Table.__init__(self)
|
Table.__init__(self)
|
||||||
#: The row being currently edited by the user. ``None`` if no edit is taking place.
|
#: The row being currently edited by the user. ``None`` if no edit is taking place.
|
||||||
self.edited = None
|
self.edited: Union["Row", None] = None
|
||||||
self._sort_descriptor = None
|
self._sort_descriptor: Union[SortDescriptor, None] = None
|
||||||
|
|
||||||
# --- Virtual
|
# --- Virtual
|
||||||
def _do_add(self):
|
def _do_add(self) -> Tuple["Row", int]:
|
||||||
"""(Virtual) Creates a new row, adds it in the table.
|
"""(Virtual) Creates a new row, adds it in the table.
|
||||||
|
|
||||||
Returns ``(row, insert_index)``.
|
Returns ``(row, insert_index)``.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _do_delete(self):
|
def _do_delete(self) -> None:
|
||||||
"""(Virtual) Delete the selected rows."""
|
"""(Virtual) Delete the selected rows."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _fill(self):
|
def _fill(self) -> None:
|
||||||
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
|
"""(Virtual/Required) Fills the table with all the rows that this table is supposed to have.
|
||||||
|
|
||||||
Called by :meth:`refresh`. Does nothing by default.
|
Called by :meth:`refresh`. Does nothing by default.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_edited_new(self):
|
def _is_edited_new(self) -> bool:
|
||||||
"""(Virtual) Returns whether the currently edited row should be considered "new".
|
"""(Virtual) Returns whether the currently edited row should be considered "new".
|
||||||
|
|
||||||
This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a
|
This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a
|
||||||
@@ -315,7 +322,7 @@ class GUITable(Table, GUIObject):
|
|||||||
self.select([len(self) - 1])
|
self.select([len(self) - 1])
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def add(self):
|
def add(self) -> None:
|
||||||
"""Add a new row in edit mode.
|
"""Add a new row in edit mode.
|
||||||
|
|
||||||
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
|
Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit
|
||||||
@@ -334,7 +341,7 @@ class GUITable(Table, GUIObject):
|
|||||||
self.edited = row
|
self.edited = row
|
||||||
self.view.start_editing()
|
self.view.start_editing()
|
||||||
|
|
||||||
def can_edit_cell(self, column_name, row_index):
|
def can_edit_cell(self, column_name: str, row_index: int) -> bool:
|
||||||
"""Returns whether the cell at ``row_index`` and ``column_name`` can be edited.
|
"""Returns whether the cell at ``row_index`` and ``column_name`` can be edited.
|
||||||
|
|
||||||
A row is, by default, editable as soon as it has an attr with the same name as `column`.
|
A row is, by default, editable as soon as it has an attr with the same name as `column`.
|
||||||
@@ -346,7 +353,7 @@ class GUITable(Table, GUIObject):
|
|||||||
row = self[row_index]
|
row = self[row_index]
|
||||||
return row.can_edit_cell(column_name)
|
return row.can_edit_cell(column_name)
|
||||||
|
|
||||||
def cancel_edits(self):
|
def cancel_edits(self) -> None:
|
||||||
"""Cancels the current edit operation.
|
"""Cancels the current edit operation.
|
||||||
|
|
||||||
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
|
If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).
|
||||||
@@ -364,7 +371,7 @@ class GUITable(Table, GUIObject):
|
|||||||
self.edited = None
|
self.edited = None
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self) -> None:
|
||||||
"""Delete the currently selected rows.
|
"""Delete the currently selected rows.
|
||||||
|
|
||||||
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
|
Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if
|
||||||
@@ -377,7 +384,7 @@ class GUITable(Table, GUIObject):
|
|||||||
if self:
|
if self:
|
||||||
self._do_delete()
|
self._do_delete()
|
||||||
|
|
||||||
def refresh(self, refresh_view=True):
|
def refresh(self, refresh_view: bool = True) -> None:
|
||||||
"""Empty the table and re-create its rows.
|
"""Empty the table and re-create its rows.
|
||||||
|
|
||||||
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
|
:meth:`_fill` is called after we emptied the table to create our rows. Previous sort order
|
||||||
@@ -399,7 +406,7 @@ class GUITable(Table, GUIObject):
|
|||||||
if refresh_view:
|
if refresh_view:
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
def save_edits(self):
|
def save_edits(self) -> None:
|
||||||
"""Commit user edits to the model.
|
"""Commit user edits to the model.
|
||||||
|
|
||||||
This is done by calling :meth:`Row.save`.
|
This is done by calling :meth:`Row.save`.
|
||||||
@@ -410,7 +417,7 @@ class GUITable(Table, GUIObject):
|
|||||||
self.edited = None
|
self.edited = None
|
||||||
row.save()
|
row.save()
|
||||||
|
|
||||||
def sort_by(self, column_name, desc=False):
|
def sort_by(self, column_name: str, desc: bool = False) -> None:
|
||||||
"""Sort table by ``column_name``.
|
"""Sort table by ``column_name``.
|
||||||
|
|
||||||
Overrides :meth:`Table.sort_by`. After having performed sorting, calls
|
Overrides :meth:`Table.sort_by`. After having performed sorting, calls
|
||||||
@@ -450,18 +457,18 @@ class Row:
|
|||||||
Of course, this is only default behavior. This can be overriden.
|
Of course, this is only default behavior. This can be overriden.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, table):
|
def __init__(self, table: GUITable) -> None:
|
||||||
super(Row, self).__init__()
|
super().__init__()
|
||||||
self.table = table
|
self.table = table
|
||||||
|
|
||||||
def _edit(self):
|
def _edit(self) -> None:
|
||||||
if self.table.edited is self:
|
if self.table.edited is self:
|
||||||
return
|
return
|
||||||
assert self.table.edited is None
|
assert self.table.edited is None
|
||||||
self.table.edited = self
|
self.table.edited = self
|
||||||
|
|
||||||
# --- Virtual
|
# --- Virtual
|
||||||
def can_edit(self):
|
def can_edit(self) -> bool:
|
||||||
"""(Virtual) Whether the whole row can be edited.
|
"""(Virtual) Whether the whole row can be edited.
|
||||||
|
|
||||||
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
By default, always returns ``True``. This is for the *whole* row. For individual cells, it's
|
||||||
@@ -469,7 +476,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load(self):
|
def load(self) -> None:
|
||||||
"""(Virtual/Required) Loads up values from the model to be presented in the table.
|
"""(Virtual/Required) Loads up values from the model to be presented in the table.
|
||||||
|
|
||||||
Usually, our model instances contain values that are not quite ready for display. If you
|
Usually, our model instances contain values that are not quite ready for display. If you
|
||||||
@@ -478,7 +485,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save(self):
|
def save(self) -> None:
|
||||||
"""(Virtual/Required) Saves user edits into your model.
|
"""(Virtual/Required) Saves user edits into your model.
|
||||||
|
|
||||||
If your table is editable, this is called when the user commits his changes. Usually, these
|
If your table is editable, this is called when the user commits his changes. Usually, these
|
||||||
@@ -487,7 +494,7 @@ class Row:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def sort_key_for_column(self, column_name):
|
def sort_key_for_column(self, column_name: str) -> Any:
|
||||||
"""(Virtual) Return the value that is to be used to sort by column ``column_name``.
|
"""(Virtual) Return the value that is to be used to sort by column ``column_name``.
|
||||||
|
|
||||||
By default, looks for an attribute with the same name as ``column_name``, but with an
|
By default, looks for an attribute with the same name as ``column_name``, but with an
|
||||||
@@ -500,7 +507,7 @@ class Row:
|
|||||||
return getattr(self, column_name)
|
return getattr(self, column_name)
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def can_edit_cell(self, column_name):
|
def can_edit_cell(self, column_name: str) -> bool:
|
||||||
"""Returns whether cell for column ``column_name`` can be edited.
|
"""Returns whether cell for column ``column_name`` can be edited.
|
||||||
|
|
||||||
By the default, the check is done in many steps:
|
By the default, the check is done in many steps:
|
||||||
@@ -530,7 +537,7 @@ class Row:
|
|||||||
return False
|
return False
|
||||||
return bool(getattr(prop, "fset", None))
|
return bool(getattr(prop, "fset", None))
|
||||||
|
|
||||||
def get_cell_value(self, attrname):
|
def get_cell_value(self, attrname: str) -> Any:
|
||||||
"""Get cell value for ``attrname``.
|
"""Get cell value for ``attrname``.
|
||||||
|
|
||||||
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
By default, does a simple ``getattr()``, but it is used to allow subclasses to have
|
||||||
@@ -540,7 +547,7 @@ class Row:
|
|||||||
attrname = "from_"
|
attrname = "from_"
|
||||||
return getattr(self, attrname)
|
return getattr(self, attrname)
|
||||||
|
|
||||||
def set_cell_value(self, attrname, value):
|
def set_cell_value(self, attrname: str, value: Any) -> None:
|
||||||
"""Set cell value to ``value`` for ``attrname``.
|
"""Set cell value to ``value`` for ``attrname``.
|
||||||
|
|
||||||
By default, does a simple ``setattr()``, but it is used to allow subclasses to have
|
By default, does a simple ``setattr()``, but it is used to allow subclasses to have
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# 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 .base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
from ..util import nonone
|
from hscommon.util import nonone
|
||||||
|
|
||||||
|
|
||||||
class TextFieldView:
|
class TextFieldView:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
from collections.abc import MutableSequence
|
from collections.abc import MutableSequence
|
||||||
|
|
||||||
from .base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
|
|
||||||
|
|
||||||
class Node(MutableSequence):
|
class Node(MutableSequence):
|
||||||
@@ -77,8 +77,7 @@ class Node(MutableSequence):
|
|||||||
if include_self and predicate(self):
|
if include_self and predicate(self):
|
||||||
yield self
|
yield self
|
||||||
for child in self:
|
for child in self:
|
||||||
for found in child.findall(predicate, include_self=True):
|
yield from child.findall(predicate, include_self=True)
|
||||||
yield found
|
|
||||||
|
|
||||||
def get_node(self, index_path):
|
def get_node(self, index_path):
|
||||||
"""Returns the node at ``index_path``.
|
"""Returns the node at ``index_path``.
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Callable, Generator, Iterator, List, Union
|
||||||
|
|
||||||
|
|
||||||
class JobCancelled(Exception):
|
class JobCancelled(Exception):
|
||||||
"The user has cancelled the job"
|
"The user has cancelled the job"
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class Job:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# ---Magic functions
|
# ---Magic functions
|
||||||
def __init__(self, job_proportions, callback):
|
def __init__(self, job_proportions: Union[List[int], int], callback: Callable) -> None:
|
||||||
"""Initialize the Job with 'jobcount' jobs. Start every job with
|
"""Initialize the Job with 'jobcount' jobs. Start every job with
|
||||||
start_job(). Every time the job progress is updated, 'callback' is called
|
start_job(). Every time the job progress is updated, 'callback' is called
|
||||||
'callback' takes a 'progress' int param, and a optional 'desc'
|
'callback' takes a 'progress' int param, and a optional 'desc'
|
||||||
@@ -55,12 +58,12 @@ class Job:
|
|||||||
self._currmax = 1
|
self._currmax = 1
|
||||||
|
|
||||||
# ---Private
|
# ---Private
|
||||||
def _subjob_callback(self, progress, desc=""):
|
def _subjob_callback(self, progress: int, desc: str = "") -> bool:
|
||||||
"""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
|
||||||
|
|
||||||
def _do_update(self, desc):
|
def _do_update(self, desc: str) -> None:
|
||||||
"""Calls the callback function with a % progress as a parameter.
|
"""Calls the callback function with a % progress as a parameter.
|
||||||
|
|
||||||
The parameter is a int in the 0-100 range.
|
The parameter is a int in the 0-100 range.
|
||||||
@@ -78,13 +81,16 @@ class Job:
|
|||||||
raise JobCancelled()
|
raise JobCancelled()
|
||||||
|
|
||||||
# ---Public
|
# ---Public
|
||||||
def add_progress(self, progress=1, desc=""):
|
def add_progress(self, progress: int = 1, desc: str = "") -> None:
|
||||||
self.set_progress(self._progress + progress, desc)
|
self.set_progress(self._progress + progress, desc)
|
||||||
|
|
||||||
def check_if_cancelled(self):
|
def check_if_cancelled(self) -> None:
|
||||||
self._do_update("")
|
self._do_update("")
|
||||||
|
|
||||||
def iter_with_progress(self, iterable, desc_format=None, every=1, count=None):
|
# TODO type hint iterable
|
||||||
|
def iter_with_progress(
|
||||||
|
self, iterable, desc_format: Union[str, None] = None, every: int = 1, count: Union[int, None] = None
|
||||||
|
) -> Generator[Any, None, None]:
|
||||||
"""Iterate through ``iterable`` while automatically adding progress.
|
"""Iterate through ``iterable`` while automatically adding progress.
|
||||||
|
|
||||||
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
|
WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,
|
||||||
@@ -107,7 +113,7 @@ class Job:
|
|||||||
desc = desc_format % (count, count)
|
desc = desc_format % (count, count)
|
||||||
self.set_progress(100, desc)
|
self.set_progress(100, desc)
|
||||||
|
|
||||||
def start_job(self, max_progress=100, desc=""):
|
def start_job(self, max_progress: int = 100, desc: str = "") -> None:
|
||||||
"""Begin work on the next job. You must not call start_job more than
|
"""Begin work on the next job. You must not call start_job more than
|
||||||
'jobcount' (in __init__) times.
|
'jobcount' (in __init__) times.
|
||||||
'max' is the job units you are to perform.
|
'max' is the job units you are to perform.
|
||||||
@@ -122,7 +128,7 @@ class Job:
|
|||||||
self._currmax = max(1, max_progress)
|
self._currmax = max(1, max_progress)
|
||||||
self._do_update(desc)
|
self._do_update(desc)
|
||||||
|
|
||||||
def start_subjob(self, job_proportions, desc=""):
|
def start_subjob(self, job_proportions: Union[List[int], int], desc: str = "") -> "Job":
|
||||||
"""Starts a sub job. Use this when you want to split a job into
|
"""Starts a sub job. Use this when you want to split a job into
|
||||||
multiple smaller jobs. Pretty handy when starting a process where you
|
multiple smaller jobs. Pretty handy when starting a process where you
|
||||||
know how many subjobs you will have, but don't know the work unit count
|
know how many subjobs you will have, but don't know the work unit count
|
||||||
@@ -132,7 +138,7 @@ class Job:
|
|||||||
self.start_job(100, desc)
|
self.start_job(100, desc)
|
||||||
return Job(job_proportions, self._subjob_callback)
|
return Job(job_proportions, self._subjob_callback)
|
||||||
|
|
||||||
def set_progress(self, progress, desc=""):
|
def set_progress(self, progress: int, desc: str = "") -> None:
|
||||||
"""Sets the progress of the current job to 'progress', and call the
|
"""Sets the progress of the current job to 'progress', and call the
|
||||||
callback
|
callback
|
||||||
"""
|
"""
|
||||||
@@ -143,29 +149,29 @@ class Job:
|
|||||||
|
|
||||||
|
|
||||||
class NullJob:
|
class NullJob:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_progress(self, *args, **kwargs):
|
def add_progress(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_if_cancelled(self):
|
def check_if_cancelled(self) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
def iter_with_progress(self, sequence, *args, **kwargs) -> Iterator:
|
||||||
return iter(sequence)
|
return iter(sequence)
|
||||||
|
|
||||||
def start_job(self, *args, **kwargs):
|
def start_job(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_subjob(self, *args, **kwargs):
|
def start_subjob(self, *args, **kwargs) -> "NullJob":
|
||||||
return NullJob()
|
return NullJob()
|
||||||
|
|
||||||
def set_progress(self, *args, **kwargs):
|
def set_progress(self, *args, **kwargs) -> None:
|
||||||
# Null job does nothing
|
# Null job does nothing
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Callable, Tuple, Union
|
||||||
|
|
||||||
from .job import Job, JobInProgressError, JobCancelled
|
from hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled
|
||||||
|
|
||||||
|
|
||||||
class ThreadedJobPerformer:
|
class ThreadedJobPerformer:
|
||||||
@@ -28,15 +29,15 @@ class ThreadedJobPerformer:
|
|||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
# --- Protected
|
# --- Protected
|
||||||
def create_job(self):
|
def create_job(self) -> Job:
|
||||||
if self._job_running:
|
if self._job_running:
|
||||||
raise JobInProgressError()
|
raise JobInProgressError()
|
||||||
self.last_progress = -1
|
self.last_progress: Union[int, None] = -1
|
||||||
self.last_desc = ""
|
self.last_desc = ""
|
||||||
self.job_cancelled = False
|
self.job_cancelled = False
|
||||||
return Job(1, self._update_progress)
|
return Job(1, self._update_progress)
|
||||||
|
|
||||||
def _async_run(self, *args):
|
def _async_run(self, *args) -> None:
|
||||||
target = args[0]
|
target = args[0]
|
||||||
args = tuple(args[1:])
|
args = tuple(args[1:])
|
||||||
self._job_running = True
|
self._job_running = True
|
||||||
@@ -52,7 +53,7 @@ class ThreadedJobPerformer:
|
|||||||
self._job_running = False
|
self._job_running = False
|
||||||
self.last_progress = None
|
self.last_progress = None
|
||||||
|
|
||||||
def reraise_if_error(self):
|
def reraise_if_error(self) -> None:
|
||||||
"""Reraises the error that happened in the thread if any.
|
"""Reraises the error that happened in the thread if any.
|
||||||
|
|
||||||
Call this after the caller of run_threaded detected that self._job_running returned to False
|
Call this after the caller of run_threaded detected that self._job_running returned to False
|
||||||
@@ -60,13 +61,13 @@ class ThreadedJobPerformer:
|
|||||||
if self.last_error is not None:
|
if self.last_error is not None:
|
||||||
raise self.last_error.with_traceback(self.last_traceback)
|
raise self.last_error.with_traceback(self.last_traceback)
|
||||||
|
|
||||||
def _update_progress(self, newprogress, newdesc=""):
|
def _update_progress(self, newprogress: int, newdesc: str = "") -> bool:
|
||||||
self.last_progress = newprogress
|
self.last_progress = newprogress
|
||||||
if newdesc:
|
if newdesc:
|
||||||
self.last_desc = newdesc
|
self.last_desc = newdesc
|
||||||
return not self.job_cancelled
|
return not self.job_cancelled
|
||||||
|
|
||||||
def run_threaded(self, target, args=()):
|
def run_threaded(self, target: Callable, args: Tuple = ()) -> None:
|
||||||
if self._job_running:
|
if self._job_running:
|
||||||
raise JobInProgressError()
|
raise JobInProgressError()
|
||||||
args = (target,) + args
|
args = (target,) + args
|
||||||
|
|||||||
@@ -2,34 +2,24 @@ import os
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
import polib
|
import polib
|
||||||
|
|
||||||
from . import pygettext
|
from hscommon import pygettext
|
||||||
|
|
||||||
LC_MESSAGES = "LC_MESSAGES"
|
LC_MESSAGES = "LC_MESSAGES"
|
||||||
|
|
||||||
# There isn't a 1-on-1 exact fit between .po language codes and cocoa ones
|
|
||||||
PO2COCOA = {
|
|
||||||
"pl_PL": "pl",
|
|
||||||
"pt_BR": "pt-BR",
|
|
||||||
"zh_CN": "zh-Hans",
|
|
||||||
}
|
|
||||||
|
|
||||||
COCOA2PO = {v: k for k, v in PO2COCOA.items()}
|
def get_langs(folder: str) -> List[str]:
|
||||||
|
|
||||||
STRING_EXT = ".strings"
|
|
||||||
|
|
||||||
|
|
||||||
def get_langs(folder):
|
|
||||||
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
|
return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]
|
||||||
|
|
||||||
|
|
||||||
def files_with_ext(folder, ext):
|
def files_with_ext(folder: str, ext: str) -> List[str]:
|
||||||
return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)]
|
return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)]
|
||||||
|
|
||||||
|
|
||||||
def generate_pot(folders, outpath, keywords, merge=False):
|
def generate_pot(folders: List[str], outpath: str, keywords: Any, merge: bool = False) -> None:
|
||||||
if merge and not op.exists(outpath):
|
if merge and not op.exists(outpath):
|
||||||
merge = False
|
merge = False
|
||||||
if merge:
|
if merge:
|
||||||
@@ -50,7 +40,7 @@ def generate_pot(folders, outpath, keywords, merge=False):
|
|||||||
print("Exception while removing temporary folder %s\n", genpath)
|
print("Exception while removing temporary folder %s\n", genpath)
|
||||||
|
|
||||||
|
|
||||||
def compile_all_po(base_folder):
|
def compile_all_po(base_folder: str) -> None:
|
||||||
langs = get_langs(base_folder)
|
langs = get_langs(base_folder)
|
||||||
for lang in langs:
|
for lang in langs:
|
||||||
pofolder = op.join(base_folder, lang, LC_MESSAGES)
|
pofolder = op.join(base_folder, lang, LC_MESSAGES)
|
||||||
@@ -60,7 +50,7 @@ def compile_all_po(base_folder):
|
|||||||
p.save_as_mofile(pofile[:-3] + ".mo")
|
p.save_as_mofile(pofile[:-3] + ".mo")
|
||||||
|
|
||||||
|
|
||||||
def merge_locale_dir(target, mergeinto):
|
def merge_locale_dir(target: str, mergeinto: str) -> None:
|
||||||
langs = get_langs(target)
|
langs = get_langs(target)
|
||||||
for lang in langs:
|
for lang in langs:
|
||||||
if not op.exists(op.join(mergeinto, lang)):
|
if not op.exists(op.join(mergeinto, lang)):
|
||||||
@@ -71,7 +61,7 @@ def merge_locale_dir(target, mergeinto):
|
|||||||
shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES))
|
shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES))
|
||||||
|
|
||||||
|
|
||||||
def merge_pots_into_pos(folder):
|
def merge_pots_into_pos(folder: str) -> None:
|
||||||
# We're going to take all pot files in `folder` and for each lang, merge it with the po file
|
# We're going to take all pot files in `folder` and for each lang, merge it with the po file
|
||||||
# with the same name.
|
# with the same name.
|
||||||
potfiles = files_with_ext(folder, ".pot")
|
potfiles = files_with_ext(folder, ".pot")
|
||||||
@@ -84,7 +74,7 @@ def merge_pots_into_pos(folder):
|
|||||||
po.save()
|
po.save()
|
||||||
|
|
||||||
|
|
||||||
def merge_po_and_preserve(source, dest):
|
def merge_po_and_preserve(source: str, dest: str) -> None:
|
||||||
# Merges source entries into dest, but keep old entries intact
|
# Merges source entries into dest, but keep old entries intact
|
||||||
sourcepo = polib.pofile(source)
|
sourcepo = polib.pofile(source)
|
||||||
destpo = polib.pofile(dest)
|
destpo = polib.pofile(dest)
|
||||||
@@ -96,7 +86,7 @@ def merge_po_and_preserve(source, dest):
|
|||||||
destpo.save()
|
destpo.save()
|
||||||
|
|
||||||
|
|
||||||
def normalize_all_pos(base_folder):
|
def normalize_all_pos(base_folder: str) -> None:
|
||||||
"""Normalize the format of .po files in base_folder.
|
"""Normalize the format of .po files in base_folder.
|
||||||
|
|
||||||
When getting POs from external sources, such as Transifex, we end up with spurious diffs because
|
When getting POs from external sources, such as Transifex, we end up with spurious diffs because
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ the method with the same name as the broadcasted message is called on the listen
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Callable, DefaultDict, List
|
||||||
|
|
||||||
|
|
||||||
class Broadcaster:
|
class Broadcaster:
|
||||||
@@ -21,10 +22,10 @@ class Broadcaster:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.listeners = set()
|
self.listeners = set()
|
||||||
|
|
||||||
def add_listener(self, listener):
|
def add_listener(self, listener: "Listener") -> None:
|
||||||
self.listeners.add(listener)
|
self.listeners.add(listener)
|
||||||
|
|
||||||
def notify(self, msg):
|
def notify(self, msg: str) -> None:
|
||||||
"""Notify all connected listeners of ``msg``.
|
"""Notify all connected listeners of ``msg``.
|
||||||
|
|
||||||
That means that each listeners will have their method with the same name as ``msg`` called.
|
That means that each listeners will have their method with the same name as ``msg`` called.
|
||||||
@@ -33,18 +34,18 @@ class Broadcaster:
|
|||||||
if listener in self.listeners: # disconnected during notification
|
if listener in self.listeners: # disconnected during notification
|
||||||
listener.dispatch(msg)
|
listener.dispatch(msg)
|
||||||
|
|
||||||
def remove_listener(self, listener):
|
def remove_listener(self, listener: "Listener") -> None:
|
||||||
self.listeners.discard(listener)
|
self.listeners.discard(listener)
|
||||||
|
|
||||||
|
|
||||||
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: Broadcaster) -> None:
|
||||||
self.broadcaster = broadcaster
|
self.broadcaster = broadcaster
|
||||||
self._bound_notifications = defaultdict(list)
|
self._bound_notifications: DefaultDict[str, List[Callable]] = defaultdict(list)
|
||||||
|
|
||||||
def bind_messages(self, messages, func):
|
def bind_messages(self, messages: str, func: Callable) -> None:
|
||||||
"""Binds multiple message to the same function.
|
"""Binds multiple message to the same function.
|
||||||
|
|
||||||
Often, we perform the same thing on multiple messages. Instead of having the same function
|
Often, we perform the same thing on multiple messages. Instead of having the same function
|
||||||
@@ -54,15 +55,15 @@ class Listener:
|
|||||||
for message in messages:
|
for message in messages:
|
||||||
self._bound_notifications[message].append(func)
|
self._bound_notifications[message].append(func)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self) -> None:
|
||||||
"""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) -> None:
|
||||||
"""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: str) -> None:
|
||||||
if msg in self._bound_notifications:
|
if msg in self._bound_notifications:
|
||||||
for func in self._bound_notifications[msg]:
|
for func in self._bound_notifications[msg]:
|
||||||
func()
|
func()
|
||||||
@@ -74,14 +75,14 @@ class Listener:
|
|||||||
class Repeater(Broadcaster, Listener):
|
class Repeater(Broadcaster, Listener):
|
||||||
REPEATED_NOTIFICATIONS = None
|
REPEATED_NOTIFICATIONS = None
|
||||||
|
|
||||||
def __init__(self, broadcaster):
|
def __init__(self, broadcaster: Broadcaster) -> None:
|
||||||
Broadcaster.__init__(self)
|
Broadcaster.__init__(self)
|
||||||
Listener.__init__(self, broadcaster)
|
Listener.__init__(self, broadcaster)
|
||||||
|
|
||||||
def _repeat_message(self, msg):
|
def _repeat_message(self, msg: str) -> None:
|
||||||
if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS:
|
if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS:
|
||||||
self.notify(msg)
|
self.notify(msg)
|
||||||
|
|
||||||
def dispatch(self, msg):
|
def dispatch(self, msg: str) -> None:
|
||||||
Listener.dispatch(self, msg)
|
Listener.dispatch(self, msg)
|
||||||
self._repeat_message(msg)
|
self._repeat_message(msg)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def log_io_error(func):
|
|||||||
def wrapper(path, *args, **kwargs):
|
def wrapper(path, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return func(path, *args, **kwargs)
|
return func(path, *args, **kwargs)
|
||||||
except (IOError, OSError) as e:
|
except OSError as e:
|
||||||
msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"'
|
msg = 'Error "{0}" during operation "{1}" on "{2}": "{3}"'
|
||||||
classname = e.__class__.__name__
|
classname = e.__class__.__name__
|
||||||
funcname = func.__name__
|
funcname = func.__name__
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ def main(source_files, outpath, keywords=None):
|
|||||||
fp = open(options.excludefilename, encoding="utf-8")
|
fp = open(options.excludefilename, encoding="utf-8")
|
||||||
options.toexclude = fp.readlines()
|
options.toexclude = fp.readlines()
|
||||||
fp.close()
|
fp.close()
|
||||||
except IOError:
|
except OSError:
|
||||||
print(
|
print(
|
||||||
"Can't read --exclude-file: %s" % options.excludefilename,
|
"Can't read --exclude-file: %s" % options.excludefilename,
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
from typing import Callable, Dict, Union
|
||||||
|
|
||||||
from .build import read_changelog_file, filereplace
|
from hscommon.build import read_changelog_file, filereplace
|
||||||
from sphinx.cmd.build import build_main as sphinx_build
|
from sphinx.cmd.build import build_main as sphinx_build
|
||||||
|
|
||||||
CHANGELOG_FORMAT = """
|
CHANGELOG_FORMAT = """
|
||||||
@@ -18,25 +19,25 @@ CHANGELOG_FORMAT = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def tixgen(tixurl):
|
def tixgen(tixurl: str) -> Callable[[str], str]:
|
||||||
"""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("\\1") # will be replaced buy the content of the first group in re
|
urlpattern = tixurl.format("\\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 = f"`#\\1 <{urlpattern}>`__"
|
||||||
return lambda text: R.sub(repl, text)
|
return lambda text: R.sub(repl, text)
|
||||||
|
|
||||||
|
|
||||||
def gen(
|
def gen(
|
||||||
basepath,
|
basepath: Path,
|
||||||
destpath,
|
destpath: Path,
|
||||||
changelogpath,
|
changelogpath: Path,
|
||||||
tixurl,
|
tixurl: str,
|
||||||
confrepl=None,
|
confrepl: Union[Dict[str, str], None] = None,
|
||||||
confpath=None,
|
confpath: Union[Path, None] = None,
|
||||||
changelogtmpl=None,
|
changelogtmpl: Union[Path, None] = None,
|
||||||
):
|
) -> None:
|
||||||
"""Generate sphinx docs with all bells and whistles.
|
"""Generate sphinx docs with all bells and whistles.
|
||||||
|
|
||||||
basepath: The base sphinx source path.
|
basepath: The base sphinx source path.
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2007/05/19
|
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
|
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
||||||
# which should be included with this package. The terms are also available at
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import os
|
|
||||||
import os.path as op
|
|
||||||
import threading
|
|
||||||
from queue import Queue
|
|
||||||
import sqlite3 as sqlite
|
|
||||||
|
|
||||||
STOP = object()
|
|
||||||
COMMIT = object()
|
|
||||||
ROLLBACK = object()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeCursor(list):
|
|
||||||
# It's not possible to use sqlite cursors on another thread than the connection. Thus,
|
|
||||||
# we can't directly return the cursor. We have to fatch all results, and support its interface.
|
|
||||||
def fetchall(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def fetchone(self):
|
|
||||||
try:
|
|
||||||
return self.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class _ActualThread(threading.Thread):
|
|
||||||
"""We can't use this class directly because thread object are not automatically freed when
|
|
||||||
nothing refers to it, making it hang the application if not explicitely closed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, dbname, autocommit):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self._queries = Queue()
|
|
||||||
self._results = Queue()
|
|
||||||
self._dbname = dbname
|
|
||||||
self._autocommit = autocommit
|
|
||||||
self._waiting_list = set()
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self._run = True
|
|
||||||
self.lastrowid = -1
|
|
||||||
self.daemon = True
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def _query(self, query):
|
|
||||||
with self._lock:
|
|
||||||
wait_token = object()
|
|
||||||
self._waiting_list.add(wait_token)
|
|
||||||
self._queries.put(query)
|
|
||||||
self._waiting_list.remove(wait_token)
|
|
||||||
result = self._results.get()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if not self._run:
|
|
||||||
return
|
|
||||||
self._query(STOP)
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
if not self._run:
|
|
||||||
return None # Connection closed
|
|
||||||
self._query(COMMIT)
|
|
||||||
|
|
||||||
def execute(self, sql, values=()):
|
|
||||||
if not self._run:
|
|
||||||
return None # Connection closed
|
|
||||||
result = self._query((sql, values))
|
|
||||||
if isinstance(result, Exception):
|
|
||||||
raise result
|
|
||||||
return result
|
|
||||||
|
|
||||||
def rollback(self):
|
|
||||||
if not self._run:
|
|
||||||
return None # Connection closed
|
|
||||||
self._query(ROLLBACK)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# The whole chdir thing is because sqlite doesn't handle directory names with non-asci char in the AT ALL.
|
|
||||||
oldpath = os.getcwd()
|
|
||||||
dbdir, dbname = op.split(self._dbname)
|
|
||||||
if dbdir:
|
|
||||||
os.chdir(dbdir)
|
|
||||||
if self._autocommit:
|
|
||||||
con = sqlite.connect(dbname, isolation_level=None)
|
|
||||||
else:
|
|
||||||
con = sqlite.connect(dbname)
|
|
||||||
os.chdir(oldpath)
|
|
||||||
while self._run or self._waiting_list:
|
|
||||||
query = self._queries.get()
|
|
||||||
result = None
|
|
||||||
if query is STOP:
|
|
||||||
self._run = False
|
|
||||||
elif query is COMMIT:
|
|
||||||
con.commit()
|
|
||||||
elif query is ROLLBACK:
|
|
||||||
con.rollback()
|
|
||||||
else:
|
|
||||||
sql, values = query
|
|
||||||
try:
|
|
||||||
cur = con.execute(sql, values)
|
|
||||||
self.lastrowid = cur.lastrowid
|
|
||||||
result = FakeCursor(cur.fetchall())
|
|
||||||
result.lastrowid = cur.lastrowid
|
|
||||||
except Exception as e:
|
|
||||||
result = e
|
|
||||||
self._results.put(result)
|
|
||||||
con.close()
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadedConn:
|
|
||||||
"""``sqlite`` connections can't be used across threads. ``TheadedConn`` opens a sqlite
|
|
||||||
connection in its own thread and sends it queries through a queue, making it suitable in
|
|
||||||
multi-threaded environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, dbname, autocommit):
|
|
||||||
self._t = _ActualThread(dbname, autocommit)
|
|
||||||
self.lastrowid = -1
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._t.close()
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
self._t.commit()
|
|
||||||
|
|
||||||
def execute(self, sql, values=()):
|
|
||||||
result = self._t.execute(sql, values)
|
|
||||||
self.lastrowid = self._t.lastrowid
|
|
||||||
return result
|
|
||||||
|
|
||||||
def rollback(self):
|
|
||||||
self._t.rollback()
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..conflict import (
|
from hscommon.conflict import (
|
||||||
get_conflicted_name,
|
get_conflicted_name,
|
||||||
get_unconflicted_name,
|
get_unconflicted_name,
|
||||||
is_conflicted,
|
is_conflicted,
|
||||||
@@ -16,7 +16,7 @@ from ..conflict import (
|
|||||||
smart_move,
|
smart_move,
|
||||||
)
|
)
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGetConflictedName:
|
class TestCaseGetConflictedName:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# 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 ..testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from ..notify import Broadcaster, Listener, Repeater
|
from hscommon.notify import Broadcaster, Listener, Repeater
|
||||||
|
|
||||||
|
|
||||||
class HelloListener(Listener):
|
class HelloListener(Listener):
|
||||||
@@ -113,7 +113,7 @@ def test_repeater_with_repeated_notifications():
|
|||||||
# If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're
|
# If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're
|
||||||
# still dispatched locally).
|
# still dispatched locally).
|
||||||
class MyRepeater(HelloRepeater):
|
class MyRepeater(HelloRepeater):
|
||||||
REPEATED_NOTIFICATIONS = set(["hello"])
|
REPEATED_NOTIFICATIONS = {"hello"}
|
||||||
|
|
||||||
def __init__(self, broadcaster):
|
def __init__(self, broadcaster):
|
||||||
HelloRepeater.__init__(self, broadcaster)
|
HelloRepeater.__init__(self, broadcaster)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 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 ..path import pathify
|
from hscommon.path import pathify
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
# 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 ..testutil import eq_, callcounter, CallLogger
|
from hscommon.testutil import eq_, callcounter, CallLogger
|
||||||
from ..gui.selectable_list import SelectableList, GUISelectableList
|
from hscommon.gui.selectable_list import SelectableList, GUISelectableList
|
||||||
|
|
||||||
|
|
||||||
def test_in():
|
def test_in():
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
# Created By: Virgil Dupras
|
|
||||||
# Created On: 2007/05/19
|
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
|
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
||||||
# which should be included with this package. The terms are also available at
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
import sqlite3 as sqlite
|
|
||||||
|
|
||||||
from pytest import raises
|
|
||||||
|
|
||||||
from ..testutil import eq_
|
|
||||||
from ..sqlite import ThreadedConn
|
|
||||||
|
|
||||||
# Threading is hard to test. In a lot of those tests, a failure means that the test run will
|
|
||||||
# hang forever. Well... I don't know a better alternative.
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_access_from_multiple_threads():
|
|
||||||
def run():
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
t = threading.Thread(target=run)
|
|
||||||
t.start()
|
|
||||||
t.join()
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
eq_(1, len(result))
|
|
||||||
eq_("baz", result[0][0])
|
|
||||||
|
|
||||||
|
|
||||||
def test_exception_during_query():
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
with raises(sqlite.OperationalError):
|
|
||||||
con.execute("select * from bleh")
|
|
||||||
|
|
||||||
|
|
||||||
def test_not_autocommit(tmpdir):
|
|
||||||
dbpath = str(tmpdir.join("foo.db"))
|
|
||||||
con = ThreadedConn(dbpath, False)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
del con
|
|
||||||
# The data shouldn't have been inserted
|
|
||||||
con = ThreadedConn(dbpath, False)
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
eq_(0, len(result))
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
con.commit()
|
|
||||||
del con
|
|
||||||
# Now the data should be there
|
|
||||||
con = ThreadedConn(dbpath, False)
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
eq_(1, len(result))
|
|
||||||
|
|
||||||
|
|
||||||
def test_rollback():
|
|
||||||
con = ThreadedConn(":memory:", False)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
con.rollback()
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
eq_(0, len(result))
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_palceholders():
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
con.execute("insert into foo(bar) values(?)", ["baz"])
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
eq_(1, len(result))
|
|
||||||
eq_("baz", result[0][0])
|
|
||||||
|
|
||||||
|
|
||||||
def test_make_sure_theres_no_messup_between_queries():
|
|
||||||
def run(expected_rowid):
|
|
||||||
time.sleep(0.1)
|
|
||||||
result = con.execute("select rowid from foo where rowid = ?", [expected_rowid])
|
|
||||||
assert expected_rowid == result[0][0]
|
|
||||||
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
for i in range(100):
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
threads = []
|
|
||||||
for i in range(1, 101):
|
|
||||||
t = threading.Thread(target=run, args=(i,))
|
|
||||||
t.start()
|
|
||||||
threads.append(t)
|
|
||||||
while threads:
|
|
||||||
time.sleep(0.1)
|
|
||||||
threads = [t for t in threads if t.is_alive()]
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_after_close():
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.close()
|
|
||||||
con.execute("select 1")
|
|
||||||
|
|
||||||
|
|
||||||
def test_lastrowid():
|
|
||||||
# It's not possible to return a cursor because of the threading, but lastrowid should be
|
|
||||||
# fetchable from the connection itself
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
con.execute("insert into foo(bar) values('baz')")
|
|
||||||
eq_(1, con.lastrowid)
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_fetchone_fetchall_interface_to_results():
|
|
||||||
con = ThreadedConn(":memory:", True)
|
|
||||||
con.execute("create table foo(bar TEXT)")
|
|
||||||
con.execute("insert into foo(bar) values('baz1')")
|
|
||||||
con.execute("insert into foo(bar) values('baz2')")
|
|
||||||
result = con.execute("select * from foo")
|
|
||||||
ref = result[:]
|
|
||||||
eq_(ref, result.fetchall())
|
|
||||||
eq_(ref[0], result.fetchone())
|
|
||||||
eq_(ref[1], result.fetchone())
|
|
||||||
assert result.fetchone() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii_dbname(tmpdir):
|
|
||||||
ThreadedConn(str(tmpdir.join("foo\u00e9.db")), True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii_dbdir(tmpdir):
|
|
||||||
# when this test fails, it doesn't fail gracefully, it brings the whole test suite with it.
|
|
||||||
dbdir = tmpdir.join("foo\u00e9")
|
|
||||||
os.mkdir(str(dbdir))
|
|
||||||
ThreadedConn(str(dbdir.join("foo.db")), True)
|
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
# 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 ..testutil import CallLogger, eq_
|
from hscommon.testutil import CallLogger, eq_
|
||||||
from ..gui.table import Table, GUITable, Row
|
from hscommon.gui.table import Table, GUITable, Row
|
||||||
|
|
||||||
|
|
||||||
class TestRow(Row):
|
class TestRow(Row):
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
# 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 ..testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from ..gui.tree import Tree, Node
|
from hscommon.gui.tree import Tree, Node
|
||||||
|
|
||||||
|
|
||||||
def tree_with_some_nodes():
|
def tree_with_some_nodes():
|
||||||
@@ -98,7 +98,7 @@ def test_selection_override():
|
|||||||
def test_findall():
|
def test_findall():
|
||||||
t = tree_with_some_nodes()
|
t = tree_with_some_nodes()
|
||||||
r = t.findall(lambda n: n.name.startswith("sub"))
|
r = t.findall(lambda n: n.name.startswith("sub"))
|
||||||
eq_(set(r), set([t[0][0], t[0][1]]))
|
eq_(set(r), {t[0][0], t[0][1]})
|
||||||
|
|
||||||
|
|
||||||
def test_findall_dont_include_self():
|
def test_findall_dont_include_self():
|
||||||
@@ -106,7 +106,7 @@ def test_findall_dont_include_self():
|
|||||||
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(lambda n: not n.name.startswith("sub"), include_self=False) # no crash
|
r = t.findall(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), {t[0], t[1], t[2]})
|
||||||
|
|
||||||
|
|
||||||
def test_find_dont_include_self():
|
def test_find_dont_include_self():
|
||||||
|
|||||||
@@ -10,23 +10,19 @@ from io import StringIO
|
|||||||
|
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from ..testutil import eq_
|
from hscommon.testutil import eq_
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..util import (
|
from hscommon.util import (
|
||||||
nonone,
|
nonone,
|
||||||
tryint,
|
tryint,
|
||||||
minmax,
|
|
||||||
first,
|
first,
|
||||||
flatten,
|
flatten,
|
||||||
dedupe,
|
dedupe,
|
||||||
stripfalse,
|
|
||||||
extract,
|
extract,
|
||||||
allsame,
|
allsame,
|
||||||
trailiter,
|
|
||||||
format_time,
|
format_time,
|
||||||
format_time_decimal,
|
format_time_decimal,
|
||||||
format_size,
|
format_size,
|
||||||
remove_invalid_xml,
|
|
||||||
multi_replace,
|
multi_replace,
|
||||||
delete_if_empty,
|
delete_if_empty,
|
||||||
open_if_filename,
|
open_if_filename,
|
||||||
@@ -51,12 +47,6 @@ def test_tryint():
|
|||||||
eq_(42, tryint(None, 42))
|
eq_(42, tryint(None, 42))
|
||||||
|
|
||||||
|
|
||||||
def test_minmax():
|
|
||||||
eq_(minmax(2, 1, 3), 2)
|
|
||||||
eq_(minmax(0, 1, 3), 1)
|
|
||||||
eq_(minmax(4, 1, 3), 3)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Sequence
|
# --- Sequence
|
||||||
|
|
||||||
|
|
||||||
@@ -75,10 +65,6 @@ def test_dedupe():
|
|||||||
eq_(dedupe(reflist), [0, 7, 1, 2, 3, 4, 5, 6])
|
eq_(dedupe(reflist), [0, 7, 1, 2, 3, 4, 5, 6])
|
||||||
|
|
||||||
|
|
||||||
def test_stripfalse():
|
|
||||||
eq_([1, 2, 3], stripfalse([None, 0, 1, 2, 3, None]))
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract():
|
def test_extract():
|
||||||
wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10)))
|
wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10)))
|
||||||
eq_(wheat, [0, 2, 4, 6, 8])
|
eq_(wheat, [0, 2, 4, 6, 8])
|
||||||
@@ -93,14 +79,6 @@ def test_allsame():
|
|||||||
assert allsame(iter([42, 42, 42]))
|
assert allsame(iter([42, 42, 42]))
|
||||||
|
|
||||||
|
|
||||||
def test_trailiter():
|
|
||||||
eq_(list(trailiter([])), [])
|
|
||||||
eq_(list(trailiter(["foo"])), [(None, "foo")])
|
|
||||||
eq_(list(trailiter(["foo", "bar"])), [(None, "foo"), ("foo", "bar")])
|
|
||||||
eq_(list(trailiter(["foo", "bar"], skipfirst=True)), [("foo", "bar")])
|
|
||||||
eq_(list(trailiter([], skipfirst=True)), []) # no crash
|
|
||||||
|
|
||||||
|
|
||||||
def test_iterconsume():
|
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.
|
||||||
@@ -213,14 +191,6 @@ def test_format_size():
|
|||||||
eq_(format_size(999999999999999999999999), "848 ZB")
|
eq_(format_size(999999999999999999999999), "848 ZB")
|
||||||
|
|
||||||
|
|
||||||
def test_remove_invalid_xml():
|
|
||||||
eq_(remove_invalid_xml("foo\0bar\x0bbaz"), "foo bar baz")
|
|
||||||
# surrogate blocks have to be replaced, but not the rest
|
|
||||||
eq_(remove_invalid_xml("foo\ud800bar\udfffbaz\ue000"), "foo bar baz\ue000")
|
|
||||||
# replace with something else
|
|
||||||
eq_(remove_invalid_xml("foo\0baz", replace_with="bar"), "foobarbaz")
|
|
||||||
|
|
||||||
|
|
||||||
def test_multi_replace():
|
def test_multi_replace():
|
||||||
eq_("136", multi_replace("123456", ("2", "45")))
|
eq_("136", multi_replace("123456", ("2", "45")))
|
||||||
eq_("1 3 6", multi_replace("123456", ("2", "45"), " "))
|
eq_("1 3 6", multi_replace("123456", ("2", "45"), " "))
|
||||||
|
|||||||
@@ -8,26 +8,10 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import threading
|
|
||||||
import py.path
|
|
||||||
|
|
||||||
|
|
||||||
def eq_(a, b, msg=None):
|
def eq_(a, b, msg=None):
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
assert a == b, msg or "%r != %r" % (a, b)
|
assert a == b, msg or "{!r} != {!r}".format(a, b)
|
||||||
|
|
||||||
|
|
||||||
def eq_sorted(a, b, msg=None):
|
|
||||||
"""If both a and b are iterable sort them and compare using eq_, otherwise just pass them through to eq_ anyway."""
|
|
||||||
try:
|
|
||||||
eq_(sorted(a), sorted(b), msg)
|
|
||||||
except TypeError:
|
|
||||||
eq_(a, b, msg)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_almost_equal(a, b, places=7):
|
|
||||||
__tracebackhide__ = True
|
|
||||||
assert round(a, ndigits=places) == round(b, ndigits=places)
|
|
||||||
|
|
||||||
|
|
||||||
def callcounter():
|
def callcounter():
|
||||||
@@ -38,23 +22,6 @@ def callcounter():
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
class TestData:
|
|
||||||
def __init__(self, datadirpath):
|
|
||||||
self.datadirpath = py.path.local(datadirpath)
|
|
||||||
|
|
||||||
def filepath(self, relative_path, *args):
|
|
||||||
"""Returns the path of a file in testdata.
|
|
||||||
|
|
||||||
'relative_path' can be anything that can be added to a Path
|
|
||||||
if args is not empty, it will be joined to relative_path
|
|
||||||
"""
|
|
||||||
resultpath = self.datadirpath.join(relative_path)
|
|
||||||
if args:
|
|
||||||
resultpath = resultpath.join(*args)
|
|
||||||
assert resultpath.check()
|
|
||||||
return str(resultpath)
|
|
||||||
|
|
||||||
|
|
||||||
class CallLogger:
|
class CallLogger:
|
||||||
"""This is a dummy object that logs all calls made to it.
|
"""This is a dummy object that logs all calls made to it.
|
||||||
|
|
||||||
@@ -97,17 +64,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(not_called)
|
assert not not_called, f"These calls haven't been made: {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("The call {0} hasn't been made in the correct order".format(call))
|
raise AssertionError(f"The call {call} hasn't been made in the correct order")
|
||||||
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(called)
|
assert not called, f"These calls shouldn't have been made: {called}"
|
||||||
self.clear_calls()
|
self.clear_calls()
|
||||||
|
|
||||||
|
|
||||||
@@ -133,7 +100,7 @@ class TestApp:
|
|||||||
parent = self.default_parent
|
parent = self.default_parent
|
||||||
if holder is None:
|
if holder is None:
|
||||||
holder = self
|
holder = self
|
||||||
setattr(holder, "{0}_gui".format(name), view)
|
setattr(holder, f"{name}_gui", view)
|
||||||
gui = class_(parent)
|
gui = class_(parent)
|
||||||
gui.view = view
|
gui.view = view
|
||||||
setattr(holder, name, gui)
|
setattr(holder, name, gui)
|
||||||
@@ -168,20 +135,6 @@ def app(request):
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def jointhreads():
|
|
||||||
"""Join all threads to the main thread"""
|
|
||||||
for thread in threading.enumerate():
|
|
||||||
if hasattr(thread, "BUGGY"):
|
|
||||||
continue
|
|
||||||
if thread.getName() != "MainThread" and thread.isAlive():
|
|
||||||
if hasattr(thread, "close"):
|
|
||||||
thread.close()
|
|
||||||
thread.join(1)
|
|
||||||
if thread.isAlive():
|
|
||||||
print("Thread problem. Some thread doesn't want to stop.")
|
|
||||||
thread.BUGGY = True
|
|
||||||
|
|
||||||
|
|
||||||
def _unify_args(func, args, kwargs, args_to_ignore=None):
|
def _unify_args(func, args, kwargs, args_to_ignore=None):
|
||||||
"""Unify args and kwargs in the same dictionary.
|
"""Unify args and kwargs in the same dictionary.
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,18 @@
|
|||||||
|
|
||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import os.path as op
|
import os.path as op
|
||||||
|
from typing import Callable, Union
|
||||||
|
|
||||||
from .plat import ISLINUX
|
from hscommon.plat import ISLINUX
|
||||||
|
|
||||||
_trfunc = None
|
_trfunc = None
|
||||||
_trget = None
|
_trget = None
|
||||||
installed_lang = None
|
installed_lang = None
|
||||||
|
|
||||||
|
|
||||||
def tr(s, context=None):
|
def tr(s: str, context: Union[str, None] = None) -> str:
|
||||||
if _trfunc is None:
|
if _trfunc is None:
|
||||||
return s
|
return s
|
||||||
else:
|
else:
|
||||||
@@ -30,7 +32,7 @@ def tr(s, context=None):
|
|||||||
return _trfunc(s)
|
return _trfunc(s)
|
||||||
|
|
||||||
|
|
||||||
def trget(domain):
|
def trget(domain: str) -> Callable[[str], str]:
|
||||||
# Returns a tr() function for the specified domain.
|
# Returns a tr() function for the specified domain.
|
||||||
if _trget is None:
|
if _trget is None:
|
||||||
return lambda s: tr(s, domain)
|
return lambda s: tr(s, domain)
|
||||||
@@ -38,14 +40,16 @@ def trget(domain):
|
|||||||
return _trget(domain)
|
return _trget(domain)
|
||||||
|
|
||||||
|
|
||||||
def set_tr(new_tr, new_trget=None):
|
def set_tr(
|
||||||
|
new_tr: Callable[[str, Union[str, None]], str], new_trget: Union[Callable[[str], Callable[[str], str]], None] = None
|
||||||
|
) -> None:
|
||||||
global _trfunc, _trget
|
global _trfunc, _trget
|
||||||
_trfunc = new_tr
|
_trfunc = new_tr
|
||||||
if new_trget is not None:
|
if new_trget is not None:
|
||||||
_trget = new_trget
|
_trget = new_trget
|
||||||
|
|
||||||
|
|
||||||
def get_locale_name(lang):
|
def get_locale_name(lang: str) -> Union[str, None]:
|
||||||
# Removed old conversion code as windows seems to support these
|
# Removed old conversion code as windows seems to support these
|
||||||
LANG2LOCALENAME = {
|
LANG2LOCALENAME = {
|
||||||
"cs": "cs_CZ",
|
"cs": "cs_CZ",
|
||||||
@@ -77,7 +81,7 @@ def get_locale_name(lang):
|
|||||||
|
|
||||||
|
|
||||||
# --- Qt
|
# --- Qt
|
||||||
def install_qt_trans(lang=None):
|
def install_qt_trans(lang: str = None) -> None:
|
||||||
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale
|
from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale
|
||||||
|
|
||||||
if not lang:
|
if not lang:
|
||||||
@@ -97,27 +101,29 @@ def install_qt_trans(lang=None):
|
|||||||
qtr2.load(":/%s" % lang)
|
qtr2.load(":/%s" % lang)
|
||||||
QCoreApplication.installTranslator(qtr2)
|
QCoreApplication.installTranslator(qtr2)
|
||||||
|
|
||||||
def qt_tr(s, context="core"):
|
def qt_tr(s: str, context: Union[str, None] = "core") -> str:
|
||||||
|
if context is None:
|
||||||
|
context = "core"
|
||||||
return str(QCoreApplication.translate(context, s, None))
|
return str(QCoreApplication.translate(context, s, None))
|
||||||
|
|
||||||
set_tr(qt_tr)
|
set_tr(qt_tr)
|
||||||
|
|
||||||
|
|
||||||
# --- gettext
|
# --- gettext
|
||||||
def install_gettext_trans(base_folder, lang):
|
def install_gettext_trans(base_folder: os.PathLike, lang: str) -> None:
|
||||||
import gettext
|
import gettext
|
||||||
|
|
||||||
def gettext_trget(domain):
|
def gettext_trget(domain: str) -> Callable[[str], str]:
|
||||||
if not lang:
|
if not lang:
|
||||||
return lambda s: s
|
return lambda s: s
|
||||||
try:
|
try:
|
||||||
return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
|
return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
|
||||||
except IOError:
|
except OSError:
|
||||||
return lambda s: s
|
return lambda s: s
|
||||||
|
|
||||||
default_gettext = gettext_trget("core")
|
default_gettext = gettext_trget("core")
|
||||||
|
|
||||||
def gettext_tr(s, context=None):
|
def gettext_tr(s: str, context: Union[str, None] = None) -> str:
|
||||||
if not context:
|
if not context:
|
||||||
return default_gettext(s)
|
return default_gettext(s)
|
||||||
else:
|
else:
|
||||||
@@ -129,7 +135,7 @@ def install_gettext_trans(base_folder, lang):
|
|||||||
installed_lang = lang
|
installed_lang = lang
|
||||||
|
|
||||||
|
|
||||||
def install_gettext_trans_under_qt(base_folder, lang=None):
|
def install_gettext_trans_under_qt(base_folder: os.PathLike, lang: str = None) -> None:
|
||||||
# So, we install the gettext locale, great, but we also should try to install qt_*.qm if
|
# So, we install the gettext locale, great, but we also should try to install qt_*.qm if
|
||||||
# available so that strings that are inside Qt itself over which I have no control are in the
|
# available so that strings that are inside Qt itself over which I have no control are in the
|
||||||
# right language.
|
# right language.
|
||||||
|
|||||||
176
hscommon/util.py
176
hscommon/util.py
@@ -6,20 +6,14 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import os.path as op
|
|
||||||
import re
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
import glob
|
|
||||||
import shutil
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .path import pathify, log_io_error
|
from hscommon.path import pathify, log_io_error
|
||||||
|
|
||||||
|
from typing import IO, Any, Callable, Generator, Iterable, List, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
def nonone(value, replace_value):
|
def nonone(value: Any, replace_value: Any) -> Any:
|
||||||
"""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
|
||||||
@@ -27,7 +21,7 @@ def nonone(value, replace_value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def tryint(value, default=0):
|
def tryint(value: Any, default: int = 0) -> int:
|
||||||
"""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)
|
||||||
@@ -35,15 +29,10 @@ def tryint(value, default=0):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def minmax(value, min_value, max_value):
|
|
||||||
"""Returns `value` or one of the min/max bounds if `value` is not between them."""
|
|
||||||
return min(max(value, min_value), max_value)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Sequence related
|
# --- Sequence related
|
||||||
|
|
||||||
|
|
||||||
def dedupe(iterable):
|
def dedupe(iterable: Iterable[Any]) -> List[Any]:
|
||||||
"""Returns a list of elements in ``iterable`` with all dupes removed.
|
"""Returns a list of elements in ``iterable`` with all dupes removed.
|
||||||
|
|
||||||
The order of the elements is preserved.
|
The order of the elements is preserved.
|
||||||
@@ -58,13 +47,13 @@ def dedupe(iterable):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def flatten(iterables, start_with=None):
|
def flatten(iterables: Iterable[Iterable], start_with: Iterable[Any] = None) -> List[Any]:
|
||||||
"""Takes a list of lists ``iterables`` and returns a list containing elements of every list.
|
"""Takes a list of lists ``iterables`` and returns a list containing elements of every list.
|
||||||
|
|
||||||
If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as
|
If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as
|
||||||
if ``start_with`` would be the first item of lists.
|
if ``start_with`` would be the first item of lists.
|
||||||
"""
|
"""
|
||||||
result = []
|
result: List[Any] = []
|
||||||
if start_with:
|
if start_with:
|
||||||
result.extend(start_with)
|
result.extend(start_with)
|
||||||
for iterable in iterables:
|
for iterable in iterables:
|
||||||
@@ -72,7 +61,7 @@ def flatten(iterables, start_with=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def first(iterable):
|
def first(iterable: Iterable[Any]):
|
||||||
"""Returns the first item of ``iterable``."""
|
"""Returns the first item of ``iterable``."""
|
||||||
try:
|
try:
|
||||||
return next(iter(iterable))
|
return next(iter(iterable))
|
||||||
@@ -80,12 +69,7 @@ def first(iterable):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def stripfalse(seq):
|
def extract(predicate: Callable[[Any], bool], iterable: Iterable[Any]) -> Tuple[List[Any], List[Any]]:
|
||||||
"""Returns a sequence with all false elements stripped out of seq."""
|
|
||||||
return [x for x in seq if x]
|
|
||||||
|
|
||||||
|
|
||||||
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 = []
|
||||||
@@ -97,7 +81,7 @@ def extract(predicate, iterable):
|
|||||||
return wheat, shaft
|
return wheat, shaft
|
||||||
|
|
||||||
|
|
||||||
def allsame(iterable):
|
def allsame(iterable: Iterable[Any]) -> bool:
|
||||||
"""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:
|
||||||
@@ -107,26 +91,7 @@ def allsame(iterable):
|
|||||||
return all(element == first_item for element in it)
|
return all(element == first_item for element in it)
|
||||||
|
|
||||||
|
|
||||||
def trailiter(iterable, skipfirst=False):
|
def iterconsume(seq: List[Any], reverse: bool = True) -> Generator[Any, None, None]:
|
||||||
"""Yields (prev_element, element), starting with (None, first_element).
|
|
||||||
|
|
||||||
If skipfirst is True, there will be no (None, item1) element and we'll start
|
|
||||||
directly with (item1, item2).
|
|
||||||
"""
|
|
||||||
it = iter(iterable)
|
|
||||||
if skipfirst:
|
|
||||||
try:
|
|
||||||
prev = next(it)
|
|
||||||
except StopIteration:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
prev = None
|
|
||||||
for item in it:
|
|
||||||
yield prev, item
|
|
||||||
prev = item
|
|
||||||
|
|
||||||
|
|
||||||
def iterconsume(seq, reverse=True):
|
|
||||||
"""Iterate over ``seq`` and pops yielded objects.
|
"""Iterate over ``seq`` and pops yielded objects.
|
||||||
|
|
||||||
Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need
|
Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need
|
||||||
@@ -145,12 +110,12 @@ def iterconsume(seq, reverse=True):
|
|||||||
# --- String related
|
# --- String related
|
||||||
|
|
||||||
|
|
||||||
def escape(s, to_escape, escape_with="\\"):
|
def escape(s: str, to_escape: str, escape_with: str = "\\") -> str:
|
||||||
"""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: str) -> str:
|
||||||
"""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:
|
||||||
@@ -159,7 +124,7 @@ def get_file_ext(filename):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def rem_file_ext(filename):
|
def rem_file_ext(filename: str) -> str:
|
||||||
"""Returns the filename without extension."""
|
"""Returns the filename without extension."""
|
||||||
pos = filename.rfind(".")
|
pos = filename.rfind(".")
|
||||||
if pos > -1:
|
if pos > -1:
|
||||||
@@ -168,7 +133,8 @@ def rem_file_ext(filename):
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def pluralize(number, word, decimals=0, plural_word=None):
|
# TODO type hint number
|
||||||
|
def pluralize(number, word: str, decimals: int = 0, plural_word: Union[str, None] = None) -> str:
|
||||||
"""Returns a pluralized string with ``number`` in front of ``word``.
|
"""Returns a pluralized string with ``number`` in front of ``word``.
|
||||||
|
|
||||||
Adds a 's' to s if ``number`` > 1.
|
Adds a 's' to s if ``number`` > 1.
|
||||||
@@ -187,7 +153,7 @@ def pluralize(number, word, decimals=0, plural_word=None):
|
|||||||
return plural_format % (number, word)
|
return plural_format % (number, word)
|
||||||
|
|
||||||
|
|
||||||
def format_time(seconds, with_hours=True):
|
def format_time(seconds: int, with_hours: bool = True) -> str:
|
||||||
"""Transforms seconds in a hh:mm:ss string.
|
"""Transforms seconds in a hh:mm:ss string.
|
||||||
|
|
||||||
If ``with_hours`` if false, the format is mm:ss.
|
If ``with_hours`` if false, the format is mm:ss.
|
||||||
@@ -207,7 +173,7 @@ def format_time(seconds, with_hours=True):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def format_time_decimal(seconds):
|
def format_time_decimal(seconds: int) -> str:
|
||||||
"""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:
|
||||||
@@ -230,7 +196,7 @@ SIZE_DESC = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
|||||||
SIZE_VALS = tuple(1024**i for i in range(1, 9))
|
SIZE_VALS = tuple(1024**i for i in range(1, 9))
|
||||||
|
|
||||||
|
|
||||||
def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
def format_size(size: int, decimal: int = 0, forcepower: int = -1, showdesc: bool = True) -> str:
|
||||||
"""Transform a byte count in a formatted string (KB, MB etc..).
|
"""Transform a byte count in a formatted string (KB, MB etc..).
|
||||||
|
|
||||||
``size`` is the number of bytes to format.
|
``size`` is the number of bytes to format.
|
||||||
@@ -268,17 +234,7 @@ def format_size(size, decimal=0, forcepower=-1, showdesc=True):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
_valid_xml_range = "\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD"
|
def multi_replace(s: str, replace_from: Union[str, List[str]], replace_to: Union[str, List[str]] = "") -> str:
|
||||||
if sys.maxunicode > 0x10000:
|
|
||||||
_valid_xml_range += "%s-%s" % (chr(0x10000), chr(min(sys.maxunicode, 0x10FFFF)))
|
|
||||||
RE_INVALID_XML_SUB = re.compile("[^%s]" % _valid_xml_range, re.U).sub
|
|
||||||
|
|
||||||
|
|
||||||
def remove_invalid_xml(s, replace_with=" "):
|
|
||||||
return RE_INVALID_XML_SUB(replace_with, s)
|
|
||||||
|
|
||||||
|
|
||||||
def multi_replace(s, replace_from, replace_to=""):
|
|
||||||
"""A function like str.replace() with multiple replacements.
|
"""A function like str.replace() with multiple replacements.
|
||||||
|
|
||||||
``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d']
|
``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d']
|
||||||
@@ -302,61 +258,15 @@ def multi_replace(s, replace_from, replace_to=""):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
# --- Date related
|
|
||||||
|
|
||||||
# It might seem like needless namespace pollution, but the speedup gained by this constant is
|
|
||||||
# significant, so it stays.
|
|
||||||
ONE_DAY = timedelta(1)
|
|
||||||
|
|
||||||
|
|
||||||
def iterdaterange(start, end):
|
|
||||||
"""Yields every day between ``start`` and ``end``."""
|
|
||||||
date = start
|
|
||||||
while date <= end:
|
|
||||||
yield date
|
|
||||||
date += ONE_DAY
|
|
||||||
|
|
||||||
|
|
||||||
# --- Files related
|
# --- Files related
|
||||||
|
|
||||||
|
|
||||||
@pathify
|
|
||||||
def modified_after(first_path: Path, second_path: Path):
|
|
||||||
"""Returns ``True`` if first_path's mtime is higher than second_path's mtime.
|
|
||||||
|
|
||||||
If one of the files doesn't exist or is ``None``, it is considered "never modified".
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
first_mtime = first_path.stat().st_mtime
|
|
||||||
except (EnvironmentError, AttributeError):
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
second_mtime = second_path.stat().st_mtime
|
|
||||||
except (EnvironmentError, AttributeError):
|
|
||||||
return True
|
|
||||||
return first_mtime > second_mtime
|
|
||||||
|
|
||||||
|
|
||||||
def find_in_path(name, paths=None):
|
|
||||||
"""Search for `name` in all directories of `paths` and return the absolute path of the first
|
|
||||||
occurrence. If `paths` is None, $PATH is used.
|
|
||||||
"""
|
|
||||||
if paths is None:
|
|
||||||
paths = os.environ["PATH"]
|
|
||||||
if isinstance(paths, str): # if it's not a string, it's already a list
|
|
||||||
paths = paths.split(os.pathsep)
|
|
||||||
for path in paths:
|
|
||||||
if op.exists(op.join(path, name)):
|
|
||||||
return op.join(path, name)
|
|
||||||
return 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: List[str] = []) -> bool:
|
||||||
"""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.is_dir():
|
if not path.exists() or not path.is_dir():
|
||||||
return
|
return False
|
||||||
contents = list(path.glob("*"))
|
contents = list(path.glob("*"))
|
||||||
if any(p for p in contents if (p.name not in files_to_delete) or p.is_dir()):
|
if any(p for p in contents if (p.name not in files_to_delete) or p.is_dir()):
|
||||||
return False
|
return False
|
||||||
@@ -366,7 +276,10 @@ def delete_if_empty(path: Path, files_to_delete=[]):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def open_if_filename(infile, mode="rb"):
|
def open_if_filename(
|
||||||
|
infile: Union[Path, str, IO],
|
||||||
|
mode: str = "rb",
|
||||||
|
) -> Tuple[IO, bool]:
|
||||||
"""If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.
|
"""If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.
|
||||||
|
|
||||||
This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has
|
This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has
|
||||||
@@ -386,33 +299,6 @@ def open_if_filename(infile, mode="rb"):
|
|||||||
return (infile, False)
|
return (infile, False)
|
||||||
|
|
||||||
|
|
||||||
def ensure_folder(path):
|
|
||||||
"Create `path` as a folder if it doesn't exist."
|
|
||||||
if not op.exists(path):
|
|
||||||
os.makedirs(path)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_file(path):
|
|
||||||
"Create `path` as an empty file if it doesn't exist."
|
|
||||||
if not op.exists(path):
|
|
||||||
open(path, "w").close()
|
|
||||||
|
|
||||||
|
|
||||||
def delete_files_with_pattern(folder_path, pattern, recursive=True):
|
|
||||||
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`."""
|
|
||||||
to_delete = glob.glob(op.join(folder_path, pattern))
|
|
||||||
for fn in to_delete:
|
|
||||||
if op.isdir(fn):
|
|
||||||
shutil.rmtree(fn)
|
|
||||||
else:
|
|
||||||
os.remove(fn)
|
|
||||||
if recursive:
|
|
||||||
subpaths = [op.join(folder_path, fn) for fn in os.listdir(folder_path)]
|
|
||||||
subfolders = [p for p in subpaths if op.isdir(p)]
|
|
||||||
for p in subfolders:
|
|
||||||
delete_files_with_pattern(p, pattern, True)
|
|
||||||
|
|
||||||
|
|
||||||
class FileOrPath:
|
class FileOrPath:
|
||||||
"""Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.
|
"""Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.
|
||||||
|
|
||||||
@@ -422,16 +308,16 @@ class FileOrPath:
|
|||||||
dostuff()
|
dostuff()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, file_or_path, mode="rb"):
|
def __init__(self, file_or_path: Union[Path, str], mode: str = "rb") -> None:
|
||||||
self.file_or_path = file_or_path
|
self.file_or_path = file_or_path
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.mustclose = False
|
self.mustclose = False
|
||||||
self.fp = None
|
self.fp: Union[IO, None] = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> IO:
|
||||||
self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)
|
self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)
|
||||||
return self.fp
|
return self.fp
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||||
if self.fp and self.mustclose:
|
if self.fp and self.mustclose:
|
||||||
self.fp.close()
|
self.fp.close()
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
@@ -25,7 +25,7 @@ msgstr ""
|
|||||||
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:94
|
||||||
#: core\se\result_table.py:19
|
#: core\se\result_table.py:19
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -53,7 +53,7 @@ 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:165 core\se\result_table.py:23
|
||||||
msgid "Modification"
|
msgid "Modification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ msgstr ""
|
|||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:156
|
#: core\prioritize.py:158
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -4,123 +4,123 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
|
|
||||||
#: core\app.py:42
|
#: core\app.py:44
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:43
|
#: core\app.py:45
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:44
|
#: core\app.py:46
|
||||||
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?"
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:71
|
#: core\app.py:73
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:72
|
#: core\app.py:74
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:73
|
#: core\app.py:75
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:74
|
#: core\app.py:76
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:75
|
#: core\app.py:77
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:289
|
#: core\app.py:291
|
||||||
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:300
|
#: core\app.py:302
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:315
|
#: core\app.py:317
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:319
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:319
|
#: core\app.py:321
|
||||||
msgid "All marked files were deleted successfully."
|
msgid "All marked files were deleted successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:323
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:326
|
#: core\app.py:328
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:382
|
#: core\app.py:384
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:392
|
#: core\app.py:394
|
||||||
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:469
|
#: core\app.py:471
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:512
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
#: core\app.py:518 core\app.py:773 core\app.py:783
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:539
|
#: core\app.py:541
|
||||||
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:695 core\app.py:707
|
#: core\app.py:697 core\app.py:709
|
||||||
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:743
|
#: core\app.py:745
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:790
|
#: core\app.py:792
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:803
|
#: core\app.py:808
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\app.py:850
|
#: core\app.py:858
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:190
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\directories.py:207
|
#: core\directories.py:206
|
||||||
msgid "Collected {} folders to scan"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -188,35 +188,35 @@ msgstr ""
|
|||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:100
|
#: core\prioritize.py:102
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:101
|
#: core\prioritize.py:103
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:102
|
#: core\prioritize.py:104
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:103
|
#: core\prioritize.py:105
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n"
|
"Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\n"
|
||||||
"Language: cs\n"
|
"Language: cs\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -977,3 +977,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "O {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Verze {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Licencován pod GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Chybové hlášení"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Něco se pokazilo. Co takhle nahlásit chybu?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Chybové zprávy by měly být vykázáno jako emise GitHub. Můžete zkopírovat chybovou TraceBack výše a vložit ji do nové emise.\n"
|
||||||
|
"\n"
|
||||||
|
"Prosím, ujistěte se, že ke spuštění hledání jakýchkoli již existujících otázek předem. Také se ujistěte, vyzkoušet nejnovější dostupnou verzi z úložiště, protože chyba jste se setkali již mohla být oprava.\n"
|
||||||
|
"\n"
|
||||||
|
"To, co obvykle opravdu pomáhá, je přidat popis toho, jak jste se dostali k chybě. Dík!\n"
|
||||||
|
"\n"
|
||||||
|
"Přestože by aplikace měla po této chybě pokračovat, může být v nestabilním stavu, proto se doporučuje aplikaci restartovat."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Přejít na Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "česky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Německy"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "řecky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Anglicky."
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "španělsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Francouzsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "arménsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "italsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japonština"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "korejsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malajština"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "holandsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "polsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "brazilsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "rusky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turečtina"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "ukrajinsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "vietnamsky"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "čínsky (zjednodušeně)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Vymazání seznamu"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Hledat..."
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Robert M, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Robert M, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Robert M, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -989,3 +989,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "Über {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Version {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Lizenziert unter GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Fehlermeldung"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Etwas ist schief gelaufen. Wie wäre es, den Fehler zu melden?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Fehlerberichte sollten als Github-Probleme gemeldet werden. Sie können den obigen Fehler-Traceback kopieren und in eine neue Ausgabe einfügen.\n"
|
||||||
|
"\n"
|
||||||
|
"Bitte stellen Sie sicher, dass Sie vorher nach bereits vorhandenen Problemen suchen. Stellen Sie außerdem sicher, dass Sie die neueste Version testen, die im Repository verfügbar ist, da der aufgetretene Fehler möglicherweise bereits behoben wurde.\n"
|
||||||
|
"\n"
|
||||||
|
"Was normalerweise wirklich hilft, ist, wenn Sie eine Beschreibung hinzufügen, wie Sie den Fehler erhalten haben. Vielen Dank!\n"
|
||||||
|
"\n"
|
||||||
|
"Obwohl die Anwendung nach diesem Fehler weiterhin ausgeführt werden sollte, befindet sie sich möglicherweise in einem instabilen Zustand. Es wird daher empfohlen, die Anwendung neu zu starten."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Geh zu Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Tschechisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Deutsch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Griechisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Englisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Spanisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Französisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Armenisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Italienisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japanisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Koreanisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malaiisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Niederländisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Polnisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brasilianisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Russisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Türkisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ukrainisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnamesisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Chinesisch (Vereinfachtes)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Liste löschen"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Suche..."
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\n"
|
||||||
"Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\n"
|
"Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\n"
|
||||||
"Language: el\n"
|
"Language: el\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -994,3 +994,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "Σχετικά {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Έκδοση {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Άδεια χρήσης βάσει GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Αναφορά σφάλματος"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Κάτι πήγε στραβά. Μήπως να αναφερθεί το σφάλμα;"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Οι αναφορές σφαλμάτων πρέπει να αναφέρονται ως ζητήματα Github. Μπορείτε να αντιγράψετε την ανίχνευση σφαλμάτων παραπάνω και να την επικολλήσετε σε ένα νέο ζήτημα.\n"
|
||||||
|
"\n"
|
||||||
|
"Βεβαιωθείτε ότι έχετε πραγματοποιήσει αναζήτηση για τυχόν υπάρχοντα ζητήματα εκ των προτέρων. Επίσης, φροντίστε να δοκιμάσετε την πιο πρόσφατη διαθέσιμη έκδοση από το αποθετήριο, καθώς το σφάλμα που αντιμετωπίζετε ενδέχεται να έχει ήδη διορθωθεί.\n"
|
||||||
|
"\n"
|
||||||
|
"Αυτό που συνήθως βοηθάει συνήθως είναι εάν προσθέσετε μια περιγραφή για το πώς λάβατε το σφάλμα. Ευχαριστώ!\n"
|
||||||
|
"\n"
|
||||||
|
"Παρόλο που η εφαρμογή θα πρέπει να συνεχίσει να εκτελείται μετά από αυτό το σφάλμα, ενδέχεται να βρίσκεται σε ασταθή κατάσταση, επομένως συνιστάται να κάνετε επανεκκίνηση της εφαρμογής."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Επίσκεψη Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Τσέχικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Γερμανικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Ελληνικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Αγγλικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Ισπανικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Γαλλικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Αρμένικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Ιταλικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Ιαπωνικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Κορεάτικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Μαλαϊκά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Γερμανικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Πολωνικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Βραζιλιάνικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Ρώσικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Τουρκικά"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ουκρανέζικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Βιετναμέζικα"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Κινέζικα (Απλοποιημένα)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Εκκαθάριση λίστας"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Αναζήτηση..."
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# IlluminatiWave, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: IlluminatiWave, 2022\n"
|
||||||
"Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n"
|
"Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: core\app.py:42
|
#: core\app.py:44
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr "No hay duplicados marcados. No se ha hecho nada."
|
msgstr "No hay duplicados marcados. No se ha hecho nada."
|
||||||
|
|
||||||
#: core\app.py:43
|
#: core\app.py:45
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr "No hay duplicados seleccionados. No se ha hecho nada."
|
msgstr "No hay duplicados seleccionados. No se ha hecho nada."
|
||||||
|
|
||||||
#: core\app.py:44
|
#: core\app.py:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You're about to open many files at once. Depending on what those files are "
|
"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?"
|
"opened with, doing so can create quite a mess. Continue?"
|
||||||
@@ -27,27 +28,27 @@ msgstr ""
|
|||||||
"Está a punto de abrir muchas imágenes. Dependiendo de los ficheros que se "
|
"Está a punto de abrir muchas imágenes. Dependiendo de los ficheros que se "
|
||||||
"abran, abrirlos puede colgar la máquina. ¿Continuar?"
|
"abran, abrirlos puede colgar la máquina. ¿Continuar?"
|
||||||
|
|
||||||
#: core\app.py:71
|
#: core\app.py:73
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
msgstr "Buscando duplicados"
|
msgstr "Buscando duplicados"
|
||||||
|
|
||||||
#: core\app.py:72
|
#: core\app.py:74
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr "Cargando"
|
msgstr "Cargando"
|
||||||
|
|
||||||
#: core\app.py:73
|
#: core\app.py:75
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr "Moviendo"
|
msgstr "Moviendo"
|
||||||
|
|
||||||
#: core\app.py:74
|
#: core\app.py:76
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr "Copiando"
|
msgstr "Copiando"
|
||||||
|
|
||||||
#: core\app.py:75
|
#: core\app.py:77
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "Enviando a la Papelera"
|
msgstr "Enviando a la Papelera"
|
||||||
|
|
||||||
#: core\app.py:289
|
#: core\app.py:291
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
@@ -55,40 +56,40 @@ msgstr ""
|
|||||||
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
|
"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. "
|
||||||
"Espere unos segundos y vuelva a intentarlo."
|
"Espere unos segundos y vuelva a intentarlo."
|
||||||
|
|
||||||
#: core\app.py:300
|
#: core\app.py:302
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "No se han encontrado duplicados."
|
msgstr "No se han encontrado duplicados."
|
||||||
|
|
||||||
#: core\app.py:315
|
#: core\app.py:317
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Todos los ficheros seleccionados han sido copiados satisfactoriamente."
|
"Todos los ficheros seleccionados han sido copiados satisfactoriamente."
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:319
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
|
msgstr "Todos los ficheros seleccionados se han movidos satisfactoriamente."
|
||||||
|
|
||||||
#: core\app.py:319
|
|
||||||
msgid "All marked files were deleted successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:321
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr "Todos los ficheros seleccionados se han eliminado satisfactoriamente."
|
||||||
|
|
||||||
|
#: core\app.py:323
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
|
msgstr "Todo los ficheros marcados se han enviado a la papelera exitosamente."
|
||||||
|
|
||||||
#: core\app.py:326
|
#: core\app.py:328
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "No se pudo cargar el archivo: {}"
|
msgstr "No se pudo cargar el archivo: {}"
|
||||||
|
|
||||||
#: core\app.py:382
|
#: core\app.py:384
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' ya está en la lista."
|
msgstr "'{}' ya está en la lista."
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' no existe."
|
msgstr "'{}' no existe."
|
||||||
|
|
||||||
#: core\app.py:392
|
#: core\app.py:394
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
@@ -96,57 +97,57 @@ msgstr ""
|
|||||||
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
|
"Todas las %d coincidencias seleccionadas van a ser ignoradas en las "
|
||||||
"subsiguientes exploraciones. ¿Continuar?"
|
"subsiguientes exploraciones. ¿Continuar?"
|
||||||
|
|
||||||
#: core\app.py:469
|
#: core\app.py:471
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
|
msgstr "Seleccione un directorio donde desee copiar los archivos marcados"
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "Seleccione un directorio al que desee mover los archivos marcados"
|
msgstr "Seleccione un directorio al que desee mover los archivos marcados"
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:512
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "Seleccionar un destino para el CSV seleccionado"
|
msgstr "Seleccionar un destino para el CSV seleccionado"
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
#: core\app.py:518 core\app.py:773 core\app.py:783
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "No se pudo escribir en el archivo: {}"
|
msgstr "No se pudo escribir en el archivo: {}"
|
||||||
|
|
||||||
#: core\app.py:539
|
#: core\app.py:541
|
||||||
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 "No hay comandos configurados. Establézcalos en sus preferencias."
|
msgstr "No hay comandos configurados. Establézcalos en sus preferencias."
|
||||||
|
|
||||||
#: core\app.py:695 core\app.py:707
|
#: core\app.py:697 core\app.py:709
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
|
msgstr "Está a punto de eliminar %d ficheros de resultados. ¿Continuar?"
|
||||||
|
|
||||||
#: core\app.py:743
|
#: core\app.py:745
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización"
|
msgstr "{} grupos de duplicados han sido cambiados por la re-priorización."
|
||||||
|
|
||||||
#: core\app.py:790
|
#: core\app.py:792
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
|
msgstr "Las carpetas seleccionadas no contienen ficheros para explorar."
|
||||||
|
|
||||||
#: core\app.py:803
|
#: core\app.py:808
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "Recopilando ficheros a explorar"
|
msgstr "Recopilando ficheros a explorar"
|
||||||
|
|
||||||
#: core\app.py:850
|
#: core\app.py:858
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d descartados)"
|
msgstr "%s (%d descartados)"
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:190
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr "{} ficheros recopilados para explorar"
|
||||||
|
|
||||||
#: core\directories.py:207
|
#: core\directories.py:206
|
||||||
msgid "Collected {} folders to scan"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr "{} carpetas recopiladas para explorar"
|
||||||
|
|
||||||
#: core\engine.py:27
|
#: core\engine.py:27
|
||||||
msgid "%d matches found from %d groups"
|
msgid "%d matches found from %d groups"
|
||||||
msgstr ""
|
msgstr "%d coincidencias encontradas en %d grupos"
|
||||||
|
|
||||||
#: 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."
|
||||||
@@ -209,35 +210,35 @@ msgstr "Marca horaria EXIF"
|
|||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "Ninguno"
|
msgstr "Ninguno"
|
||||||
|
|
||||||
#: core\prioritize.py:100
|
#: core\prioritize.py:102
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr "Termina con un número"
|
msgstr "Termina con un número"
|
||||||
|
|
||||||
#: core\prioritize.py:101
|
#: core\prioritize.py:103
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr "No termina con un número"
|
msgstr "No termina con un número"
|
||||||
|
|
||||||
#: core\prioritize.py:102
|
#: core\prioritize.py:104
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr "El más largo"
|
msgstr "El más largo"
|
||||||
|
|
||||||
#: core\prioritize.py:103
|
#: core\prioritize.py:105
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr "El más corto"
|
msgstr "El más corto"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr "El más alto"
|
msgstr "El más alto"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr "El más bajo"
|
msgstr "El más bajo"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr "El más nuevo"
|
msgstr "El más nuevo"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "El más antiguo"
|
msgstr "El más antiguo"
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
|
# IlluminatiWave, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: IlluminatiWave, 2022\n"
|
||||||
"Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n"
|
"Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -333,7 +334,7 @@ msgstr "Tamaño de fuente:"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:85
|
#: qt/preferences_dialog.py:85
|
||||||
msgid "Language:"
|
msgid "Language:"
|
||||||
msgstr "Lenguaje:"
|
msgstr "Idioma:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Copy and Move:"
|
msgid "Copy and Move:"
|
||||||
@@ -950,40 +951,200 @@ msgstr "Visualización"
|
|||||||
|
|
||||||
#: qt\se\preferences_dialog.py:70
|
#: qt\se\preferences_dialog.py:70
|
||||||
msgid "Partially hash files bigger than"
|
msgid "Partially hash files bigger than"
|
||||||
msgstr ""
|
msgstr "Archivos de hash parcialmente mayores a"
|
||||||
|
|
||||||
#: qt\se\preferences_dialog.py:80
|
#: qt\se\preferences_dialog.py:80
|
||||||
msgid "MB"
|
msgid "MB"
|
||||||
msgstr ""
|
msgstr "MB"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:163
|
#: qt\preferences_dialog.py:163
|
||||||
msgid "Use native OS dialogs"
|
msgid "Use native OS dialogs"
|
||||||
msgstr ""
|
msgstr "Usar diálogos nativos del SO"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:166
|
#: qt\preferences_dialog.py:166
|
||||||
msgid ""
|
msgid ""
|
||||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
"Some native dialogs have limited functionality."
|
"Some native dialogs have limited functionality."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Para acciones como la selección de archivos/carpetas, utilice los diálogos nativos del SO\n"
|
||||||
|
"Algunos diálogos nativos tienen una funcionalidad limitada."
|
||||||
|
|
||||||
#: qt\se\preferences_dialog.py:68
|
#: qt\se\preferences_dialog.py:68
|
||||||
msgid "Ignore files larger than"
|
msgid "Ignore files larger than"
|
||||||
msgstr ""
|
msgstr "Ignorar los ficheros mayores a"
|
||||||
|
|
||||||
#: qt\app.py:135 qt\app.py:293
|
#: qt\app.py:135 qt\app.py:293
|
||||||
msgid "Clear Cache"
|
msgid "Clear Cache"
|
||||||
msgstr ""
|
msgstr "Borrar caché"
|
||||||
|
|
||||||
#: qt\app.py:294
|
#: qt\app.py:294
|
||||||
msgid ""
|
msgid ""
|
||||||
"Do you really want to clear the cache? This will remove all cached file "
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
"hashes and picture analysis."
|
"hashes and picture analysis."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"¿Seguro que quieres borrar la caché? Esto eliminará todos los hashes de "
|
||||||
|
"ficheros y análisis de imágenes almacenados en la caché."
|
||||||
|
|
||||||
#: qt\app.py:299
|
#: qt\app.py:299
|
||||||
msgid "Cache cleared."
|
msgid "Cache cleared."
|
||||||
msgstr ""
|
msgstr "Caché eliminada."
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
|
msgstr "Usar tema oscuro"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr "Perfilar operación de análisis"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Perfilar la operación de análisis y guardar los registros para su "
|
||||||
|
"optimización."
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr "Registro guardado en: <a href=\"{}\">{}</a>"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr "Depurar"
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "Acerca de {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Versión {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr "Buscando actualizaciones..."
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Licenciado en GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr "Sin actualizaciones disponibles."
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr "Nueva versión disponible {}, descargar <a href=\"{}\">aquí</a> "
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Informe de error"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Algo salió mal. ¿Qué tal informar el error?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Los informes de errores deben notificarse en Problemas de Github. Puede copiar el seguimiento del error anterior y pegarlo en un nuevo número.\n"
|
||||||
|
"\n"
|
||||||
|
"Asegúrese de realizar una búsqueda de los problemas ya existentes de antemano. También asegúrese de probar la última versión disponible en el repositorio, ya que es posible que el error que está experimentando ya se haya corregido.\n"
|
||||||
|
"\n"
|
||||||
|
"Lo que generalmente ayuda es agregar una descripción de cómo obtuvo el error. ¡Gracias!\n"
|
||||||
|
"\n"
|
||||||
|
"Aunque la aplicación debería continuar ejecutándose después de este error, puede estar en un estado inestable, por lo que se recomienda que reinicie la aplicación."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Ir a Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Checo"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Alemán"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Griego"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Inglés"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Español"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Francés"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Armenio"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Italiano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japonés"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Coreano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malayo"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Holandés"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Polaco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brasileño"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Ruso"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ucraniano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnamita"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Chino (simplificado)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Limpiar lista"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Búsqueda..."
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n"
|
"Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -982,3 +982,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "A propos de {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Version {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Sous licence GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Rapport d'erreur"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Un problème est survenu. Rapporter l'erreur?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Les rapports d'erreur doivent être envoyé via les tickets Github. Vous pouvez copier l'historique d'erreur ci-dessus et le coller dans un nouveau ticket.\n"
|
||||||
|
"\n"
|
||||||
|
"Veuillez vous assurer auparavant d'avoir fait une recherche pour un ticket similaire. Assurez-vous aussi d'avoir testé la toute dernière version disponible depuis le dépôt car le bug que vous avez rencontré a peut-être déjà été corrigé. \n"
|
||||||
|
"\n"
|
||||||
|
"Décrire comment vous avez rencontré cette erreur est aussi très précieux. Merci!\n"
|
||||||
|
"\n"
|
||||||
|
" Même si cette application continue de fonctionner après cette erreur, elle peut être dans un état instable, et il est donc recommandé de relancer l'application."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Aller sur Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Tchèque"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Allemand"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Grecque"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Anglais"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Espagnol"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Français"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Arménien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Italien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japonais"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Coréen"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malaisien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Néerlandais"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Polonais"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brésilien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Russe"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turc"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ukrainien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnamien"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Chinois (Simplifié)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Vider la liste"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Recherche..."
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n"
|
"Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\n"
|
||||||
"Language: hy\n"
|
"Language: hy\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -922,3 +922,197 @@ msgstr "Գեներալ"
|
|||||||
#: qt\preferences_dialog.py:286
|
#: qt\preferences_dialog.py:286
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
msgstr "Ցուցադրման"
|
msgstr "Ցուցադրման"
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:70
|
||||||
|
msgid "Partially hash files bigger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:80
|
||||||
|
msgid "MB"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:163
|
||||||
|
msgid "Use native OS dialogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:166
|
||||||
|
msgid ""
|
||||||
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
|
"Some native dialogs have limited functionality."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\se\preferences_dialog.py:68
|
||||||
|
msgid "Ignore files larger than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:135 qt\app.py:293
|
||||||
|
msgid "Clear Cache"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:294
|
||||||
|
msgid ""
|
||||||
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
|
"hashes and picture analysis."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\app.py:299
|
||||||
|
msgid "Cache cleared."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:173
|
||||||
|
msgid "Use dark style"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "{}- ի մասին"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "{}-րդ տարբերակ"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "իցենզավորված տակ GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Սխալների մասին հաղորդագրություն"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Ինչ որ բան այնպես չգնաց. Հաղորդել սխալը?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Error հաշվետվությունները պետք է հրապարակվեն որպես Github հարցերի շուրջ: Կարող եք վերևում պատճենել սխալի հետևումը և տեղադրել այն նոր համարում:\n"
|
||||||
|
"\n"
|
||||||
|
"Խնդրում ենք համոզվեք, որ նախապես փնտրեք արդեն գոյություն ունեցող ցանկացած խնդիր: Նաեւ համոզվեք, որ ստուգել են հենց վերջին տարբերակը մատչելի շտեմարան, քանի որ Bug դուք ապրում գուցե արդեն patched.\n"
|
||||||
|
"\n"
|
||||||
|
"Սովորաբար այն, ինչ օգնում է իրականում, այն է, եթե ավելացնեք նկարագրությունը, թե ինչպես եք ստացել սխալը: Շնորհակալություն\n"
|
||||||
|
"\n"
|
||||||
|
"Չնայած այս սխալից հետո ծրագիրը պետք է շարունակի գործել, այն կարող է լինել անկայուն վիճակում, ուստի խորհուրդ է տրվում վերագործարկել ծրագիրը:"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Գնացեք Գիթուբ"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Չեխերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Գերմաներեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "հունարեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Անգլերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Իսպաներեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Ֆրանսերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "հայերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Իտալերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "ճապոներեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "կորեերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Մալայերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "հոլանդերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "լեհերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "բրազիլական"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "ռուսերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Թուրքերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "ուկրաիներեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "վիետնամերեն"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Չինարեն (Պարզեցված)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Մաքրել ցանկը"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Որոնել..."
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Emanuele, 2022
|
||||||
# Emanuele, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
|
# Giovanni, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Emanuele, 2021\n"
|
"Last-Translator: Giovanni, 2022\n"
|
||||||
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\n"
|
||||||
"Language: it\n"
|
"Language: it\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||||
|
|
||||||
#: qt/app.py:81
|
#: qt/app.py:81
|
||||||
msgid "Quit"
|
msgid "Quit"
|
||||||
@@ -979,18 +980,175 @@ msgstr "Ignora file più grandi di"
|
|||||||
|
|
||||||
#: qt\app.py:135 qt\app.py:293
|
#: qt\app.py:135 qt\app.py:293
|
||||||
msgid "Clear Cache"
|
msgid "Clear Cache"
|
||||||
msgstr ""
|
msgstr "Svuota cache"
|
||||||
|
|
||||||
#: qt\app.py:294
|
#: qt\app.py:294
|
||||||
msgid ""
|
msgid ""
|
||||||
"Do you really want to clear the cache? This will remove all cached file "
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
"hashes and picture analysis."
|
"hashes and picture analysis."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Vuoi davvero svuotare la cache? Ciò rimuoverà tutti gli hash dei file "
|
||||||
|
"memorizzati nella cache e le analisi delle immagini."
|
||||||
|
|
||||||
#: qt\app.py:299
|
#: qt\app.py:299
|
||||||
msgid "Cache cleared."
|
msgid "Cache cleared."
|
||||||
msgstr ""
|
msgstr "Cache svuotata"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
|
msgstr "Usa stile scuro"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr "Profila l'operazione di scansione"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Profila l'operazione di scansione e salva i registri per l'ottimizzazione."
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr "I log si trovano in: <a href=\"{}\">{}</a>"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr "Debug"
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "A proposito di {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Versione {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr "Controllo degli aggiornamenti..."
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Distribuito sotto licenza GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr "Nessun aggiornamento disponibile."
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr "È disponibile la nuova versione {}, scaricabile <a href=\"{}\">qui</a>."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Rapporto di errore"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Qualcosa è andato storto. Che ne dici di segnalare l'errore?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"I rapporti di errore dovrebbero essere segnalati come problemi di Github. Puoi copiare il traceback degli errori sopra e incollarlo in un nuovo numero.\n"
|
||||||
|
"\n"
|
||||||
|
"Assicurati di eseguire prima una ricerca per eventuali problemi già esistenti. Assicurati anche di testare l'ultima versione disponibile dal repository, poiché il bug che stai riscontrando potrebbe essere già stato corretto.\n"
|
||||||
|
"\n"
|
||||||
|
"Ciò che di solito aiuta davvero è aggiungere una descrizione di come hai ottenuto l'errore. Grazie!\n"
|
||||||
|
"\n"
|
||||||
|
"Sebbene l'applicazione debba continuare a essere eseguita dopo questo errore, potrebbe essere in uno stato instabile, quindi si consiglia di riavviare l'applicazione."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Apri in Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Ceco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Tedesco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Greco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Inglese"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Spagnolo"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Francese"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Armeno"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Italiano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Giapponese"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Coreano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malese"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Olandese"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Polacco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brasiliano"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Russo"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turco"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ucraino"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnamita"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Cinese (semplificato)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Cancellare l'elenco"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Ricerca..."
|
||||||
|
|||||||
@@ -1,138 +1,139 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Yuji Sasaki, 2022
|
||||||
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
||||||
"Language: ja\n"
|
"Language: ja\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
#: core\app.py:42
|
#: core\app.py:44
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr "マークされた重複はありません。 何も行われていません。"
|
msgstr "チェックを入れた重複はありません。 何も行われませんでした。"
|
||||||
|
|
||||||
#: core\app.py:43
|
#: core\app.py:45
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr "選択された重複はありません。 何も行われていません。"
|
msgstr "選択された重複はありません。 何も行われていません。"
|
||||||
|
|
||||||
#: core\app.py:44
|
#: core\app.py:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You're about to open many files at once. Depending on what those files are "
|
"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?"
|
"opened with, doing so can create quite a mess. Continue?"
|
||||||
msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?"
|
msgstr "一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:71
|
#: core\app.py:73
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
msgstr "重複のスキャン"
|
msgstr "重複のスキャン"
|
||||||
|
|
||||||
#: core\app.py:72
|
#: core\app.py:74
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr "読み込み中"
|
msgstr "読み込み中"
|
||||||
|
|
||||||
#: core\app.py:73
|
#: core\app.py:75
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr "移動します"
|
msgstr "移動します"
|
||||||
|
|
||||||
#: core\app.py:74
|
#: core\app.py:76
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr "コピー中"
|
msgstr "コピー中"
|
||||||
|
|
||||||
#: core\app.py:75
|
#: core\app.py:77
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "ごみ箱に送信します"
|
msgstr "ごみ箱に送信します"
|
||||||
|
|
||||||
#: core\app.py:289
|
#: core\app.py:291
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
msgstr "前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。"
|
||||||
|
|
||||||
#: core\app.py:300
|
#: core\app.py:302
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "重複は見つかりませんでした。"
|
msgstr "重複は見つかりませんでした。"
|
||||||
|
|
||||||
#: core\app.py:315
|
|
||||||
msgid "All marked files were copied successfully."
|
|
||||||
msgstr "マークされたファイルはすべて正常にコピーされました。"
|
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:317
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "マークされたファイルはすべて正常に移動されました。"
|
msgstr "チェックを入れたファイルをすべてコピーしました。"
|
||||||
|
|
||||||
#: core\app.py:319
|
#: core\app.py:319
|
||||||
msgid "All marked files were deleted successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr ""
|
msgstr "チェックを入れたファイルをすべて移動しました。"
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:321
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were deleted successfully."
|
||||||
msgstr "マークされたファイルはすべてごみ箱に正常に送信されました。"
|
msgstr "チェックを入れたファイルをすべて削除しました。"
|
||||||
|
|
||||||
#: core\app.py:326
|
#: core\app.py:323
|
||||||
|
msgid "All marked files were successfully sent to Trash."
|
||||||
|
msgstr "チェックを入れたファイルをすべてごみ箱に移動しました。"
|
||||||
|
|
||||||
|
#: core\app.py:328
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "ファイルを読み込めませんでした:{}"
|
msgstr "ファイルを読み込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:382
|
#: core\app.py:384
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "「{}」既にリストに含まれています。"
|
msgstr "「{}」既にリストに含まれています。"
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' 存在しません。"
|
msgstr "'{}' 存在しません。"
|
||||||
|
|
||||||
#: core\app.py:392
|
#: core\app.py:394
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
msgstr "選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:469
|
#: core\app.py:471
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
msgstr "マークされたファイルをコピーするディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
msgstr "マークされたファイルを移動するディレクトリを選択してください"
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:512
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "エクスポートしたCSVの宛先を選択します。"
|
msgstr "エクスポートしたCSVの宛先を選択します。"
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
#: core\app.py:518 core\app.py:773 core\app.py:783
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "ファイルに書き込めませんでした:{}"
|
msgstr "ファイルに書き込めませんでした:{}"
|
||||||
|
|
||||||
#: core\app.py:539
|
#: core\app.py:541
|
||||||
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:695 core\app.py:707
|
#: core\app.py:697 core\app.py:709
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
msgstr "結果から%d個のファイルを削除しようとしています。 継続する?"
|
||||||
|
|
||||||
#: core\app.py:743
|
#: core\app.py:745
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
msgstr "{}重複するグループは、再優先順位付けによって変更されました。"
|
||||||
|
|
||||||
#: core\app.py:790
|
#: core\app.py:792
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
msgstr "選択したディレクトリにはスキャン可能なファイルが含まれていません。"
|
||||||
|
|
||||||
#: core\app.py:803
|
#: core\app.py:808
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "スキャンするファイルを収集しています"
|
msgstr "スキャンするファイルを収集しています"
|
||||||
|
|
||||||
#: core\app.py:850
|
#: core\app.py:858
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d 廃棄)"
|
msgstr "%s (%d 廃棄)"
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:190
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core\directories.py:207
|
#: core\directories.py:206
|
||||||
msgid "Collected {} folders to scan"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -200,35 +201,35 @@ msgstr "EXIFタイムスタンプ"
|
|||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "無し"
|
msgstr "無し"
|
||||||
|
|
||||||
#: core\prioritize.py:100
|
#: core\prioritize.py:102
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr "番号で終わっている"
|
msgstr "番号で終わっている"
|
||||||
|
|
||||||
#: core\prioritize.py:101
|
#: core\prioritize.py:103
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr "数字で終わっていない"
|
msgstr "数字で終わっていない"
|
||||||
|
|
||||||
#: core\prioritize.py:102
|
#: core\prioritize.py:104
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr "最長"
|
msgstr "最長"
|
||||||
|
|
||||||
#: core\prioritize.py:103
|
#: core\prioritize.py:105
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr "最短"
|
msgstr "最短"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr "最高"
|
msgstr "最高"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr "最低"
|
msgstr "最低"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr "最新"
|
msgstr "最新"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "最古"
|
msgstr "最古"
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\n"
|
||||||
"Language: ja\n"
|
"Language: ja\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -80,7 +81,7 @@ msgstr "(非対応)"
|
|||||||
|
|
||||||
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Directly delete files"
|
msgid "Directly delete files"
|
||||||
msgstr "ファイルを直接削除する"
|
msgstr "ファイルを完全に削除"
|
||||||
|
|
||||||
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -99,7 +100,7 @@ msgstr "キャンセル"
|
|||||||
|
|
||||||
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Attribute"
|
msgid "Attribute"
|
||||||
msgstr "アトリビュート"
|
msgstr "属性"
|
||||||
|
|
||||||
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Selected"
|
msgid "Selected"
|
||||||
@@ -162,7 +163,7 @@ msgstr "スキャンの種類:"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:135
|
#: qt/directories_dialog.py:135
|
||||||
msgid "More Options"
|
msgid "More Options"
|
||||||
msgstr "もっとオプション"
|
msgstr "詳細設定"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Select folders to scan and press \"Scan\"."
|
msgid "Select folders to scan and press \"Scan\"."
|
||||||
@@ -178,7 +179,7 @@ msgstr "スキャン"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:230
|
#: qt/directories_dialog.py:230
|
||||||
msgid "Unsaved results"
|
msgid "Unsaved results"
|
||||||
msgstr "保存されていない結果"
|
msgstr "未保存の結果"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to quit?"
|
msgid "You have unsaved results, do you really want to quit?"
|
||||||
@@ -279,27 +280,27 @@ msgstr "単語の重み付け"
|
|||||||
#: 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 "類似の単語に一致する"
|
msgstr "類似の単語を一致"
|
||||||
|
|
||||||
#: 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
|
||||||
msgid "Can mix file kind"
|
msgid "Can mix file kind"
|
||||||
msgstr "ファイルの種類を混在させることができる"
|
msgstr "ファイルの種類を混在"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
||||||
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Use regular expressions when filtering"
|
msgid "Use regular expressions when filtering"
|
||||||
msgstr "フィルタリング時に正規表現を使用する"
|
msgstr "フィルタに正規表現を使用"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
|
#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25
|
||||||
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Remove empty folders on delete or move"
|
msgid "Remove empty folders on delete or move"
|
||||||
msgstr "削除または移動時に空のフォルダを削除する"
|
msgstr "削除や移動で空になったフォルダを削除"
|
||||||
|
|
||||||
#: 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 "同じファイルへの重複ハードリンクを無視する"
|
msgstr "同じファイルへの重複ハードリンクを無視"
|
||||||
|
|
||||||
#: 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
|
||||||
@@ -308,11 +309,11 @@ msgstr "デバッグモード(再起動が必要)"
|
|||||||
|
|
||||||
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Match pictures of different dimensions"
|
msgid "Match pictures of different dimensions"
|
||||||
msgstr "異なる寸法の写真を一致させる"
|
msgstr "異なるサイズの写真を一致"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:43
|
#: qt/preferences_dialog.py:43
|
||||||
msgid "Filter Hardness:"
|
msgid "Filter Hardness:"
|
||||||
msgstr "フィルター硬度:"
|
msgstr "フィルタの強さ:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:69
|
#: qt/preferences_dialog.py:69
|
||||||
msgid "More Results"
|
msgid "More Results"
|
||||||
@@ -324,7 +325,7 @@ msgstr "より少ない結果"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:81
|
#: qt/preferences_dialog.py:81
|
||||||
msgid "Font size:"
|
msgid "Font size:"
|
||||||
msgstr "フォントサイズ:"
|
msgstr "文字サイズ:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:85
|
#: qt/preferences_dialog.py:85
|
||||||
msgid "Language:"
|
msgid "Language:"
|
||||||
@@ -332,7 +333,7 @@ msgstr "言語:"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Copy and Move:"
|
msgid "Copy and Move:"
|
||||||
msgstr "コピーと移動:"
|
msgstr "コピーと移動:"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
|
#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Right in destination"
|
msgid "Right in destination"
|
||||||
@@ -348,7 +349,7 @@ msgstr "絶対パスを再作成"
|
|||||||
|
|
||||||
#: qt/preferences_dialog.py:99
|
#: qt/preferences_dialog.py:99
|
||||||
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
msgid "Custom Command (arguments: %d for dupe, %r for ref):"
|
||||||
msgstr "カスタムコマンド (引数:重複の場合は%d、参照の場合は%r):"
|
msgstr "カスタムコマンド (引数: %dは重複・%rは参照):"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:174
|
#: qt/preferences_dialog.py:174
|
||||||
msgid "dupeGuru has to restart for language changes to take effect."
|
msgid "dupeGuru has to restart for language changes to take effect."
|
||||||
@@ -718,7 +719,7 @@ msgstr "ウィンドウ"
|
|||||||
|
|
||||||
#: cocoa/en.lproj/Localizable.strings:0
|
#: cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Zoom"
|
msgid "Zoom"
|
||||||
msgstr "ズーム"
|
msgstr "拡大"
|
||||||
|
|
||||||
#: qt\app.py:158
|
#: qt\app.py:158
|
||||||
msgid "Exclusion Filters"
|
msgid "Exclusion Filters"
|
||||||
@@ -908,15 +909,15 @@ msgstr "結果"
|
|||||||
|
|
||||||
#: qt\preferences_dialog.py:150
|
#: qt\preferences_dialog.py:150
|
||||||
msgid "General Interface"
|
msgid "General Interface"
|
||||||
msgstr "一般的なインターフェイス"
|
msgstr "一般"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:176
|
#: qt\preferences_dialog.py:176
|
||||||
msgid "Result Table"
|
msgid "Result Table"
|
||||||
msgstr "結果表"
|
msgstr "結果"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:205
|
#: qt\preferences_dialog.py:205
|
||||||
msgid "Details Window"
|
msgid "Details Window"
|
||||||
msgstr "詳細ウィンドウ"
|
msgstr "詳細画面"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:285
|
#: qt\preferences_dialog.py:285
|
||||||
msgid "General"
|
msgid "General"
|
||||||
@@ -965,3 +966,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "{}について"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "バージョン {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "GPLv3のもとでライセンスされています"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "エラーレポート"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "不明な理由により失敗しました。問題を報告しませんか?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"エラーレポートはGithubの問題として報告する必要があります。 上記のエラートレースバックをコピーして、新しい問題に貼り付けることができます。\n"
|
||||||
|
"\n"
|
||||||
|
"事前に既存の問題を検索してください。 また、発生しているバグにはすでにパッチが適用されている可能性があるため、リポジトリから入手できる最新バージョンをテストしてください。\n"
|
||||||
|
"\n"
|
||||||
|
"通常本当に役立つのは、エラーが発生した方法の説明を追加することです。 ありがとう!\n"
|
||||||
|
"\n"
|
||||||
|
"このエラーの後もアプリケーションは実行を継続するはずですが、不安定な状態になっている可能性があるため、アプリケーションを再起動することをお勧めします。"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Githubに移動"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "チェコ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "ドイツ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "ギリシャ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "英語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "スペイン語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "フランス語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "アルメニア語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "イタリア語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "日本語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "韓国語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "マレー語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "オランダ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "ポーランド語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "ブラジル語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "ロシア語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "トルコ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "ウクライナ語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "ベトナム語"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "中国語(簡体字)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "リストをクリア"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "探索..."
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
|
# Sangdon Lim, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\n"
|
"Last-Translator: Sangdon Lim, 2022\n"
|
||||||
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
||||||
"Language: ko\n"
|
"Language: ko\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -31,7 +32,7 @@ msgstr "비트레이트"
|
|||||||
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:94
|
||||||
#: core\se\result_table.py:19
|
#: core\se\result_table.py:19
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr "폴더명"
|
msgstr "폴더명"
|
||||||
@@ -59,7 +60,7 @@ 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:165 core\se\result_table.py:23
|
||||||
msgid "Modification"
|
msgid "Modification"
|
||||||
msgstr "수정날짜"
|
msgstr "수정날짜"
|
||||||
|
|
||||||
@@ -115,8 +116,8 @@ msgstr "크기 (KB)"
|
|||||||
|
|
||||||
#: core\pe\result_table.py:24
|
#: core\pe\result_table.py:24
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
msgstr "EXIF 타임스태프"
|
msgstr "EXIF 타임스탬프"
|
||||||
|
|
||||||
#: core\prioritize.py:156
|
#: core\prioritize.py:158
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr "크기"
|
msgstr "크기"
|
||||||
|
|||||||
@@ -1,149 +1,150 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2021
|
||||||
|
# Sangdon Lim, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Sangdon Lim, 2022\n"
|
||||||
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
||||||
"Language: ko\n"
|
"Language: ko\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: utf-8\n"
|
"Content-Transfer-Encoding: utf-8\n"
|
||||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
#: core\app.py:42
|
#: core\app.py:44
|
||||||
msgid "There are no marked duplicates. Nothing has been done."
|
msgid "There are no marked duplicates. Nothing has been done."
|
||||||
msgstr "표시된 중복 항목이 없습니다. 아무것도하지 않았습니다."
|
msgstr "표시된 중복 항목이 없습니다. 아무것도하지 않았습니다."
|
||||||
|
|
||||||
#: core\app.py:43
|
#: core\app.py:45
|
||||||
msgid "There are no selected duplicates. Nothing has been done."
|
msgid "There are no selected duplicates. Nothing has been done."
|
||||||
msgstr "선택한 중복 항목이 없습니다. 아무것도하지 않았습니다."
|
msgstr "선택한 중복 항목이 없습니다. 아무것도하지 않았습니다."
|
||||||
|
|
||||||
#: core\app.py:44
|
#: core\app.py:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You're about to open many files at once. Depending on what those files are "
|
"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?"
|
"opened with, doing so can create quite a mess. Continue?"
|
||||||
msgstr "한 번에 많은 파일을 열려고합니다. 그 파일을 여는 것에 따라 그렇게하면 꽤 엉망이 될 수 있습니다. 계속하다?"
|
msgstr "한 번에 많은 파일을 열려고 합니다. 시스템 설정에 따라 너무 많은 프로그램이 실행될 수도 있습니다. 진행할까요?"
|
||||||
|
|
||||||
#: core\app.py:71
|
#: core\app.py:73
|
||||||
msgid "Scanning for duplicates"
|
msgid "Scanning for duplicates"
|
||||||
msgstr "중복 검색"
|
msgstr "중복 검색"
|
||||||
|
|
||||||
#: core\app.py:72
|
#: core\app.py:74
|
||||||
msgid "Loading"
|
msgid "Loading"
|
||||||
msgstr "불러오는중"
|
msgstr "불러오는중"
|
||||||
|
|
||||||
#: core\app.py:73
|
#: core\app.py:75
|
||||||
msgid "Moving"
|
msgid "Moving"
|
||||||
msgstr "이동중"
|
msgstr "이동중"
|
||||||
|
|
||||||
#: core\app.py:74
|
#: core\app.py:76
|
||||||
msgid "Copying"
|
msgid "Copying"
|
||||||
msgstr "복사중"
|
msgstr "복사중"
|
||||||
|
|
||||||
#: core\app.py:75
|
#: core\app.py:77
|
||||||
msgid "Sending to Trash"
|
msgid "Sending to Trash"
|
||||||
msgstr "휴지통으로 보내기"
|
msgstr "휴지통으로 보내기"
|
||||||
|
|
||||||
#: core\app.py:289
|
#: core\app.py:291
|
||||||
msgid ""
|
msgid ""
|
||||||
"A previous action is still hanging in there. You can't start a new one yet. "
|
"A previous action is still hanging in there. You can't start a new one yet. "
|
||||||
"Wait a few seconds, then try again."
|
"Wait a few seconds, then try again."
|
||||||
msgstr "이전 작업이 여전히 거기에 걸려 있습니다. 아직 새로운 것을 시작할 수 없습니다. 몇 초 후에 다시 시도하십시오."
|
msgstr "이전 작업이 아직 진행 중이어서 새 작업을 시작할 수 없습니다. 몇 초 후에 다시 시도해 보세요."
|
||||||
|
|
||||||
#: core\app.py:300
|
#: core\app.py:302
|
||||||
msgid "No duplicates found."
|
msgid "No duplicates found."
|
||||||
msgstr "중복 파일이 없습니다."
|
msgstr "중복 파일이 없습니다."
|
||||||
|
|
||||||
#: core\app.py:315
|
#: core\app.py:317
|
||||||
msgid "All marked files were copied successfully."
|
msgid "All marked files were copied successfully."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 복사되었습니다."
|
||||||
|
|
||||||
#: core\app.py:317
|
#: core\app.py:319
|
||||||
msgid "All marked files were moved successfully."
|
msgid "All marked files were moved successfully."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 이동되었습니다."
|
||||||
|
|
||||||
#: core\app.py:319
|
|
||||||
msgid "All marked files were deleted successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: core\app.py:321
|
#: core\app.py:321
|
||||||
|
msgid "All marked files were deleted successfully."
|
||||||
|
msgstr "표시된 모든 파일이 성공적으로 제거되었습니다."
|
||||||
|
|
||||||
|
#: core\app.py:323
|
||||||
msgid "All marked files were successfully sent to Trash."
|
msgid "All marked files were successfully sent to Trash."
|
||||||
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
|
msgstr "표시된 모든 파일이 성공적으로 휴지통으로 전송되었습니다."
|
||||||
|
|
||||||
#: core\app.py:326
|
#: core\app.py:328
|
||||||
msgid "Could not load file: {}"
|
msgid "Could not load file: {}"
|
||||||
msgstr "파일을로드 할 수 없습니다 : {}"
|
msgstr "파일을로드 할 수 없습니다 : {}"
|
||||||
|
|
||||||
#: core\app.py:382
|
#: core\app.py:384
|
||||||
msgid "'{}' already is in the list."
|
msgid "'{}' already is in the list."
|
||||||
msgstr "'{}' 는 이미 목록에 있습니다."
|
msgstr "'{}' 는 이미 목록에 있습니다."
|
||||||
|
|
||||||
#: core\app.py:384
|
#: core\app.py:386
|
||||||
msgid "'{}' does not exist."
|
msgid "'{}' does not exist."
|
||||||
msgstr "'{}' 가 존재하지 않습니다."
|
msgstr "'{}' 가 존재하지 않습니다."
|
||||||
|
|
||||||
#: core\app.py:392
|
#: core\app.py:394
|
||||||
msgid ""
|
msgid ""
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. "
|
"All selected %d matches are going to be ignored in all subsequent scans. "
|
||||||
"Continue?"
|
"Continue?"
|
||||||
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
|
msgstr "선택된 %d개의 일치 항목은 모든 후속 검색에서 무시됩니다. 계속하다?"
|
||||||
|
|
||||||
#: core\app.py:469
|
#: core\app.py:471
|
||||||
msgid "Select a directory to copy marked files to"
|
msgid "Select a directory to copy marked files to"
|
||||||
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
|
msgstr "표시된 파일을 복사 할 디렉토리를 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:471
|
#: core\app.py:473
|
||||||
msgid "Select a directory to move marked files to"
|
msgid "Select a directory to move marked files to"
|
||||||
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
|
msgstr "표시된 파일을 이동할 디렉토리를 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:510
|
#: core\app.py:512
|
||||||
msgid "Select a destination for your exported CSV"
|
msgid "Select a destination for your exported CSV"
|
||||||
msgstr "내 보낸 CSV의 대상을 선택하십시오"
|
msgstr "내 보낸 CSV의 대상을 선택하십시오"
|
||||||
|
|
||||||
#: core\app.py:516 core\app.py:771 core\app.py:781
|
#: core\app.py:518 core\app.py:773 core\app.py:783
|
||||||
msgid "Couldn't write to file: {}"
|
msgid "Couldn't write to file: {}"
|
||||||
msgstr "파일에 쓸 수 없습니다 : {}"
|
msgstr "파일에 쓸 수 없습니다 : {}"
|
||||||
|
|
||||||
#: core\app.py:539
|
#: core\app.py:541
|
||||||
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:695 core\app.py:707
|
#: core\app.py:697 core\app.py:709
|
||||||
msgid "You are about to remove %d files from results. Continue?"
|
msgid "You are about to remove %d files from results. Continue?"
|
||||||
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 계속하다?"
|
msgstr "결과에서 %d 개의 파일을 제거하려고합니다. 실행할까요?"
|
||||||
|
|
||||||
#: core\app.py:743
|
#: core\app.py:745
|
||||||
msgid "{} duplicate groups were changed by the re-prioritization."
|
msgid "{} duplicate groups were changed by the re-prioritization."
|
||||||
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
|
msgstr "{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다."
|
||||||
|
|
||||||
#: core\app.py:790
|
#: core\app.py:792
|
||||||
msgid "The selected directories contain no scannable file."
|
msgid "The selected directories contain no scannable file."
|
||||||
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
|
msgstr "선택한 디렉토리에 스캔 가능한 파일이 없습니다."
|
||||||
|
|
||||||
#: core\app.py:803
|
#: core\app.py:808
|
||||||
msgid "Collecting files to scan"
|
msgid "Collecting files to scan"
|
||||||
msgstr "스캔 할 파일 수집"
|
msgstr "스캔 할 파일 수집"
|
||||||
|
|
||||||
#: core\app.py:850
|
#: core\app.py:858
|
||||||
msgid "%s (%d discarded)"
|
msgid "%s (%d discarded)"
|
||||||
msgstr "%s (%d 폐기)"
|
msgstr "%s (%d 폐기)"
|
||||||
|
|
||||||
#: core\directories.py:191
|
#: core\directories.py:190
|
||||||
msgid "Collected {} files to scan"
|
msgid "Collected {} files to scan"
|
||||||
msgstr ""
|
msgstr "파일 목록 생성 중: {}개 파일"
|
||||||
|
|
||||||
#: core\directories.py:207
|
#: core\directories.py:206
|
||||||
msgid "Collected {} folders to scan"
|
msgid "Collected {} folders to scan"
|
||||||
msgstr ""
|
msgstr "폴더 목록 생성 중: {}개 폴더"
|
||||||
|
|
||||||
#: core\engine.py:27
|
#: core\engine.py:27
|
||||||
msgid "%d matches found from %d groups"
|
msgid "%d matches found from %d groups"
|
||||||
msgstr ""
|
msgstr "중복 파일 %d개 확인됨: %d개 그룹"
|
||||||
|
|
||||||
#: 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."
|
||||||
msgstr "{}개의 파일을 휴지통으로 보내고 있습니다."
|
msgstr "{}개 파일을 휴지통으로 보내려고 합니다."
|
||||||
|
|
||||||
#: core\gui\exclude_list_table.py:14
|
#: core\gui\exclude_list_table.py:14
|
||||||
msgid "Regular Expressions"
|
msgid "Regular Expressions"
|
||||||
@@ -163,7 +164,7 @@ msgstr "파일 이름 - 필드"
|
|||||||
|
|
||||||
#: core\me\scanner.py:22
|
#: core\me\scanner.py:22
|
||||||
msgid "Filename - Fields (No Order)"
|
msgid "Filename - Fields (No Order)"
|
||||||
msgstr "파일 이름 - 필드 (주문 없음)"
|
msgstr "파일 이름 - 필드 (순서 없음)"
|
||||||
|
|
||||||
#: core\me\scanner.py:23
|
#: core\me\scanner.py:23
|
||||||
msgid "Tags"
|
msgid "Tags"
|
||||||
@@ -191,7 +192,7 @@ msgstr "%d/%d 일치 확인"
|
|||||||
|
|
||||||
#: core\pe\matchexif.py:19
|
#: core\pe\matchexif.py:19
|
||||||
msgid "Read EXIF of %d/%d pictures"
|
msgid "Read EXIF of %d/%d pictures"
|
||||||
msgstr "%d/%d 사진의 EXIF 읽기"
|
msgstr "사진 EXIF 읽는 중: %d/%d"
|
||||||
|
|
||||||
#: core\pe\scanner.py:22
|
#: core\pe\scanner.py:22
|
||||||
msgid "EXIF Timestamp"
|
msgid "EXIF Timestamp"
|
||||||
@@ -201,35 +202,35 @@ msgstr "EXIF 타임 스탬프"
|
|||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr "없음"
|
msgstr "없음"
|
||||||
|
|
||||||
#: core\prioritize.py:100
|
#: core\prioritize.py:102
|
||||||
msgid "Ends with number"
|
msgid "Ends with number"
|
||||||
msgstr "숫자로 끝남"
|
msgstr "숫자로 끝남"
|
||||||
|
|
||||||
#: core\prioritize.py:101
|
#: core\prioritize.py:103
|
||||||
msgid "Doesn't end with number"
|
msgid "Doesn't end with number"
|
||||||
msgstr "숫자로 끝나지 않음"
|
msgstr "숫자로 끝나지 않음"
|
||||||
|
|
||||||
#: core\prioritize.py:102
|
#: core\prioritize.py:104
|
||||||
msgid "Longest"
|
msgid "Longest"
|
||||||
msgstr "최장"
|
msgstr "최장"
|
||||||
|
|
||||||
#: core\prioritize.py:103
|
#: core\prioritize.py:105
|
||||||
msgid "Shortest"
|
msgid "Shortest"
|
||||||
msgstr "최단"
|
msgstr "최단"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Highest"
|
msgid "Highest"
|
||||||
msgstr "제일 높은"
|
msgstr "제일 높은"
|
||||||
|
|
||||||
#: core\prioritize.py:140
|
#: core\prioritize.py:142
|
||||||
msgid "Lowest"
|
msgid "Lowest"
|
||||||
msgstr "최저"
|
msgstr "최저"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
msgstr "최신"
|
msgstr "최신"
|
||||||
|
|
||||||
#: core\prioritize.py:169
|
#: core\prioritize.py:171
|
||||||
msgid "Oldest"
|
msgid "Oldest"
|
||||||
msgstr "가장 오래된"
|
msgstr "가장 오래된"
|
||||||
|
|
||||||
@@ -243,15 +244,15 @@ msgstr "필터: %s"
|
|||||||
|
|
||||||
#: core\scanner.py:90
|
#: core\scanner.py:90
|
||||||
msgid "Read size of %d/%d files"
|
msgid "Read size of %d/%d files"
|
||||||
msgstr "%d/%d 개의 파일을 읽을 수 있습니다."
|
msgstr "파일 크기 읽는 중: %d/%d"
|
||||||
|
|
||||||
#: core\scanner.py:116
|
#: core\scanner.py:116
|
||||||
msgid "Read metadata of %d/%d files"
|
msgid "Read metadata of %d/%d files"
|
||||||
msgstr "%d/%d 개 파일의 메타 데이터를 읽었습니다."
|
msgstr "파일 메타데이터 읽는 중: %d/%d"
|
||||||
|
|
||||||
#: core\scanner.py:154
|
#: core\scanner.py:154
|
||||||
msgid "Almost done! Fiddling with results..."
|
msgid "Almost done! Fiddling with results..."
|
||||||
msgstr "거의 완료되었습니다! 결과를 만지작 거리는 중..."
|
msgstr "거의 완료되었습니다! 결과를 취합하고 있습니다."
|
||||||
|
|
||||||
#: core\se\scanner.py:18
|
#: core\se\scanner.py:18
|
||||||
msgid "Folders"
|
msgid "Folders"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
|
# Sangdon Lim, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Sangdon Lim, 2022\n"
|
||||||
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
"Language-Team: Korean (https://www.transifex.com/voltaicideas/teams/116153/ko/)\n"
|
||||||
"Language: ko\n"
|
"Language: ko\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -59,13 +60,13 @@ msgstr "삭제 옵션"
|
|||||||
|
|
||||||
#: 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"
|
||||||
msgstr "삭제 된 파일 연결"
|
msgstr "링크 생성"
|
||||||
|
|
||||||
#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"After having deleted a duplicate, place a link targeting the reference file "
|
"After having deleted a duplicate, place a link targeting the reference file "
|
||||||
"to replace the deleted file."
|
"to replace the deleted file."
|
||||||
msgstr "복제본을 삭제 한 후 참조 파일을 대상으로하는 링크를 배치하여 삭제 된 파일을 대체합니다."
|
msgstr "중복 파일들을 삭제한 후 원본 파일을 참조하는 링크로 대체합니다."
|
||||||
|
|
||||||
#: qt/deletion_options.py:44
|
#: qt/deletion_options.py:44
|
||||||
msgid "Hardlink"
|
msgid "Hardlink"
|
||||||
@@ -73,7 +74,7 @@ msgstr "하드링크"
|
|||||||
|
|
||||||
#: qt/deletion_options.py:44
|
#: qt/deletion_options.py:44
|
||||||
msgid "Symlink"
|
msgid "Symlink"
|
||||||
msgstr "심볼링크"
|
msgstr "심볼릭 링크"
|
||||||
|
|
||||||
#: qt/deletion_options.py:48
|
#: qt/deletion_options.py:48
|
||||||
msgid " (unsupported)"
|
msgid " (unsupported)"
|
||||||
@@ -87,12 +88,11 @@ msgstr "즉시 삭제"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Instead of sending files to trash, delete them directly. This option is "
|
"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."
|
"usually used as a workaround when the normal deletion method doesn't work."
|
||||||
msgstr ""
|
msgstr "파일을 휴지통으로 보내지 않고 바로 삭제합니다. 파일을 휴지통으로 보낼 수 없는 경우 등에 사용할 수 있습니다."
|
||||||
"파일을 휴지통으로 보내는 대신 직접 삭제하십시오. 이 옵션은 일반적으로 일반 삭제 방법이 작동하지 않는 경우 해결 방법으로 사용됩니다."
|
|
||||||
|
|
||||||
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Proceed"
|
msgid "Proceed"
|
||||||
msgstr "계속하다"
|
msgstr "실행"
|
||||||
|
|
||||||
#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0
|
#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
@@ -183,7 +183,7 @@ msgstr "저장되지 않은 결과"
|
|||||||
|
|
||||||
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "You have unsaved results, do you really want to quit?"
|
msgid "You have unsaved results, do you really want to quit?"
|
||||||
msgstr "저장되지 않은 결과가 있습니다. 종료 하시겠습니까?"
|
msgstr "저장되지 않은 결과가 있습니다. 종료하시겠습니까?"
|
||||||
|
|
||||||
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
|
#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Select a folder to add to the scanning list"
|
msgid "Select a folder to add to the scanning list"
|
||||||
@@ -285,7 +285,7 @@ msgstr "유사한 단어와 일치"
|
|||||||
#: 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
|
||||||
msgid "Can mix file kind"
|
msgid "Can mix file kind"
|
||||||
msgstr "파일 종류 혼합 가능"
|
msgstr "다른 확장자의 파일도 비교에 포함"
|
||||||
|
|
||||||
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23
|
||||||
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0
|
||||||
@@ -300,7 +300,7 @@ msgstr "삭제 또는 이동시 빈 폴더 제거"
|
|||||||
#: 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 "동일한 파일에 대한 중복 하드 링크 무시"
|
msgstr "동일한 파일에 대한 중복 하드링크 무시"
|
||||||
|
|
||||||
#: 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
|
||||||
@@ -309,7 +309,7 @@ msgstr "디버그 모드 (다시 시작 필요)"
|
|||||||
|
|
||||||
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0
|
||||||
msgid "Match pictures of different dimensions"
|
msgid "Match pictures of different dimensions"
|
||||||
msgstr "다른 차원의 사진 일치"
|
msgstr "가로세로 크기가 다른 이미지도 비교"
|
||||||
|
|
||||||
#: qt/preferences_dialog.py:43
|
#: qt/preferences_dialog.py:43
|
||||||
msgid "Filter Hardness:"
|
msgid "Filter Hardness:"
|
||||||
@@ -930,40 +930,194 @@ msgstr "디스플레이"
|
|||||||
|
|
||||||
#: qt\se\preferences_dialog.py:70
|
#: qt\se\preferences_dialog.py:70
|
||||||
msgid "Partially hash files bigger than"
|
msgid "Partially hash files bigger than"
|
||||||
msgstr ""
|
msgstr "다음보다 큰 파일은 일부만 해시"
|
||||||
|
|
||||||
#: qt\se\preferences_dialog.py:80
|
#: qt\se\preferences_dialog.py:80
|
||||||
msgid "MB"
|
msgid "MB"
|
||||||
msgstr ""
|
msgstr "MB"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:163
|
#: qt\preferences_dialog.py:163
|
||||||
msgid "Use native OS dialogs"
|
msgid "Use native OS dialogs"
|
||||||
msgstr ""
|
msgstr "OS 자체 인터페이스 사용"
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:166
|
#: qt\preferences_dialog.py:166
|
||||||
msgid ""
|
msgid ""
|
||||||
"For actions such as file/folder selection use the OS native dialogs.\n"
|
"For actions such as file/folder selection use the OS native dialogs.\n"
|
||||||
"Some native dialogs have limited functionality."
|
"Some native dialogs have limited functionality."
|
||||||
msgstr ""
|
msgstr "파일 및 폴더 선택에 OS 자체 인터페이스를 사용합니다."
|
||||||
|
|
||||||
#: qt\se\preferences_dialog.py:68
|
#: qt\se\preferences_dialog.py:68
|
||||||
msgid "Ignore files larger than"
|
msgid "Ignore files larger than"
|
||||||
msgstr ""
|
msgstr "다음보다 큰 파일 무시"
|
||||||
|
|
||||||
#: qt\app.py:135 qt\app.py:293
|
#: qt\app.py:135 qt\app.py:293
|
||||||
msgid "Clear Cache"
|
msgid "Clear Cache"
|
||||||
msgstr ""
|
msgstr "캐시 제거"
|
||||||
|
|
||||||
#: qt\app.py:294
|
#: qt\app.py:294
|
||||||
msgid ""
|
msgid ""
|
||||||
"Do you really want to clear the cache? This will remove all cached file "
|
"Do you really want to clear the cache? This will remove all cached file "
|
||||||
"hashes and picture analysis."
|
"hashes and picture analysis."
|
||||||
msgstr ""
|
msgstr "캐시를 제거할까요? 캐시에는 파일 해시 및 이미지 분석 결과가 포함되어 있습니다."
|
||||||
|
|
||||||
#: qt\app.py:299
|
#: qt\app.py:299
|
||||||
msgid "Cache cleared."
|
msgid "Cache cleared."
|
||||||
msgstr ""
|
msgstr "캐시를 제거했습니다."
|
||||||
|
|
||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
|
msgstr "다크 모드 사용"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr "디버그"
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "{} 에 대한정보"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "버전 {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr "업데이트 확인 중..."
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "GPLv3 라이센스"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr "새 업데이트가 없습니다."
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr "새 버전 {}이 있습니다. 다운로드: <a href=\"{}\">링크</a>"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "오류보고"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "문제가 발생했습니다. 오류를보고하는 것은 어떻습니까?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"오류 보고서는 Github 문제로보고해야합니다. 위의 오류 추적을 복사하여 새 문제에 붙여 넣을 수 있습니다.\n"
|
||||||
|
"\n"
|
||||||
|
"이미 존재하는 문제에 대해 사전에 검색을 실행하십시오. 또한 경험하고있는 버그가 이미 패치되었을 수 있으므로 저장소에서 사용 가능한 최신 버전을 테스트해야합니다.\n"
|
||||||
|
"\n"
|
||||||
|
"일반적으로 실제로 도움이되는 것은 오류가 발생한 방법에 대한 설명을 추가하는 것입니다. 감사!\n"
|
||||||
|
"\n"
|
||||||
|
"이 오류 후에도 응용 프로그램이 계속 실행되어야하지만 불안정한 상태 일 수 있으므로 응용 프로그램을 다시 시작하는 것이 좋습니다."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Github로 이동"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "체코어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "독일어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "그리스어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "영어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "스페인어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "프랑스어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "아르메니아어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "이탈리아어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "일본어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "한국어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "말레이어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "네덜란드어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "폴란드어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "브라질 언어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "러시아어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "터키어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "우크라이나어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "베트남어"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "중국어 (간체)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "목록 지우기"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "검색.."
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2022
|
# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -988,3 +989,157 @@ msgstr "Cache dikosongkan."
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr "Guna gaya gelap"
|
msgstr "Guna gaya gelap"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr "Bukah operasi imbasan"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr "Membukah operasi imbasan dan simpan log untuk pengoptimuman."
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr "Log terletak di: <a href=\"{}\">{}</a>"
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr "Nyahpepijat"
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "Mengenai {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Versi {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr "Memeriksa kemas kini..."
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Dilesenkan bawah GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr "Tiada kemas kini tersedia."
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr "Versi baharu {} tersedia, muat turun <a href=\"{}\">di sini</a>."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Laporan Ralat"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Terdapat kesulitan yang terjadi. Apa kata laporkan ralat tersebut?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Laporan ralat patut dilaporkan sebagai isu Github. Anda boleh salin runut balik ralat di atas dan tampal dalam isu baharu.\n"
|
||||||
|
"\n"
|
||||||
|
"Sila pastikan anda menggelintar dahulu kalau-kalau isu sudah wujud. Juga pastikan untuk cuba versi paling terbaharu yang disediakan dari repositori, kerana pepijat yang anda alami mungkin sudah ditampung.\n"
|
||||||
|
"\n"
|
||||||
|
"Ia sangat membantu sekiranya anda tambah keterangan mengenai bagaimana anda dapat ralat tersebut. Terima kasih!\n"
|
||||||
|
"\n"
|
||||||
|
"Walaupun aplikasi sepatutnya masih boleh digunakan selepas ralat ini, ia mungkin berada dalam keadaan tidak stabil, jadi anda digalakkan untuk memulakan semula aplikasi ini."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Pergi ke Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Czech"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Jerman"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Yunani"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Inggeris"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Sepanyol"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Perancis"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Armenia"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Itali"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Jepun"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Korea"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Melayu"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Belanda"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Poland"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brazil"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Rusia"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turki"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ukraine"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnam"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Cina (Ringkas)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Kosongkan Senarai"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Cari..."
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Bas <duvel3@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Bas <duvel3@gmail.com>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Bas <duvel3@gmail.com>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\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"
|
||||||
@@ -985,3 +985,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "Over {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Versie {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Licentie verleend onder GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Foutenrapport"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Er is iets fout gegaan. Hoe zit het met het melden van de fout?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Foutrapporten moeten worden gerapporteerd als Github-problemen. U kunt de bovenstaande foutopsporing kopiëren en in een nieuwe uitgave plakken.\n"
|
||||||
|
"\n"
|
||||||
|
"Zorg ervoor dat u van tevoren een zoekopdracht uitvoert naar reeds bestaande problemen. Zorg er ook voor dat u de allernieuwste versie uit de repository test, aangezien de bug die u ondervindt mogelijk al gepatcht is.\n"
|
||||||
|
"\n"
|
||||||
|
"Wat meestal echt helpt, is als je een beschrijving toevoegt van hoe je de fout hebt gekregen. Bedankt!\n"
|
||||||
|
"\n"
|
||||||
|
"Hoewel de toepassing na deze fout zou moeten blijven werken, kan deze in een onstabiele toestand verkeren, dus het wordt aanbevolen de toepassing opnieuw te starten."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Ga naar Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Tsjechisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Duits"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Grieks"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Engels"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Spaans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Frans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Armeens"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Italiaans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Koreaans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Maleis"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Nederlands"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Pools"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Braziliaans"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Russisch"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turks"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Oekraïens"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Vietnamees"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "(Versimpeld) Chinees"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Lijst leegmaken"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Zoeken..."
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Translators:
|
# Translators:
|
||||||
# Andrew Senetar <arsenetar@gmail.com>, 2021
|
# Andrew Senetar <arsenetar@gmail.com>, 2022
|
||||||
# Fuan <jcfrt@posteo.net>, 2021
|
# Fuan <jcfrt@posteo.net>, 2022
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\n"
|
"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\n"
|
||||||
"Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\n"
|
"Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\n"
|
||||||
"Language: pl_PL\n"
|
"Language: pl_PL\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@@ -983,3 +983,157 @@ msgstr ""
|
|||||||
#: qt\preferences_dialog.py:173
|
#: qt\preferences_dialog.py:173
|
||||||
msgid "Use dark style"
|
msgid "Use dark style"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:241
|
||||||
|
msgid "Profile scan operation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:242
|
||||||
|
msgid "Profile the scan operation and save logs for optimization."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:246
|
||||||
|
msgid "Logs located in: <a href=\"{}\">{}</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\preferences_dialog.py:291
|
||||||
|
msgid "Debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:31
|
||||||
|
msgid "About {}"
|
||||||
|
msgstr "O {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:47
|
||||||
|
msgid "Version {}"
|
||||||
|
msgstr "Wersja {}"
|
||||||
|
|
||||||
|
#: qt\about_box.py:49 qt\about_box.py:75
|
||||||
|
msgid "Checking for updates..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:54
|
||||||
|
msgid "Licensed under GPLv3"
|
||||||
|
msgstr "Licencjonowany w ramach GPLv3"
|
||||||
|
|
||||||
|
#: qt\about_box.py:68
|
||||||
|
msgid "No update available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\about_box.py:71
|
||||||
|
msgid "New version {} available, download <a href=\"{}\">here</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:50
|
||||||
|
msgid "Error Report"
|
||||||
|
msgstr "Raport błędów"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:54
|
||||||
|
msgid "Something went wrong. How about reporting the error?"
|
||||||
|
msgstr "Coś poszło nie tak. Co powiesz na zgłoszenie błędu?"
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:60
|
||||||
|
msgid ""
|
||||||
|
"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\n"
|
||||||
|
"\n"
|
||||||
|
"Please make sure to run a search for any already existing issues beforehand. Also make sure to test the very latest version available from the repository, since the bug you are experiencing might have already been patched.\n"
|
||||||
|
"\n"
|
||||||
|
"What usually really helps is if you add a description of how you got the error. Thanks!\n"
|
||||||
|
"\n"
|
||||||
|
"Although the application should continue to run after this error, it may be in an unstable state, so it is recommended that you restart the application."
|
||||||
|
msgstr ""
|
||||||
|
"Raporty o błędach powinny być zgłaszane jako problemy z Github. Możesz skopiować powyższy opis błędu i wkleić go w nowym zgłoszeniu.\n"
|
||||||
|
"\n"
|
||||||
|
"Upewnij się, że wcześniej wyszukałeś już istniejący bilet. Upewnij się również, że przetestowałeś najnowszą wersję dostępną w repozytorium, ponieważ napotkany błąd mógł już zostać załatany.\n"
|
||||||
|
"\n"
|
||||||
|
"To, co zwykle naprawdę pomaga, to dodanie opisu tego, w jaki sposób wystąpił błąd. Dzięki!\n"
|
||||||
|
"\n"
|
||||||
|
"Chociaż aplikacja powinna nadal działać po tym błędzie, może być w stanie niestabilnym, dlatego zaleca się ponowne uruchomienie aplikacji."
|
||||||
|
|
||||||
|
#: qt\error_report_dialog.py:80
|
||||||
|
msgid "Go to Github"
|
||||||
|
msgstr "Przejdź do Github"
|
||||||
|
|
||||||
|
#: qt\preferences.py:24
|
||||||
|
msgid "Czech"
|
||||||
|
msgstr "Czech"
|
||||||
|
|
||||||
|
#: qt\preferences.py:25
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Niemiecki"
|
||||||
|
|
||||||
|
#: qt\preferences.py:26
|
||||||
|
msgid "Greek"
|
||||||
|
msgstr "Grecki"
|
||||||
|
|
||||||
|
#: qt\preferences.py:27
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Angielski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:28
|
||||||
|
msgid "Spanish"
|
||||||
|
msgstr "Hiszpański"
|
||||||
|
|
||||||
|
#: qt\preferences.py:29
|
||||||
|
msgid "French"
|
||||||
|
msgstr "Francuski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:30
|
||||||
|
msgid "Armenian"
|
||||||
|
msgstr "Ormiański"
|
||||||
|
|
||||||
|
#: qt\preferences.py:31
|
||||||
|
msgid "Italian"
|
||||||
|
msgstr "Włoski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:32
|
||||||
|
msgid "Japanese"
|
||||||
|
msgstr "Japońsku"
|
||||||
|
|
||||||
|
#: qt\preferences.py:33
|
||||||
|
msgid "Korean"
|
||||||
|
msgstr "Koreański"
|
||||||
|
|
||||||
|
#: qt\preferences.py:34
|
||||||
|
msgid "Malay"
|
||||||
|
msgstr "Malajski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:35
|
||||||
|
msgid "Dutch"
|
||||||
|
msgstr "Holenderski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:36
|
||||||
|
msgid "Polish"
|
||||||
|
msgstr "Polskie"
|
||||||
|
|
||||||
|
#: qt\preferences.py:37
|
||||||
|
msgid "Brazilian"
|
||||||
|
msgstr "Brazylijski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:38
|
||||||
|
msgid "Russian"
|
||||||
|
msgstr "Rosyjski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:39
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr "Turecki"
|
||||||
|
|
||||||
|
#: qt\preferences.py:40
|
||||||
|
msgid "Ukrainian"
|
||||||
|
msgstr "Ukraiński"
|
||||||
|
|
||||||
|
#: qt\preferences.py:41
|
||||||
|
msgid "Vietnamese"
|
||||||
|
msgstr "Wietnamski"
|
||||||
|
|
||||||
|
#: qt\preferences.py:42
|
||||||
|
msgid "Chinese (Simplified)"
|
||||||
|
msgstr "Chiński (uproszczony)"
|
||||||
|
|
||||||
|
#: qt\recent.py:54
|
||||||
|
msgid "Clear List"
|
||||||
|
msgstr "Wyczyść listę"
|
||||||
|
|
||||||
|
#: qt\search_edit.py:78
|
||||||
|
msgid "Search..."
|
||||||
|
msgstr "Szukaj..."
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user