backup
This commit is contained in:
981
src/server/services/config_service.py
Normal file
981
src/server/services/config_service.py
Normal file
@@ -0,0 +1,981 @@
|
||||
"""
|
||||
User Preferences and Settings Persistence Manager
|
||||
|
||||
This module provides user preferences management, settings persistence,
|
||||
and customization options for the AniWorld web interface.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
|
||||
class UserPreferencesManager:
|
||||
"""Manages user preferences and settings persistence."""
|
||||
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
self.preferences_file = 'data/user_preferences.json'
|
||||
self.preferences = {} # Initialize preferences attribute
|
||||
self.default_preferences = {
|
||||
'ui': {
|
||||
'theme': 'auto', # 'light', 'dark', 'auto'
|
||||
'density': 'comfortable', # 'compact', 'comfortable', 'spacious'
|
||||
'language': 'en',
|
||||
'animations_enabled': True,
|
||||
'sidebar_collapsed': False,
|
||||
'grid_view': True,
|
||||
'items_per_page': 20
|
||||
},
|
||||
'downloads': {
|
||||
'auto_download': False,
|
||||
'download_quality': 'best',
|
||||
'concurrent_downloads': 3,
|
||||
'retry_failed': True,
|
||||
'notification_sound': True,
|
||||
'auto_organize': True
|
||||
},
|
||||
'notifications': {
|
||||
'browser_notifications': True,
|
||||
'email_notifications': False,
|
||||
'webhook_notifications': False,
|
||||
'notification_types': {
|
||||
'download_complete': True,
|
||||
'download_error': True,
|
||||
'series_updated': False,
|
||||
'system_alerts': True
|
||||
}
|
||||
},
|
||||
'keyboard_shortcuts': {
|
||||
'enabled': True,
|
||||
'shortcuts': {
|
||||
'search': 'ctrl+f',
|
||||
'download': 'ctrl+d',
|
||||
'refresh': 'f5',
|
||||
'select_all': 'ctrl+a',
|
||||
'help': 'f1',
|
||||
'settings': 'ctrl+comma'
|
||||
}
|
||||
},
|
||||
'advanced': {
|
||||
'debug_mode': False,
|
||||
'performance_mode': False,
|
||||
'cache_enabled': True,
|
||||
'auto_backup': True,
|
||||
'log_level': 'info'
|
||||
}
|
||||
}
|
||||
|
||||
# Initialize with defaults if no app provided
|
||||
if app is None:
|
||||
self.preferences = self.default_preferences.copy()
|
||||
else:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initialize with Flask app."""
|
||||
self.app = app
|
||||
self.preferences_file = os.path.join(app.instance_path, 'data/user_preferences.json')
|
||||
|
||||
# Ensure instance path exists
|
||||
os.makedirs(app.instance_path, exist_ok=True)
|
||||
|
||||
# Load or create preferences file
|
||||
self.load_preferences()
|
||||
|
||||
def load_preferences(self) -> Dict[str, Any]:
|
||||
"""Load preferences from file."""
|
||||
try:
|
||||
if os.path.exists(self.preferences_file):
|
||||
with open(self.preferences_file, 'r', encoding='utf-8') as f:
|
||||
loaded_prefs = json.load(f)
|
||||
|
||||
# Merge with defaults to ensure all keys exist
|
||||
self.preferences = self.merge_preferences(self.default_preferences, loaded_prefs)
|
||||
else:
|
||||
self.preferences = self.default_preferences.copy()
|
||||
self.save_preferences()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading preferences: {e}")
|
||||
self.preferences = self.default_preferences.copy()
|
||||
|
||||
return self.preferences
|
||||
|
||||
def save_preferences(self) -> bool:
|
||||
"""Save preferences to file."""
|
||||
try:
|
||||
with open(self.preferences_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.preferences, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving preferences: {e}")
|
||||
return False
|
||||
|
||||
def merge_preferences(self, defaults: Dict, user_prefs: Dict) -> Dict:
|
||||
"""Recursively merge user preferences with defaults."""
|
||||
result = defaults.copy()
|
||||
|
||||
for key, value in user_prefs.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = self.merge_preferences(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
def get_preference(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a specific preference using dot notation (e.g., 'ui.theme')."""
|
||||
keys = key.split('.')
|
||||
value = self.preferences
|
||||
|
||||
try:
|
||||
for k in keys:
|
||||
value = value[k]
|
||||
return value
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
def set_preference(self, key: str, value: Any) -> bool:
|
||||
"""Set a specific preference using dot notation."""
|
||||
keys = key.split('.')
|
||||
pref_dict = self.preferences
|
||||
|
||||
try:
|
||||
# Navigate to parent dictionary
|
||||
for k in keys[:-1]:
|
||||
if k not in pref_dict:
|
||||
pref_dict[k] = {}
|
||||
pref_dict = pref_dict[k]
|
||||
|
||||
# Set the value
|
||||
pref_dict[keys[-1]] = value
|
||||
|
||||
# Save to file
|
||||
return self.save_preferences()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting preference {key}: {e}")
|
||||
return False
|
||||
|
||||
def reset_preferences(self) -> bool:
|
||||
"""Reset all preferences to defaults."""
|
||||
self.preferences = self.default_preferences.copy()
|
||||
return self.save_preferences()
|
||||
|
||||
def export_preferences(self) -> str:
|
||||
"""Export preferences as JSON string."""
|
||||
try:
|
||||
return json.dumps(self.preferences, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error exporting preferences: {e}")
|
||||
return "{}"
|
||||
|
||||
def import_preferences(self, json_data: str) -> bool:
|
||||
"""Import preferences from JSON string."""
|
||||
try:
|
||||
imported_prefs = json.loads(json_data)
|
||||
self.preferences = self.merge_preferences(self.default_preferences, imported_prefs)
|
||||
return self.save_preferences()
|
||||
except Exception as e:
|
||||
print(f"Error importing preferences: {e}")
|
||||
return False
|
||||
|
||||
def get_user_session_preferences(self) -> Dict[str, Any]:
|
||||
"""Get preferences for current user session."""
|
||||
# For now, return global preferences
|
||||
# In the future, could be user-specific
|
||||
return self.preferences.copy()
|
||||
|
||||
def get_preferences_js(self):
|
||||
"""Generate JavaScript code for preferences management."""
|
||||
return f"""
|
||||
// AniWorld User Preferences Manager
|
||||
class UserPreferencesManager {{
|
||||
constructor() {{
|
||||
this.preferences = {json.dumps(self.preferences)};
|
||||
this.defaultPreferences = {json.dumps(self.default_preferences)};
|
||||
this.changeListeners = new Map();
|
||||
this.init();
|
||||
}}
|
||||
|
||||
init() {{
|
||||
this.loadFromServer();
|
||||
this.applyPreferences();
|
||||
this.setupPreferencesUI();
|
||||
this.setupAutoSave();
|
||||
}}
|
||||
|
||||
async loadFromServer() {{
|
||||
try {{
|
||||
const response = await fetch('/api/preferences');
|
||||
if (response.ok) {{
|
||||
this.preferences = await response.json();
|
||||
this.applyPreferences();
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error loading preferences:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
async saveToServer() {{
|
||||
try {{
|
||||
const response = await fetch('/api/preferences', {{
|
||||
method: 'PUT',
|
||||
headers: {{
|
||||
'Content-Type': 'application/json'
|
||||
}},
|
||||
body: JSON.stringify(this.preferences)
|
||||
}});
|
||||
|
||||
if (!response.ok) {{
|
||||
console.error('Error saving preferences to server');
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Error saving preferences:', error);
|
||||
}}
|
||||
}}
|
||||
|
||||
get(key, defaultValue = null) {{
|
||||
const keys = key.split('.');
|
||||
let value = this.preferences;
|
||||
|
||||
try {{
|
||||
for (const k of keys) {{
|
||||
value = value[k];
|
||||
}}
|
||||
return value !== undefined ? value : defaultValue;
|
||||
}} catch (error) {{
|
||||
return defaultValue;
|
||||
}}
|
||||
}}
|
||||
|
||||
set(key, value, save = true) {{
|
||||
const keys = key.split('.');
|
||||
let obj = this.preferences;
|
||||
|
||||
// Navigate to parent object
|
||||
for (let i = 0; i < keys.length - 1; i++) {{
|
||||
const k = keys[i];
|
||||
if (!obj[k] || typeof obj[k] !== 'object') {{
|
||||
obj[k] = {{}};
|
||||
}}
|
||||
obj = obj[k];
|
||||
}}
|
||||
|
||||
// Set the value
|
||||
const lastKey = keys[keys.length - 1];
|
||||
const oldValue = obj[lastKey];
|
||||
obj[lastKey] = value;
|
||||
|
||||
// Apply the change immediately
|
||||
this.applyPreference(key, value);
|
||||
|
||||
// Notify listeners
|
||||
this.notifyChangeListeners(key, value, oldValue);
|
||||
|
||||
// Save to server
|
||||
if (save) {{
|
||||
this.saveToServer();
|
||||
}}
|
||||
|
||||
// Store in localStorage as backup
|
||||
localStorage.setItem('aniworld_preferences', JSON.stringify(this.preferences));
|
||||
}}
|
||||
|
||||
applyPreferences() {{
|
||||
// Apply all preferences
|
||||
this.applyTheme();
|
||||
this.applyUIPreferences();
|
||||
this.applyKeyboardShortcuts();
|
||||
this.applyNotificationSettings();
|
||||
}}
|
||||
|
||||
applyPreference(key, value) {{
|
||||
// Apply individual preference change
|
||||
if (key.startsWith('ui.theme')) {{
|
||||
this.applyTheme();
|
||||
}} else if (key.startsWith('ui.')) {{
|
||||
this.applyUIPreferences();
|
||||
}} else if (key.startsWith('keyboard_shortcuts.')) {{
|
||||
this.applyKeyboardShortcuts();
|
||||
}} else if (key.startsWith('notifications.')) {{
|
||||
this.applyNotificationSettings();
|
||||
}}
|
||||
}}
|
||||
|
||||
applyTheme() {{
|
||||
const theme = this.get('ui.theme', 'auto');
|
||||
const html = document.documentElement;
|
||||
|
||||
html.classList.remove('theme-light', 'theme-dark');
|
||||
|
||||
if (theme === 'auto') {{
|
||||
// Use system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
html.classList.add(prefersDark ? 'theme-dark' : 'theme-light');
|
||||
}} else {{
|
||||
html.classList.add(`theme-${{theme}}`);
|
||||
}}
|
||||
|
||||
// Update Bootstrap theme
|
||||
html.setAttribute('data-bs-theme', theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light');
|
||||
}}
|
||||
|
||||
applyUIPreferences() {{
|
||||
const density = this.get('ui.density', 'comfortable');
|
||||
const animations = this.get('ui.animations_enabled', true);
|
||||
const gridView = this.get('ui.grid_view', true);
|
||||
|
||||
// Apply UI density
|
||||
document.body.className = document.body.className.replace(/density-\\w+/g, '');
|
||||
document.body.classList.add(`density-${{density}}`);
|
||||
|
||||
// Apply animations
|
||||
if (!animations) {{
|
||||
document.body.classList.add('no-animations');
|
||||
}} else {{
|
||||
document.body.classList.remove('no-animations');
|
||||
}}
|
||||
|
||||
// Apply view mode
|
||||
const viewToggle = document.querySelector('.view-toggle');
|
||||
if (viewToggle) {{
|
||||
viewToggle.classList.toggle('grid-view', gridView);
|
||||
viewToggle.classList.toggle('list-view', !gridView);
|
||||
}}
|
||||
}}
|
||||
|
||||
applyKeyboardShortcuts() {{
|
||||
const enabled = this.get('keyboard_shortcuts.enabled', true);
|
||||
const shortcuts = this.get('keyboard_shortcuts.shortcuts', {{}});
|
||||
|
||||
if (window.keyboardManager) {{
|
||||
window.keyboardManager.setEnabled(enabled);
|
||||
window.keyboardManager.updateShortcuts(shortcuts);
|
||||
}}
|
||||
}}
|
||||
|
||||
applyNotificationSettings() {{
|
||||
const browserNotifications = this.get('notifications.browser_notifications', true);
|
||||
|
||||
// Request notification permission if needed
|
||||
if (browserNotifications && 'Notification' in window && Notification.permission === 'default') {{
|
||||
Notification.requestPermission();
|
||||
}}
|
||||
}}
|
||||
|
||||
setupPreferencesUI() {{
|
||||
this.createSettingsModal();
|
||||
this.bindSettingsEvents();
|
||||
}}
|
||||
|
||||
createSettingsModal() {{
|
||||
const existingModal = document.getElementById('preferences-modal');
|
||||
if (existingModal) return;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'preferences-modal';
|
||||
modal.className = 'modal fade';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Preferences</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#ui-tab">Interface</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#downloads-tab">Downloads</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#notifications-tab">Notifications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#shortcuts-tab">Shortcuts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#advanced-tab">Advanced</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
${{this.createUITab()}}
|
||||
${{this.createDownloadsTab()}}
|
||||
${{this.createNotificationsTab()}}
|
||||
${{this.createShortcutsTab()}}
|
||||
${{this.createAdvancedTab()}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-outline-danger" id="reset-preferences">Reset to Defaults</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="export-preferences">Export</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="import-preferences">Import</button>
|
||||
<button type="button" class="btn btn-primary" id="save-preferences">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}}
|
||||
|
||||
createUITab() {{
|
||||
return `
|
||||
<div class="tab-pane fade show active" id="ui-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Theme</label>
|
||||
<select class="form-select" id="pref-theme">
|
||||
<option value="auto">Auto (System)</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">UI Density</label>
|
||||
<select class="form-select" id="pref-density">
|
||||
<option value="compact">Compact</option>
|
||||
<option value="comfortable">Comfortable</option>
|
||||
<option value="spacious">Spacious</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Language</label>
|
||||
<select class="form-select" id="pref-language">
|
||||
<option value="en">English</option>
|
||||
<option value="de">German</option>
|
||||
<option value="ja">Japanese</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Items per page</label>
|
||||
<select class="form-select" id="pref-items-per-page">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-animations">
|
||||
<label class="form-check-label" for="pref-animations">
|
||||
Enable animations
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-grid-view">
|
||||
<label class="form-check-label" for="pref-grid-view">
|
||||
Default to grid view
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
|
||||
createDownloadsTab() {{
|
||||
return `
|
||||
<div class="tab-pane fade" id="downloads-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Download Quality</label>
|
||||
<select class="form-select" id="pref-download-quality">
|
||||
<option value="best">Best Available</option>
|
||||
<option value="1080p">1080p</option>
|
||||
<option value="720p">720p</option>
|
||||
<option value="480p">480p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Concurrent Downloads</label>
|
||||
<input type="number" class="form-control" id="pref-concurrent-downloads" min="1" max="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-auto-download">
|
||||
<label class="form-check-label" for="pref-auto-download">
|
||||
Auto-download new episodes
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-retry-failed">
|
||||
<label class="form-check-label" for="pref-retry-failed">
|
||||
Retry failed downloads
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-auto-organize">
|
||||
<label class="form-check-label" for="pref-auto-organize">
|
||||
Auto-organize downloads
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
|
||||
createNotificationsTab() {{
|
||||
return `
|
||||
<div class="tab-pane fade" id="notifications-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>General</h6>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-browser-notifications">
|
||||
<label class="form-check-label" for="pref-browser-notifications">
|
||||
Browser notifications
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-notification-sound">
|
||||
<label class="form-check-label" for="pref-notification-sound">
|
||||
Notification sound
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Notification Types</h6>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="pref-notify-download-complete">
|
||||
<label class="form-check-label" for="pref-notify-download-complete">
|
||||
Download complete
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="pref-notify-download-error">
|
||||
<label class="form-check-label" for="pref-notify-download-error">
|
||||
Download errors
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="pref-notify-series-updated">
|
||||
<label class="form-check-label" for="pref-notify-series-updated">
|
||||
Series updates
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
|
||||
createShortcutsTab() {{
|
||||
return `
|
||||
<div class="tab-pane fade" id="shortcuts-tab">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-shortcuts-enabled">
|
||||
<label class="form-check-label" for="pref-shortcuts-enabled">
|
||||
Enable keyboard shortcuts
|
||||
</label>
|
||||
</div>
|
||||
<div id="shortcuts-list">
|
||||
<!-- Shortcuts will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
|
||||
createAdvancedTab() {{
|
||||
return `
|
||||
<div class="tab-pane fade" id="advanced-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-debug-mode">
|
||||
<label class="form-check-label" for="pref-debug-mode">
|
||||
Debug mode
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-performance-mode">
|
||||
<label class="form-check-label" for="pref-performance-mode">
|
||||
Performance mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-cache-enabled">
|
||||
<label class="form-check-label" for="pref-cache-enabled">
|
||||
Enable caching
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="pref-auto-backup">
|
||||
<label class="form-check-label" for="pref-auto-backup">
|
||||
Auto backup settings
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
|
||||
bindSettingsEvents() {{
|
||||
// Theme system preference listener
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {{
|
||||
if (this.get('ui.theme') === 'auto') {{
|
||||
this.applyTheme();
|
||||
}}
|
||||
}});
|
||||
|
||||
// Settings modal events will be bound when modal is shown
|
||||
document.addEventListener('show.bs.modal', (e) => {{
|
||||
if (e.target.id === 'preferences-modal') {{
|
||||
this.populateSettingsForm();
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
populateSettingsForm() {{
|
||||
// Populate form fields with current preferences
|
||||
const fields = [
|
||||
{{ id: 'pref-theme', key: 'ui.theme' }},
|
||||
{{ id: 'pref-density', key: 'ui.density' }},
|
||||
{{ id: 'pref-language', key: 'ui.language' }},
|
||||
{{ id: 'pref-items-per-page', key: 'ui.items_per_page' }},
|
||||
{{ id: 'pref-animations', key: 'ui.animations_enabled' }},
|
||||
{{ id: 'pref-grid-view', key: 'ui.grid_view' }},
|
||||
{{ id: 'pref-download-quality', key: 'downloads.download_quality' }},
|
||||
{{ id: 'pref-concurrent-downloads', key: 'downloads.concurrent_downloads' }},
|
||||
{{ id: 'pref-auto-download', key: 'downloads.auto_download' }},
|
||||
{{ id: 'pref-retry-failed', key: 'downloads.retry_failed' }},
|
||||
{{ id: 'pref-auto-organize', key: 'downloads.auto_organize' }},
|
||||
{{ id: 'pref-browser-notifications', key: 'notifications.browser_notifications' }},
|
||||
{{ id: 'pref-notification-sound', key: 'downloads.notification_sound' }},
|
||||
{{ id: 'pref-shortcuts-enabled', key: 'keyboard_shortcuts.enabled' }},
|
||||
{{ id: 'pref-debug-mode', key: 'advanced.debug_mode' }},
|
||||
{{ id: 'pref-performance-mode', key: 'advanced.performance_mode' }},
|
||||
{{ id: 'pref-cache-enabled', key: 'advanced.cache_enabled' }},
|
||||
{{ id: 'pref-auto-backup', key: 'advanced.auto_backup' }}
|
||||
];
|
||||
|
||||
fields.forEach(field => {{
|
||||
const element = document.getElementById(field.id);
|
||||
if (element) {{
|
||||
const value = this.get(field.key);
|
||||
if (element.type === 'checkbox') {{
|
||||
element.checked = value;
|
||||
}} else {{
|
||||
element.value = value;
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
setupAutoSave() {{
|
||||
// Auto-save preferences on change
|
||||
document.addEventListener('change', (e) => {{
|
||||
if (e.target.id && e.target.id.startsWith('pref-')) {{
|
||||
this.saveFormValue(e.target);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
saveFormValue(element) {{
|
||||
const keyMap = {{
|
||||
'pref-theme': 'ui.theme',
|
||||
'pref-density': 'ui.density',
|
||||
'pref-language': 'ui.language',
|
||||
'pref-items-per-page': 'ui.items_per_page',
|
||||
'pref-animations': 'ui.animations_enabled',
|
||||
'pref-grid-view': 'ui.grid_view',
|
||||
'pref-download-quality': 'downloads.download_quality',
|
||||
'pref-concurrent-downloads': 'downloads.concurrent_downloads',
|
||||
'pref-auto-download': 'downloads.auto_download',
|
||||
'pref-retry-failed': 'downloads.retry_failed',
|
||||
'pref-auto-organize': 'downloads.auto_organize',
|
||||
'pref-browser-notifications': 'notifications.browser_notifications',
|
||||
'pref-notification-sound': 'downloads.notification_sound',
|
||||
'pref-shortcuts-enabled': 'keyboard_shortcuts.enabled',
|
||||
'pref-debug-mode': 'advanced.debug_mode',
|
||||
'pref-performance-mode': 'advanced.performance_mode',
|
||||
'pref-cache-enabled': 'advanced.cache_enabled',
|
||||
'pref-auto-backup': 'advanced.auto_backup'
|
||||
}};
|
||||
|
||||
const key = keyMap[element.id];
|
||||
if (key) {{
|
||||
let value = element.type === 'checkbox' ? element.checked : element.value;
|
||||
if (element.type === 'number') {{
|
||||
value = parseInt(value, 10);
|
||||
}}
|
||||
this.set(key, value);
|
||||
}}
|
||||
}}
|
||||
|
||||
showPreferences() {{
|
||||
const modal = document.getElementById('preferences-modal');
|
||||
if (modal) {{
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
}}
|
||||
}}
|
||||
|
||||
onPreferenceChange(key, callback) {{
|
||||
if (!this.changeListeners.has(key)) {{
|
||||
this.changeListeners.set(key, []);
|
||||
}}
|
||||
this.changeListeners.get(key).push(callback);
|
||||
}}
|
||||
|
||||
notifyChangeListeners(key, newValue, oldValue) {{
|
||||
const listeners = this.changeListeners.get(key) || [];
|
||||
listeners.forEach(callback => {{
|
||||
try {{
|
||||
callback(newValue, oldValue, key);
|
||||
}} catch (error) {{
|
||||
console.error('Error in preference change listener:', error);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
reset() {{
|
||||
this.preferences = JSON.parse(JSON.stringify(this.defaultPreferences));
|
||||
this.applyPreferences();
|
||||
this.saveToServer();
|
||||
localStorage.removeItem('aniworld_preferences');
|
||||
}}
|
||||
|
||||
export() {{
|
||||
const data = JSON.stringify(this.preferences, null, 2);
|
||||
const blob = new Blob([data], {{ type: 'application/json' }});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'aniworld_preferences.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
|
||||
import(file) {{
|
||||
return new Promise((resolve, reject) => {{
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {{
|
||||
try {{
|
||||
const imported = JSON.parse(e.target.result);
|
||||
this.preferences = this.mergePreferences(this.defaultPreferences, imported);
|
||||
this.applyPreferences();
|
||||
this.saveToServer();
|
||||
resolve(true);
|
||||
}} catch (error) {{
|
||||
reject(error);
|
||||
}}
|
||||
}};
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(file);
|
||||
}});
|
||||
}}
|
||||
|
||||
mergePreferences(defaults, userPrefs) {{
|
||||
const result = {{ ...defaults }};
|
||||
|
||||
for (const [key, value] of Object.entries(userPrefs)) {{
|
||||
if (key in result && typeof result[key] === 'object' && typeof value === 'object') {{
|
||||
result[key] = this.mergePreferences(result[key], value);
|
||||
}} else {{
|
||||
result[key] = value;
|
||||
}}
|
||||
}}
|
||||
|
||||
return result;
|
||||
}}
|
||||
}}
|
||||
|
||||
// Initialize preferences when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {{
|
||||
window.preferencesManager = new UserPreferencesManager();
|
||||
}});
|
||||
"""
|
||||
|
||||
def get_css(self):
|
||||
"""Generate CSS for user preferences."""
|
||||
return """
|
||||
/* User Preferences Styles */
|
||||
.density-compact {
|
||||
--spacing: 0.5rem;
|
||||
--font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.density-comfortable {
|
||||
--spacing: 1rem;
|
||||
--font-size: 1rem;
|
||||
}
|
||||
|
||||
.density-spacious {
|
||||
--spacing: 1.5rem;
|
||||
--font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.no-animations * {
|
||||
animation-duration: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
--bs-body-bg: #ffffff;
|
||||
--bs-body-color: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
--bs-body-bg: #121212;
|
||||
--bs-body-color: #e9ecef;
|
||||
--bs-primary: #0d6efd;
|
||||
}
|
||||
|
||||
#preferences-modal .nav-tabs {
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
#preferences-modal .tab-pane {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.preference-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.preference-group h6 {
|
||||
color: var(--bs-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive preferences modal */
|
||||
@media (max-width: 768px) {
|
||||
#preferences-modal .modal-dialog {
|
||||
max-width: 95vw;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
#preferences-modal .nav-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#preferences-modal .nav-link {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Create the preferences API blueprint
|
||||
preferences_bp = Blueprint('preferences', __name__, url_prefix='/api')
|
||||
|
||||
# Global preferences manager instance
|
||||
preferences_manager = UserPreferencesManager()
|
||||
|
||||
@preferences_bp.route('/preferences', methods=['GET'])
|
||||
def get_preferences():
|
||||
"""Get user preferences."""
|
||||
try:
|
||||
return jsonify(preferences_manager.get_user_session_preferences())
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences', methods=['PUT'])
|
||||
def update_preferences():
|
||||
"""Update user preferences."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
preferences_manager.preferences = preferences_manager.merge_preferences(
|
||||
preferences_manager.default_preferences,
|
||||
data
|
||||
)
|
||||
|
||||
if preferences_manager.save_preferences():
|
||||
return jsonify({'success': True, 'message': 'Preferences updated'})
|
||||
else:
|
||||
return jsonify({'error': 'Failed to save preferences'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences/<key>', methods=['GET'])
|
||||
def get_preference(key):
|
||||
"""Get a specific preference."""
|
||||
try:
|
||||
value = preferences_manager.get_preference(key)
|
||||
return jsonify({'key': key, 'value': value})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences/<key>', methods=['PUT'])
|
||||
def set_preference(key):
|
||||
"""Set a specific preference."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
value = data.get('value')
|
||||
|
||||
if preferences_manager.set_preference(key, value):
|
||||
return jsonify({'success': True, 'key': key, 'value': value})
|
||||
else:
|
||||
return jsonify({'error': 'Failed to set preference'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences/reset', methods=['POST'])
|
||||
def reset_preferences():
|
||||
"""Reset preferences to defaults."""
|
||||
try:
|
||||
if preferences_manager.reset_preferences():
|
||||
return jsonify({'success': True, 'message': 'Preferences reset to defaults'})
|
||||
else:
|
||||
return jsonify({'error': 'Failed to reset preferences'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences/export', methods=['GET'])
|
||||
def export_preferences():
|
||||
"""Export preferences as JSON file."""
|
||||
try:
|
||||
from flask import Response
|
||||
json_data = preferences_manager.export_preferences()
|
||||
|
||||
return Response(
|
||||
json_data,
|
||||
mimetype='application/json',
|
||||
headers={'Content-Disposition': 'attachment; filename=aniworld_preferences.json'}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@preferences_bp.route('/preferences/import', methods=['POST'])
|
||||
def import_preferences():
|
||||
"""Import preferences from JSON file."""
|
||||
try:
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error': 'No file provided'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({'error': 'No file selected'}), 400
|
||||
|
||||
json_data = file.read().decode('utf-8')
|
||||
|
||||
if preferences_manager.import_preferences(json_data):
|
||||
return jsonify({'success': True, 'message': 'Preferences imported successfully'})
|
||||
else:
|
||||
return jsonify({'error': 'Failed to import preferences'}), 500
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
Reference in New Issue
Block a user