Skip to content

Commit 3e77527

Browse files
committed
Merge branch 'nicozanf-themes'
2 parents f2cb78c + 49471d8 commit 3e77527

File tree

12 files changed

+1307
-21
lines changed

12 files changed

+1307
-21
lines changed

apps/_dashboard/DASHBOARD_GUIDE.md

Lines changed: 510 additions & 0 deletions
Large diffs are not rendered by default.

apps/_dashboard/__init__.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ def wrapper(*args, **kwargs):
6868
return wrapper
6969

7070

71+
def get_available_themes():
72+
"""Get list of available themes by reading static/themes/ folder"""
73+
themes_dir = os.path.join(settings.APP_FOLDER, "static", "themes")
74+
try:
75+
if os.path.isdir(themes_dir):
76+
themes = sorted([
77+
d for d in os.listdir(themes_dir)
78+
if os.path.isdir(os.path.join(themes_dir, d)) and not d.startswith('.')
79+
])
80+
return themes
81+
except (OSError, IOError):
82+
pass
83+
return ["AlienDark", "AlienLight"] # Fallback
84+
85+
7186
session = Session()
7287
T = Translator(settings.T_FOLDER)
7388
authenticated = ActionFactory(Logged(session))
@@ -88,6 +103,7 @@ def index():
88103
languages=dumps(getattr(T.local, "language", {})),
89104
mode=MODE,
90105
user_id=(session.get("user") or {}).get("id"),
106+
themes=get_available_themes(),
91107
)
92108

93109
@action("login", method="POST")
@@ -137,11 +153,12 @@ def make_grid():
137153
)
138154

139155
grid = action.uses(db)(make_grid)()
140-
return dict(table_name="py4web_error", grid=grid)
156+
return dict(table_name="py4web_error", grid=grid, themes=get_available_themes())
141157

142158
@action("dbadmin/<app_name>/<db_name>/<table_name>")
143159
@action.uses(Logged(session), "dbadmin.html")
144160
def dbadmin(app_name, db_name, table_name):
161+
themes = get_available_themes()
145162
module = Reloader.MODULES.get(app_name)
146163
db = getattr(module, db_name)
147164

@@ -177,7 +194,7 @@ def make_grid():
177194
return Grid(table, columns=columns)
178195

179196
grid = action.uses(db)(make_grid)()
180-
return dict(table_name=table_name, grid=grid)
197+
return dict(app_name=app_name, table_name=table_name, grid=grid, themes=themes)
181198

182199
@action("info")
183200
@session_secured

apps/_dashboard/static/css/future.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ p {
1717
margin-bottom: 10px;
1818
}
1919
.logo {
20-
padding: 18px;
20+
padding: 0 0 0 8px;
2121
font-size: 64px;
22-
position: absolute;
23-
top: 0;
22+
position: static;
23+
line-height: 1;
24+
display: inline-block;
2425
}
2526
.spinner-top {
2627
height: 80px;
31.3 KB
Binary file not shown.
925 KB
Loading
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/**
2+
* Dashboard Theme Selector Module
3+
*
4+
* Manages dynamic theme switching for the py4web dashboard with browser storage persistence.
5+
* Themes are dynamically discovered from the select element, allowing new themes to be added
6+
* without code changes. Theme selection is persisted using localStorage.
7+
*
8+
* Features:
9+
* - Dynamic theme detection from DOM select element
10+
* - localStorage persistence across sessions
11+
* - Automatic synchronization of multiple theme selectors
12+
* - Intelligent default theme selection (AlienDark if available, else first alphabetically)
13+
* - Graceful fallback handling for storage errors
14+
*/
15+
(function () {
16+
"use strict";
17+
18+
var STORAGE_KEY = "py4web-dashboard-theme";
19+
20+
/**
21+
* Retrieves all available themes by reading options from the theme selector dropdown.
22+
* This method dynamically discovers themes without requiring hardcoded lists.
23+
*
24+
* @returns {Array<string>} Array of theme names (e.g., ["AlienDark", "AlienLight"])
25+
* Returns ["AlienDark"] as fallback if selector not found
26+
*/
27+
function getAvailableThemes() {
28+
var selector = document.getElementById("dashboard-theme-select");
29+
if (selector) {
30+
var themes = [];
31+
for (var i = 0; i < selector.options.length; i += 1) {
32+
themes.push(selector.options[i].value);
33+
}
34+
return themes.length > 0 ? themes : ["AlienDark", "AlienLight"];
35+
}
36+
// Fallback to known themes if selector not found (useful during page load)
37+
return ["AlienDark", "AlienLight"];
38+
}
39+
40+
/**
41+
* Determines the default theme to use when no theme is stored or invalid theme is requested.
42+
*
43+
* Selection logic:
44+
* 1. If "AlienDark" is available, use it (preferred default)
45+
* 2. Otherwise use the first available theme (alphabetically sorted)
46+
* 3. Fallback to "AlienDark" if no themes available
47+
*
48+
* @returns {string} The default theme name
49+
*/
50+
function getDefaultTheme() {
51+
var themes = getAvailableThemes();
52+
// Prefer AlienDark if available, otherwise use first alphabetically
53+
if (themes.indexOf("AlienDark") !== -1) {
54+
return "AlienDark";
55+
}
56+
return themes.length > 0 ? themes[0] : "AlienDark";
57+
}
58+
59+
/**
60+
* Retrieves the previously stored theme from browser localStorage, if valid.
61+
* Validates that the stored theme is still in the available themes list
62+
* (handles case where a theme was removed after being selected).
63+
*
64+
* @returns {string|null} The stored theme name if valid and localStorage accessible,
65+
* null otherwise (will fallback to default theme)
66+
*/
67+
function getStoredTheme() {
68+
try {
69+
var stored = localStorage.getItem(STORAGE_KEY);
70+
var themes = getAvailableThemes();
71+
if (stored && themes.indexOf(stored) !== -1) {
72+
return stored;
73+
}
74+
} catch (err) {
75+
return null;
76+
}
77+
return null;
78+
}
79+
80+
/**
81+
* Applies a theme by:
82+
* 1. Validating the requested theme against available options
83+
* 2. Updating the theme CSS link href
84+
* 3. Setting data-theme attribute on document root
85+
* 4. Updating the favicon based on theme
86+
* 5. Updating app icons based on theme
87+
* 6. Persisting the selection to localStorage
88+
* 7. Syncing all theme selector dropdowns
89+
*
90+
* @param {string} theme - The theme name to apply
91+
*/
92+
function applyTheme(theme) {
93+
var themes = getAvailableThemes();
94+
var defaultTheme = getDefaultTheme();
95+
var selected = themes.indexOf(theme) !== -1 ? theme : defaultTheme;
96+
97+
// Load theme CSS file by updating the link element href
98+
var link = document.getElementById("dashboard-theme");
99+
if (link) {
100+
link.setAttribute("href", "themes/" + selected + "/theme.css");
101+
}
102+
103+
// Update browser favicon based on theme
104+
var favicon = document.querySelector("link[rel='shortcut icon']");
105+
if (favicon) {
106+
if (selected === "AlienLight") {
107+
favicon.setAttribute("href", "favicon_green.ico");
108+
} else {
109+
favicon.setAttribute("href", "favicon.ico");
110+
}
111+
}
112+
113+
// Update the top-left spinner image for light theme
114+
var spinner = document.querySelector("img.spinner-top");
115+
if (spinner) {
116+
var originalSpinnerSrc = spinner.getAttribute("data-original-src");
117+
if (!originalSpinnerSrc) {
118+
originalSpinnerSrc = spinner.getAttribute("src");
119+
spinner.setAttribute("data-original-src", originalSpinnerSrc);
120+
}
121+
if (selected === "AlienLight") {
122+
spinner.setAttribute("src", "images/widget-transparent.gif");
123+
} else {
124+
spinner.setAttribute("src", originalSpinnerSrc);
125+
}
126+
}
127+
128+
// Update app icons based on theme (images with favicon.ico src)
129+
var appIcons = document.querySelectorAll("img[src*='favicon']");
130+
for (var i = 0; i < appIcons.length; i += 1) {
131+
var img = appIcons[i];
132+
var currentSrc = img.getAttribute("src");
133+
134+
// Skip if not a favicon icon
135+
if (!currentSrc.includes("favicon")) continue;
136+
137+
if (selected === "AlienLight") {
138+
// Store original src if not already stored
139+
if (!img.getAttribute("data-original-src")) {
140+
img.setAttribute("data-original-src", currentSrc);
141+
}
142+
// Point all app icons to the dashboard's green favicon
143+
img.setAttribute("src", "/_dashboard/static/favicon_green.ico");
144+
} else {
145+
// Restore original favicon path
146+
var originalSrc = img.getAttribute("data-original-src");
147+
if (originalSrc && originalSrc !== "/_dashboard/static/favicon_green.ico") {
148+
// Use stored original
149+
img.setAttribute("src", originalSrc);
150+
} else if (currentSrc.includes("_dashboard") && currentSrc.includes("favicon_green")) {
151+
// Currently pointing to green, extract the original app path
152+
// For dashboard: /static/favicon.ico or /{app}/static/favicon.ico
153+
var parts = document.location.pathname.split("/");
154+
if (parts[1] && parts[1] !== "_dashboard") {
155+
img.setAttribute("src", "/" + parts[1] + "/static/favicon.ico");
156+
} else {
157+
img.setAttribute("src", "/static/favicon.ico");
158+
}
159+
} else if (currentSrc.includes("favicon_green")) {
160+
// Try to reconstruct original from URL pattern
161+
img.setAttribute("src", "/static/favicon.ico");
162+
}
163+
}
164+
}
165+
166+
// Set data attribute for CSS selectors that might use it
167+
document.documentElement.setAttribute("data-theme", selected);
168+
169+
// Persist theme selection to localStorage
170+
try {
171+
localStorage.setItem(STORAGE_KEY, selected);
172+
} catch (err) {
173+
// Ignore storage errors (private browsing, full storage, etc.)
174+
}
175+
176+
// Update all theme selector dropdowns to reflect current theme
177+
syncSelectors(selected);
178+
}
179+
180+
/**
181+
* Synchronizes all theme selector dropdowns on the page to the same value.
182+
* Allows multiple theme selectors (e.g., on different pages) to stay in sync.
183+
*
184+
* @param {string} theme - The theme value to set on all selectors
185+
*/
186+
function syncSelectors(theme) {
187+
var selectors = document.querySelectorAll("[data-theme-selector]");
188+
for (var i = 0; i < selectors.length; i += 1) {
189+
selectors[i].value = theme;
190+
}
191+
}
192+
193+
/**
194+
* Initializes the theme system on page load.
195+
* Stores original favicon src values before applying any theme.
196+
* Loads the previously saved theme, or applies the default if none saved.
197+
*/
198+
function init() {
199+
// Store all original favicon srcs before applying theme
200+
var appIcons = document.querySelectorAll("img[src*='favicon']");
201+
for (var i = 0; i < appIcons.length; i += 1) {
202+
var img = appIcons[i];
203+
var currentSrc = img.getAttribute("src");
204+
if (currentSrc && !img.getAttribute("data-original-src")) {
205+
img.setAttribute("data-original-src", currentSrc);
206+
}
207+
}
208+
209+
var initial = getStoredTheme() || getDefaultTheme();
210+
applyTheme(initial);
211+
}
212+
213+
/**
214+
* Public API: Exposed globally to allow HTML onclick handlers and external code
215+
* to trigger theme changes. Called by the theme selector dropdown's onchange event.
216+
*
217+
* Usage: setDashboardTheme("AlienDark")
218+
*/
219+
window.setDashboardTheme = applyTheme;
220+
221+
// Initialize theme on page load
222+
if (document.readyState === "loading") {
223+
document.addEventListener("DOMContentLoaded", init);
224+
} else {
225+
init();
226+
}
227+
})();

0 commit comments

Comments
 (0)