commit two
This commit is contained in:
3
__init__.py
Normal file
3
__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
BIN
__pycache__/app_config.cpython-312.pyc
Normal file
BIN
__pycache__/app_config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/backup_manager.cpython-312.pyc
Normal file
BIN
__pycache__/backup_manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main_app.cpython-312.pyc
Normal file
BIN
__pycache__/main_app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/schedule_job_dialog.cpython-312.pyc
Normal file
BIN
__pycache__/schedule_job_dialog.cpython-312.pyc
Normal file
Binary file not shown.
541
animated_icon.py
Normal file
541
animated_icon.py
Normal file
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
A Tkinter widget for displaying animated icons.
|
||||
|
||||
This module provides the AnimatedIcon class, a custom Tkinter Canvas widget
|
||||
that can display various types of animations. It supports both native Tkinter
|
||||
drawing and Pillow (PIL) for anti-aliased graphics if available.
|
||||
"""
|
||||
import tkinter as tk
|
||||
from math import sin, cos, pi
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
|
||||
|
||||
def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
||||
"""Converts a hex color string to an RGB tuple."""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
class AnimatedIcon(tk.Canvas):
|
||||
"""A custom Tkinter Canvas widget for displaying animations."""
|
||||
|
||||
def __init__(self, master: tk.Misc, width: int = 20, height: int = 20, animation_type: str = "counter_arc", color: str = "#2a6fde", highlight_color: str = "#5195ff", use_pillow: bool = False, bg: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initializes the AnimatedIcon widget.
|
||||
|
||||
Args:
|
||||
master: The parent widget.
|
||||
width (int): The width of the icon.
|
||||
height (int): The height of the icon.
|
||||
animation_type (str): The type of animation to display.
|
||||
Options: "counter_arc", "double_arc", "line", "blink".
|
||||
color (str): The primary color of the icon.
|
||||
highlight_color (str): The highlight color of the icon.
|
||||
use_pillow (bool): Whether to use Pillow for drawing if available.
|
||||
bg (str): The background color of the canvas.
|
||||
"""
|
||||
if bg is None:
|
||||
try:
|
||||
bg = master.cget("background")
|
||||
except tk.TclError:
|
||||
bg = "#f0f0f0" # Fallback color
|
||||
super().__init__(master, width=width, height=height, bg=bg, highlightthickness=0)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.animation_type = animation_type
|
||||
self.color = color
|
||||
self.highlight_color = highlight_color
|
||||
self.use_pillow = use_pillow and PIL_AVAILABLE
|
||||
self.running = False
|
||||
self.is_disabled = False
|
||||
self.pause_count = 0
|
||||
self.angle = 0
|
||||
self.pulse_animation = False
|
||||
|
||||
self.color_rgb = _hex_to_rgb(self.color)
|
||||
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
||||
|
||||
if self.use_pillow:
|
||||
self.image = Image.new(
|
||||
"RGBA", (width * 4, height * 4), (0, 0, 0, 0))
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.photo_image = None
|
||||
|
||||
def _draw_frame(self) -> None:
|
||||
"""Draws a single frame of the animation."""
|
||||
if self.use_pillow:
|
||||
self._draw_pillow_frame()
|
||||
else:
|
||||
self._draw_canvas_frame()
|
||||
|
||||
def _draw_canvas_frame(self) -> None:
|
||||
"""Draws a frame using native Tkinter canvas methods."""
|
||||
self.delete("all")
|
||||
if self.pulse_animation:
|
||||
self._draw_canvas_pulse()
|
||||
elif self.animation_type == "line":
|
||||
self._draw_canvas_line()
|
||||
elif self.animation_type == "double_arc":
|
||||
self._draw_canvas_double_arc()
|
||||
elif self.animation_type == "counter_arc":
|
||||
self._draw_canvas_counter_arc()
|
||||
elif self.animation_type == "blink":
|
||||
self._draw_canvas_blink()
|
||||
|
||||
def _draw_canvas_pulse(self) -> None:
|
||||
"""Draws the pulse animation using canvas methods."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
pulse_color = f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
if self.animation_type == "line":
|
||||
for i in range(8):
|
||||
angle = i * (pi / 4)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.2)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||
end_y = center_y + sin(angle) * (self.height * 0.4)
|
||||
self.create_line(start_x, start_y, end_x,
|
||||
end_y, fill=pulse_color, width=2)
|
||||
elif self.animation_type == "double_arc":
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
self.create_arc(bbox, start=0, extent=359.9,
|
||||
style=tk.ARC, outline=pulse_color, width=2)
|
||||
elif self.animation_type == "counter_arc":
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
self.create_arc(bbox_outer, start=0, extent=359.9,
|
||||
style=tk.ARC, outline=pulse_color, width=2)
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
self.create_arc(bbox_inner, start=0, extent=359.9,
|
||||
style=tk.ARC, outline=self.color, width=2)
|
||||
|
||||
def _draw_canvas_line(self) -> None:
|
||||
"""Draws the line animation using canvas methods."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
for i in range(8):
|
||||
angle = self.angle + i * (pi / 4)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.2)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||
end_y = center_y + sin(angle) * (self.height * 0.4)
|
||||
alpha = (cos(self.angle * 2 + i * (pi / 4)) + 1) / 2
|
||||
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
color = f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
self.create_line(start_x, start_y, end_x,
|
||||
end_y, fill=color, width=2)
|
||||
|
||||
def _draw_canvas_double_arc(self) -> None:
|
||||
"""Draws the double arc animation using canvas methods."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
|
||||
start_angle1 = -self.angle * 180 / pi
|
||||
extent1 = 120 + 60 * sin(-self.angle)
|
||||
self.create_arc(bbox, start=start_angle1, extent=extent1,
|
||||
style=tk.ARC, outline=self.highlight_color, width=2)
|
||||
|
||||
start_angle2 = (-self.angle + pi) * 180 / pi
|
||||
extent2 = 120 + 60 * sin(-self.angle + pi / 2)
|
||||
self.create_arc(bbox, start=start_angle2, extent=extent2,
|
||||
style=tk.ARC, outline=self.color, width=2)
|
||||
|
||||
def _draw_canvas_counter_arc(self) -> None:
|
||||
"""Draws the counter arc animation using canvas methods."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
start_angle1 = -self.angle * 180 / pi
|
||||
self.create_arc(bbox_outer, start=start_angle1, extent=150,
|
||||
style=tk.ARC, outline=self.highlight_color, width=2)
|
||||
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
start_angle2 = self.angle * 180 / pi + 60
|
||||
self.create_arc(bbox_inner, start=start_angle2, extent=150,
|
||||
style=tk.ARC, outline=self.color, width=2)
|
||||
|
||||
def _draw_canvas_blink(self) -> None:
|
||||
"""Draws the blink animation using canvas methods."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
blink_color = f"#{r:02x}{g:02x}{b:02x}"
|
||||
self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y +
|
||||
radius, start=0, extent=359.9, style=tk.ARC, outline=blink_color, width=4)
|
||||
|
||||
def _draw_pillow_frame(self) -> None:
|
||||
"""Draws a frame using Pillow for anti-aliased graphics."""
|
||||
self.draw.rectangle(
|
||||
[0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0))
|
||||
if self.pulse_animation:
|
||||
self._draw_pillow_pulse()
|
||||
elif self.animation_type == "line":
|
||||
self._draw_pillow_line()
|
||||
elif self.animation_type == "double_arc":
|
||||
self._draw_pillow_double_arc()
|
||||
elif self.animation_type == "counter_arc":
|
||||
self._draw_pillow_counter_arc()
|
||||
elif self.animation_type == "blink":
|
||||
self._draw_pillow_blink()
|
||||
|
||||
resized_image = self.image.resize(
|
||||
(self.width, self.height), Image.Resampling.LANCZOS)
|
||||
self.photo_image = ImageTk.PhotoImage(resized_image)
|
||||
self.delete("all")
|
||||
self.create_image(0, 0, anchor="nw", image=self.photo_image)
|
||||
|
||||
def _draw_pillow_pulse(self) -> None:
|
||||
"""Draws the pulse animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
alpha = (sin(self.angle * 5) + 1) / 2 # Faster pulse
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
pulse_color = (r, g, b)
|
||||
|
||||
if self.animation_type == "line":
|
||||
for i in range(12):
|
||||
angle = i * (pi / 6)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.8)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||
end_y = center_y + sin(angle) * (self.height * 1.6)
|
||||
self.draw.line([(start_x, start_y), (end_x, end_y)],
|
||||
fill=pulse_color, width=6, joint="curve")
|
||||
elif self.animation_type == "double_arc":
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
self.draw.arc(bbox, start=0, end=360, fill=pulse_color, width=5)
|
||||
elif self.animation_type == "counter_arc":
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
self.draw.arc(bbox_outer, start=0, end=360,
|
||||
fill=pulse_color, width=7)
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
self.draw.arc(bbox_inner, start=0, end=360,
|
||||
fill=self.color_rgb, width=7)
|
||||
|
||||
def _draw_pillow_line(self) -> None:
|
||||
"""Draws the line animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
for i in range(12):
|
||||
angle = self.angle + i * (pi / 6)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.8)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||
end_y = center_y + sin(angle) * (self.height * 1.6)
|
||||
alpha = (cos(self.angle * 2.5 + i * (pi / 6)) + 1) / 2
|
||||
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
color = (r, g, b)
|
||||
|
||||
self.draw.line([(start_x, start_y), (end_x, end_y)],
|
||||
fill=color, width=6, joint="curve")
|
||||
|
||||
def _draw_pillow_double_arc(self) -> None:
|
||||
"""Draws the double arc animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
|
||||
start_angle1 = self.angle * 180 / pi
|
||||
extent1 = 120 + 60 * sin(self.angle)
|
||||
self.draw.arc(bbox, start=start_angle1, end=start_angle1 +
|
||||
extent1, fill=self.highlight_color_rgb, width=5)
|
||||
|
||||
start_angle2 = (self.angle + pi) * 180 / pi
|
||||
extent2 = 120 + 60 * sin(self.angle + pi / 2)
|
||||
self.draw.arc(bbox, start=start_angle2, end=start_angle2 +
|
||||
extent2, fill=self.color_rgb, width=5)
|
||||
|
||||
def _draw_pillow_counter_arc(self) -> None:
|
||||
"""Draws the counter arc animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
start_angle1 = self.angle * 180 / pi
|
||||
self.draw.arc(bbox_outer, start=start_angle1, end=start_angle1 +
|
||||
150, fill=self.highlight_color_rgb, width=7)
|
||||
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
start_angle2 = -self.angle * 180 / pi + 60
|
||||
self.draw.arc(bbox_inner, start=start_angle2,
|
||||
end=start_angle2 + 150, fill=self.color_rgb, width=7)
|
||||
|
||||
def _draw_pillow_blink(self) -> None:
|
||||
"""Draws the blink animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
alpha = (sin(self.angle * 2) + 1) / 2 # Slower blinking speed
|
||||
r = int(
|
||||
alpha * (self.highlight_color_rgb[0] - self.color_rgb[0]) + self.color_rgb[0])
|
||||
g = int(
|
||||
alpha * (self.highlight_color_rgb[1] - self.color_rgb[1]) + self.color_rgb[1])
|
||||
b = int(
|
||||
alpha * (self.highlight_color_rgb[2] - self.color_rgb[2]) + self.color_rgb[2])
|
||||
blink_color = (r, g, b)
|
||||
self.draw.arc((center_x - radius, center_y - radius, center_x + radius,
|
||||
center_y + radius), start=0, end=360, fill=blink_color, width=10)
|
||||
|
||||
def _draw_stopped_frame(self) -> None:
|
||||
"""Draws the icon in its stopped (static) state."""
|
||||
self.delete("all")
|
||||
|
||||
original_highlight_color = self.highlight_color
|
||||
original_highlight_color_rgb = self.highlight_color_rgb
|
||||
if self.is_disabled:
|
||||
self.highlight_color = "#8f99aa"
|
||||
self.highlight_color_rgb = _hex_to_rgb(self.highlight_color)
|
||||
|
||||
try:
|
||||
if self.use_pillow:
|
||||
self._draw_pillow_stopped_frame()
|
||||
else:
|
||||
self._draw_canvas_stopped_frame()
|
||||
finally:
|
||||
if self.is_disabled:
|
||||
self.highlight_color = original_highlight_color
|
||||
self.highlight_color_rgb = original_highlight_color_rgb
|
||||
|
||||
def _draw_canvas_stopped_frame(self) -> None:
|
||||
"""Draws the stopped state using canvas methods."""
|
||||
if self.animation_type == "line":
|
||||
self._draw_canvas_line_stopped()
|
||||
elif self.animation_type == "double_arc":
|
||||
self._draw_canvas_double_arc_stopped()
|
||||
elif self.animation_type == "counter_arc":
|
||||
self._draw_canvas_counter_arc_stopped()
|
||||
elif self.animation_type == "blink":
|
||||
self._draw_canvas_blink_stopped()
|
||||
|
||||
def _draw_canvas_line_stopped(self) -> None:
|
||||
"""Draws the stopped state for the line animation."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
for i in range(8):
|
||||
angle = i * (pi / 4)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.2)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.2)
|
||||
end_x = center_x + cos(angle) * (self.width * 0.4)
|
||||
end_y = center_y + sin(angle) * (self.height * 0.4)
|
||||
self.create_line(start_x, start_y, end_x, end_y,
|
||||
fill=self.highlight_color, width=2)
|
||||
|
||||
def _draw_canvas_double_arc_stopped(self) -> None:
|
||||
"""Draws the stopped state for the double arc animation."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
self.create_arc(bbox, start=0, extent=359.9, style=tk.ARC,
|
||||
outline=self.highlight_color, width=2)
|
||||
|
||||
def _draw_canvas_counter_arc_stopped(self) -> None:
|
||||
"""Draws the stopped state for the counter arc animation."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
self.create_arc(bbox_outer, start=0, extent=359.9,
|
||||
style=tk.ARC, outline=self.highlight_color, width=2)
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
self.create_arc(bbox_inner, start=0, extent=359.9,
|
||||
style=tk.ARC, outline=self.color, width=2)
|
||||
|
||||
def _draw_canvas_blink_stopped(self) -> None:
|
||||
"""Draws the stopped state for the blink animation."""
|
||||
center_x, center_y = self.width / 2, self.height / 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
self.create_arc(center_x - radius, center_y - radius, center_x + radius, center_y +
|
||||
radius, start=0, extent=359.9, style=tk.ARC, outline=self.highlight_color, width=4)
|
||||
|
||||
def _draw_pillow_stopped_frame(self) -> None:
|
||||
"""Draws the stopped state using Pillow."""
|
||||
self.draw.rectangle(
|
||||
[0, 0, self.width * 4, self.height * 4], fill=(0, 0, 0, 0))
|
||||
if self.animation_type == "line":
|
||||
self._draw_pillow_line_stopped()
|
||||
elif self.animation_type == "double_arc":
|
||||
self._draw_pillow_double_arc_stopped()
|
||||
elif self.animation_type == "counter_arc":
|
||||
self._draw_pillow_counter_arc_stopped()
|
||||
elif self.animation_type == "blink":
|
||||
self._draw_pillow_blink_stopped()
|
||||
|
||||
resized_image = self.image.resize(
|
||||
(self.width, self.height), Image.Resampling.LANCZOS)
|
||||
self.photo_image = ImageTk.PhotoImage(resized_image)
|
||||
self.create_image(0, 0, anchor="nw", image=self.photo_image)
|
||||
|
||||
def _draw_pillow_line_stopped(self) -> None:
|
||||
"""Draws the stopped state for the line animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
for i in range(12):
|
||||
angle = i * (pi / 6)
|
||||
start_x = center_x + cos(angle) * (self.width * 0.8)
|
||||
start_y = center_y + sin(angle) * (self.height * 0.8)
|
||||
end_x = center_x + cos(angle) * (self.width * 1.6)
|
||||
end_y = center_y + sin(angle) * (self.height * 1.6)
|
||||
self.draw.line([(start_x, start_y), (end_x, end_y)],
|
||||
fill=self.highlight_color_rgb, width=6, joint="curve")
|
||||
|
||||
def _draw_pillow_double_arc_stopped(self) -> None:
|
||||
"""Draws the stopped state for the double arc animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
bbox = (center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius)
|
||||
self.draw.arc(bbox, start=0, end=360,
|
||||
fill=self.highlight_color_rgb, width=5)
|
||||
|
||||
def _draw_pillow_counter_arc_stopped(self) -> None:
|
||||
"""Draws the stopped state for the counter arc animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
radius_outer = min(center_x, center_y) * 0.8
|
||||
bbox_outer = (center_x - radius_outer, center_y - radius_outer,
|
||||
center_x + radius_outer, center_y + radius_outer)
|
||||
self.draw.arc(bbox_outer, start=0, end=360,
|
||||
fill=self.highlight_color_rgb, width=7)
|
||||
radius_inner = min(center_x, center_y) * 0.6
|
||||
bbox_inner = (center_x - radius_inner, center_y - radius_inner,
|
||||
center_x + radius_inner, center_y + radius_inner)
|
||||
self.draw.arc(bbox_inner, start=0, end=360,
|
||||
fill=self.color_rgb, width=7)
|
||||
|
||||
def _draw_pillow_blink_stopped(self) -> None:
|
||||
"""Draws the stopped state for the blink animation using Pillow."""
|
||||
center_x, center_y = self.width * 2, self.height * 2
|
||||
radius = min(center_x, center_y) * 0.8
|
||||
self.draw.arc((center_x - radius, center_y - radius, center_x + radius,
|
||||
center_y + radius), start=0, end=360, fill=self.highlight_color_rgb, width=10)
|
||||
|
||||
def _animate(self) -> None:
|
||||
"""The main animation loop."""
|
||||
if self.pause_count > 0 or not self.running or not self.winfo_exists():
|
||||
return
|
||||
|
||||
# Do not animate if a grab is active on a different window.
|
||||
try:
|
||||
toplevel = self.winfo_toplevel()
|
||||
grab_widget = toplevel.grab_current()
|
||||
if grab_widget is not None and grab_widget != toplevel:
|
||||
self.after(100, self._animate) # Check again after a short delay
|
||||
return
|
||||
except Exception:
|
||||
# This can happen if a grabbed widget (like a combobox dropdown)
|
||||
# is destroyed at the exact moment this check runs.
|
||||
# It's safest to just skip this animation frame.
|
||||
self.after(30, self._animate)
|
||||
return
|
||||
|
||||
self.angle += 0.1
|
||||
if self.angle > 2 * pi:
|
||||
self.angle -= 2 * pi
|
||||
self._draw_frame()
|
||||
self.after(30, self._animate)
|
||||
|
||||
def start(self, pulse: bool = False) -> None:
|
||||
"""
|
||||
Starts the animation.
|
||||
|
||||
Args:
|
||||
pulse (bool): If True, plays a pulsing animation instead of the main one.
|
||||
"""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.running = True
|
||||
self.is_disabled = False
|
||||
self.pulse_animation = pulse
|
||||
if self.pause_count == 0:
|
||||
self._animate()
|
||||
|
||||
def stop(self, status: Optional[str] = None) -> None:
|
||||
"""Stops the animation and shows the static 'stopped' frame."""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.running = False
|
||||
self.pulse_animation = False
|
||||
self.is_disabled = status == "DISABLE"
|
||||
self._draw_stopped_frame()
|
||||
|
||||
def hide(self) -> None:
|
||||
"""Stops the animation and clears the canvas."""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.running = False
|
||||
self.pulse_animation = False
|
||||
self.delete("all")
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pauses the animation and draws a static frame."""
|
||||
self.pause_count += 1
|
||||
self._draw_stopped_frame()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resumes the animation if the pause count is zero."""
|
||||
self.pause_count = max(0, self.pause_count - 1)
|
||||
if self.pause_count == 0 and self.running:
|
||||
self._animate()
|
||||
|
||||
def show_full_circle(self) -> None:
|
||||
"""Shows the static 'stopped' frame without starting the animation."""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
if not self.running:
|
||||
self._draw_stopped_frame()
|
126
app_config.py
Normal file
126
app_config.py
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
# Assuming the Translate class exists in shared_libs as per the template
|
||||
from shared_libs.common_tools import Translate
|
||||
|
||||
class AppConfig:
|
||||
"""Central configuration and system setup manager for PyImage Backup."""
|
||||
|
||||
# --- Core Paths ---
|
||||
BASE_DIR: Path = Path.home()
|
||||
CONFIG_DIR: Path = BASE_DIR / ".config/pyimage_backup"
|
||||
SETTINGS_FILE: Path = CONFIG_DIR / "settings.conf"
|
||||
EXCLUDE_LIST_PATH: Path = Path(__file__).parent / "rsync-exclude-liste"
|
||||
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
|
||||
|
||||
# --- Application Info ---
|
||||
VERSION: str = "v.1.0.0"
|
||||
UPDATE_URL: str = "" # To be defined later
|
||||
|
||||
# --- UI Configuration ---
|
||||
UI_CONFIG: Dict[str, Any] = {
|
||||
"window_title": "PyImage Backup",
|
||||
"window_size": "1200x800",
|
||||
"font_family": "Ubuntu",
|
||||
"font_size": 11,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def ensure_directories(cls) -> None:
|
||||
"""Ensures that all required application directories exist."""
|
||||
if not cls.CONFIG_DIR.exists():
|
||||
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
# In the future, we can create a default settings file here
|
||||
|
||||
# --- Translation Setup ---
|
||||
# Initialize translations for the app domain "pyimage_backup"
|
||||
_ = Translate.setup_translations("pyimage_backup")
|
||||
|
||||
class Msg:
|
||||
"""Provides centralized access to all translated UI strings."""
|
||||
|
||||
TTIP: Dict[str, str] = {
|
||||
"theme_toggle": "",
|
||||
"tooltips_toggle": "",
|
||||
"updates_toggle": "",
|
||||
"show_log": "",
|
||||
"about_app": "",
|
||||
"updates_disabled": "",
|
||||
"no_server_conn_tt": "",
|
||||
"up_to_date": "",
|
||||
"install_new_version": "",
|
||||
}
|
||||
STR: Dict[str, str] = {
|
||||
# General
|
||||
"app_title": _("PyImage Backup"),
|
||||
"welcome_title": _("Willkommen bei PyImage Backup!"),
|
||||
"welcome_msg": _("Bitte wählen Sie eine Aktion aus dem Menü."),
|
||||
"app_started": _("Anwendung erfolgreich gestartet."),
|
||||
"app_quit": _("Anwendung wird beendet."),
|
||||
"browse": _("Durchsuchen..."),
|
||||
"start_backup": _("Backup starten"),
|
||||
"restore": _("Wiederherstellen"),
|
||||
"error": _("Fehler"),
|
||||
"confirm": _("Bestätigen"),
|
||||
"warning": _("Warnung"),
|
||||
"cancel": _("Abbrechen"),
|
||||
"yes": _("Ja"),
|
||||
"no": _("Nein"),
|
||||
|
||||
# Menus
|
||||
"file_menu": _("Datei"),
|
||||
"exit_menu": _("Beenden"),
|
||||
"backup_menu": _("Backup"),
|
||||
"system_backup_menu": _("System-Backup"),
|
||||
"user_backup_menu": _("Benutzerdaten-Backup"),
|
||||
"restore_menu": _("Wiederherstellung"),
|
||||
"browse_backups_menu": _("Backups durchsuchen"),
|
||||
"help_menu": _("Hilfe"),
|
||||
"info_menu": _("Info"),
|
||||
|
||||
# Backup Views
|
||||
"source_folders": _("Zu sichernde Ordner"),
|
||||
"dest_folder": _("Zielordner"),
|
||||
"backup_type": _("Backup-Typ"),
|
||||
"type_incremental": _("Inkrementell (Empfohlen, spart Speicherplatz)"),
|
||||
"type_full": _("Vollständig (Erzwingt ein neues, komplettes Backup)"),
|
||||
"cat_images": _("Bilder"),
|
||||
"cat_documents": _("Dokumente"),
|
||||
"cat_music": _("Musik"),
|
||||
"cat_videos": _("Videos"),
|
||||
|
||||
# Browser View
|
||||
"backup_location": _("Backup-Speicherort"),
|
||||
"found_backups": _("Gefundene Backups"),
|
||||
"col_backup_name": _("Backup-Name"),
|
||||
"col_type": _("Typ"),
|
||||
"col_date": _("Datum"),
|
||||
"type_system": _("System"),
|
||||
"type_user": _("Benutzerdaten"),
|
||||
|
||||
# Dialogs & Messages
|
||||
"select_dest_folder_title": _("Zielordner für Backup auswählen"),
|
||||
"select_backup_location_title": _("Backup-Speicherort auswählen"),
|
||||
"err_no_dest_folder": _("Bitte wählen Sie einen Zielordner aus."),
|
||||
"err_no_source_folder": _("Bitte wählen Sie mindestens einen Quellordner aus."),
|
||||
"err_no_backup_selected": _("Bitte wählen Sie ein Backup aus der Liste aus."),
|
||||
"confirm_user_restore_title": _("Benutzerdaten-Wiederherstellung bestätigen"),
|
||||
"confirm_user_restore_msg": _("Möchten Sie das Backup von '{backup_name}' wirklich am ursprünglichen Ort wiederherstellen? \n\nEventuell vorhandene neuere Dateien werden überschrieben."),
|
||||
"final_warning_system_restore_title": _("FINALE WARNUNG"),
|
||||
"final_warning_system_restore_msg": _("ACHTUNG: Sie sind im Begriff, das System wiederherzustellen. Dieser Vorgang kann nicht sicher unterbrochen werden. Alle Änderungen seit dem Backup gehen verloren. \n\nDer Computer wird nach Abschluss automatisch neu gestartet. \n\nWIRKLICH FORTFAHREN?"),
|
||||
"btn_continue": _("FORTFAHREN"),
|
||||
|
||||
# Lock Screen
|
||||
"lock_title": _("Systemwiederherstellung läuft"),
|
||||
"lock_msg1": _("Bitte unterbrechen Sie diesen Vorgang nicht."),
|
||||
"lock_msg2": _("Der Computer wird nach Abschluss automatisch neu gestartet."),
|
||||
"add_job_title": _("Neuen Job hinzufügen"),
|
||||
"frequency": _("Häufigkeit"),
|
||||
"freq_daily": _("Täglich"),
|
||||
"freq_weekly": _("Wöchentlich"),
|
||||
"freq_monthly": _("Monatlich"),
|
||||
"save": _("Speichern"),
|
||||
}
|
||||
|
155
backup_manager.py
Normal file
155
backup_manager.py
Normal file
@@ -0,0 +1,155 @@
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import threading
|
||||
import re
|
||||
import datetime
|
||||
import signal
|
||||
from typing import Callable, Optional, List, Dict, Any
|
||||
from crontab import CronTab
|
||||
|
||||
class BackupManager:
|
||||
"""
|
||||
Handles the logic for creating and managing backups using rsync.
|
||||
"""
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
self.process = None
|
||||
self.app_tag = "# PyImageBackup Job"
|
||||
|
||||
def pause_backup(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGSTOP)
|
||||
self.logger.log("Backup paused.")
|
||||
|
||||
def resume_backup(self):
|
||||
if self.process and self.process.poll() is None:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGCONT)
|
||||
self.logger.log("Backup resumed.")
|
||||
|
||||
def start_restore_path(self, source_path: str, dest_path: str, is_system: bool, on_progress: Optional[Callable[[int], None]] = None, on_completion: Optional[Callable[[], None]] = None, on_error: Optional[Callable[[], None]] = None):
|
||||
"""Starts a generic restore process for a specific path."""
|
||||
thread = threading.Thread(target=self._run_restore_path, args=(source_path, dest_path, is_system, on_progress, on_completion, on_error))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _run_restore_path(self, source_path: str, dest_path: str, is_system: bool, on_progress: Optional[Callable[[int], None]], on_completion: Optional[Callable[[], None]], on_error: Optional[Callable[[], None]]):
|
||||
try:
|
||||
self.logger.log(f"Starte Wiederherstellung von '{source_path}' nach '{dest_path}'...")
|
||||
|
||||
if os.path.isdir(source_path) and not source_path.endswith('/'):
|
||||
source_path += '/'
|
||||
|
||||
parent_dest = os.path.dirname(dest_path)
|
||||
if not os.path.exists(parent_dest):
|
||||
os.makedirs(parent_dest, exist_ok=True)
|
||||
|
||||
command = []
|
||||
if is_system:
|
||||
command.extend(['pkexec', 'rsync', '-aAXH'])
|
||||
else:
|
||||
command.extend(['rsync', '-a'])
|
||||
|
||||
command.extend(['--dry-run', '--info=progress2', source_path, dest_path])
|
||||
|
||||
self._execute_rsync(command, on_progress, on_error)
|
||||
self.logger.log(f"Wiederherstellung nach '{dest_path}' abgeschlossen.")
|
||||
finally:
|
||||
if on_completion: on_completion()
|
||||
|
||||
def _execute_rsync(self, command: List[str], on_progress: Optional[Callable[[int], None]], on_error: Optional[Callable[[], None]]):
|
||||
try:
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, preexec_fn=os.setsid)
|
||||
|
||||
# Regex to find percentage from rsync output
|
||||
progress_regex = re.compile(r'\s*(\d+)%')
|
||||
|
||||
if self.process.stdout:
|
||||
for line in iter(self.process.stdout.readline, ''):
|
||||
self.logger.log(line.strip())
|
||||
match = progress_regex.search(line)
|
||||
if match and on_progress:
|
||||
percentage = int(match.group(1))
|
||||
on_progress(percentage)
|
||||
|
||||
self.process.wait()
|
||||
if self.process.stderr:
|
||||
stderr_output = self.process.stderr.read()
|
||||
if stderr_output:
|
||||
self.logger.log(f"Rsync Error: {stderr_output.strip()}")
|
||||
if on_error:
|
||||
on_error()
|
||||
|
||||
except FileNotFoundError:
|
||||
self.logger.log("Error: 'rsync' command not found. Please ensure it is installed and in your PATH.")
|
||||
if on_error:
|
||||
on_error()
|
||||
except Exception as e:
|
||||
self.logger.log(f"An unexpected error occurred: {e}")
|
||||
if on_error:
|
||||
on_error()
|
||||
|
||||
def get_scheduled_jobs(self) -> List[Dict[str, Any]]:
|
||||
"""Retrieves all scheduled jobs created by this application."""
|
||||
jobs_list = []
|
||||
try:
|
||||
user_cron = CronTab(user=True)
|
||||
for job in user_cron:
|
||||
if self.app_tag in job.comment:
|
||||
details = self._parse_job_comment(job.comment)
|
||||
if details:
|
||||
jobs_list.append({
|
||||
"id": job.comment,
|
||||
"active": job.is_enabled(),
|
||||
"type": details.get("type", "N/A"),
|
||||
"frequency": details.get("freq", "N/A"),
|
||||
"destination": details.get("dest", "N/A"),
|
||||
"sources": details.get("sources", []),
|
||||
"command": job.command
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.log(f"Fehler beim Laden der Cron-Jobs: {e}")
|
||||
return jobs_list
|
||||
|
||||
def add_scheduled_job(self, job_details: Dict[str, Any]):
|
||||
"""Adds a new job to the user's crontab."""
|
||||
try:
|
||||
user_cron = CronTab(user=True)
|
||||
job = user_cron.new(command=job_details["command"], comment=job_details["comment"])
|
||||
|
||||
# Set frequency
|
||||
if job_details["frequency"] == "daily":
|
||||
job.day.every(1)
|
||||
elif job_details["frequency"] == "weekly":
|
||||
job.dow.every(1) # Every Monday
|
||||
elif job_details["frequency"] == "monthly":
|
||||
job.dom.every(1) # First day of month
|
||||
|
||||
job.enable()
|
||||
user_cron.write()
|
||||
self.logger.log(f"Job erfolgreich hinzugefügt: {job_details['comment']}")
|
||||
except Exception as e:
|
||||
self.logger.log(f"Fehler beim Hinzufügen des Cron-Jobs: {e}")
|
||||
|
||||
def remove_scheduled_job(self, job_id: str): # job_id is the comment
|
||||
"""Removes a job from the user's crontab by its ID (comment)."""
|
||||
try:
|
||||
user_cron = CronTab(user=True)
|
||||
user_cron.remove_all(comment=job_id)
|
||||
user_cron.write()
|
||||
self.logger.log(f"Job erfolgreich entfernt: {job_id}")
|
||||
except Exception as e:
|
||||
self.logger.log(f"Fehler beim Entfernen des Cron-Jobs: {e}")
|
||||
|
||||
def _parse_job_comment(self, comment: str) -> Dict[str, Any]:
|
||||
"""Parses job details from the comment string."""
|
||||
details = {}
|
||||
parts = comment.split("; ")
|
||||
for part in parts:
|
||||
if ":" in part:
|
||||
key, value = part.split(":", 1)
|
||||
if key.strip() == "sources":
|
||||
details[key.strip()] = [s.strip() for s in value.split(",")]
|
||||
else:
|
||||
details[key.strip()] = value.strip()
|
||||
return details
|
558
main_app.py
Normal file
558
main_app.py
Normal file
@@ -0,0 +1,558 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
import datetime
|
||||
import shutil
|
||||
from typing import List
|
||||
|
||||
from shared_libs.log_window import LogWindow
|
||||
from shared_libs.logger import app_logger
|
||||
from shared_libs.message import MessageDialog
|
||||
from shared_libs.custom_file_dialog import CustomFileDialog
|
||||
from shared_libs.animated_icon import AnimatedIcon
|
||||
from shared_libs.common_tools import IconManager, ConfigManager
|
||||
from backup_manager import BackupManager
|
||||
from app_config import AppConfig, Msg
|
||||
|
||||
|
||||
class MainApplication(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
AppConfig.ensure_directories()
|
||||
|
||||
self.title("PyImage Backup")
|
||||
self.geometry("1000x700")
|
||||
|
||||
self.style = ttk.Style()
|
||||
self.tk.call("source", "/usr/share/TK-Themes/water.tcl")
|
||||
self.tk.call("set_theme", "light")
|
||||
self.style.configure("Custom.TFrame", background="#2b3e4f")
|
||||
self.style.configure("Sidebar.TButton", background="#2b3e4f",
|
||||
foreground="white", font=("Helvetica", 12, "bold"))
|
||||
self.style.map("Sidebar.TButton", background=[
|
||||
("active", "#3a526a")], foreground=[("active", "white")])
|
||||
self.style.layout("Sidebar.TButton", self.style.layout(
|
||||
"TButton.Borderless.Round"))
|
||||
|
||||
self.style.layout("Gray.TButton.Borderless", self.style.layout("TButton.Borderless.Round"))
|
||||
self.style.configure("Gray.TButton.Borderless", foreground="gray")
|
||||
|
||||
|
||||
# --- Main Layout ---
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
sidebar = ttk.Frame(main_frame, width=200, style="Custom.TFrame")
|
||||
sidebar.grid(row=0, column=0, sticky="nsew")
|
||||
main_frame.grid_rowconfigure(0, weight=1)
|
||||
main_frame.grid_columnconfigure(0, weight=0) # Sidebar fixed width
|
||||
main_frame.grid_columnconfigure(1, weight=1) # Content frame expands
|
||||
|
||||
self.content_frame = ttk.Frame(main_frame)
|
||||
self.content_frame.grid(row=0, column=1, sticky="nsew")
|
||||
self.content_frame.grid_rowconfigure(
|
||||
0, weight=0) # top_bar fixed height
|
||||
self.content_frame.grid_rowconfigure(
|
||||
1, weight=1) # content expands
|
||||
self.content_frame.grid_rowconfigure(
|
||||
2, weight=0) # info/checkbox fixed height
|
||||
self.content_frame.grid_rowconfigure(
|
||||
3, weight=0) # action_frame fixed height
|
||||
self.content_frame.grid_columnconfigure(
|
||||
0, weight=1) # content expands horizontally
|
||||
|
||||
# --- Initialize Backend ---
|
||||
self.backup_manager = BackupManager(app_logger)
|
||||
|
||||
# --- Sidebar ---
|
||||
sidebar_buttons_frame = ttk.Frame(sidebar, style="Custom.TFrame")
|
||||
sidebar_buttons_frame.pack(pady=20)
|
||||
|
||||
buttons = [
|
||||
("Computer", None),
|
||||
("Dokumente", "folder-water-documents"),
|
||||
("Bilder", "pictures_extralarge"),
|
||||
("Musik", "music_extralarge"),
|
||||
("Videos", "video_extralarge_folder"),
|
||||
]
|
||||
|
||||
self.image_manager = IconManager()
|
||||
|
||||
for text, icon_name in buttons:
|
||||
button = ttk.Button(sidebar_buttons_frame, text=text, style="Sidebar.TButton",
|
||||
command=lambda t=text: self.on_sidebar_button_click(t))
|
||||
button.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
# Add a button to open the scheduling dialog
|
||||
schedule_dialog_button = ttk.Button(
|
||||
sidebar_buttons_frame, text="Zeitplanung", command=lambda: self._toggle_scheduler_frame(2), style="Sidebar.TButton")
|
||||
schedule_dialog_button.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
free_space_frame = ttk.Frame(sidebar, style="Custom.TFrame")
|
||||
free_space_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
|
||||
ttk.Label(free_space_frame, text="Freier Speicherplatz",
|
||||
background="#2b3e4f", foreground="white").pack()
|
||||
progress_bar = ttk.Progressbar(
|
||||
free_space_frame, orient="horizontal", length=100, mode="determinate")
|
||||
progress_bar.pack(fill=tk.X, pady=5)
|
||||
self.free_space_label = ttk.Label(
|
||||
free_space_frame, text="", background="#2b3e4f", foreground="white")
|
||||
self.free_space_label.pack()
|
||||
self.progress_bar = progress_bar
|
||||
self._update_disk_usage()
|
||||
|
||||
# --- Main Content ---
|
||||
top_bar = ttk.Frame(self.content_frame)
|
||||
top_bar.grid(row=0, column=0, sticky="ew", pady=10)
|
||||
|
||||
# Top bar navigation (left side)
|
||||
top_nav_frame = ttk.Frame(top_bar)
|
||||
top_nav_frame.pack(side=tk.LEFT)
|
||||
|
||||
nav_buttons_defs = [
|
||||
("Backup", lambda: self.toggle_mode("backup", 0)),
|
||||
("Restore", lambda: self.toggle_mode("restore", 1)),
|
||||
("Zeitplanung", lambda: self._toggle_scheduler_frame(2)),
|
||||
("Log", lambda: self._toggle_log_window(3)),
|
||||
]
|
||||
|
||||
self.nav_buttons = []
|
||||
self.nav_progress_bars = []
|
||||
|
||||
for i, (text, command) in enumerate(nav_buttons_defs):
|
||||
button_frame = ttk.Frame(top_nav_frame)
|
||||
button_frame.pack(side=tk.LEFT, padx=5)
|
||||
button = ttk.Button(button_frame, text=text,
|
||||
command=command, style="TButton.Borderless.Round")
|
||||
button.pack(side=tk.TOP)
|
||||
self.nav_buttons.append(button)
|
||||
progress_bar = ttk.Progressbar(
|
||||
button_frame, orient="horizontal", length=50, mode="determinate", style="Small.Horizontal.TProgressbar")
|
||||
progress_bar.pack_forget()
|
||||
self.nav_progress_bars.append(progress_bar)
|
||||
|
||||
if i < len(nav_buttons_defs) - 1:
|
||||
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
|
||||
side=tk.LEFT, fill=tk.Y, padx=2)
|
||||
|
||||
self.canvas_frame = ttk.Frame(self.content_frame)
|
||||
self.canvas_frame.grid(row=1, column=0, sticky="nsew")
|
||||
|
||||
self.left_canvas = tk.Canvas(
|
||||
self.canvas_frame, width=80, height=80, relief="solid", borderwidth=1)
|
||||
self.left_canvas.pack(side=tk.LEFT, fill=tk.BOTH,
|
||||
expand=True, padx=10, pady=10)
|
||||
computer_icon = self.image_manager.get_icon("computer_extralarge")
|
||||
self.left_canvas.create_image(100, 100, image=computer_icon)
|
||||
|
||||
button_frame = ttk.Frame(self.canvas_frame)
|
||||
button_frame.pack(side=tk.LEFT, fill=tk.Y)
|
||||
|
||||
self.mode_button_icon = self.image_manager.get_icon(
|
||||
"forward_extralarge")
|
||||
self.mode_button = ttk.Button(
|
||||
button_frame, image=self.mode_button_icon, command=self.toggle_mode)
|
||||
self.mode_button.pack(pady=200)
|
||||
|
||||
self.right_canvas = tk.Canvas(
|
||||
self.canvas_frame, width=80, height=80, relief="solid", borderwidth=1)
|
||||
self.right_canvas.pack(side=tk.RIGHT, fill=tk.BOTH,
|
||||
expand=True, padx=10, pady=10)
|
||||
device_icon = self.image_manager.get_icon("device_extralarge")
|
||||
self.right_canvas.create_image(100, 100, image=device_icon)
|
||||
|
||||
# --- Log Window ---
|
||||
self._setup_log_window()
|
||||
|
||||
# --- Scheduler Frame ---
|
||||
self._setup_scheduler_frame()
|
||||
|
||||
# --- Task Bar ---
|
||||
self._setup_task_bar()
|
||||
|
||||
self.mode = "backup"
|
||||
self._update_nav_buttons(0)
|
||||
|
||||
def _setup_log_window(self):
|
||||
self.log_frame = ttk.Frame(self.content_frame)
|
||||
self.log_window = LogWindow(self.log_frame)
|
||||
self.log_window.pack(fill=tk.BOTH, expand=True)
|
||||
app_logger.init_logger(self.log_window.log_message)
|
||||
self.log_frame.grid(row=1, column=0, sticky="nsew")
|
||||
self.log_frame.grid_remove()
|
||||
|
||||
def _setup_scheduler_frame(self):
|
||||
self.scheduler_frame = ttk.Frame(self.content_frame, padding=10)
|
||||
self.scheduler_frame.grid(row=1, column=0, sticky="nsew")
|
||||
self.scheduler_frame.grid_remove()
|
||||
|
||||
# --- Jobs List View ---
|
||||
self.jobs_frame = ttk.LabelFrame(self.scheduler_frame, text="Geplante Jobs", padding=10)
|
||||
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
columns = ("active", "type", "frequency", "destination", "sources")
|
||||
self.jobs_tree = ttk.Treeview(self.jobs_frame, columns=columns, show="headings")
|
||||
for col in columns:
|
||||
self.jobs_tree.heading(col, text=col.capitalize())
|
||||
self.jobs_tree.column(col, width=100)
|
||||
self.jobs_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
self._load_scheduled_jobs()
|
||||
|
||||
list_button_frame = ttk.Frame(self.jobs_frame)
|
||||
list_button_frame.pack(pady=10)
|
||||
ttk.Button(list_button_frame, text="Hinzufügen", command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(list_button_frame, text="Entfernen", command=self._remove_selected_job).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# --- Add Job View ---
|
||||
self.add_job_frame = ttk.LabelFrame(self.scheduler_frame, text="Neuer Job hinzufügen", padding=10)
|
||||
|
||||
self.backup_type = tk.StringVar(value="system")
|
||||
self.destination = tk.StringVar()
|
||||
self.user_sources = {
|
||||
Msg.STR["cat_images"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_documents"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_music"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_videos"]: tk.BooleanVar(value=False)
|
||||
}
|
||||
self.frequency = tk.StringVar(value="daily")
|
||||
|
||||
type_frame = ttk.LabelFrame(self.add_job_frame, text=Msg.STR["backup_type"], padding=10)
|
||||
type_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Radiobutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type, value="system", command=self._toggle_user_sources).pack(anchor=tk.W)
|
||||
ttk.Radiobutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type, value="user", command=self._toggle_user_sources).pack(anchor=tk.W)
|
||||
|
||||
dest_frame = ttk.LabelFrame(self.add_job_frame, text=Msg.STR["dest_folder"], padding=10)
|
||||
dest_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Entry(dest_frame, textvariable=self.destination, state="readonly", width=50).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
ttk.Button(dest_frame, text=Msg.STR["browse"], command=self._select_destination).pack(side=tk.RIGHT)
|
||||
|
||||
self.user_sources_frame = ttk.LabelFrame(self.add_job_frame, text=Msg.STR["source_folders"], padding=10)
|
||||
for name, var in self.user_sources.items():
|
||||
ttk.Checkbutton(self.user_sources_frame, text=name, variable=var).pack(anchor=tk.W)
|
||||
self._toggle_user_sources()
|
||||
|
||||
freq_frame = ttk.LabelFrame(self.add_job_frame, text=Msg.STR["frequency"], padding=10)
|
||||
freq_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_daily"], variable=self.frequency, value="daily").pack(anchor=tk.W)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_weekly"], variable=self.frequency, value="weekly").pack(anchor=tk.W)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_monthly"], variable=self.frequency, value="monthly").pack(anchor=tk.W)
|
||||
|
||||
add_button_frame = ttk.Frame(self.add_job_frame)
|
||||
add_button_frame.pack(pady=10)
|
||||
ttk.Button(add_button_frame, text=Msg.STR["save"], command=self._save_job).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(add_button_frame, text="Zurück", command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _setup_task_bar(self):
|
||||
self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10)
|
||||
self.info_checkbox_frame.grid(row=2, column=0, sticky="ew")
|
||||
|
||||
self.info_label = ttk.Label(self.info_checkbox_frame, text="Info text about the current view.")
|
||||
self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5)
|
||||
|
||||
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
|
||||
checkbox_frame.pack(fill=tk.X, pady=5)
|
||||
self.vollbackup_var = tk.BooleanVar()
|
||||
self.inkrementell_var = tk.BooleanVar()
|
||||
self.testlauf_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(checkbox_frame, text="Vollbackup", variable=self.vollbackup_var).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Checkbutton(checkbox_frame, text="Inkrementell", variable=self.inkrementell_var).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Checkbutton(checkbox_frame, text="Testlauf", variable=self.testlauf_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.action_frame = ttk.Frame(self.content_frame, padding=10)
|
||||
self.action_frame.grid(row=3, column=0, sticky="ew")
|
||||
|
||||
self.animated_icon = AnimatedIcon(self.action_frame, width=20, height=20, use_pillow=True)
|
||||
self.animated_icon.pack(side=tk.LEFT, padx=5)
|
||||
self.animated_icon.stop("DISABLE")
|
||||
|
||||
self.task_progress = ttk.Progressbar(self.action_frame, orient="horizontal", length=100, mode="determinate")
|
||||
self.task_progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
||||
|
||||
self.start_pause_button = ttk.Button(self.action_frame, text="Start", command=self._toggle_start_pause)
|
||||
self.start_pause_button.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def _toggle_scheduler_view(self):
|
||||
if self.jobs_frame.winfo_ismapped():
|
||||
self.jobs_frame.pack_forget()
|
||||
self.add_job_frame.pack(fill=tk.BOTH, expand=True)
|
||||
else:
|
||||
self.add_job_frame.pack_forget()
|
||||
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
def _toggle_user_sources(self):
|
||||
if self.backup_type.get() == "user":
|
||||
self.user_sources_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
else:
|
||||
self.user_sources_frame.pack_forget()
|
||||
|
||||
def _select_destination(self):
|
||||
dialog = CustomFileDialog(self, mode="dir", title=Msg.STR["select_dest_folder_title"])
|
||||
result = dialog.get_result()
|
||||
if result:
|
||||
self.destination.set(result)
|
||||
|
||||
def _save_job(self):
|
||||
dest = self.destination.get()
|
||||
if not dest:
|
||||
MessageDialog(master=self, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
|
||||
return
|
||||
|
||||
job_type = self.backup_type.get()
|
||||
job_frequency = self.frequency.get()
|
||||
job_sources = []
|
||||
|
||||
if job_type == "user":
|
||||
job_sources = [name for name, var in self.user_sources.items() if var.get()]
|
||||
if not job_sources:
|
||||
MessageDialog(master=self, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_no_source_folder"])
|
||||
return
|
||||
|
||||
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "main_app.py"))
|
||||
command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\""
|
||||
if job_type == "user":
|
||||
command += f" --sources ";
|
||||
for s in job_sources:
|
||||
command += f'\"{s}\" ';
|
||||
|
||||
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
|
||||
if job_type == "user":
|
||||
comment += f"; sources:{ ','.join(job_sources)}"
|
||||
|
||||
job_details = {
|
||||
"command": command,
|
||||
"comment": comment,
|
||||
"type": job_type,
|
||||
"frequency": job_frequency,
|
||||
"destination": dest,
|
||||
"sources": job_sources
|
||||
}
|
||||
self.backup_manager.add_scheduled_job(job_details)
|
||||
self._load_scheduled_jobs()
|
||||
self._toggle_scheduler_view()
|
||||
|
||||
def _load_scheduled_jobs(self):
|
||||
for i in self.jobs_tree.get_children():
|
||||
self.jobs_tree.delete(i)
|
||||
jobs = self.backup_manager.get_scheduled_jobs()
|
||||
for job in jobs:
|
||||
self.jobs_tree.insert("", "end", values=(
|
||||
job["active"], job["type"], job["frequency"], job["destination"], ", ".join(job["sources"])
|
||||
), iid=job["id"])
|
||||
|
||||
def _remove_selected_job(self):
|
||||
selected_item = self.jobs_tree.focus()
|
||||
if not selected_item:
|
||||
MessageDialog(master=self, message_type="error", title="Fehler", text="Kein Job ausgewählt.")
|
||||
return
|
||||
|
||||
job_id = self.jobs_tree.item(selected_item)[ "values"][0]
|
||||
self.backup_manager.remove_scheduled_job(job_id)
|
||||
self._load_scheduled_jobs()
|
||||
|
||||
def _update_nav_buttons(self, active_index):
|
||||
for i, button in enumerate(self.nav_buttons):
|
||||
if i == active_index:
|
||||
button.configure(style="TButton.Borderless.Round")
|
||||
self.nav_progress_bars[i].pack(side=tk.BOTTOM, fill=tk.X)
|
||||
self.nav_progress_bars[i]['value'] = 100
|
||||
else:
|
||||
button.configure(style="Gray.TButton.Borderless")
|
||||
self.nav_progress_bars[i].pack_forget()
|
||||
|
||||
def _toggle_start_pause(self):
|
||||
if self.start_pause_button["text"] == "Start":
|
||||
self.start_pause_button["text"] = "Pause"
|
||||
self.animated_icon.start()
|
||||
if self.mode == "backup":
|
||||
if self.vollbackup_var.get():
|
||||
self._start_system_backup("full")
|
||||
else:
|
||||
self._start_system_backup("incremental")
|
||||
else: # restore
|
||||
# placeholder for restore
|
||||
pass
|
||||
elif self.start_pause_button["text"] == "Pause":
|
||||
self.start_pause_button["text"] = "Resume"
|
||||
if self.animated_icon.animation_type == "blink":
|
||||
self.animated_icon.pause()
|
||||
else:
|
||||
self.animated_icon.start(pulse=True)
|
||||
self.backup_manager.pause_backup()
|
||||
elif self.start_pause_button["text"] == "Resume":
|
||||
self.start_pause_button["text"] = "Pause"
|
||||
self.animated_icon.resume()
|
||||
self.backup_manager.resume_backup()
|
||||
|
||||
def _on_backup_error(self):
|
||||
self.animated_icon.stop("DISABLE")
|
||||
self.start_pause_button["text"] = "Start"
|
||||
|
||||
def _update_task_bar_visibility(self, mode):
|
||||
if mode in ["backup", "restore"]:
|
||||
self.info_checkbox_frame.grid()
|
||||
self.action_frame.grid()
|
||||
elif mode == "log":
|
||||
self.info_checkbox_frame.grid_remove()
|
||||
self.action_frame.grid()
|
||||
elif mode == "scheduler":
|
||||
self.info_checkbox_frame.grid_remove()
|
||||
self.action_frame.grid_remove()
|
||||
|
||||
def toggle_mode(self, mode=None, active_index=None):
|
||||
if active_index is not None:
|
||||
self._update_nav_buttons(active_index)
|
||||
|
||||
self.log_frame.grid_remove()
|
||||
self.scheduler_frame.grid_remove()
|
||||
self.canvas_frame.grid()
|
||||
self._update_task_bar_visibility(self.mode)
|
||||
|
||||
if mode:
|
||||
self.mode = mode
|
||||
else:
|
||||
# Toggle if no mode is provided
|
||||
if self.mode == "backup":
|
||||
self.mode = "restore"
|
||||
else:
|
||||
self.mode = "backup"
|
||||
|
||||
if self.mode == "backup":
|
||||
self.mode_button_icon = self.image_manager.get_icon(
|
||||
"forward_extralarge")
|
||||
self.mode_button.config(image=self.mode_button_icon)
|
||||
self.info_label.config(text="Backup-Modus: Hier können Sie ein Backup starten.")
|
||||
else:
|
||||
self.mode_button_icon = self.image_manager.get_icon(
|
||||
"back_extralarge")
|
||||
self.mode_button.config(image=self.mode_button_icon)
|
||||
self.info_label.config(text="Restore-Modus: Hier können Sie eine Wiederherstellung starten.")
|
||||
|
||||
def _update_disk_usage(self):
|
||||
total, used, free = shutil.disk_usage("/")
|
||||
free_gb = free / (1024**3)
|
||||
total_gb = total / (1024**3)
|
||||
self.free_space_label.config(text=f"Freier Speicher: {free_gb:.2f} GB")
|
||||
self.progress_bar["value"] = (used / total) * 100
|
||||
|
||||
def on_sidebar_button_click(self, button_text):
|
||||
# Update the image in the left canvas
|
||||
image_map = {
|
||||
"Computer": "computer_extralarge",
|
||||
"Dokumente": "documents_extralarge",
|
||||
"Bilder": "pictures_extralarge",
|
||||
"Musik": "music_extralarge",
|
||||
"Videos": "video_extralarge_folder",
|
||||
}
|
||||
image_key = image_map.get(button_text)
|
||||
if image_key:
|
||||
new_image = self.image_manager.get_icon(image_key)
|
||||
self.left_canvas.delete("all")
|
||||
self.left_canvas.create_image(100, 100, image=new_image)
|
||||
self.left_canvas.image = new_image # Keep a reference
|
||||
|
||||
if self.mode == "backup":
|
||||
if button_text == "Computer":
|
||||
self._start_system_backup("full")
|
||||
else:
|
||||
# For user data backup, we need to map the button text to the actual source folder
|
||||
source_map = {
|
||||
"Dokumente": Msg.STR["cat_documents"],
|
||||
"Bilder": Msg.STR["cat_images"],
|
||||
"Musik": Msg.STR["cat_music"],
|
||||
"Videos": Msg.STR["cat_videos"],
|
||||
}
|
||||
source = source_map.get(button_text)
|
||||
if source:
|
||||
self._start_user_backup(sources=[source])
|
||||
else:
|
||||
print(f"Unknown user data source: {button_text}")
|
||||
else:
|
||||
print(
|
||||
f"Restore functionality not yet implemented for: {button_text}")
|
||||
|
||||
def _start_system_backup(self, mode):
|
||||
dialog = CustomFileDialog(self, mode="dir", title=Msg.STR["select_dest_folder_title"])
|
||||
dest = dialog.get_result()
|
||||
if dest:
|
||||
self.backup_manager.start_restore_path("/", dest, True, on_progress=self._update_task_progress, on_error=self._on_backup_error)
|
||||
|
||||
def _start_user_backup(self, sources):
|
||||
dialog = CustomFileDialog(self, mode="dir", title=Msg.STR["select_dest_folder_title"])
|
||||
dest = dialog.get_result()
|
||||
if dest:
|
||||
for source in sources:
|
||||
self.backup_manager.start_restore_path(source, dest, False, on_progress=self._update_task_progress, on_error=self._on_backup_error)
|
||||
|
||||
def _update_task_progress(self, percentage):
|
||||
self.task_progress["value"] = percentage
|
||||
|
||||
def _toggle_log_window(self, active_index=None):
|
||||
if active_index is not None:
|
||||
self._update_nav_buttons(active_index)
|
||||
|
||||
self.canvas_frame.grid_remove()
|
||||
self.scheduler_frame.grid_remove()
|
||||
self.log_frame.grid()
|
||||
self._update_task_bar_visibility("log")
|
||||
|
||||
def _toggle_scheduler_frame(self, active_index=None):
|
||||
if active_index is not None:
|
||||
self._update_nav_buttons(active_index)
|
||||
|
||||
self.canvas_frame.grid_remove()
|
||||
self.log_frame.grid_remove()
|
||||
self.scheduler_frame.grid()
|
||||
self._load_scheduled_jobs()
|
||||
self._update_task_bar_visibility("scheduler")
|
||||
|
||||
def quit(self):
|
||||
app_logger.log(Msg.STR["app_quit"])
|
||||
self.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description="PyImage Backup Application.")
|
||||
parser.add_argument(
|
||||
"--backup-type", choices=['user', 'system'], help="Type of backup to perform.")
|
||||
parser.add_argument(
|
||||
"--destination", help="Destination directory for the backup.")
|
||||
parser.add_argument("--sources", nargs='+',
|
||||
help="List of sources for user backup (e.g., Bilder, Dokumente).")
|
||||
parser.add_argument("--mode", choices=['full', 'incremental'],
|
||||
default='incremental', help="Mode for system backup.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.backup_type and args.destination:
|
||||
# Command-line mode
|
||||
# We need a logger that prints to the console
|
||||
from shared_libs.logger import app_logger as cli_logger
|
||||
# cli_logger.set_log_widget(None) # Ensure it logs to console
|
||||
|
||||
backup_manager = BackupManager(cli_logger)
|
||||
|
||||
if args.backup_type == 'user':
|
||||
if not args.sources:
|
||||
print("Error: --sources are required for user backup.")
|
||||
sys.exit(1)
|
||||
# For CLI, we run the task in the main thread to wait for it.
|
||||
backup_manager._run_user_backup(
|
||||
args.sources, args.destination, None, None)
|
||||
|
||||
elif args.backup_type == 'system':
|
||||
# For CLI, we run the task in the main thread to wait for it.
|
||||
backup_manager._run_system_backup(
|
||||
args.destination, args.mode, None, None)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
# GUI mode
|
||||
app = MainApplication()
|
||||
app.mainloop()
|
51
org.pyimage.policy
Normal file
51
org.pyimage.policy
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||
|
||||
<!--
|
||||
Policy definitions for ssl_encrypt and ssl_decrypt
|
||||
|
||||
Copyright (C) 2025 Désiré Werner Menrath <polunga40@unity-mail.de>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library. If not, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<policyconfig>
|
||||
<action id="org.ssl_encrypt">
|
||||
<defaults>
|
||||
<allow_any>auth_admin_keep</allow_any>
|
||||
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/ssl_encrypt.py</annotate>
|
||||
</action>
|
||||
|
||||
<action id="org.ssl_decrypt">
|
||||
<defaults>
|
||||
<allow_any>auth_admin_keep</allow_any>
|
||||
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/ssl_decrypt.py</annotate>
|
||||
</action>
|
||||
|
||||
<action id="org.match_found">
|
||||
<defaults>
|
||||
<allow_any>auth_admin_keep</allow_any>
|
||||
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/match_found.py</annotate>
|
||||
</action>
|
||||
</policyconfig>
|
71
rsync-exclude-liste
Normal file
71
rsync-exclude-liste
Normal file
@@ -0,0 +1,71 @@
|
||||
/dev/*
|
||||
/proc/*
|
||||
/sys/*
|
||||
/media/*
|
||||
/mnt/*
|
||||
**/tmp/
|
||||
/run/*
|
||||
/home/*/Pyapps
|
||||
/home/*/Scripts
|
||||
/home/*/Apps
|
||||
/var/run/*
|
||||
/var/lock/*
|
||||
/var/lib/docker/*
|
||||
/var/lib/schroot/*
|
||||
/data/*
|
||||
/DATA/*
|
||||
/cdrom/*
|
||||
/sdcard/*
|
||||
/lost+found
|
||||
/swapfile
|
||||
/system/*
|
||||
**/root/.gvfs
|
||||
/snap/*
|
||||
/home/*/.gvfs
|
||||
/var/cache/pacman/pkg/*
|
||||
/var/lib/libvirt/images/
|
||||
/var/cache/apt/archives/*
|
||||
/var/cache/yum/*
|
||||
/var/cache/dnf/*
|
||||
/var/cache/eopkg/*
|
||||
/var/cache/xbps/*
|
||||
/var/cache/zypp/*
|
||||
/var/cache/edb/*
|
||||
/home/*/Downloads/*
|
||||
/home/*/PDF-Dateien/*
|
||||
/home/*/Warpinator/
|
||||
/home/*/web/
|
||||
/home/*/.local/share/gnome-boxes/
|
||||
/home/*/Bilder/*
|
||||
/home/*/Dokumente/*
|
||||
/home/*/Musik/*
|
||||
/home/*/Videos/*
|
||||
/home/*/Skripte/
|
||||
/home/*/'VirtualBox VMs'/
|
||||
/home/*/Games/
|
||||
/home/pundsl/leagueoflegends/
|
||||
/home/*/.local/share/leagueoflegends/
|
||||
/home/*/.config/lutris/
|
||||
/home/*/.local/share/lutris/
|
||||
/home/*/.minecraft
|
||||
/home/*/.thunderbird
|
||||
/home/*/.minetest
|
||||
/home/*/.steam
|
||||
/home/*/.local/share/Steam/
|
||||
/home/*/.local/share/supertux2/
|
||||
/home/*/.local/share/supertuxkart/
|
||||
/home/*/.cache/supertuxkart/
|
||||
/home/*/.config/supertuxkart/
|
||||
#/home/*/.local/share/applications/wine/
|
||||
#/home/*/.wine/
|
||||
/home/*/rsynclog
|
||||
/home/*/rsynclog.old
|
||||
/home/*/.local/share/[Tt]rash
|
||||
/home/*/.opera/cache
|
||||
/home/*/.kde/share/apps/kio_http/cache
|
||||
/home/*/.kde/share/cache/http
|
||||
/Dateien
|
||||
/Sicherung
|
||||
/Files
|
||||
/Backup
|
||||
|
129
schedule_job_dialog.py
Normal file
129
schedule_job_dialog.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
|
||||
from shared_libs.message import MessageDialog
|
||||
from shared_libs.custom_file_dialog import CustomFileDialog
|
||||
from app_config import AppConfig, Msg
|
||||
|
||||
class ScheduleJobDialog(tk.Toplevel):
|
||||
def __init__(self, parent, backup_manager):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.backup_manager = backup_manager
|
||||
self.result = None
|
||||
|
||||
self.title(Msg.STR["add_job_title"])
|
||||
self.geometry("500x400")
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
|
||||
self.backup_type = tk.StringVar(value="system")
|
||||
self.destination = tk.StringVar()
|
||||
self.user_sources = {
|
||||
Msg.STR["cat_images"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_documents"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_music"]: tk.BooleanVar(value=False),
|
||||
Msg.STR["cat_videos"]: tk.BooleanVar(value=False)
|
||||
}
|
||||
self.frequency = tk.StringVar(value="daily")
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||
self.wait_window()
|
||||
|
||||
def _create_widgets(self):
|
||||
main_frame = ttk.Frame(self, padding=10)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Backup Type
|
||||
type_frame = ttk.LabelFrame(main_frame, text=Msg.STR["backup_type"], padding=10)
|
||||
type_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Radiobutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type, value="system", command=self._toggle_user_sources).pack(anchor=tk.W)
|
||||
ttk.Radiobutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type, value="user", command=self._toggle_user_sources).pack(anchor=tk.W)
|
||||
|
||||
# Destination
|
||||
dest_frame = ttk.LabelFrame(main_frame, text=Msg.STR["dest_folder"], padding=10)
|
||||
dest_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Entry(dest_frame, textvariable=self.destination, state="readonly", width=50).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
ttk.Button(dest_frame, text=Msg.STR["browse"], command=self._select_destination).pack(side=tk.RIGHT)
|
||||
|
||||
# User Sources (initially hidden)
|
||||
self.user_sources_frame = ttk.LabelFrame(main_frame, text=Msg.STR["source_folders"], padding=10)
|
||||
for name, var in self.user_sources.items():
|
||||
ttk.Checkbutton(self.user_sources_frame, text=name, variable=var).pack(anchor=tk.W)
|
||||
self._toggle_user_sources() # Set initial visibility
|
||||
|
||||
# Frequency
|
||||
freq_frame = ttk.LabelFrame(main_frame, text=Msg.STR["frequency"], padding=10)
|
||||
freq_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_daily"], variable=self.frequency, value="daily").pack(anchor=tk.W)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_weekly"], variable=self.frequency, value="weekly").pack(anchor=tk.W)
|
||||
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_monthly"], variable=self.frequency, value="monthly").pack(anchor=tk.W)
|
||||
|
||||
# Buttons
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(pady=10)
|
||||
ttk.Button(button_frame, text=Msg.STR["save"], command=self._on_save).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self._on_cancel).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def _toggle_user_sources(self):
|
||||
if self.backup_type.get() == "user":
|
||||
self.user_sources_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||
else:
|
||||
self.user_sources_frame.pack_forget()
|
||||
|
||||
def _select_destination(self):
|
||||
dialog = CustomFileDialog(self, mode="dir", title=Msg.STR["select_dest_folder_title"])
|
||||
self.wait_window(dialog)
|
||||
result = dialog.get_result()
|
||||
if result:
|
||||
self.destination.set(result)
|
||||
|
||||
def _on_save(self):
|
||||
dest = self.destination.get()
|
||||
if not dest:
|
||||
MessageDialog(master=self, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
|
||||
return
|
||||
|
||||
job_type = self.backup_type.get()
|
||||
job_frequency = self.frequency.get()
|
||||
job_sources = []
|
||||
|
||||
if job_type == "user":
|
||||
job_sources = [name for name, var in self.user_sources.items() if var.get()]
|
||||
if not job_sources:
|
||||
MessageDialog(master=self, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_no_source_folder"])
|
||||
return
|
||||
|
||||
# Construct the CLI command
|
||||
script_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "main_app.py"))
|
||||
command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\""
|
||||
if job_type == "user":
|
||||
command += f" --sources ";
|
||||
for s in job_sources:
|
||||
command += f'\"{s}\" ';
|
||||
|
||||
# Construct the cron job comment
|
||||
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
|
||||
if job_type == "user":
|
||||
comment += f"; sources:{ ','.join(job_sources)}"
|
||||
|
||||
self.result = {
|
||||
"command": command,
|
||||
"comment": comment,
|
||||
"type": job_type,
|
||||
"frequency": job_frequency,
|
||||
"destination": dest,
|
||||
"sources": job_sources
|
||||
}
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self):
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
def show(self):
|
||||
self.parent.wait_window(self)
|
||||
return self.result
|
Reference in New Issue
Block a user