Skip to content

Commit bfe72c6

Browse files
committed
feat: release v1.5.0 with custom GUI and restore UX improvements
1 parent 7b4869e commit bfe72c6

12 files changed

Lines changed: 1860 additions & 335 deletions

File tree

service.libreelec.backupper/addon.py

Lines changed: 265 additions & 72 deletions
Large diffs are not rendered by default.

service.libreelec.backupper/addon.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="service.libreelec.backupper" name="LibreELEC Backupper" version="1.4.1.7" provider-name="Nigel">
2+
<addon id="service.libreelec.backupper" name="LibreELEC Backupper" version="1.5.0" provider-name="Nigel">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0"/>
55
<import addon="script.module.requests" version="2.22.0"/>
@@ -14,7 +14,14 @@
1414
<license>GPL-2.0-or-later</license>
1515
<forum>https://forum.libreelec.tv/</forum>
1616
<source>https://github.com/Nigel1992/service.libreelec.backupper</source>
17-
<news>v1.4.1.7 (2026-01-12) - TEST RELEASE
17+
<news>v1.5.0 (2026-04-12) - FEATURE RELEASE
18+
- Added fully custom dashboard and backup browser windows with skin XML layouts (1080i/720p)
19+
- Improved restore UX with reliable progress updates and reduced notification noise
20+
- Added detailed, colorized operation summaries with scroll-friendly formatting
21+
- Improved backup detection and date parsing for legacy and current backup naming formats
22+
- Hardened restore handling for non-critical file errors (permissions/long names/.git metadata)
23+
24+
v1.4.1.7 (2026-01-12) - TEST RELEASE
1825
- Added switchable verbose logging that elevates debug details into the Kodi log
1926
- Expanded diagnostic logging for backup creation, uploads, and remote connections
2027
- Default verbose logging enabled for this test build to aid investigation

service.libreelec.backupper/changelog.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
[B]1.5.0[/B] (2026-04-12)
2+
- Added a fully custom dashboard and backup browser GUI (1080i/720p skin layouts)
3+
- Improved restore progress behavior and reduced intrusive restore notifications
4+
- Added professional, colorized operation summaries with scroll-friendly formatting
5+
- Improved backup listing/date detection for legacy and new backup naming formats
6+
- Hardened restore behavior for non-critical permission/long-name/git-metadata file issues
7+
18
[B]1.4.1.1[/B] (2025-03-29)
29
- Fixed datetime parsing issue in scheduler
310
- Improved error handling for schedule time parsing

service.libreelec.backupper/default.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def main():
1111

1212
# Set author and version in settings
1313
addon.setSetting('author', 'Nigel1992')
14-
addon.setSetting('version', 'Version 1.4.1.7')
14+
addon.setSetting('version', 'Version 1.5.0')
1515

1616
# Get command line arguments
1717
if len(sys.argv) < 2:
@@ -20,9 +20,17 @@ def main():
2020
command = sys.argv[1]
2121

2222
if command == 'backup_now':
23-
backup_manager.create_backup()
23+
success, message = backup_manager.create_backup()
24+
if success:
25+
backup_manager.show_last_operation_summary()
26+
else:
27+
xbmcgui.Dialog().ok(addon.getAddonInfo("name"), f"Backup failed: {message}")
2428
elif command == 'restore':
25-
backup_manager.restore_backup()
29+
success, message = backup_manager.restore_backup()
30+
if success:
31+
backup_manager.show_last_operation_summary()
32+
elif message not in ("Backup restore cancelled", "No backup files found"):
33+
xbmcgui.Dialog().ok(addon.getAddonInfo("name"), f"Restore failed: {message}")
2634
elif command == 'view':
2735
backup_manager.view_backups()
2836
elif command == 'test_connection':

service.libreelec.backupper/resources/lib/backup_utils.py

Lines changed: 666 additions & 258 deletions
Large diffs are not rendered by default.
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
#!/usr/bin/python3
2+
# -*- coding: utf-8 -*-
3+
4+
import xbmc
5+
import xbmcgui
6+
7+
8+
BACK_ACTIONS = {9, 10, 92, 216, 247, 257, 275, 61467, 61448}
9+
SELECT_ACTIONS = {7, 100}
10+
NAV_ACTIONS = {1, 2, 3, 4, 5, 6, 104, 105}
11+
12+
13+
class CustomDashboardWindow(xbmcgui.WindowXMLDialog):
14+
"""Custom dashboard window for the add-on main menu."""
15+
16+
CONTROL_ACTION_LIST = 1000
17+
CONTROL_TITLE = 1100
18+
CONTROL_SUBTITLE = 1101
19+
CONTROL_DESCRIPTION = 2100
20+
21+
CONTROL_BACKUP_COUNT = 2200
22+
CONTROL_STORED_SIZE = 2201
23+
CONTROL_LAST_BACKUP = 2202
24+
CONTROL_LOCATION = 2203
25+
CONTROL_SCHEDULER = 2204
26+
27+
def __init__(self, *args, **kwargs):
28+
self.menu_items = []
29+
self.info = {}
30+
self.selected_action = None
31+
32+
def set_data(self, menu_items, info):
33+
self.menu_items = menu_items or []
34+
self.info = info or {}
35+
36+
def onInit(self):
37+
self._set_label(self.CONTROL_TITLE, self.info.get("title", "Backup Dashboard"))
38+
self._set_label(self.CONTROL_SUBTITLE, self.info.get("subtitle", "Choose an action"))
39+
40+
self._set_label(self.CONTROL_BACKUP_COUNT, self.info.get("backup_count", "Backups Available: 0"))
41+
self._set_label(self.CONTROL_STORED_SIZE, self.info.get("stored_size", "Stored Size: 0 B"))
42+
self._set_label(self.CONTROL_LAST_BACKUP, self.info.get("last_backup", "Last Backup: No backup yet"))
43+
self._set_label(self.CONTROL_LOCATION, self.info.get("location", "Location: Unknown"))
44+
self._set_label(self.CONTROL_SCHEDULER, self.info.get("scheduler", "Scheduler: Disabled"))
45+
46+
self._populate_action_list()
47+
self._update_description()
48+
49+
def onClick(self, controlId):
50+
if controlId == self.CONTROL_ACTION_LIST:
51+
self._select_current_action()
52+
53+
def onAction(self, action):
54+
action_id = action.getId()
55+
56+
if action_id in BACK_ACTIONS:
57+
self.selected_action = None
58+
self.close()
59+
return
60+
61+
if action_id in SELECT_ACTIONS:
62+
try:
63+
if self.getFocusId() == self.CONTROL_ACTION_LIST:
64+
self._select_current_action()
65+
return
66+
except Exception:
67+
pass
68+
69+
if action_id in NAV_ACTIONS:
70+
self._update_description()
71+
72+
def _set_label(self, control_id, text):
73+
try:
74+
self.getControl(control_id).setLabel(str(text))
75+
except Exception:
76+
pass
77+
78+
def _populate_action_list(self):
79+
try:
80+
list_control = self.getControl(self.CONTROL_ACTION_LIST)
81+
list_control.reset()
82+
83+
for item in self.menu_items:
84+
list_item = xbmcgui.ListItem(label=item.get("label", "Action"), label2=item.get("description", ""))
85+
list_item.setProperty("description", item.get("description", ""))
86+
list_control.addItem(list_item)
87+
88+
if self.menu_items:
89+
list_control.selectItem(0)
90+
self.setFocusId(self.CONTROL_ACTION_LIST)
91+
except Exception:
92+
pass
93+
94+
def _update_description(self):
95+
try:
96+
list_control = self.getControl(self.CONTROL_ACTION_LIST)
97+
selected_pos = list_control.getSelectedPosition()
98+
if 0 <= selected_pos < len(self.menu_items):
99+
description = self.menu_items[selected_pos].get("description", "")
100+
self._set_label(self.CONTROL_DESCRIPTION, description)
101+
except Exception:
102+
pass
103+
104+
def _select_current_action(self):
105+
try:
106+
list_control = self.getControl(self.CONTROL_ACTION_LIST)
107+
selected_pos = list_control.getSelectedPosition()
108+
if 0 <= selected_pos < len(self.menu_items):
109+
self.selected_action = self.menu_items[selected_pos].get("action")
110+
else:
111+
self.selected_action = None
112+
except Exception:
113+
self.selected_action = None
114+
115+
self.close()
116+
117+
118+
class CustomBackupBrowserWindow(xbmcgui.WindowXMLDialog):
119+
"""Custom backup browser window for restore/view flows."""
120+
121+
CONTROL_BACKUP_LIST = 1000
122+
CONTROL_TITLE = 1100
123+
CONTROL_SUBTITLE = 1101
124+
CONTROL_DETAILS = 2100
125+
CONTROL_COUNTER = 2200
126+
127+
def __init__(self, *args, **kwargs):
128+
self.backups = []
129+
self.mode = "view"
130+
self.addon_name = ""
131+
self.selected_index = None
132+
133+
def set_data(self, backups, mode, addon_name):
134+
self.backups = backups or []
135+
self.mode = mode or "view"
136+
self.addon_name = addon_name or ""
137+
138+
def onInit(self):
139+
mode_title = "Restore Backup" if self.mode == "restore" else "Browse Backups"
140+
mode_subtitle = "Select a backup to restore" if self.mode == "restore" else "Select a backup to inspect"
141+
142+
self._set_label(self.CONTROL_TITLE, f"{self.addon_name} - {mode_title}" if self.addon_name else mode_title)
143+
self._set_label(self.CONTROL_SUBTITLE, mode_subtitle)
144+
145+
self._populate_backup_list()
146+
self._update_details()
147+
148+
def onClick(self, controlId):
149+
if controlId == self.CONTROL_BACKUP_LIST:
150+
self._select_current_backup()
151+
152+
def onAction(self, action):
153+
action_id = action.getId()
154+
155+
if action_id in BACK_ACTIONS:
156+
self.selected_index = None
157+
self.close()
158+
return
159+
160+
if action_id in SELECT_ACTIONS:
161+
try:
162+
if self.getFocusId() == self.CONTROL_BACKUP_LIST:
163+
self._select_current_backup()
164+
return
165+
except Exception:
166+
pass
167+
168+
if action_id in NAV_ACTIONS:
169+
self._update_details()
170+
171+
def _set_label(self, control_id, text):
172+
try:
173+
self.getControl(control_id).setLabel(str(text))
174+
except Exception:
175+
pass
176+
177+
def _set_text(self, control_id, text):
178+
try:
179+
self.getControl(control_id).setText(str(text))
180+
except Exception:
181+
pass
182+
183+
def _populate_backup_list(self):
184+
try:
185+
list_control = self.getControl(self.CONTROL_BACKUP_LIST)
186+
list_control.reset()
187+
188+
for entry in self.backups:
189+
label = entry.get("display", entry.get("name", "Backup"))
190+
label2 = entry.get("size", "")
191+
item = xbmcgui.ListItem(label=label, label2=label2)
192+
list_control.addItem(item)
193+
194+
if self.backups:
195+
list_control.selectItem(0)
196+
self.setFocusId(self.CONTROL_BACKUP_LIST)
197+
198+
self._set_label(self.CONTROL_COUNTER, f"Backups Found: {len(self.backups)}")
199+
except Exception:
200+
pass
201+
202+
def _update_details(self):
203+
try:
204+
list_control = self.getControl(self.CONTROL_BACKUP_LIST)
205+
selected_pos = list_control.getSelectedPosition()
206+
if not (0 <= selected_pos < len(self.backups)):
207+
return
208+
209+
entry = self.backups[selected_pos]
210+
lines = [
211+
"[COLOR FF4FC3F7][B]Backup Overview[/B][/COLOR]",
212+
"[COLOR FFB0BEC5]------------------------------------------------------------[/COLOR]",
213+
f"[COLOR FF90CAF9]Name:[/COLOR] {entry.get('name', 'Unknown')}",
214+
f"[COLOR FF90CAF9]Date:[/COLOR] {entry.get('date', 'Unknown date')}",
215+
f"[COLOR FF90CAF9]Size:[/COLOR] {entry.get('size', 'Unknown size')}",
216+
]
217+
218+
if entry.get("location"):
219+
lines.append(f"[COLOR FF90CAF9]Location:[/COLOR] {entry.get('location')}")
220+
221+
if entry.get("extra"):
222+
lines.append("")
223+
lines.append("[COLOR FFFFD54F][B]Extra[/B][/COLOR]")
224+
lines.append("[COLOR FFB0BEC5]------------------------------------------------------------[/COLOR]")
225+
lines.append(str(entry.get("extra")))
226+
227+
self._set_text(self.CONTROL_DETAILS, "\n".join(lines))
228+
except Exception:
229+
pass
230+
231+
def _select_current_backup(self):
232+
try:
233+
list_control = self.getControl(self.CONTROL_BACKUP_LIST)
234+
selected_pos = list_control.getSelectedPosition()
235+
if 0 <= selected_pos < len(self.backups):
236+
self.selected_index = selected_pos
237+
else:
238+
self.selected_index = None
239+
except Exception:
240+
self.selected_index = None
241+
242+
self.close()

0 commit comments

Comments
 (0)