add new button for refresh log disable
This commit is contained in:
@@ -1,11 +1,8 @@
|
|||||||
# pyimage/core/data_processing.py
|
# pyimage/core/data_processing.py
|
||||||
import os
|
import os
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import shutil
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
from core.pbp_app_config import AppConfig
|
||||||
from queue import Empty
|
|
||||||
from core.pbp_app_config import AppConfig, Msg
|
|
||||||
from shared_libs.logger import app_logger
|
from shared_libs.logger import app_logger
|
||||||
|
|
||||||
|
|
||||||
@@ -114,4 +111,4 @@ class DataProcessing:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if not stop_event.is_set():
|
if not stop_event.is_set():
|
||||||
self.app.queue.put((button_text, total_size, mode))
|
self.app.queue.put((button_text, total_size, mode))
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ from keyring.backends import SecretService
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
|
||||||
import stat
|
|
||||||
import re
|
|
||||||
import math
|
import math
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from core.pbp_app_config import AppConfig
|
from core.pbp_app_config import AppConfig
|
||||||
from pyimage_ui.password_dialog import PasswordDialog
|
from pyimage_ui.password_dialog import PasswordDialog
|
||||||
@@ -50,7 +47,8 @@ class EncryptionManager:
|
|||||||
|
|
||||||
def remove_from_lock_file(self, base_path):
|
def remove_from_lock_file(self, base_path):
|
||||||
locks = self._read_lock_file()
|
locks = self._read_lock_file()
|
||||||
updated_locks = [lock for lock in locks if lock['base_path'] != base_path]
|
updated_locks = [
|
||||||
|
lock for lock in locks if lock['base_path'] != base_path]
|
||||||
self._write_lock_file(updated_locks)
|
self._write_lock_file(updated_locks)
|
||||||
|
|
||||||
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||||
@@ -123,10 +121,12 @@ class EncryptionManager:
|
|||||||
|
|
||||||
def _get_password_or_key_cmd(self, base_dest_path: str, username: str) -> Tuple[str, Optional[str]]:
|
def _get_password_or_key_cmd(self, base_dest_path: str, username: str) -> Tuple[str, Optional[str]]:
|
||||||
# 1. Check cache and keyring (without triggering dialog)
|
# 1. Check cache and keyring (without triggering dialog)
|
||||||
password = self.password_cache.get(username) or self.get_password_from_keyring(username)
|
password = self.password_cache.get(
|
||||||
|
username) or self.get_password_from_keyring(username)
|
||||||
if password:
|
if password:
|
||||||
self.logger.log("Using password from cache or keyring for LUKS operation.")
|
self.logger.log(
|
||||||
self.password_cache[username] = password # ensure it's cached
|
"Using password from cache or keyring for LUKS operation.")
|
||||||
|
self.password_cache[username] = password # ensure it's cached
|
||||||
return "-", password
|
return "-", password
|
||||||
|
|
||||||
# 2. Check for key file
|
# 2. Check for key file
|
||||||
@@ -137,8 +137,10 @@ class EncryptionManager:
|
|||||||
return f'--key-file "{key_file_path}"'
|
return f'--key-file "{key_file_path}"'
|
||||||
|
|
||||||
# 3. If nothing found, prompt for password
|
# 3. If nothing found, prompt for password
|
||||||
self.logger.log("No password in keyring and no keyfile found. Prompting user.")
|
self.logger.log(
|
||||||
password = self.get_password(username, confirm=False) # This will now definitely open the dialog
|
"No password in keyring and no keyfile found. Prompting user.")
|
||||||
|
# This will now definitely open the dialog
|
||||||
|
password = self.get_password(username, confirm=False)
|
||||||
if not password:
|
if not password:
|
||||||
return "", None
|
return "", None
|
||||||
return "-", password
|
return "-", password
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class Msg:
|
|||||||
"log": _("Log"),
|
"log": _("Log"),
|
||||||
"full_backup": _("Full backup"),
|
"full_backup": _("Full backup"),
|
||||||
"incremental": _("Incremental"),
|
"incremental": _("Incremental"),
|
||||||
"incremental_backup": _("Incremental backup"), # New
|
"incremental_backup": _("Incremental backup"), # New
|
||||||
"test_run": _("Test run"),
|
"test_run": _("Test run"),
|
||||||
"start": _("Start"),
|
"start": _("Start"),
|
||||||
"cancel_backup": _("Cancel"),
|
"cancel_backup": _("Cancel"),
|
||||||
@@ -346,10 +346,11 @@ class Msg:
|
|||||||
"header_subtitle": _("Simple GUI for rsync"),
|
"header_subtitle": _("Simple GUI for rsync"),
|
||||||
"encrypted_backup_content": _("Encrypted Backups"),
|
"encrypted_backup_content": _("Encrypted Backups"),
|
||||||
"compressed": _("Compressed"),
|
"compressed": _("Compressed"),
|
||||||
"compression": _("Compression"), # New
|
"compression": _("Compression"), # New
|
||||||
"encrypted": _("Encrypted"),
|
"encrypted": _("Encrypted"),
|
||||||
"encryption": _("Encryption"), # New
|
"encryption": _("Encryption"), # New
|
||||||
"bypass_security": _("Bypass security"),
|
"bypass_security": _("Bypass security"),
|
||||||
|
"refresh_log": _("Refresh log"),
|
||||||
"comment": _("Kommentar"),
|
"comment": _("Kommentar"),
|
||||||
"force_full_backup": _("Always force full backup"),
|
"force_full_backup": _("Always force full backup"),
|
||||||
"force_incremental_backup": _("Always force incremental backup (except first)"),
|
"force_incremental_backup": _("Always force incremental backup (except first)"),
|
||||||
@@ -367,5 +368,5 @@ class Msg:
|
|||||||
"automation_settings_title": _("Automation Settings"), # New
|
"automation_settings_title": _("Automation Settings"), # New
|
||||||
"create_add_key_file": _("Create/Add Key File"), # New
|
"create_add_key_file": _("Create/Add Key File"), # New
|
||||||
"key_file_not_created": _("Key file not created."), # New
|
"key_file_not_created": _("Key file not created."), # New
|
||||||
"backup_options": _("Backup Options"), # New
|
"backup_options": _("Backup Options"), # New
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class MainApplication(tk.Tk):
|
|||||||
self.compressed_var = tk.BooleanVar()
|
self.compressed_var = tk.BooleanVar()
|
||||||
self.encrypted_var = tk.BooleanVar()
|
self.encrypted_var = tk.BooleanVar()
|
||||||
self.bypass_security_var = tk.BooleanVar()
|
self.bypass_security_var = tk.BooleanVar()
|
||||||
|
self.refresh_log_var = tk.BooleanVar(value=True)
|
||||||
|
|
||||||
self.mode = "backup" # Default mode
|
self.mode = "backup" # Default mode
|
||||||
self.backup_is_running = False
|
self.backup_is_running = False
|
||||||
@@ -177,6 +178,9 @@ class MainApplication(tk.Tk):
|
|||||||
self.bypass_security_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["bypass_security"],
|
self.bypass_security_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["bypass_security"],
|
||||||
variable=self.bypass_security_var, style="Switch2.TCheckbutton")
|
variable=self.bypass_security_var, style="Switch2.TCheckbutton")
|
||||||
self.bypass_security_cb.pack(fill=tk.X, pady=10)
|
self.bypass_security_cb.pack(fill=tk.X, pady=10)
|
||||||
|
self.refresh_log_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["refresh_log"],
|
||||||
|
variable=self.refresh_log_var, style="Switch2.TCheckbutton")
|
||||||
|
self.refresh_log_cb.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
self.header_frame = HeaderFrame(
|
self.header_frame = HeaderFrame(
|
||||||
self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
|
self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
|
||||||
@@ -333,6 +337,8 @@ class MainApplication(tk.Tk):
|
|||||||
def _load_state_and_initialize(self):
|
def _load_state_and_initialize(self):
|
||||||
# self.log_window.clear_log()
|
# self.log_window.clear_log()
|
||||||
last_mode = self.config_manager.get_setting("last_mode", "backup")
|
last_mode = self.config_manager.get_setting("last_mode", "backup")
|
||||||
|
refresh_log = self.config_manager.get_setting("refresh_log", True)
|
||||||
|
self.refresh_log_var.set(refresh_log)
|
||||||
|
|
||||||
backup_source_path = self.config_manager.get_setting(
|
backup_source_path = self.config_manager.get_setting(
|
||||||
"backup_source_path")
|
"backup_source_path")
|
||||||
@@ -529,6 +535,7 @@ class MainApplication(tk.Tk):
|
|||||||
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
|
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
|
self.config_manager.set_setting("refresh_log", self.refresh_log_var.get())
|
||||||
self.backup_manager.encryption_manager.unmount_all()
|
self.backup_manager.encryption_manager.unmount_all()
|
||||||
|
|
||||||
self.config_manager.set_setting("last_mode", self.mode)
|
self.config_manager.set_setting("last_mode", self.mode)
|
||||||
|
|||||||
@@ -10,23 +10,35 @@ from shared_libs.logger import app_logger
|
|||||||
from core.pbp_app_config import AppConfig
|
from core.pbp_app_config import AppConfig
|
||||||
|
|
||||||
# A simple logger for the CLI that just prints to the console
|
# A simple logger for the CLI that just prints to the console
|
||||||
|
|
||||||
|
|
||||||
class CliLogger:
|
class CliLogger:
|
||||||
def log(self, message):
|
def log(self, message):
|
||||||
print(f"[CLI] {message}")
|
print(f"[CLI] {message}")
|
||||||
|
|
||||||
def init_logger(self, log_method):
|
def init_logger(self, log_method):
|
||||||
pass # Not needed for CLI
|
pass # Not needed for CLI
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Py-Backup Command-Line Interface.")
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument("--backup-type", choices=['user', 'system'], required=True, help="Type of backup to perform.")
|
description="Py-Backup Command-Line Interface.")
|
||||||
parser.add_argument("--destination", required=True, help="Destination directory for the backup.")
|
parser.add_argument(
|
||||||
parser.add_argument("--source", help="Source directory for user backup. Required for --backup-type user.")
|
"--backup-type", choices=['user', 'system'], required=True, help="Type of backup to perform.")
|
||||||
parser.add_argument("--mode", choices=['full', 'incremental'], default='incremental', help="Mode for system backup.")
|
parser.add_argument("--destination", required=True,
|
||||||
parser.add_argument("--encrypted", action='store_true', help="Flag to indicate the backup should be encrypted.")
|
help="Destination directory for the backup.")
|
||||||
parser.add_argument("--key-file", help="Path to the key file for unlocking an encrypted container.")
|
parser.add_argument(
|
||||||
parser.add_argument("--password", help="Password for the encrypted container (use with caution). If --key-file is not provided, this will be used.")
|
"--source", help="Source directory for user backup. Required for --backup-type user.")
|
||||||
parser.add_argument("--compressed", action='store_true', help="Flag to indicate the backup should be compressed.")
|
parser.add_argument("--mode", choices=['full', 'incremental'],
|
||||||
|
default='incremental', help="Mode for system backup.")
|
||||||
|
parser.add_argument("--encrypted", action='store_true',
|
||||||
|
help="Flag to indicate the backup should be encrypted.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--key-file", help="Path to the key file for unlocking an encrypted container.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", help="Password for the encrypted container (use with caution). If --key-file is not provided, this will be used.")
|
||||||
|
parser.add_argument("--compressed", action='store_true',
|
||||||
|
help="Flag to indicate the backup should be compressed.")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -34,17 +46,19 @@ def main():
|
|||||||
parser.error("--source is required for --backup-type 'user'.")
|
parser.error("--source is required for --backup-type 'user'.")
|
||||||
|
|
||||||
if args.encrypted and not (args.key_file or args.password):
|
if args.encrypted and not (args.key_file or args.password):
|
||||||
parser.error("For encrypted backups, either --key-file or --password must be provided.")
|
parser.error(
|
||||||
|
"For encrypted backups, either --key-file or --password must be provided.")
|
||||||
|
|
||||||
cli_logger = CliLogger()
|
cli_logger = CliLogger()
|
||||||
backup_manager = BackupManager(cli_logger)
|
backup_manager = BackupManager(cli_logger)
|
||||||
queue = Queue() # Dummy queue for now, might be used for progress later
|
queue = Queue() # Dummy queue for now, might be used for progress later
|
||||||
|
|
||||||
source_path = "/" # Default for system backup
|
source_path = "/" # Default for system backup
|
||||||
if args.backup_type == 'user':
|
if args.backup_type == 'user':
|
||||||
source_path = args.source
|
source_path = args.source
|
||||||
if not os.path.isdir(source_path):
|
if not os.path.isdir(source_path):
|
||||||
cli_logger.log(f"Error: Source path '{source_path}' does not exist or is not a directory.")
|
cli_logger.log(
|
||||||
|
f"Error: Source path '{source_path}' does not exist or is not a directory.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Determine password or key_file to pass
|
# Determine password or key_file to pass
|
||||||
@@ -54,7 +68,8 @@ def main():
|
|||||||
if args.key_file:
|
if args.key_file:
|
||||||
auth_key_file = args.key_file
|
auth_key_file = args.key_file
|
||||||
if not os.path.exists(auth_key_file):
|
if not os.path.exists(auth_key_file):
|
||||||
cli_logger.log(f"Error: Key file '{auth_key_file}' does not exist.")
|
cli_logger.log(
|
||||||
|
f"Error: Key file '{auth_key_file}' does not exist.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif args.password:
|
elif args.password:
|
||||||
auth_password = args.password
|
auth_password = args.password
|
||||||
@@ -78,8 +93,8 @@ def main():
|
|||||||
dest_path=args.destination,
|
dest_path=args.destination,
|
||||||
is_system=(args.backup_type == 'system'),
|
is_system=(args.backup_type == 'system'),
|
||||||
is_dry_run=False,
|
is_dry_run=False,
|
||||||
exclude_files=None, # Excludes are handled by AppConfig.MANUAL_EXCLUDE_LIST_PATH
|
exclude_files=None, # Excludes are handled by AppConfig.MANUAL_EXCLUDE_LIST_PATH
|
||||||
source_size=0, # Not accurately calculable in CLI without scanning, set to 0
|
source_size=0, # Not accurately calculable in CLI without scanning, set to 0
|
||||||
is_compressed=args.compressed,
|
is_compressed=args.compressed,
|
||||||
is_encrypted=args.encrypted,
|
is_encrypted=args.encrypted,
|
||||||
mode=args.mode,
|
mode=args.mode,
|
||||||
@@ -92,5 +107,6 @@ def main():
|
|||||||
|
|
||||||
cli_logger.log("CLI backup process finished.")
|
cli_logger.log("CLI backup process finished.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -157,12 +157,13 @@ class Actions:
|
|||||||
is_encrypted = self.app.encrypted_var.get()
|
is_encrypted = self.app.encrypted_var.get()
|
||||||
|
|
||||||
exclude_files = []
|
exclude_files = []
|
||||||
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
|
if is_system:
|
||||||
exclude_files.append(AppConfig.GENERATED_EXCLUDE_LIST_PATH)
|
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
|
||||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
exclude_files.append(AppConfig.GENERATED_EXCLUDE_LIST_PATH)
|
||||||
exclude_files.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
exclude_files.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
||||||
exclude_files.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
|
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||||
|
exclude_files.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
|
||||||
|
|
||||||
size = self.app.backup_manager.estimate_incremental_size(
|
size = self.app.backup_manager.estimate_incremental_size(
|
||||||
source_path=folder_path,
|
source_path=folder_path,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from core.pbp_app_config import Msg
|
|||||||
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
|
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
|
||||||
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
|
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
|
||||||
from shared_libs.logger import app_logger
|
from shared_libs.logger import app_logger
|
||||||
from shared_libs.message import MessageDialog
|
|
||||||
|
|
||||||
|
|
||||||
class BackupContentFrame(ttk.Frame):
|
class BackupContentFrame(ttk.Frame):
|
||||||
@@ -149,7 +148,7 @@ class BackupContentFrame(ttk.Frame):
|
|||||||
# Check if the destination is encrypted and trigger mount if necessary
|
# Check if the destination is encrypted and trigger mount if necessary
|
||||||
is_encrypted = self.backup_manager.encryption_manager.is_encrypted(
|
is_encrypted = self.backup_manager.encryption_manager.is_encrypted(
|
||||||
backup_path)
|
backup_path)
|
||||||
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
|
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
|
||||||
|
|
||||||
pybackup_dir = os.path.join(backup_path, "pybackup")
|
pybackup_dir = os.path.join(backup_path, "pybackup")
|
||||||
|
|
||||||
@@ -161,7 +160,8 @@ class BackupContentFrame(ttk.Frame):
|
|||||||
self.user_backups_frame.show(backup_path, [])
|
self.user_backups_frame.show(backup_path, [])
|
||||||
return
|
return
|
||||||
|
|
||||||
all_backups = self.backup_manager.list_all_backups(backup_path, mount_if_needed=True)
|
all_backups = self.backup_manager.list_all_backups(
|
||||||
|
backup_path, mount_if_needed=True)
|
||||||
if all_backups:
|
if all_backups:
|
||||||
system_backups, user_backups = all_backups
|
system_backups, user_backups = all_backups
|
||||||
self.system_backups_frame.show(backup_path, system_backups)
|
self.system_backups_frame.show(backup_path, system_backups)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
class CommentEditorDialog(tk.Toplevel):
|
class CommentEditorDialog(tk.Toplevel):
|
||||||
def __init__(self, master, info_file_path, backup_manager):
|
def __init__(self, master, info_file_path, backup_manager):
|
||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
@@ -13,17 +14,20 @@ class CommentEditorDialog(tk.Toplevel):
|
|||||||
main_frame = ttk.Frame(self, padding=10)
|
main_frame = ttk.Frame(self, padding=10)
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
self.text_widget = tk.Text(main_frame, wrap="word", height=10, width=40)
|
self.text_widget = tk.Text(
|
||||||
|
main_frame, wrap="word", height=10, width=40)
|
||||||
self.text_widget.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
self.text_widget.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
button_frame = ttk.Frame(main_frame)
|
button_frame = ttk.Frame(main_frame)
|
||||||
button_frame.pack(fill=tk.X)
|
button_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
ttk.Button(button_frame, text="Speichern & Schließen", command=self._save_and_close).pack(side=tk.RIGHT)
|
ttk.Button(button_frame, text="Speichern & Schließen",
|
||||||
ttk.Button(button_frame, text="Abbrechen", command=self.destroy).pack(side=tk.RIGHT, padx=5)
|
command=self._save_and_close).pack(side=tk.RIGHT)
|
||||||
|
ttk.Button(button_frame, text="Abbrechen",
|
||||||
|
command=self.destroy).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
self._load_comment()
|
self._load_comment()
|
||||||
|
|
||||||
self.transient(master)
|
self.transient(master)
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
self.wait_window(self)
|
self.wait_window(self)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from shared_libs.message import MessageDialog
|
|
||||||
import keyring
|
import keyring
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +19,10 @@ class EncryptionFrame(ttk.Frame):
|
|||||||
self.keyring_status_label = ttk.Label(self, text="")
|
self.keyring_status_label = ttk.Label(self, text="")
|
||||||
self.keyring_status_label.grid(
|
self.keyring_status_label.grid(
|
||||||
row=1, column=0, sticky="ew", padx=10, pady=5)
|
row=1, column=0, sticky="ew", padx=10, pady=5)
|
||||||
|
|
||||||
self.keyring_usage_label = ttk.Label(self, text="")
|
self.keyring_usage_label = ttk.Label(self, text="")
|
||||||
self.keyring_usage_label.grid(row=4, column=0, sticky="ew", padx=10, pady=5)
|
self.keyring_usage_label.grid(
|
||||||
|
row=4, column=0, sticky="ew", padx=10, pady=5)
|
||||||
|
|
||||||
self.check_keyring_availability()
|
self.check_keyring_availability()
|
||||||
|
|
||||||
@@ -51,7 +51,8 @@ class EncryptionFrame(ttk.Frame):
|
|||||||
clear_password_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")
|
clear_password_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")
|
||||||
|
|
||||||
self.status_message_label = ttk.Label(self, text="", foreground="blue")
|
self.status_message_label = ttk.Label(self, text="", foreground="blue")
|
||||||
self.status_message_label.grid(row=3, column=0, sticky="ew", padx=10, pady=5)
|
self.status_message_label.grid(
|
||||||
|
row=3, column=0, sticky="ew", padx=10, pady=5)
|
||||||
|
|
||||||
def set_context(self, username):
|
def set_context(self, username):
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -75,39 +76,47 @@ class EncryptionFrame(ttk.Frame):
|
|||||||
def set_session_password(self):
|
def set_session_password(self):
|
||||||
password = self.password_entry.get()
|
password = self.password_entry.get()
|
||||||
if not password:
|
if not password:
|
||||||
self.status_message_label.config(text="Password cannot be empty.", foreground="red")
|
self.status_message_label.config(
|
||||||
|
text="Password cannot be empty.", foreground="red")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.encryption_manager.set_session_password(password, self.save_to_keyring_var.get())
|
self.encryption_manager.set_session_password(
|
||||||
|
password, self.save_to_keyring_var.get())
|
||||||
|
|
||||||
if self.save_to_keyring_var.get():
|
if self.save_to_keyring_var.get():
|
||||||
if not self.username:
|
if not self.username:
|
||||||
self.status_message_label.config(text="Please select a backup destination first.", foreground="orange")
|
self.status_message_label.config(
|
||||||
|
text="Please select a backup destination first.", foreground="orange")
|
||||||
return
|
return
|
||||||
if self.encryption_manager.set_password_in_keyring(self.username, password):
|
if self.encryption_manager.set_password_in_keyring(self.username, password):
|
||||||
self.status_message_label.config(text="Password set for this session and saved to keyring.", foreground="green")
|
self.status_message_label.config(
|
||||||
|
text="Password set for this session and saved to keyring.", foreground="green")
|
||||||
self.update_keyring_status()
|
self.update_keyring_status()
|
||||||
else:
|
else:
|
||||||
self.status_message_label.config(text="Password set for this session, but failed to save to keyring.", foreground="orange")
|
self.status_message_label.config(
|
||||||
|
text="Password set for this session, but failed to save to keyring.", foreground="orange")
|
||||||
else:
|
else:
|
||||||
self.status_message_label.config(text="Password set for this session.", foreground="green")
|
self.status_message_label.config(
|
||||||
|
text="Password set for this session.", foreground="green")
|
||||||
|
|
||||||
def clear_session_password(self):
|
def clear_session_password(self):
|
||||||
self.encryption_manager.clear_session_password()
|
self.encryption_manager.clear_session_password()
|
||||||
self.password_entry.delete(0, tk.END)
|
self.password_entry.delete(0, tk.END)
|
||||||
self.status_message_label.config(text="Session password cleared.", foreground="green")
|
self.status_message_label.config(
|
||||||
|
text="Session password cleared.", foreground="green")
|
||||||
if self.username:
|
if self.username:
|
||||||
self.encryption_manager.delete_password_from_keyring(self.username)
|
self.encryption_manager.delete_password_from_keyring(self.username)
|
||||||
self.update_keyring_status()
|
self.update_keyring_status()
|
||||||
|
|
||||||
def update_keyring_status(self):
|
def update_keyring_status(self):
|
||||||
if not self.username:
|
if not self.username:
|
||||||
self.keyring_usage_label.config(text="Select a backup destination to see keyring status.", foreground="blue")
|
self.keyring_usage_label.config(
|
||||||
|
text="Select a backup destination to see keyring status.", foreground="blue")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.encryption_manager.get_password_from_keyring(self.username):
|
if self.encryption_manager.get_password_from_keyring(self.username):
|
||||||
self.keyring_usage_label.config(text=f'Password for "{self.username}" is stored in the keyring.', foreground="green")
|
self.keyring_usage_label.config(
|
||||||
|
text=f'Password for "{self.username}" is stored in the keyring.', foreground="green")
|
||||||
else:
|
else:
|
||||||
self.keyring_usage_label.config(text=f'No password for "{self.username}" found in the keyring.', foreground="orange")
|
self.keyring_usage_label.config(
|
||||||
|
text=f'No password for "{self.username}" found in the keyring.', foreground="orange")
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import tkinter as tk
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from core.pbp_app_config import Msg
|
from core.pbp_app_config import Msg
|
||||||
from shared_libs.common_tools import IconManager
|
|
||||||
from shared_libs.logger import app_logger
|
from shared_libs.logger import app_logger
|
||||||
|
|
||||||
|
|
||||||
class HeaderFrame(tk.Frame):
|
class HeaderFrame(tk.Frame):
|
||||||
def __init__(self, container, image_manager, encryption_manager, app, **kwargs):
|
def __init__(self, container, image_manager, encryption_manager, app, **kwargs):
|
||||||
super().__init__(container, bg="#455A64", **kwargs)
|
super().__init__(container, bg="#455A64", **kwargs)
|
||||||
@@ -63,8 +63,9 @@ class HeaderFrame(tk.Frame):
|
|||||||
font=("Helvetica", 10, "bold"),
|
font=("Helvetica", 10, "bold"),
|
||||||
bg="#455A64",
|
bg="#455A64",
|
||||||
)
|
)
|
||||||
self.keyring_status_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
|
self.keyring_status_label.grid(
|
||||||
|
row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
|
||||||
|
|
||||||
self.refresh_status()
|
self.refresh_status()
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
@@ -74,10 +75,12 @@ class HeaderFrame(tk.Frame):
|
|||||||
app_logger.log(f"HeaderFrame: Destination path is '{dest_path}'")
|
app_logger.log(f"HeaderFrame: Destination path is '{dest_path}'")
|
||||||
|
|
||||||
if not dest_path or not self.encryption_manager.is_encrypted(dest_path):
|
if not dest_path or not self.encryption_manager.is_encrypted(dest_path):
|
||||||
app_logger.log("HeaderFrame: No destination path or not encrypted. Clearing status.")
|
app_logger.log(
|
||||||
self.keyring_status_label.config(text="") # Clear status if not encrypted
|
"HeaderFrame: No destination path or not encrypted. Clearing status.")
|
||||||
|
# Clear status if not encrypted
|
||||||
|
self.keyring_status_label.config(text="")
|
||||||
return
|
return
|
||||||
|
|
||||||
app_logger.log("HeaderFrame: Destination is encrypted.")
|
app_logger.log("HeaderFrame: Destination is encrypted.")
|
||||||
username = os.path.basename(dest_path.rstrip('/'))
|
username = os.path.basename(dest_path.rstrip('/'))
|
||||||
app_logger.log(f"HeaderFrame: Username is '{username}'")
|
app_logger.log(f"HeaderFrame: Username is '{username}'")
|
||||||
@@ -97,9 +100,11 @@ class HeaderFrame(tk.Frame):
|
|||||||
fg="#2E8B57" # SeaGreen
|
fg="#2E8B57" # SeaGreen
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
key_in_keyring = self.encryption_manager.is_key_in_keyring(username)
|
key_in_keyring = self.encryption_manager.is_key_in_keyring(
|
||||||
|
username)
|
||||||
app_logger.log(f"HeaderFrame: Key in keyring? {key_in_keyring}")
|
app_logger.log(f"HeaderFrame: Key in keyring? {key_in_keyring}")
|
||||||
key_file_exists = os.path.exists(self.encryption_manager.get_key_file_path(dest_path))
|
key_file_exists = os.path.exists(
|
||||||
|
self.encryption_manager.get_key_file_path(dest_path))
|
||||||
app_logger.log(f"HeaderFrame: Key file exists? {key_file_exists}")
|
app_logger.log(f"HeaderFrame: Key file exists? {key_file_exists}")
|
||||||
|
|
||||||
if key_in_keyring:
|
if key_in_keyring:
|
||||||
@@ -117,4 +122,4 @@ class HeaderFrame(tk.Frame):
|
|||||||
text="Key: Not Available",
|
text="Key: Not Available",
|
||||||
fg="#A9A9A9" # DarkGray
|
fg="#A9A9A9" # DarkGray
|
||||||
)
|
)
|
||||||
app_logger.log("HeaderFrame: Status refresh complete.")
|
app_logger.log("HeaderFrame: Status refresh complete.")
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ class Navigation:
|
|||||||
self.app.drawing.redraw_right_canvas()
|
self.app.drawing.redraw_right_canvas()
|
||||||
|
|
||||||
def toggle_mode(self, mode=None, active_index=None, trigger_calculation=True):
|
def toggle_mode(self, mode=None, active_index=None, trigger_calculation=True):
|
||||||
|
if self.app.refresh_log_var.get():
|
||||||
|
self.app.log_window.clear_log()
|
||||||
if self.app.backup_is_running:
|
if self.app.backup_is_running:
|
||||||
# If a backup is running, we only want to switch the view to the main backup screen.
|
# If a backup is running, we only want to switch the view to the main backup screen.
|
||||||
# We don't reset anything.
|
# We don't reset anything.
|
||||||
@@ -140,7 +142,6 @@ class Navigation:
|
|||||||
self.app.scheduler_frame.hide()
|
self.app.scheduler_frame.hide()
|
||||||
self.app.settings_frame.hide()
|
self.app.settings_frame.hide()
|
||||||
self.app.backup_content_frame.grid_remove()
|
self.app.backup_content_frame.grid_remove()
|
||||||
|
|
||||||
|
|
||||||
# Show the main content frames
|
# Show the main content frames
|
||||||
self.app.canvas_frame.grid()
|
self.app.canvas_frame.grid()
|
||||||
@@ -185,7 +186,7 @@ class Navigation:
|
|||||||
self.app.scheduler_frame.hide()
|
self.app.scheduler_frame.hide()
|
||||||
self.app.settings_frame.hide()
|
self.app.settings_frame.hide()
|
||||||
self.app.backup_content_frame.grid_remove()
|
self.app.backup_content_frame.grid_remove()
|
||||||
|
|
||||||
self.app.canvas_frame.grid()
|
self.app.canvas_frame.grid()
|
||||||
self.app.source_size_frame.grid()
|
self.app.source_size_frame.grid()
|
||||||
self.app.target_size_frame.grid()
|
self.app.target_size_frame.grid()
|
||||||
@@ -232,6 +233,8 @@ class Navigation:
|
|||||||
self._update_task_bar_visibility("log")
|
self._update_task_bar_visibility("log")
|
||||||
|
|
||||||
def toggle_scheduler_frame(self, active_index=None):
|
def toggle_scheduler_frame(self, active_index=None):
|
||||||
|
if self.app.refresh_log_var.get():
|
||||||
|
self.app.log_window.clear_log()
|
||||||
self._cancel_calculation()
|
self._cancel_calculation()
|
||||||
if active_index is not None:
|
if active_index is not None:
|
||||||
self.app.drawing.update_nav_buttons(active_index)
|
self.app.drawing.update_nav_buttons(active_index)
|
||||||
@@ -240,7 +243,7 @@ class Navigation:
|
|||||||
self.app.log_frame.grid_remove()
|
self.app.log_frame.grid_remove()
|
||||||
self.app.settings_frame.hide()
|
self.app.settings_frame.hide()
|
||||||
self.app.backup_content_frame.grid_remove()
|
self.app.backup_content_frame.grid_remove()
|
||||||
|
|
||||||
self.app.source_size_frame.grid_remove()
|
self.app.source_size_frame.grid_remove()
|
||||||
self.app.target_size_frame.grid_remove()
|
self.app.target_size_frame.grid_remove()
|
||||||
self.app.restore_size_frame_before.grid_remove()
|
self.app.restore_size_frame_before.grid_remove()
|
||||||
@@ -250,6 +253,8 @@ class Navigation:
|
|||||||
self._update_task_bar_visibility("scheduler")
|
self._update_task_bar_visibility("scheduler")
|
||||||
|
|
||||||
def toggle_settings_frame(self, active_index=None):
|
def toggle_settings_frame(self, active_index=None):
|
||||||
|
if self.app.refresh_log_var.get():
|
||||||
|
self.app.log_window.clear_log()
|
||||||
self._cancel_calculation()
|
self._cancel_calculation()
|
||||||
if active_index is not None:
|
if active_index is not None:
|
||||||
self.app.drawing.update_nav_buttons(active_index)
|
self.app.drawing.update_nav_buttons(active_index)
|
||||||
@@ -258,7 +263,7 @@ class Navigation:
|
|||||||
self.app.log_frame.grid_remove()
|
self.app.log_frame.grid_remove()
|
||||||
self.app.backup_content_frame.grid_remove()
|
self.app.backup_content_frame.grid_remove()
|
||||||
self.app.scheduler_frame.hide()
|
self.app.scheduler_frame.hide()
|
||||||
|
|
||||||
self.app.source_size_frame.grid_remove()
|
self.app.source_size_frame.grid_remove()
|
||||||
self.app.target_size_frame.grid_remove()
|
self.app.target_size_frame.grid_remove()
|
||||||
self.app.restore_size_frame_before.grid_remove()
|
self.app.restore_size_frame_before.grid_remove()
|
||||||
@@ -267,7 +272,9 @@ class Navigation:
|
|||||||
self.app.top_bar.grid()
|
self.app.top_bar.grid()
|
||||||
self._update_task_bar_visibility("settings")
|
self._update_task_bar_visibility("settings")
|
||||||
|
|
||||||
def toggle_backup_content_frame(self, _=None): # Accept argument but ignore it
|
def toggle_backup_content_frame(self, _=None): # Accept argument but ignore it
|
||||||
|
if self.app.refresh_log_var.get():
|
||||||
|
self.app.log_window.clear_log()
|
||||||
self._cancel_calculation()
|
self._cancel_calculation()
|
||||||
self.app.drawing.update_nav_buttons(2) # Index 2 for Backup Content
|
self.app.drawing.update_nav_buttons(2) # Index 2 for Backup Content
|
||||||
|
|
||||||
@@ -283,7 +290,7 @@ class Navigation:
|
|||||||
self.app.log_frame.grid_remove()
|
self.app.log_frame.grid_remove()
|
||||||
self.app.scheduler_frame.hide()
|
self.app.scheduler_frame.hide()
|
||||||
self.app.settings_frame.hide()
|
self.app.settings_frame.hide()
|
||||||
|
|
||||||
self.app.source_size_frame.grid_remove()
|
self.app.source_size_frame.grid_remove()
|
||||||
self.app.target_size_frame.grid_remove()
|
self.app.target_size_frame.grid_remove()
|
||||||
self.app.restore_size_frame_before.grid_remove()
|
self.app.restore_size_frame_before.grid_remove()
|
||||||
@@ -301,4 +308,4 @@ class Navigation:
|
|||||||
self.app.next_backup_content_view = 'system'
|
self.app.next_backup_content_view = 'system'
|
||||||
|
|
||||||
self.app.top_bar.grid()
|
self.app.top_bar.grid()
|
||||||
self._update_task_bar_visibility("scheduler")
|
self._update_task_bar_visibility("scheduler")
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
self, text=Msg.STR["scheduled_jobs"], padding=10)
|
self, text=Msg.STR["scheduled_jobs"], padding=10)
|
||||||
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
columns = ("active", "type", "frequency", "destination", "sources", "options")
|
columns = ("active", "type", "frequency",
|
||||||
|
"destination", "sources", "options")
|
||||||
self.jobs_tree = ttk.Treeview(
|
self.jobs_tree = ttk.Treeview(
|
||||||
self.jobs_frame, columns=columns, show="headings")
|
self.jobs_frame, columns=columns, show="headings")
|
||||||
self.jobs_tree.heading("active", text=Msg.STR["active"])
|
self.jobs_tree.heading("active", text=Msg.STR["active"])
|
||||||
@@ -55,7 +56,7 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
|
|
||||||
self.backup_type_system_var = tk.BooleanVar(value=True)
|
self.backup_type_system_var = tk.BooleanVar(value=True)
|
||||||
self.backup_type_user_var = tk.BooleanVar(value=False)
|
self.backup_type_user_var = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
self.freq_daily_var = tk.BooleanVar(value=True)
|
self.freq_daily_var = tk.BooleanVar(value=True)
|
||||||
self.freq_weekly_var = tk.BooleanVar(value=False)
|
self.freq_weekly_var = tk.BooleanVar(value=False)
|
||||||
self.freq_monthly_var = tk.BooleanVar(value=False)
|
self.freq_monthly_var = tk.BooleanVar(value=False)
|
||||||
@@ -76,7 +77,8 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
|
|
||||||
self.user_sources_frame = ttk.LabelFrame(
|
self.user_sources_frame = ttk.LabelFrame(
|
||||||
source_options_container, text=Msg.STR["source_folders"], padding=10)
|
source_options_container, text=Msg.STR["source_folders"], padding=10)
|
||||||
self.user_sources_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
self.user_sources_frame.grid(
|
||||||
|
row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||||
for name, var in self.user_sources.items():
|
for name, var in self.user_sources.items():
|
||||||
ttk.Checkbutton(self.user_sources_frame, text=name,
|
ttk.Checkbutton(self.user_sources_frame, text=name,
|
||||||
variable=var, style="Switch.TCheckbutton").pack(anchor=tk.W)
|
variable=var, style="Switch.TCheckbutton").pack(anchor=tk.W)
|
||||||
@@ -90,13 +92,17 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
self.compress_var = tk.BooleanVar(value=False)
|
self.compress_var = tk.BooleanVar(value=False)
|
||||||
self.encrypt_var = tk.BooleanVar(value=False)
|
self.encrypt_var = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(self.full_var, self.incremental_var, self.full_var.get()), style="Switch.TCheckbutton")
|
self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(
|
||||||
|
self.full_var, self.incremental_var, self.full_var.get()), style="Switch.TCheckbutton")
|
||||||
self.full_checkbutton.pack(anchor=tk.W)
|
self.full_checkbutton.pack(anchor=tk.W)
|
||||||
self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(self.incremental_var, self.full_var, self.incremental_var.get()), style="Switch.TCheckbutton")
|
self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(
|
||||||
|
self.incremental_var, self.full_var, self.incremental_var.get()), style="Switch.TCheckbutton")
|
||||||
self.incremental_checkbutton.pack(anchor=tk.W)
|
self.incremental_checkbutton.pack(anchor=tk.W)
|
||||||
self.compress_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler, style="Switch.TCheckbutton")
|
self.compress_checkbutton = ttk.Checkbutton(
|
||||||
|
options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler, style="Switch.TCheckbutton")
|
||||||
self.compress_checkbutton.pack(anchor=tk.W)
|
self.compress_checkbutton.pack(anchor=tk.W)
|
||||||
self.encrypt_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var, style="Switch.TCheckbutton")
|
self.encrypt_checkbutton = ttk.Checkbutton(
|
||||||
|
options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var, style="Switch.TCheckbutton")
|
||||||
self.encrypt_checkbutton.pack(anchor=tk.W)
|
self.encrypt_checkbutton.pack(anchor=tk.W)
|
||||||
|
|
||||||
dest_frame = ttk.LabelFrame(
|
dest_frame = ttk.LabelFrame(
|
||||||
@@ -147,7 +153,8 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
self._toggle_user_sources()
|
self._toggle_user_sources()
|
||||||
|
|
||||||
def _handle_freq_switch(self, changed_var):
|
def _handle_freq_switch(self, changed_var):
|
||||||
vars = {"daily": self.freq_daily_var, "weekly": self.freq_weekly_var, "monthly": self.freq_monthly_var}
|
vars = {"daily": self.freq_daily_var,
|
||||||
|
"weekly": self.freq_weekly_var, "monthly": self.freq_monthly_var}
|
||||||
if vars[changed_var].get():
|
if vars[changed_var].get():
|
||||||
self.frequency.set(changed_var)
|
self.frequency.set(changed_var)
|
||||||
for var_name, var_obj in vars.items():
|
for var_name, var_obj in vars.items():
|
||||||
@@ -187,7 +194,7 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
self.destination.set("")
|
self.destination.set("")
|
||||||
for var in self.user_sources.values():
|
for var in self.user_sources.values():
|
||||||
var.set(False)
|
var.set(False)
|
||||||
self._on_compression_toggle_scheduler() # Update state of incremental checkbox
|
self._on_compression_toggle_scheduler() # Update state of incremental checkbox
|
||||||
else:
|
else:
|
||||||
self.add_job_frame.pack_forget()
|
self.add_job_frame.pack_forget()
|
||||||
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
@@ -240,7 +247,7 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
if job_type == "user":
|
if job_type == "user":
|
||||||
command += f" --sources "
|
command += f" --sources "
|
||||||
for s in job_sources:
|
for s in job_sources:
|
||||||
command += f'\"{s}\" '
|
command += f'\"{s}\" '
|
||||||
|
|
||||||
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
|
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
|
||||||
if job_type == "user":
|
if job_type == "user":
|
||||||
@@ -289,4 +296,4 @@ class SchedulerFrame(ttk.Frame):
|
|||||||
|
|
||||||
job_id = self.jobs_tree.item(selected_item)["values"][0]
|
job_id = self.jobs_tree.item(selected_item)["values"][0]
|
||||||
self.backup_manager.remove_scheduled_job(job_id)
|
self.backup_manager.remove_scheduled_job(job_id)
|
||||||
self._load_scheduled_jobs()
|
self._load_scheduled_jobs()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
from core.pbp_app_config import Msg
|
from core.pbp_app_config import Msg
|
||||||
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
|
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user