981 lines
38 KiB
Python
981 lines
38 KiB
Python
"""
|
|
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 |