mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-03-10 05:34:36 +00:00
Merge core_se.app into core.app
This commit is contained in:
parent
9a25670552
commit
773f6651e6
141
core/app.py
141
core/app.py
@ -9,7 +9,6 @@ import os.path as op
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
@ -18,14 +17,26 @@ from hscommon.notify import Broadcaster
|
|||||||
from hscommon.path import Path
|
from hscommon.path import Path
|
||||||
from hscommon.conflict import smart_move, smart_copy
|
from hscommon.conflict import smart_move, smart_copy
|
||||||
from hscommon.gui.progress_window import ProgressWindow
|
from hscommon.gui.progress_window import ProgressWindow
|
||||||
from hscommon.util import delete_if_empty, first, escape, nonone, format_time_decimal, allsame
|
from hscommon.util import delete_if_empty, first, escape, nonone, allsame
|
||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from hscommon.plat import ISWINDOWS
|
|
||||||
from hscommon import desktop
|
from hscommon import desktop
|
||||||
|
|
||||||
from . import directories, results, export, fs
|
import core_se.fs
|
||||||
|
import core_se.result_table
|
||||||
|
import core_se.scanner
|
||||||
|
import core_me.fs
|
||||||
|
import core_me.prioritize
|
||||||
|
import core_me.result_table
|
||||||
|
import core_me.scanner
|
||||||
|
import core_pe.photo
|
||||||
|
import core_pe.prioritize
|
||||||
|
import core_pe.result_table
|
||||||
|
import core_pe.scanner
|
||||||
|
from core_pe.photo import get_delta_dimensions
|
||||||
|
from .util import cmp_value, fix_surrogate_encoding
|
||||||
|
from . import directories, results, export, fs, prioritize
|
||||||
from .ignore import IgnoreList
|
from .ignore import IgnoreList
|
||||||
from .scanner import ScanType, Scanner
|
from .scanner import ScanType
|
||||||
from .gui.deletion_options import DeletionOptions
|
from .gui.deletion_options import DeletionOptions
|
||||||
from .gui.details_panel import DetailsPanel
|
from .gui.details_panel import DetailsPanel
|
||||||
from .gui.directory_tree import DirectoryTree
|
from .gui.directory_tree import DirectoryTree
|
||||||
@ -67,53 +78,6 @@ JOBID2TITLE = {
|
|||||||
JobType.Copy: tr("Copying"),
|
JobType.Copy: tr("Copying"),
|
||||||
JobType.Delete: tr("Sending to Trash"),
|
JobType.Delete: tr("Sending to Trash"),
|
||||||
}
|
}
|
||||||
if ISWINDOWS:
|
|
||||||
JOBID2TITLE[JobType.Delete] = tr("Sending files to the recycle bin")
|
|
||||||
|
|
||||||
def format_timestamp(t, delta):
|
|
||||||
if delta:
|
|
||||||
return format_time_decimal(t)
|
|
||||||
else:
|
|
||||||
if t > 0:
|
|
||||||
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
|
|
||||||
else:
|
|
||||||
return '---'
|
|
||||||
|
|
||||||
def format_words(w):
|
|
||||||
def do_format(w):
|
|
||||||
if isinstance(w, list):
|
|
||||||
return '(%s)' % ', '.join(do_format(item) for item in w)
|
|
||||||
else:
|
|
||||||
return w.replace('\n', ' ')
|
|
||||||
|
|
||||||
return ', '.join(do_format(item) for item in w)
|
|
||||||
|
|
||||||
def format_perc(p):
|
|
||||||
return "%0.0f" % p
|
|
||||||
|
|
||||||
def format_dupe_count(c):
|
|
||||||
return str(c) if c else '---'
|
|
||||||
|
|
||||||
def cmp_value(dupe, attrname):
|
|
||||||
value = getattr(dupe, attrname, '')
|
|
||||||
return value.lower() if isinstance(value, str) else value
|
|
||||||
|
|
||||||
def fix_surrogate_encoding(s, encoding='utf-8'):
|
|
||||||
# ref #210. It's possible to end up with file paths that, while correct unicode strings, are
|
|
||||||
# decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix
|
|
||||||
# these strings here by trying to encode them and, if it fails, we do an encode/decode dance
|
|
||||||
# to remove the problematic characters. This dance is *lossy* but there's not much we can do
|
|
||||||
# because if we end up with this type of string, it means that we don't know the encoding of the
|
|
||||||
# underlying filesystem that brought them. Don't use this for strings you're going to re-use in
|
|
||||||
# fs-related functions because you're going to lose your path (it's going to change). Use this
|
|
||||||
# if you need to export the path somewhere else, outside of the unicode realm.
|
|
||||||
# See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/
|
|
||||||
try:
|
|
||||||
s.encode(encoding)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
return s.encode(encoding, 'replace').decode(encoding)
|
|
||||||
else:
|
|
||||||
return s
|
|
||||||
|
|
||||||
class DupeGuru(Broadcaster):
|
class DupeGuru(Broadcaster):
|
||||||
"""Holds everything together.
|
"""Holds everything together.
|
||||||
@ -160,8 +124,7 @@ class DupeGuru(Broadcaster):
|
|||||||
# select_dest_folder(prompt: str) --> str
|
# select_dest_folder(prompt: str) --> str
|
||||||
# select_dest_file(prompt: str, ext: str) --> str
|
# select_dest_file(prompt: str, ext: str) --> str
|
||||||
|
|
||||||
PROMPT_NAME = "dupeGuru"
|
NAME = PROMPT_NAME = "dupeGuru"
|
||||||
SCANNER_CLASS = Scanner
|
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
if view.get_default(DEBUG_MODE_PREFERENCE):
|
if view.get_default(DEBUG_MODE_PREFERENCE):
|
||||||
@ -185,6 +148,7 @@ class DupeGuru(Broadcaster):
|
|||||||
'clean_empty_dirs': False,
|
'clean_empty_dirs': False,
|
||||||
'ignore_hardlink_matches': False,
|
'ignore_hardlink_matches': False,
|
||||||
'copymove_dest_type': DestType.Relative,
|
'copymove_dest_type': DestType.Relative,
|
||||||
|
'cache_path': op.join(self.appdata, 'cached_pictures.db'),
|
||||||
}
|
}
|
||||||
self.selected_dupes = []
|
self.selected_dupes = []
|
||||||
self.details_panel = DetailsPanel(self)
|
self.details_panel = DetailsPanel(self)
|
||||||
@ -199,15 +163,25 @@ class DupeGuru(Broadcaster):
|
|||||||
for child in children:
|
for child in children:
|
||||||
child.connect()
|
child.connect()
|
||||||
|
|
||||||
#--- Virtual
|
|
||||||
def _prioritization_categories(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def _create_result_table(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
|
def _create_result_table(self):
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
return core_pe.result_table.ResultTable(self)
|
||||||
|
elif self.app_mode == AppMode.Music:
|
||||||
|
return core_me.result_table.ResultTable(self)
|
||||||
|
else:
|
||||||
|
return core_se.result_table.ResultTable(self)
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
|
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||||
|
if key == 'folder_path':
|
||||||
|
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
||||||
|
return str(dupe_folder_path).lower()
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
if delta and key == 'dimensions':
|
||||||
|
r = cmp_value(dupe, key)
|
||||||
|
ref_value = cmp_value(get_group().ref, key)
|
||||||
|
return get_delta_dimensions(r, ref_value)
|
||||||
if key == 'marked':
|
if key == 'marked':
|
||||||
return self.results.is_marked(dupe)
|
return self.results.is_marked(dupe)
|
||||||
if key == 'percentage':
|
if key == 'percentage':
|
||||||
@ -227,6 +201,10 @@ class DupeGuru(Broadcaster):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
|
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||||
|
if key == 'folder_path':
|
||||||
|
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
||||||
|
return str(dupe_folder_path).lower()
|
||||||
if key == 'percentage':
|
if key == 'percentage':
|
||||||
return group.percentage
|
return group.percentage
|
||||||
if key == 'dupe_count':
|
if key == 'dupe_count':
|
||||||
@ -354,6 +332,15 @@ class DupeGuru(Broadcaster):
|
|||||||
self.selected_dupes = dupes
|
self.selected_dupes = dupes
|
||||||
self.notify('dupes_selected')
|
self.notify('dupes_selected')
|
||||||
|
|
||||||
|
#--- Protected
|
||||||
|
def _prioritization_categories(self):
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
return core_pe.prioritize.all_categories()
|
||||||
|
elif self.app_mode == AppMode.Music:
|
||||||
|
return core_me.prioritize.all_categories()
|
||||||
|
else:
|
||||||
|
return prioritize.all_categories()
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def add_directory(self, d):
|
def add_directory(self, d):
|
||||||
"""Adds folder ``d`` to :attr:`directories`.
|
"""Adds folder ``d`` to :attr:`directories`.
|
||||||
@ -767,7 +754,7 @@ class DupeGuru(Broadcaster):
|
|||||||
def do(j):
|
def do(j):
|
||||||
j.set_progress(0, tr("Collecting files to scan"))
|
j.set_progress(0, tr("Collecting files to scan"))
|
||||||
if scanner.scan_type == ScanType.Folders:
|
if scanner.scan_type == ScanType.Folders:
|
||||||
files = list(self.directories.get_folders(folderclass=self.folderclass, j=j))
|
files = list(self.directories.get_folders(folderclass=core_se.fs.folder, j=j))
|
||||||
else:
|
else:
|
||||||
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
||||||
if self.options['ignore_hardlink_matches']:
|
if self.options['ignore_hardlink_matches']:
|
||||||
@ -816,3 +803,33 @@ class DupeGuru(Broadcaster):
|
|||||||
result = tr("%s (%d discarded)") % (result, self.discarded_file_count)
|
result = tr("%s (%d discarded)") % (result, self.discarded_file_count)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fileclasses(self):
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
return [core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
|
||||||
|
elif self.app_mode == AppMode.Music:
|
||||||
|
return [core_me.fs.MusicFile]
|
||||||
|
else:
|
||||||
|
return [core_se.fs.File]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SCANNER_CLASS(self):
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
return core_pe.scanner.ScannerPE
|
||||||
|
elif self.app_mode == AppMode.Music:
|
||||||
|
return core_me.scanner.ScannerME
|
||||||
|
else:
|
||||||
|
return core_se.scanner.ScannerSE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def METADATA_TO_READ(self):
|
||||||
|
if self.app_mode == AppMode.Picture:
|
||||||
|
return ['size', 'mtime', 'dimensions', 'exif_timestamp']
|
||||||
|
elif self.app_mode == AppMode.Music:
|
||||||
|
return [
|
||||||
|
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||||
|
'album', 'genre', 'year', 'track', 'comment'
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return ['size', 'mtime']
|
||||||
|
|
||||||
|
56
core/util.py
Normal file
56
core/util.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
||||||
|
#
|
||||||
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
||||||
|
# which should be included with this package. The terms are also available at
|
||||||
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from hscommon.util import format_time_decimal
|
||||||
|
|
||||||
|
def format_timestamp(t, delta):
|
||||||
|
if delta:
|
||||||
|
return format_time_decimal(t)
|
||||||
|
else:
|
||||||
|
if t > 0:
|
||||||
|
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
|
||||||
|
else:
|
||||||
|
return '---'
|
||||||
|
|
||||||
|
def format_words(w):
|
||||||
|
def do_format(w):
|
||||||
|
if isinstance(w, list):
|
||||||
|
return '(%s)' % ', '.join(do_format(item) for item in w)
|
||||||
|
else:
|
||||||
|
return w.replace('\n', ' ')
|
||||||
|
|
||||||
|
return ', '.join(do_format(item) for item in w)
|
||||||
|
|
||||||
|
def format_perc(p):
|
||||||
|
return "%0.0f" % p
|
||||||
|
|
||||||
|
def format_dupe_count(c):
|
||||||
|
return str(c) if c else '---'
|
||||||
|
|
||||||
|
def cmp_value(dupe, attrname):
|
||||||
|
value = getattr(dupe, attrname, '')
|
||||||
|
return value.lower() if isinstance(value, str) else value
|
||||||
|
|
||||||
|
def fix_surrogate_encoding(s, encoding='utf-8'):
|
||||||
|
# ref #210. It's possible to end up with file paths that, while correct unicode strings, are
|
||||||
|
# decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix
|
||||||
|
# these strings here by trying to encode them and, if it fails, we do an encode/decode dance
|
||||||
|
# to remove the problematic characters. This dance is *lossy* but there's not much we can do
|
||||||
|
# because if we end up with this type of string, it means that we don't know the encoding of the
|
||||||
|
# underlying filesystem that brought them. Don't use this for strings you're going to re-use in
|
||||||
|
# fs-related functions because you're going to lose your path (it's going to change). Use this
|
||||||
|
# if you need to export the path somewhere else, outside of the unicode realm.
|
||||||
|
# See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/
|
||||||
|
try:
|
||||||
|
s.encode(encoding)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
return s.encode(encoding, 'replace').decode(encoding)
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
|||||||
from hsaudiotag import auto
|
from hsaudiotag import auto
|
||||||
from hscommon.util import get_file_ext, format_size, format_time
|
from hscommon.util import get_file_ext, format_size, format_time
|
||||||
|
|
||||||
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
|
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
|
||||||
from core import fs
|
from core import fs
|
||||||
|
|
||||||
TAG_FIELDS = {
|
TAG_FIELDS = {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from hscommon.util import get_file_ext, format_size
|
from hscommon.util import get_file_ext, format_size
|
||||||
|
|
||||||
from core.app import format_timestamp, format_perc, format_dupe_count
|
from core.util import format_timestamp, format_perc, format_dupe_count
|
||||||
from core import fs
|
from core import fs
|
||||||
from . import exif
|
from . import exif
|
||||||
|
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
|
|
||||||
#
|
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
||||||
# which should be included with this package. The terms are also available at
|
|
||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
import os.path as op
|
|
||||||
|
|
||||||
from core.app import DupeGuru as DupeGuruBase, AppMode, cmp_value
|
|
||||||
from core import prioritize
|
|
||||||
import core_me.fs
|
|
||||||
import core_me.prioritize
|
|
||||||
import core_me.result_table
|
|
||||||
import core_me.scanner
|
|
||||||
import core_pe.photo
|
|
||||||
import core_pe.prioritize
|
|
||||||
import core_pe.result_table
|
|
||||||
import core_pe.scanner
|
|
||||||
from core_pe.photo import get_delta_dimensions
|
|
||||||
from . import __appname__, fs, scanner
|
|
||||||
from .result_table import ResultTable
|
|
||||||
|
|
||||||
class DupeGuru(DupeGuruBase):
|
|
||||||
NAME = __appname__
|
|
||||||
|
|
||||||
def __init__(self, view):
|
|
||||||
DupeGuruBase.__init__(self, view)
|
|
||||||
self.folderclass = fs.Folder
|
|
||||||
self.options['cache_path'] = op.join(self.appdata, 'cached_pictures.db')
|
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
|
||||||
if key == 'folder_path':
|
|
||||||
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
|
||||||
return str(dupe_folder_path).lower()
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
if delta and key == 'dimensions':
|
|
||||||
r = cmp_value(dupe, key)
|
|
||||||
ref_value = cmp_value(get_group().ref, key)
|
|
||||||
return get_delta_dimensions(r, ref_value)
|
|
||||||
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
|
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
|
||||||
if key == 'folder_path':
|
|
||||||
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
|
||||||
return str(dupe_folder_path).lower()
|
|
||||||
return DupeGuruBase._get_group_sort_key(self, group, key)
|
|
||||||
|
|
||||||
def _prioritization_categories(self):
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
return core_pe.prioritize.all_categories()
|
|
||||||
elif self.app_mode == AppMode.Music:
|
|
||||||
return core_me.prioritize.all_categories()
|
|
||||||
else:
|
|
||||||
return prioritize.all_categories()
|
|
||||||
|
|
||||||
def _create_result_table(self):
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
return core_pe.result_table.ResultTable(self)
|
|
||||||
elif self.app_mode == AppMode.Music:
|
|
||||||
return core_me.result_table.ResultTable(self)
|
|
||||||
else:
|
|
||||||
return ResultTable(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fileclasses(self):
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
return [core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
|
|
||||||
elif self.app_mode == AppMode.Music:
|
|
||||||
return [core_me.fs.MusicFile]
|
|
||||||
else:
|
|
||||||
return [fs.File]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def SCANNER_CLASS(self):
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
return core_pe.scanner.ScannerPE
|
|
||||||
elif self.app_mode == AppMode.Music:
|
|
||||||
return core_me.scanner.ScannerME
|
|
||||||
else:
|
|
||||||
return scanner.ScannerSE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def METADATA_TO_READ(self):
|
|
||||||
if self.app_mode == AppMode.Picture:
|
|
||||||
return ['size', 'mtime', 'dimensions', 'exif_timestamp']
|
|
||||||
elif self.app_mode == AppMode.Music:
|
|
||||||
return [
|
|
||||||
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
|
||||||
'album', 'genre', 'year', 'track', 'comment'
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
return ['size', 'mtime']
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2013-07-14
|
# Created On: 2013-07-14
|
||||||
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
# 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
|
# 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 hscommon.util import format_size
|
from hscommon.util import format_size
|
||||||
|
|
||||||
from core import fs
|
from core import fs
|
||||||
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
|
from core.util import format_timestamp, format_perc, format_words, format_dupe_count
|
||||||
|
|
||||||
def get_display_info(dupe, group, delta):
|
def get_display_info(dupe, group, delta):
|
||||||
size = dupe.size
|
size = dupe.size
|
||||||
@ -39,9 +39,9 @@ def get_display_info(dupe, group, delta):
|
|||||||
class File(fs.File):
|
class File(fs.File):
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
return get_display_info(self, group, delta)
|
return get_display_info(self, group, delta)
|
||||||
|
|
||||||
|
|
||||||
class Folder(fs.Folder):
|
class Folder(fs.Folder):
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
return get_display_info(self, group, delta)
|
return get_display_info(self, group, delta)
|
||||||
|
|
||||||
|
@ -19,8 +19,7 @@ from qtlib.recent import Recent
|
|||||||
from qtlib.util import createActions
|
from qtlib.util import createActions
|
||||||
from qtlib.progress_window import ProgressWindow
|
from qtlib.progress_window import ProgressWindow
|
||||||
|
|
||||||
from core.app import AppMode
|
from core.app import AppMode, DupeGuru as DupeGuruModel
|
||||||
from core_se.app import DupeGuru as DupeGuruModel
|
|
||||||
import core_pe.photo
|
import core_pe.photo
|
||||||
from . import platform
|
from . import platform
|
||||||
from .preferences import Preferences
|
from .preferences import Preferences
|
||||||
|
Loading…
x
Reference in New Issue
Block a user