Skip to content

Commit 547d847

Browse files
theming overhaul
1 parent 6c5ba92 commit 547d847

File tree

9 files changed

+443
-71
lines changed

9 files changed

+443
-71
lines changed

THEME_MIGRATION.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Theme Migration Guide
2+
3+
This update adds purple theming and dark/light mode support to the DirectAdmin Email Forwarder application.
4+
5+
## Changes Made
6+
7+
### 1. Color Scheme
8+
- Changed primary color from blue to purple (`#6f42c1` for light mode, `#8e44ad` for dark mode)
9+
- Updated navigation bar to use purple tones
10+
- All buttons and links now use purple color scheme
11+
12+
### 2. Dark Mode Support
13+
- Added CSS custom properties (variables) for theme support
14+
- Created dark mode color palette
15+
- Added theme toggle in Settings page
16+
17+
### 3. Database Changes
18+
- Added `theme_preference` column to `user` table
19+
- Stores user's preferred theme ('light' or 'dark')
20+
- Defaults to 'light' theme for new and existing users
21+
22+
## Installation
23+
24+
### For Existing Installations
25+
26+
1. **Run the database migration script:**
27+
```bash
28+
python add_theme_column.py
29+
```
30+
This will add the `theme_preference` column to existing user accounts.
31+
32+
2. **Restart the application:**
33+
```bash
34+
# If using Docker
35+
docker-compose restart
36+
37+
# If running directly
38+
# Stop and restart your Flask application
39+
```
40+
41+
### For New Installations
42+
No additional steps needed - the database will be created with the new column automatically.
43+
44+
## Features
45+
46+
### Theme Toggle
47+
- Located in Settings page under "Appearance" section
48+
- Toggle switch to change between light and dark themes
49+
- Changes apply immediately
50+
- Preference is saved to the database and persists across sessions
51+
52+
### Purple Color Scheme
53+
- Navigation bar: Dark purple (`#4a2c70` for light mode, `#2d1b45` for dark mode)
54+
- Primary buttons: Purple (`#6f42c1` for light mode, `#8e44ad` for dark mode)
55+
- Links and accents: Purple theme throughout
56+
57+
### Dark Mode
58+
- Dark background colors for better readability in low light
59+
- Inverted text colors while maintaining good contrast
60+
- All components support both light and dark themes
61+
62+
## Technical Details
63+
64+
### CSS Variables Used
65+
```css
66+
:root {
67+
--primary-color: #6f42c1;
68+
--primary-hover: #5a359a;
69+
--background-color: #f5f5f5;
70+
--surface-color: #ffffff;
71+
--text-color: #333333;
72+
--nav-background: #4a2c70;
73+
/* ... and more */
74+
}
75+
76+
[data-theme="dark"] {
77+
--primary-color: #8e44ad;
78+
--background-color: #1a1a1a;
79+
--surface-color: #2d2d2d;
80+
--text-color: #e0e0e0;
81+
--nav-background: #2d1b45;
82+
/* ... and more */
83+
}
84+
```
85+
86+
### Database Schema Addition
87+
```sql
88+
ALTER TABLE user ADD COLUMN theme_preference VARCHAR(20) DEFAULT 'light';
89+
```
90+
91+
### API Endpoints
92+
- `GET /settings/api/da-config` - Returns user's theme preference along with other config
93+
- `POST /settings/api/theme` - Updates user's theme preference
94+
95+
## Troubleshooting
96+
97+
### Migration Script Issues
98+
If the migration script fails:
99+
1. Check database connectivity
100+
2. Ensure the application is not running during migration
101+
3. Manually add the column:
102+
```sql
103+
ALTER TABLE user ADD COLUMN theme_preference VARCHAR(20) DEFAULT 'light';
104+
UPDATE user SET theme_preference = 'light' WHERE theme_preference IS NULL;
105+
```
106+
107+
### Theme Not Applying
108+
1. Clear browser cache
109+
2. Check if user is logged in
110+
3. Verify the `data-theme` attribute is set on the `<body>` element
111+
4. Check browser console for JavaScript errors
112+
113+
## Compatibility
114+
- All modern browsers support CSS custom properties
115+
- Falls back gracefully to light theme if JavaScript is disabled
116+
- Mobile responsive design maintained for both themes

add_theme_column.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Database migration script to add theme_preference column to existing users.
4+
Run this script once to update the database schema.
5+
"""
6+
7+
import sys
8+
import os
9+
10+
# Add the app directory to Python path
11+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12+
13+
from app import create_app
14+
from app.models import db, User
15+
16+
def add_theme_column():
17+
"""Add theme_preference column to users table if it doesn't exist"""
18+
app = create_app()
19+
20+
with app.app_context():
21+
try:
22+
# Check if the column already exists by trying to query it
23+
result = db.session.execute("SELECT theme_preference FROM user LIMIT 1")
24+
print("Column 'theme_preference' already exists in the database.")
25+
return True
26+
except Exception as e:
27+
print(f"Column 'theme_preference' does not exist. Adding it now...")
28+
29+
try:
30+
# Add the column with default value 'light'
31+
db.session.execute("ALTER TABLE user ADD COLUMN theme_preference VARCHAR(20) DEFAULT 'light'")
32+
db.session.commit()
33+
print("Successfully added 'theme_preference' column to user table.")
34+
35+
# Update all existing users to have light theme by default
36+
users_updated = User.query.filter(User.theme_preference.is_(None)).update(
37+
{'theme_preference': 'light'}, synchronize_session=False
38+
)
39+
db.session.commit()
40+
print(f"Updated {users_updated} existing users with default light theme.")
41+
42+
return True
43+
except Exception as e:
44+
print(f"Error adding column: {e}")
45+
db.session.rollback()
46+
return False
47+
48+
if __name__ == "__main__":
49+
print("Adding theme_preference column to database...")
50+
success = add_theme_column()
51+
if success:
52+
print("Database migration completed successfully!")
53+
sys.exit(0)
54+
else:
55+
print("Database migration failed!")
56+
sys.exit(1)

app/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class User(UserMixin, db.Model):
3030
da_password_encrypted = db.Column(db.Text, nullable=True)
3131
da_domain = db.Column(db.String(255), nullable=True)
3232

33+
# User preferences
34+
theme_preference = db.Column(db.String(20), default='light', nullable=True) # 'light' or 'dark'
35+
3336
# Unique encryption key per user for DA password
3437
encryption_key = db.Column(db.String(255), nullable=True)
3538

app/settings.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def get_da_config():
2121
'da_server': current_user.da_server or '',
2222
'da_username': current_user.da_username or '',
2323
'da_domain': current_user.da_domain or '',
24-
'has_password': bool(current_user.da_password_encrypted)
24+
'has_password': bool(current_user.da_password_encrypted),
25+
'theme_preference': current_user.theme_preference or 'light'
2526
})
2627
except Exception as e:
2728
print(f"Error in GET da-config: {e}")
@@ -123,6 +124,34 @@ def test_connection():
123124
print(traceback.format_exc())
124125
return jsonify({'error': 'An internal error has occurred.', 'success': False}), 200
125126

127+
@settings_bp.route('/api/theme', methods=['POST'])
128+
@login_required
129+
def update_theme():
130+
"""Update user theme preference"""
131+
try:
132+
data = request.get_json()
133+
if not data or 'theme' not in data:
134+
return jsonify({'error': 'Theme not provided'}), 400
135+
136+
theme = data['theme']
137+
if theme not in ['light', 'dark']:
138+
return jsonify({'error': 'Invalid theme value'}), 400
139+
140+
current_user.theme_preference = theme
141+
db.session.commit()
142+
143+
return jsonify({
144+
'success': True,
145+
'message': f'Theme updated to {theme}',
146+
'theme': theme
147+
})
148+
149+
except Exception as e:
150+
print(f"Error updating theme: {str(e)}")
151+
print(traceback.format_exc())
152+
db.session.rollback()
153+
return jsonify({'error': 'An internal error has occurred.'}), 500
154+
126155
# Debug route to check available routes
127156
@settings_bp.route('/api/debug-routes', methods=['GET'])
128157
@login_required

app/templates/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
88
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='assets/img/ico_main_25.ico') }}">
99
</head>
10-
<body>
10+
<body data-theme="{{ current_user.theme_preference if current_user.is_authenticated else 'light' }}">
1111
<nav>
1212
<div class="nav-brand">
1313
<a href="{{ url_for('dashboard') }}">DirectAdmin 📧 Forwarder</a>

app/templates/settings.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ <h3>DirectAdmin Configuration</h3>
101101
</form>
102102
</div>
103103

104+
<div class="card">
105+
<h3>Appearance</h3>
106+
107+
<div class="settings-section">
108+
<h4>Theme</h4>
109+
<p class="settings-description">Choose between light and dark mode for the interface.</p>
110+
111+
<div class="theme-toggle">
112+
<span class="theme-label">Light</span>
113+
<label class="theme-switch">
114+
<input type="checkbox" id="theme-toggle" onchange="window.toggleTheme()">
115+
<span class="theme-slider"></span>
116+
</label>
117+
<span class="theme-label">Dark</span>
118+
</div>
119+
</div>
120+
</div>
121+
104122
<div class="card">
105123
<h3>Account Settings</h3>
106124

static/base.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,48 @@ document.addEventListener('DOMContentLoaded', function() {
5353
}
5454
}, 250);
5555
});
56+
57+
// Theme persistence - apply theme from localStorage if different from server preference
58+
const savedTheme = localStorage.getItem('theme-preference');
59+
const currentTheme = body.getAttribute('data-theme');
60+
61+
if (savedTheme && savedTheme !== currentTheme) {
62+
body.setAttribute('data-theme', savedTheme);
63+
}
5664
});
65+
66+
// Global theme toggle function (used in settings page)
67+
window.toggleTheme = async function() {
68+
const themeToggle = document.getElementById('theme-toggle');
69+
if (!themeToggle) return;
70+
71+
const newTheme = themeToggle.checked ? 'dark' : 'light';
72+
73+
// Apply theme immediately
74+
document.body.setAttribute('data-theme', newTheme);
75+
76+
// Save to localStorage for immediate persistence
77+
localStorage.setItem('theme-preference', newTheme);
78+
79+
try {
80+
// Save theme preference to server
81+
const response = await fetch('/settings/api/theme', {
82+
method: 'POST',
83+
headers: {
84+
'Content-Type': 'application/json',
85+
},
86+
credentials: 'same-origin',
87+
body: JSON.stringify({ theme: newTheme })
88+
});
89+
90+
if (response.ok) {
91+
const result = await response.json();
92+
if (result.success) {
93+
console.log('Theme preference saved:', newTheme);
94+
}
95+
}
96+
} catch (error) {
97+
console.error('Error saving theme preference:', error);
98+
// Don't revert the theme change on save error, user can still use it for this session
99+
}
100+
};

static/settings.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ document.addEventListener('DOMContentLoaded', async () => {
2929
if (config.has_password) {
3030
document.getElementById('da_password').placeholder = 'Password is set (leave empty to keep current)';
3131
}
32+
33+
// Load theme preference
34+
if (config.theme_preference) {
35+
const themeToggle = document.getElementById('theme-toggle');
36+
if (themeToggle) {
37+
themeToggle.checked = config.theme_preference === 'dark';
38+
}
39+
}
3240
} catch (error) {
3341
console.error('Error loading settings:', error);
3442
}

0 commit comments

Comments
 (0)